diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /dom/html | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/html')
1149 files changed, 125963 insertions, 0 deletions
diff --git a/dom/html/ConstraintValidation.cpp b/dom/html/ConstraintValidation.cpp new file mode 100644 index 0000000000..1d256a3092 --- /dev/null +++ b/dom/html/ConstraintValidation.cpp @@ -0,0 +1,66 @@ +/* -*- 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 "ConstraintValidation.h" + +#include "mozilla/ErrorResult.h" +#include "nsAString.h" +#include "nsIContent.h" + +namespace mozilla::dom { + +void ConstraintValidation::GetValidationMessage(nsAString& aValidationMessage, + ErrorResult& aError) { + aValidationMessage.Truncate(); + + if (IsCandidateForConstraintValidation() && !IsValid()) { + if (GetValidityState(VALIDITY_STATE_CUSTOM_ERROR)) { + aValidationMessage.Assign(mCustomValidity); + if (aValidationMessage.Length() > sContentSpecifiedMaxLengthMessage) { + aValidationMessage.Truncate(sContentSpecifiedMaxLengthMessage); + } + } else if (GetValidityState(VALIDITY_STATE_TOO_LONG)) { + GetValidationMessage(aValidationMessage, VALIDITY_STATE_TOO_LONG); + } else if (GetValidityState(VALIDITY_STATE_TOO_SHORT)) { + GetValidationMessage(aValidationMessage, VALIDITY_STATE_TOO_SHORT); + } else if (GetValidityState(VALIDITY_STATE_VALUE_MISSING)) { + GetValidationMessage(aValidationMessage, VALIDITY_STATE_VALUE_MISSING); + } else if (GetValidityState(VALIDITY_STATE_TYPE_MISMATCH)) { + GetValidationMessage(aValidationMessage, VALIDITY_STATE_TYPE_MISMATCH); + } else if (GetValidityState(VALIDITY_STATE_PATTERN_MISMATCH)) { + GetValidationMessage(aValidationMessage, VALIDITY_STATE_PATTERN_MISMATCH); + } else if (GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW)) { + GetValidationMessage(aValidationMessage, VALIDITY_STATE_RANGE_OVERFLOW); + } else if (GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW)) { + GetValidationMessage(aValidationMessage, VALIDITY_STATE_RANGE_UNDERFLOW); + } else if (GetValidityState(VALIDITY_STATE_STEP_MISMATCH)) { + GetValidationMessage(aValidationMessage, VALIDITY_STATE_STEP_MISMATCH); + } else if (GetValidityState(VALIDITY_STATE_BAD_INPUT)) { + GetValidationMessage(aValidationMessage, VALIDITY_STATE_BAD_INPUT); + } else { + // There should not be other validity states. + aError.Throw(NS_ERROR_UNEXPECTED); + return; + } + } else { + aValidationMessage.Truncate(); + } +} + +bool ConstraintValidation::CheckValidity() { + nsCOMPtr<nsIContent> content = do_QueryInterface(this); + MOZ_ASSERT(content, "This class should be inherited by HTML elements only!"); + return nsIConstraintValidation::CheckValidity(*content); +} + +ConstraintValidation::ConstraintValidation() = default; + +void ConstraintValidation::SetCustomValidity(const nsAString& aError) { + mCustomValidity.Assign(aError); + SetValidityState(VALIDITY_STATE_CUSTOM_ERROR, !mCustomValidity.IsEmpty()); +} + +} // namespace mozilla::dom diff --git a/dom/html/ConstraintValidation.h b/dom/html/ConstraintValidation.h new file mode 100644 index 0000000000..5bd6c89dcb --- /dev/null +++ b/dom/html/ConstraintValidation.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ConstraintValidition_h___ +#define mozilla_dom_ConstraintValidition_h___ + +#include "nsIConstraintValidation.h" +#include "nsString.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class ConstraintValidation : public nsIConstraintValidation { + public: + // Web IDL binding methods + void GetValidationMessage(nsAString& aValidationMessage, + mozilla::ErrorResult& aError); + bool CheckValidity(); + + protected: + // You can't instantiate an object from that class. + ConstraintValidation(); + + virtual ~ConstraintValidation() = default; + + void SetCustomValidity(const nsAString& aError); + + virtual nsresult GetValidationMessage(nsAString& aValidationMessage, + ValidityStateType aType) { + return NS_OK; + } + + private: + /** + * The string representing the custom error. + */ + nsString mCustomValidity; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_ConstraintValidition_h___ diff --git a/dom/html/CustomStateSet.cpp b/dom/html/CustomStateSet.cpp new file mode 100644 index 0000000000..4e24551f5e --- /dev/null +++ b/dom/html/CustomStateSet.cpp @@ -0,0 +1,82 @@ +/* -*- 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 "CustomStateSet.h" +#include "mozilla/dom/ElementInternalsBinding.h" +#include "mozilla/dom/HTMLElement.h" +#include "mozilla/dom/Document.h" +#include "mozilla/PresShell.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(CustomStateSet, mTarget); + +NS_IMPL_CYCLE_COLLECTING_ADDREF(CustomStateSet) +NS_IMPL_CYCLE_COLLECTING_RELEASE(CustomStateSet) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CustomStateSet) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY +NS_INTERFACE_MAP_END + +CustomStateSet::CustomStateSet(HTMLElement* aTarget) : mTarget(aTarget) {} + +// WebIDL interface +nsISupports* CustomStateSet::GetParentObject() const { + return ToSupports(mTarget); +} + +JSObject* CustomStateSet::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return CustomStateSet_Binding::Wrap(aCx, this, aGivenProto); +} + +void CustomStateSet::Clear(ErrorResult& aRv) { + CustomStateSet_Binding::SetlikeHelpers::Clear(this, aRv); + if (aRv.Failed()) { + return; + } + + mTarget->EnsureCustomStates().Clear(); + InvalidateStyleFromCustomStateSetChange(); +} + +void CustomStateSet::InvalidateStyleFromCustomStateSetChange() const { + Document* doc = mTarget->OwnerDoc(); + + PresShell* presShell = doc->GetPresShell(); + if (!presShell) { + return; + } + + // TODO: make this more efficient? + presShell->DestroyFramesForAndRestyle(mTarget); +} + +bool CustomStateSet::Delete(const nsAString& aState, ErrorResult& aRv) { + if (!CustomStateSet_Binding::SetlikeHelpers::Delete(this, aState, aRv) || + aRv.Failed()) { + return false; + } + + RefPtr<nsAtom> atom = NS_AtomizeMainThread(aState); + bool deleted = mTarget->EnsureCustomStates().RemoveElement(atom); + if (deleted) { + InvalidateStyleFromCustomStateSetChange(); + } + return deleted; +} + +void CustomStateSet::Add(const nsAString& aState, ErrorResult& aRv) { + CustomStateSet_Binding::SetlikeHelpers::Add(this, aState, aRv); + if (aRv.Failed()) { + return; + } + + RefPtr<nsAtom> atom = NS_AtomizeMainThread(aState); + mTarget->EnsureCustomStates().AppendElement(atom); + InvalidateStyleFromCustomStateSetChange(); +} + +} // namespace mozilla::dom diff --git a/dom/html/CustomStateSet.h b/dom/html/CustomStateSet.h new file mode 100644 index 0000000000..095974b5f4 --- /dev/null +++ b/dom/html/CustomStateSet.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_CustomStateSet_h +#define mozilla_dom_CustomStateSet_h + +#include "js/TypeDecls.h" +#include "mozilla/ErrorResult.h" + +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" +#include "nsCOMPtr.h" + +namespace mozilla::dom { + +class HTMLElement; +class GlobalObject; + +class CustomStateSet final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(CustomStateSet) + + explicit CustomStateSet(HTMLElement* aTarget); + + // WebIDL interface + nsISupports* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static MOZ_CAN_RUN_SCRIPT_BOUNDARY already_AddRefed<CustomStateSet> + Constructor(const GlobalObject& aGlobal, ErrorResult& aRv); + + void InvalidateStyleFromCustomStateSetChange() const; + + MOZ_CAN_RUN_SCRIPT void Clear(ErrorResult& aRv); + + /** + * @brief Removes a given string from the state set. + */ + MOZ_CAN_RUN_SCRIPT bool Delete(const nsAString& aState, ErrorResult& aRv); + + /** + * @brief Adds a string to this state set. + */ + MOZ_CAN_RUN_SCRIPT void Add(const nsAString& aState, ErrorResult& aRv); + + private: + virtual ~CustomStateSet() = default; + + RefPtr<HTMLElement> mTarget; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_CustomStateSet_h diff --git a/dom/html/ElementInternals.cpp b/dom/html/ElementInternals.cpp new file mode 100644 index 0000000000..aaf58f818e --- /dev/null +++ b/dom/html/ElementInternals.cpp @@ -0,0 +1,485 @@ +/* -*- 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" + +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); +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); +} + +// https://html.spec.whatwg.org/#dom-elementinternals-shadowroot +ShadowRoot* ElementInternals::GetShadowRoot() const { + MOZ_ASSERT(mTarget); + + ShadowRoot* shadowRoot = mTarget->GetShadowRoot(); + if (shadowRoot && !shadowRoot->IsAvailableToElementInternals()) { + return nullptr; + } + + return shadowRoot; +} + +// https://html.spec.whatwg.org/commit-snapshots/912a3fe1f29649ccf8229de56f604b3c07ffd242/#dom-elementinternals-setformvalue +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(); + } + } +} + +// https://html.spec.whatwg.org/#dom-elementinternals-form +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(); +} + +// https://html.spec.whatwg.org/commit-snapshots/3ad5159be8f27e110a70cefadcb50fc45ec21b05/#dom-elementinternals-setvalidity +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; +} + +// https://html.spec.whatwg.org/#dom-elementinternals-willvalidate +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(); +} + +// https://html.spec.whatwg.org/#dom-elementinternals-validity +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(); +} + +// https://html.spec.whatwg.org/#dom-elementinternals-validationmessage +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; +} + +// https://html.spec.whatwg.org/#dom-elementinternals-checkvalidity +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); +} + +// https://html.spec.whatwg.org/#dom-elementinternals-reportvalidity +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; +} + +// https://html.spec.whatwg.org/#dom-elementinternals-labels +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()); + + // https://html.spec.whatwg.org/#face-entry-construction + 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; + } +} + +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); + + bool attrHadValue; + nsAttrValue attrValue(aValue); + nsresult rs = mAttrs.SetAndSwapAttr(aName, attrValue, &attrHadValue); + nsMutationGuard::DidMutate(); + + MutationObservers::NotifyARIAAttributeDefaultChanged(mTarget, aName, modType); + + return rs; +} + +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(); +} + +} // namespace mozilla::dom diff --git a/dom/html/ElementInternals.h b/dom/html/ElementInternals.h new file mode 100644 index 0000000000..7ced3b9771 --- /dev/null +++ b/dom/html/ElementInternals.h @@ -0,0 +1,220 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_ElementInternals_h +#define mozilla_dom_ElementInternals_h + +#include "js/TypeDecls.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/ElementInternalsBinding.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/CustomStateSet.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIConstraintValidation.h" +#include "nsIFormControl.h" +#include "nsWrapperCache.h" +#include "AttrArray.h" +#include "nsGkAtoms.h" + +#define ARIA_REFLECT_ATTR(method, attr) \ + void Get##method(nsAString& aValue) const { \ + GetAttr(nsGkAtoms::attr, aValue); \ + } \ + void Set##method(const nsAString& aValue, ErrorResult& aResult) { \ + aResult = ErrorResult(SetAttr(nsGkAtoms::attr, aValue)); \ + } + +class nsINodeList; +class nsGenericHTMLElement; + +namespace mozilla::dom { + +class DocGroup; +class HTMLElement; +class HTMLFieldSetElement; +class HTMLFormElement; +class ShadowRoot; +class ValidityState; + +class ElementInternals final : public nsIFormControl, + public nsIConstraintValidation, + public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(ElementInternals, + nsIFormControl) + + explicit ElementInternals(HTMLElement* aTarget); + + nsISupports* GetParentObject(); + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL + ShadowRoot* GetShadowRoot() const; + void SetFormValue(const Nullable<FileOrUSVStringOrFormData>& aValue, + const Optional<Nullable<FileOrUSVStringOrFormData>>& aState, + ErrorResult& aRv); + mozilla::dom::HTMLFormElement* GetForm(ErrorResult& aRv) const; + void SetValidity(const ValidityStateFlags& aFlags, + const Optional<nsAString>& aMessage, + const Optional<NonNull<nsGenericHTMLElement>>& aAnchor, + ErrorResult& aRv); + bool GetWillValidate(ErrorResult& aRv) const; + ValidityState* GetValidity(ErrorResult& aRv); + void GetValidationMessage(nsAString& aValidationMessage, + ErrorResult& aRv) const; + bool CheckValidity(ErrorResult& aRv); + bool ReportValidity(ErrorResult& aRv); + already_AddRefed<nsINodeList> GetLabels(ErrorResult& aRv) const; + nsGenericHTMLElement* GetValidationAnchor(ErrorResult& aRv) const; + CustomStateSet* States(); + + // nsIFormControl + mozilla::dom::HTMLFieldSetElement* GetFieldSet() override { + return mFieldSet; + } + mozilla::dom::HTMLFormElement* GetForm() const override { return mForm; } + void SetForm(mozilla::dom::HTMLFormElement* aForm) override; + void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) override; + NS_IMETHOD Reset() override; + NS_IMETHOD SubmitNamesValues(mozilla::dom::FormData* aFormData) override; + int32_t GetParserInsertedControlNumberForStateKey() const override { + return mControlNumber; + } + + void SetFieldSet(mozilla::dom::HTMLFieldSetElement* aFieldSet) { + mFieldSet = aFieldSet; + } + + const Nullable<OwningFileOrUSVStringOrFormData>& GetFormSubmissionValue() + const { + return mSubmissionValue; + } + + const Nullable<OwningFileOrUSVStringOrFormData>& GetFormState() const { + return mState; + } + + void RestoreFormValue(Nullable<OwningFileOrUSVStringOrFormData>&& aValue, + Nullable<OwningFileOrUSVStringOrFormData>&& aState); + + const nsCString& GetStateKey() const { return mStateKey; } + void SetStateKey(nsCString&& key) { + MOZ_ASSERT(mStateKey.IsEmpty(), "FACE state key should only be set once!"); + mStateKey = key; + } + void InitializeControlNumber(); + + void UpdateFormOwner(); + void UpdateBarredFromConstraintValidation(); + + void Unlink(); + + // AccessibilityRole + ARIA_REFLECT_ATTR(Role, role) + + // AriaAttributes + ARIA_REFLECT_ATTR(AriaAtomic, aria_atomic) + ARIA_REFLECT_ATTR(AriaAutoComplete, aria_autocomplete) + ARIA_REFLECT_ATTR(AriaBusy, aria_busy) + ARIA_REFLECT_ATTR(AriaChecked, aria_checked) + ARIA_REFLECT_ATTR(AriaColCount, aria_colcount) + ARIA_REFLECT_ATTR(AriaColIndex, aria_colindex) + ARIA_REFLECT_ATTR(AriaColIndexText, aria_colindextext) + ARIA_REFLECT_ATTR(AriaColSpan, aria_colspan) + ARIA_REFLECT_ATTR(AriaCurrent, aria_current) + ARIA_REFLECT_ATTR(AriaDescription, aria_description) + ARIA_REFLECT_ATTR(AriaDisabled, aria_disabled) + ARIA_REFLECT_ATTR(AriaExpanded, aria_expanded) + ARIA_REFLECT_ATTR(AriaHasPopup, aria_haspopup) + ARIA_REFLECT_ATTR(AriaHidden, aria_hidden) + ARIA_REFLECT_ATTR(AriaInvalid, aria_invalid) + ARIA_REFLECT_ATTR(AriaKeyShortcuts, aria_keyshortcuts) + ARIA_REFLECT_ATTR(AriaLabel, aria_label) + ARIA_REFLECT_ATTR(AriaLevel, aria_level) + ARIA_REFLECT_ATTR(AriaLive, aria_live) + ARIA_REFLECT_ATTR(AriaModal, aria_modal) + ARIA_REFLECT_ATTR(AriaMultiLine, aria_multiline) + ARIA_REFLECT_ATTR(AriaMultiSelectable, aria_multiselectable) + ARIA_REFLECT_ATTR(AriaOrientation, aria_orientation) + ARIA_REFLECT_ATTR(AriaPlaceholder, aria_placeholder) + ARIA_REFLECT_ATTR(AriaPosInSet, aria_posinset) + ARIA_REFLECT_ATTR(AriaPressed, aria_pressed) + ARIA_REFLECT_ATTR(AriaReadOnly, aria_readonly) + ARIA_REFLECT_ATTR(AriaRelevant, aria_relevant) + ARIA_REFLECT_ATTR(AriaRequired, aria_required) + ARIA_REFLECT_ATTR(AriaRoleDescription, aria_roledescription) + ARIA_REFLECT_ATTR(AriaRowCount, aria_rowcount) + ARIA_REFLECT_ATTR(AriaRowIndex, aria_rowindex) + ARIA_REFLECT_ATTR(AriaRowIndexText, aria_rowindextext) + ARIA_REFLECT_ATTR(AriaRowSpan, aria_rowspan) + ARIA_REFLECT_ATTR(AriaSelected, aria_selected) + ARIA_REFLECT_ATTR(AriaSetSize, aria_setsize) + ARIA_REFLECT_ATTR(AriaSort, aria_sort) + ARIA_REFLECT_ATTR(AriaValueMax, aria_valuemax) + ARIA_REFLECT_ATTR(AriaValueMin, aria_valuemin) + ARIA_REFLECT_ATTR(AriaValueNow, aria_valuenow) + ARIA_REFLECT_ATTR(AriaValueText, aria_valuetext) + + void GetAttr(const nsAtom* aName, nsAString& aResult) const; + + nsresult SetAttr(nsAtom* aName, const nsAString& aValue); + + const AttrArray& GetAttrs() const { return mAttrs; } + + DocGroup* GetDocGroup(); + + private: + ~ElementInternals() = default; + + // It's a target element which is a custom element. + RefPtr<HTMLElement> mTarget; + + // The form that contains the target element. + // It's safe to use raw pointer because it will be reset via + // CustomElementData::Unlink when mTarget is released or unlinked. + HTMLFormElement* mForm; + + // This is a pointer to the target element's closest fieldset parent if any. + // It's safe to use raw pointer because it will be reset via + // CustomElementData::Unlink when mTarget is released or unlinked. + HTMLFieldSetElement* mFieldSet; + + // https://html.spec.whatwg.org/#face-submission-value + Nullable<OwningFileOrUSVStringOrFormData> mSubmissionValue; + + // https://html.spec.whatwg.org/#face-state + // TODO: Bug 1734841 - Figure out how to support autocomplete for + // form-associated custom element. + Nullable<OwningFileOrUSVStringOrFormData> mState; + + // https://html.spec.whatwg.org/#face-validation-message + nsString mValidationMessage; + + // https://html.spec.whatwg.org/#face-validation-anchor + RefPtr<nsGenericHTMLElement> mValidationAnchor; + + AttrArray mAttrs; + + // Used to store the key to a form-associated custom element in the current + // session. Is empty until element has been upgraded. + nsCString mStateKey; + + RefPtr<CustomStateSet> mCustomStateSet; + + // A number for a form-associated custom element that is unique within its + // owner document. This is only set to a number for elements inserted into the + // document by the parser from the network. Otherwise, it is -1. + int32_t mControlNumber; +}; + +} // namespace mozilla::dom + +#undef ARIA_REFLECT_ATTR + +#endif // mozilla_dom_ElementInternals_h diff --git a/dom/html/FetchPriority.cpp b/dom/html/FetchPriority.cpp new file mode 100644 index 0000000000..259c05c6c3 --- /dev/null +++ b/dom/html/FetchPriority.cpp @@ -0,0 +1,109 @@ +/* -*- 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/FetchPriority.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Logging.h" +#include "mozilla/dom/RequestBinding.h" +#include "nsISupportsPriority.h" +#include "nsStringFwd.h" + +namespace mozilla::dom { +const char* kFetchPriorityAttributeValueHigh = "high"; +const char* kFetchPriorityAttributeValueLow = "low"; +const char* kFetchPriorityAttributeValueAuto = "auto"; + +FetchPriority ToFetchPriority(RequestPriority aRequestPriority) { + switch (aRequestPriority) { + case RequestPriority::High: { + return FetchPriority::High; + } + case RequestPriority::Low: { + return FetchPriority::Low; + } + case RequestPriority::Auto: { + return FetchPriority::Auto; + } + default: { + MOZ_ASSERT_UNREACHABLE(); + return FetchPriority::Auto; + } + } +} + +#ifdef DEBUG +constexpr auto kPriorityHighest = "PRIORITY_HIGHEST"_ns; +constexpr auto kPriorityHigh = "PRIORITY_HIGH"_ns; +constexpr auto kPriorityNormal = "PRIORITY_NORMAL"_ns; +constexpr auto kPriorityLow = "PRIORITY_LOW"_ns; +constexpr auto kPriorityLowest = "PRIORITY_LOWEST"_ns; +constexpr auto kPriorityUnknown = "UNKNOWN"_ns; + +/** + * See <nsISupportsPriority.idl>. + */ +static void SupportsPriorityToString(int32_t aSupportsPriority, + nsACString& aResult) { + switch (aSupportsPriority) { + case nsISupportsPriority::PRIORITY_HIGHEST: { + aResult = kPriorityHighest; + break; + } + case nsISupportsPriority::PRIORITY_HIGH: { + aResult = kPriorityHigh; + break; + } + case nsISupportsPriority::PRIORITY_NORMAL: { + aResult = kPriorityNormal; + break; + } + case nsISupportsPriority::PRIORITY_LOW: { + aResult = kPriorityLow; + break; + } + case nsISupportsPriority::PRIORITY_LOWEST: { + aResult = kPriorityLowest; + break; + } + default: { + aResult = kPriorityUnknown; + break; + } + } +} + +static const char* ToString(FetchPriority aFetchPriority) { + switch (aFetchPriority) { + case FetchPriority::Auto: { + return kFetchPriorityAttributeValueAuto; + } + case FetchPriority::Low: { + return kFetchPriorityAttributeValueLow; + } + case FetchPriority::High: { + return kFetchPriorityAttributeValueHigh; + } + default: { + MOZ_ASSERT_UNREACHABLE(); + return kFetchPriorityAttributeValueAuto; + } + } +} +#endif // DEBUG + +void LogPriorityMapping(LazyLogModule& aLazyLogModule, + FetchPriority aFetchPriority, + int32_t aSupportsPriority) { +#ifdef DEBUG + nsDependentCString supportsPriority; + SupportsPriorityToString(aSupportsPriority, supportsPriority); + MOZ_LOG(aLazyLogModule, LogLevel::Debug, + ("Mapping priority: %s -> %s", ToString(aFetchPriority), + supportsPriority.get())); +#endif // DEBUG +} +} // namespace mozilla::dom diff --git a/dom/html/FetchPriority.h b/dom/html/FetchPriority.h new file mode 100644 index 0000000000..5719577771 --- /dev/null +++ b/dom/html/FetchPriority.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FetchPriority_h +#define mozilla_dom_FetchPriority_h + +#include <cstdint> + +namespace mozilla { +class LazyLogModule; + +namespace dom { + +enum class RequestPriority : uint8_t; + +// <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>. +enum class FetchPriority : uint8_t { High, Low, Auto }; + +FetchPriority ToFetchPriority(RequestPriority aRequestPriority); + +// @param aSupportsPriority see <nsISupportsPriority.idl>. +void LogPriorityMapping(LazyLogModule& aLazyLogModule, + FetchPriority aFetchPriority, + int32_t aSupportsPriority); + +extern const char* kFetchPriorityAttributeValueHigh; +extern const char* kFetchPriorityAttributeValueLow; +extern const char* kFetchPriorityAttributeValueAuto; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_FetchPriority_h diff --git a/dom/html/HTMLAllCollection.cpp b/dom/html/HTMLAllCollection.cpp new file mode 100644 index 0000000000..b26c96b3c2 --- /dev/null +++ b/dom/html/HTMLAllCollection.cpp @@ -0,0 +1,195 @@ +/* -*- 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/HTMLAllCollection.h" + +#include "jsfriendapi.h" +#include "mozilla/dom/HTMLAllCollectionBinding.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/Element.h" +#include "nsContentList.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +HTMLAllCollection::HTMLAllCollection(mozilla::dom::Document* aDocument) + : mDocument(aDocument) { + MOZ_ASSERT(mDocument); +} + +HTMLAllCollection::~HTMLAllCollection() = default; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HTMLAllCollection, mDocument, mCollection, + mNamedMap) + +NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLAllCollection) +NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLAllCollection) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLAllCollection) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +nsINode* HTMLAllCollection::GetParentObject() const { return mDocument; } + +uint32_t HTMLAllCollection::Length() { return Collection()->Length(true); } + +Element* HTMLAllCollection::Item(uint32_t aIndex) { + nsIContent* item = Collection()->Item(aIndex); + return item ? item->AsElement() : nullptr; +} + +void HTMLAllCollection::Item(const Optional<nsAString>& aNameOrIndex, + Nullable<OwningHTMLCollectionOrElement>& aResult) { + if (!aNameOrIndex.WasPassed()) { + aResult.SetNull(); + return; + } + + const nsAString& nameOrIndex = aNameOrIndex.Value(); + uint32_t indexVal; + if (js::StringIsArrayIndex(nameOrIndex.BeginReading(), nameOrIndex.Length(), + &indexVal)) { + Element* element = Item(indexVal); + if (element) { + aResult.SetValue().SetAsElement() = element; + } else { + aResult.SetNull(); + } + return; + } + + NamedItem(nameOrIndex, aResult); +} + +nsContentList* HTMLAllCollection::Collection() { + if (!mCollection) { + Document* document = mDocument; + mCollection = document->GetElementsByTagName(u"*"_ns); + MOZ_ASSERT(mCollection); + } + return mCollection; +} + +static bool IsAllNamedElement(nsIContent* aContent) { + return aContent->IsAnyOfHTMLElements( + nsGkAtoms::a, nsGkAtoms::button, nsGkAtoms::embed, nsGkAtoms::form, + nsGkAtoms::iframe, nsGkAtoms::img, nsGkAtoms::input, nsGkAtoms::map, + nsGkAtoms::meta, nsGkAtoms::object, nsGkAtoms::select, + nsGkAtoms::textarea, nsGkAtoms::frame, nsGkAtoms::frameset); +} + +static bool DocAllResultMatch(Element* aElement, int32_t aNamespaceID, + nsAtom* aAtom, void* aData) { + if (aElement->GetID() == aAtom) { + return true; + } + + nsGenericHTMLElement* elm = nsGenericHTMLElement::FromNode(aElement); + if (!elm) { + return false; + } + + if (!IsAllNamedElement(elm)) { + return false; + } + + const nsAttrValue* val = elm->GetParsedAttr(nsGkAtoms::name); + return val && val->Type() == nsAttrValue::eAtom && + val->GetAtomValue() == aAtom; +} + +nsContentList* HTMLAllCollection::GetDocumentAllList(const nsAString& aID) { + return mNamedMap + .LookupOrInsertWith(aID, + [this, &aID] { + RefPtr<nsAtom> id = NS_Atomize(aID); + return new nsContentList(mDocument, + DocAllResultMatch, nullptr, + nullptr, true, id); + }) + .get(); +} + +void HTMLAllCollection::NamedGetter( + const nsAString& aID, bool& aFound, + Nullable<OwningHTMLCollectionOrElement>& aResult) { + if (aID.IsEmpty()) { + aFound = false; + aResult.SetNull(); + return; + } + + nsContentList* docAllList = GetDocumentAllList(aID); + if (!docAllList) { + aFound = false; + aResult.SetNull(); + return; + } + + // Check if there are more than 1 entries. Do this by getting the second one + // rather than the length since getting the length always requires walking + // the entire document. + if (docAllList->Item(1, true)) { + aFound = true; + aResult.SetValue().SetAsHTMLCollection() = docAllList; + return; + } + + // There's only 0 or 1 items. Return the first one or null. + if (nsIContent* node = docAllList->Item(0, true)) { + aFound = true; + aResult.SetValue().SetAsElement() = node->AsElement(); + return; + } + + aFound = false; + aResult.SetNull(); +} + +void HTMLAllCollection::GetSupportedNames(nsTArray<nsString>& aNames) { + // XXXbz this is very similar to nsContentList::GetSupportedNames, + // but has to check IsAllNamedElement for the name case. + AutoTArray<nsAtom*, 8> atoms; + for (uint32_t i = 0; i < Length(); ++i) { + nsIContent* content = Item(i); + if (content->HasID()) { + nsAtom* id = content->GetID(); + MOZ_ASSERT(id != nsGkAtoms::_empty, "Empty ids don't get atomized"); + if (!atoms.Contains(id)) { + atoms.AppendElement(id); + } + } + + nsGenericHTMLElement* el = nsGenericHTMLElement::FromNode(content); + if (el) { + // Note: nsINode::HasName means the name is exposed on the document, + // which is false for options, so we don't check it here. + const nsAttrValue* val = el->GetParsedAttr(nsGkAtoms::name); + if (val && val->Type() == nsAttrValue::eAtom && + IsAllNamedElement(content)) { + nsAtom* name = val->GetAtomValue(); + MOZ_ASSERT(name != nsGkAtoms::_empty, "Empty names don't get atomized"); + if (!atoms.Contains(name)) { + atoms.AppendElement(name); + } + } + } + } + + uint32_t atomsLen = atoms.Length(); + nsString* names = aNames.AppendElements(atomsLen); + for (uint32_t i = 0; i < atomsLen; ++i) { + atoms[i]->ToString(names[i]); + } +} + +JSObject* HTMLAllCollection::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLAllCollection_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLAllCollection.h b/dom/html/HTMLAllCollection.h new file mode 100644 index 0000000000..6179951175 --- /dev/null +++ b/dom/html/HTMLAllCollection.h @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLAllCollection_h +#define mozilla_dom_HTMLAllCollection_h + +#include "mozilla/dom/Document.h" +#include "nsCycleCollectionParticipant.h" +#include "nsISupportsImpl.h" +#include "nsRefPtrHashtable.h" +#include "nsWrapperCache.h" + +#include <stdint.h> + +class nsContentList; +class nsINode; + +namespace mozilla::dom { + +class Document; +class Element; +class OwningHTMLCollectionOrElement; +template <typename> +struct Nullable; +template <typename> +class Optional; + +class HTMLAllCollection final : public nsISupports, public nsWrapperCache { + ~HTMLAllCollection(); + + public: + explicit HTMLAllCollection(mozilla::dom::Document* aDocument); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(HTMLAllCollection) + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + nsINode* GetParentObject() const; + + uint32_t Length(); + Element* IndexedGetter(uint32_t aIndex, bool& aFound) { + Element* result = Item(aIndex); + aFound = !!result; + return result; + } + + void NamedItem(const nsAString& aName, + Nullable<OwningHTMLCollectionOrElement>& aResult) { + bool found = false; + NamedGetter(aName, found, aResult); + } + void NamedGetter(const nsAString& aName, bool& aFound, + Nullable<OwningHTMLCollectionOrElement>& aResult); + void GetSupportedNames(nsTArray<nsString>& aNames); + + void Item(const Optional<nsAString>& aNameOrIndex, + Nullable<OwningHTMLCollectionOrElement>& aResult); + + void LegacyCall(JS::Handle<JS::Value>, + const Optional<nsAString>& aNameOrIndex, + Nullable<OwningHTMLCollectionOrElement>& aResult) { + Item(aNameOrIndex, aResult); + } + + private: + nsContentList* Collection(); + + /** + * Returns the HTMLCollection for document.all[aID], or null if there isn't + * one. + */ + nsContentList* GetDocumentAllList(const nsAString& aID); + + /** + * Helper for indexed getter and spec Item() method. + */ + Element* Item(uint32_t aIndex); + + RefPtr<mozilla::dom::Document> mDocument; + RefPtr<nsContentList> mCollection; + nsRefPtrHashtable<nsStringHashKey, nsContentList> mNamedMap; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLAllCollection_h diff --git a/dom/html/HTMLAnchorElement.cpp b/dom/html/HTMLAnchorElement.cpp new file mode 100644 index 0000000000..a5d958139e --- /dev/null +++ b/dom/html/HTMLAnchorElement.cpp @@ -0,0 +1,213 @@ +/* -*- 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/HTMLAnchorElement.h" + +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/HTMLAnchorElementBinding.h" +#include "mozilla/dom/HTMLDNSPrefetch.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/MemoryReporting.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" +#include "nsAttrValueOrString.h" +#include "mozilla/dom/Document.h" +#include "nsPresContext.h" +#include "nsIURI.h" +#include "nsWindowSizes.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Anchor) + +namespace mozilla::dom { + +HTMLAnchorElement::~HTMLAnchorElement() { + SupportsDNSPrefetch::Destroyed(*this); +} + +bool HTMLAnchorElement::IsInteractiveHTMLContent() const { + return HasAttr(nsGkAtoms::href) || + nsGenericHTMLElement::IsInteractiveHTMLContent(); +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLAnchorElement, + nsGenericHTMLElement, Link) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLAnchorElement, nsGenericHTMLElement, + mRelList) + +NS_IMPL_ELEMENT_CLONE(HTMLAnchorElement) + +JSObject* HTMLAnchorElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLAnchorElement_Binding::Wrap(aCx, this, aGivenProto); +} + +int32_t HTMLAnchorElement::TabIndexDefault() { return 0; } + +bool HTMLAnchorElement::Draggable() const { + // links can be dragged as long as there is an href and the + // draggable attribute isn't false + if (!HasAttr(nsGkAtoms::href)) { + // no href, so just use the same behavior as other elements + return nsGenericHTMLElement::Draggable(); + } + + return !AttrValueIs(kNameSpaceID_None, nsGkAtoms::draggable, + nsGkAtoms::_false, eIgnoreCase); +} + +nsresult HTMLAnchorElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + Link::BindToTree(aContext); + + // Prefetch links + if (IsInComposedDoc()) { + TryDNSPrefetch(*this); + } + + return rv; +} + +void HTMLAnchorElement::UnbindFromTree(bool aNullParent) { + // Cancel any DNS prefetches + // Note: Must come before ResetLinkState. If called after, it will recreate + // mCachedURI based on data that is invalid - due to a call to Link::GetURI() + // via GetURIForDNSPrefetch(). + CancelDNSPrefetch(*this); + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + // Without removing the link state we risk a dangling pointer in the + // mStyledLinks hashtable + Link::UnbindFromTree(); +} + +bool HTMLAnchorElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLElement::IsHTMLFocusable(aWithMouse, aIsFocusable, + aTabIndex)) { + return true; + } + + // cannot focus links if there is no link handler + if (!OwnerDoc()->LinkHandlingEnabled()) { + *aTabIndex = -1; + *aIsFocusable = false; + return false; + } + + // Links that are in an editable region should never be focusable, even if + // they are in a contenteditable="false" region. + if (nsContentUtils::IsNodeInEditableRegion(this)) { + *aTabIndex = -1; + *aIsFocusable = false; + return true; + } + + if (GetTabIndexAttrValue().isNothing()) { + // check whether we're actually a link + if (!IsLink()) { + // Not tabbable or focusable without href (bug 17605), unless + // forced to be via presence of nonnegative tabindex attribute + *aTabIndex = -1; + *aIsFocusable = false; + return false; + } + } + + if ((sTabFocusModel & eTabFocus_linksMask) == 0) { + *aTabIndex = -1; + } + *aIsFocusable = true; + return false; +} + +void HTMLAnchorElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + GetEventTargetParentForAnchors(aVisitor); +} + +nsresult HTMLAnchorElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + return PostHandleEventForAnchors(aVisitor); +} + +void HTMLAnchorElement::GetLinkTarget(nsAString& aTarget) { + GetAttr(nsGkAtoms::target, aTarget); + if (aTarget.IsEmpty()) { + GetBaseTarget(aTarget); + } +} + +void HTMLAnchorElement::GetTarget(nsAString& aValue) const { + if (!GetAttr(nsGkAtoms::target, aValue)) { + GetBaseTarget(aValue); + } +} + +nsDOMTokenList* HTMLAnchorElement::RelList() { + if (!mRelList) { + mRelList = + new nsDOMTokenList(this, nsGkAtoms::rel, sAnchorAndFormRelValues); + } + return mRelList; +} + +void HTMLAnchorElement::GetText(nsAString& aText, + mozilla::ErrorResult& aRv) const { + if (NS_WARN_IF( + !nsContentUtils::GetNodeTextContent(this, true, aText, fallible))) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + } +} + +void HTMLAnchorElement::SetText(const nsAString& aText, ErrorResult& aRv) { + aRv = nsContentUtils::SetNodeTextContent(this, aText, false); +} + +already_AddRefed<nsIURI> HTMLAnchorElement::GetHrefURI() const { + if (nsCOMPtr<nsIURI> uri = GetCachedURI()) { + return uri.forget(); + } + return GetHrefURIForAnchors(); +} + +void HTMLAnchorElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aNamespaceID == kNameSpaceID_None && aName == nsGkAtoms::href) { + CancelDNSPrefetch(*this); + } + return nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue, + aNotify); +} + +void HTMLAnchorElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::href) { + Link::ResetLinkState(aNotify, !!aValue); + if (aValue && IsInComposedDoc()) { + TryDNSPrefetch(*this); + } + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLAnchorElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes, + size_t* aNodeSize) const { + nsGenericHTMLElement::AddSizeOfExcludingThis(aSizes, aNodeSize); + *aNodeSize += Link::SizeOfExcludingThis(aSizes.mState); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLAnchorElement.h b/dom/html/HTMLAnchorElement.h new file mode 100644 index 0000000000..2a524b96c2 --- /dev/null +++ b/dom/html/HTMLAnchorElement.h @@ -0,0 +1,201 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLAnchorElement_h +#define mozilla_dom_HTMLAnchorElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Link.h" +#include "mozilla/dom/HTMLDNSPrefetch.h" +#include "nsGenericHTMLElement.h" +#include "nsDOMTokenList.h" + +namespace mozilla { +class EventChainPostVisitor; +class EventChainPreVisitor; +namespace dom { + +class HTMLAnchorElement final : public nsGenericHTMLElement, + public Link, + public SupportsDNSPrefetch { + public: + using Element::GetText; + + explicit HTMLAnchorElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), Link(this) {} + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + // CC + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLAnchorElement, + nsGenericHTMLElement) + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLAnchorElement, a); + + int32_t TabIndexDefault() override; + bool Draggable() const override; + + // Element + bool IsInteractiveHTMLContent() const override; + + // DOM memory reporter participant + NS_DECL_ADDSIZEOFEXCLUDINGTHIS + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + void GetLinkTarget(nsAString& aTarget) override; + already_AddRefed<nsIURI> GetHrefURI() const override; + + void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // WebIDL API + + void GetHref(nsAString& aValue) const { + GetURIAttr(nsGkAtoms::href, nullptr, aValue); + } + void SetHref(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::href, aValue, rv); + } + void GetTarget(nsAString& aValue) const; + void SetTarget(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::target, aValue, rv); + } + void GetDownload(DOMString& aValue) const { + GetHTMLAttr(nsGkAtoms::download, aValue); + } + void SetDownload(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::download, aValue, rv); + } + void GetPing(DOMString& aValue) const { + GetHTMLAttr(nsGkAtoms::ping, aValue); + } + void SetPing(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::ping, aValue, rv); + } + void GetRel(DOMString& aValue) const { GetHTMLAttr(nsGkAtoms::rel, aValue); } + void SetRel(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::rel, aValue, rv); + } + void SetReferrerPolicy(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::referrerpolicy, aValue, rv); + } + void GetReferrerPolicy(DOMString& aPolicy) const { + GetEnumAttr(nsGkAtoms::referrerpolicy, "", aPolicy); + } + nsDOMTokenList* RelList(); + void GetHreflang(DOMString& aValue) const { + GetHTMLAttr(nsGkAtoms::hreflang, aValue); + } + void SetHreflang(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::hreflang, aValue, rv); + } + // Needed for docshell + void GetType(nsAString& aValue) const { + GetHTMLAttr(nsGkAtoms::type, aValue); + } + void GetType(DOMString& aValue) const { + GetHTMLAttr(nsGkAtoms::type, aValue); + } + void SetType(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::type, aValue, rv); + } + void GetText(nsAString& aText, mozilla::ErrorResult& aRv) const; + void SetText(const nsAString& aText, mozilla::ErrorResult& aRv); + + // Link::GetOrigin is OK for us + + // Link::GetProtocol is OK for us + // Link::SetProtocol is OK for us + + // Link::GetUsername is OK for us + // Link::SetUsername is OK for us + + // Link::GetPassword is OK for us + // Link::SetPassword is OK for us + + // Link::Link::GetHost is OK for us + // Link::Link::SetHost is OK for us + + // Link::Link::GetHostname is OK for us + // Link::Link::SetHostname is OK for us + + // Link::Link::GetPort is OK for us + // Link::Link::SetPort is OK for us + + // Link::Link::GetPathname is OK for us + // Link::Link::SetPathname is OK for us + + // Link::Link::GetSearch is OK for us + // Link::Link::SetSearch is OK for us + + // Link::Link::GetHash is OK for us + // Link::Link::SetHash is OK for us + + void GetCoords(DOMString& aValue) const { + GetHTMLAttr(nsGkAtoms::coords, aValue); + } + void SetCoords(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::coords, aValue, rv); + } + void GetCharset(DOMString& aValue) const { + GetHTMLAttr(nsGkAtoms::charset, aValue); + } + void SetCharset(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::charset, aValue, rv); + } + void GetName(DOMString& aValue) const { + GetHTMLAttr(nsGkAtoms::name, aValue); + } + void GetName(nsAString& aValue) const { + GetHTMLAttr(nsGkAtoms::name, aValue); + } + void SetName(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::name, aValue, rv); + } + void GetRev(DOMString& aValue) const { GetHTMLAttr(nsGkAtoms::rev, aValue); } + void SetRev(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::rev, aValue, rv); + } + void GetShape(DOMString& aValue) const { + GetHTMLAttr(nsGkAtoms::shape, aValue); + } + void SetShape(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::shape, aValue, rv); + } + void Stringify(nsAString& aResult) const { GetHref(aResult); } + void ToString(nsAString& aSource) const { GetHref(aSource); } + + void NodeInfoChanged(Document* aOldDoc) final { + ClearHasPendingLinkUpdate(); + nsGenericHTMLElement::NodeInfoChanged(aOldDoc); + } + + protected: + virtual ~HTMLAnchorElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + RefPtr<nsDOMTokenList> mRelList; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_HTMLAnchorElement_h diff --git a/dom/html/HTMLAreaElement.cpp b/dom/html/HTMLAreaElement.cpp new file mode 100644 index 0000000000..81389a4d14 --- /dev/null +++ b/dom/html/HTMLAreaElement.cpp @@ -0,0 +1,115 @@ +/* -*- 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/HTMLAreaElement.h" + +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLAnchorElement.h" +#include "mozilla/dom/HTMLAreaElementBinding.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/MemoryReporting.h" +#include "nsWindowSizes.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Area) + +namespace mozilla::dom { + +HTMLAreaElement::HTMLAreaElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), Link(this) {} + +HTMLAreaElement::~HTMLAreaElement() = default; + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLAreaElement, + nsGenericHTMLElement, Link) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLAreaElement, nsGenericHTMLElement, + mRelList) + +NS_IMPL_ELEMENT_CLONE(HTMLAreaElement) + +int32_t HTMLAreaElement::TabIndexDefault() { return 0; } + +void HTMLAreaElement::GetTarget(DOMString& aValue) { + if (!GetAttr(nsGkAtoms::target, aValue)) { + GetBaseTarget(aValue); + } +} + +void HTMLAreaElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + GetEventTargetParentForAnchors(aVisitor); +} + +nsresult HTMLAreaElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + return PostHandleEventForAnchors(aVisitor); +} + +void HTMLAreaElement::GetLinkTarget(nsAString& aTarget) { + GetAttr(nsGkAtoms::target, aTarget); + if (aTarget.IsEmpty()) { + GetBaseTarget(aTarget); + } +} + +nsDOMTokenList* HTMLAreaElement::RelList() { + if (!mRelList) { + mRelList = + new nsDOMTokenList(this, nsGkAtoms::rel, sAnchorAndFormRelValues); + } + return mRelList; +} + +nsresult HTMLAreaElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + Link::BindToTree(aContext); + return rv; +} + +void HTMLAreaElement::UnbindFromTree(bool aNullParent) { + nsGenericHTMLElement::UnbindFromTree(aNullParent); + // Without removing the link state we risk a dangling pointer in the + // mStyledLinks hashtable + Link::UnbindFromTree(); +} + +void HTMLAreaElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None && aName == nsGkAtoms::href) { + Link::ResetLinkState(aNotify, !!aValue); + } + + return nsGenericHTMLElement::AfterSetAttr( + aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLAreaElement::ToString(nsAString& aSource) { GetHref(aSource); } + +already_AddRefed<nsIURI> HTMLAreaElement::GetHrefURI() const { + if (nsCOMPtr<nsIURI> uri = GetCachedURI()) { + return uri.forget(); + } + return GetHrefURIForAnchors(); +} + +void HTMLAreaElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes, + size_t* aNodeSize) const { + nsGenericHTMLElement::AddSizeOfExcludingThis(aSizes, aNodeSize); + *aNodeSize += Link::SizeOfExcludingThis(aSizes.mState); +} + +JSObject* HTMLAreaElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLAreaElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLAreaElement.h b/dom/html/HTMLAreaElement.h new file mode 100644 index 0000000000..3cba9dd833 --- /dev/null +++ b/dom/html/HTMLAreaElement.h @@ -0,0 +1,170 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLAreaElement_h +#define mozilla_dom_HTMLAreaElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/Link.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" + +namespace mozilla { +class EventChainPostVisitor; +class EventChainPreVisitor; +namespace dom { + +class HTMLAreaElement final : public nsGenericHTMLElement, public Link { + public: + explicit HTMLAreaElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + // CC + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLAreaElement, + nsGenericHTMLElement) + + NS_DECL_ADDSIZEOFEXCLUDINGTHIS + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLAreaElement, area) + + virtual int32_t TabIndexDefault() override; + + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + void GetLinkTarget(nsAString& aTarget) override; + already_AddRefed<nsIURI> GetHrefURI() const override; + + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + virtual void UnbindFromTree(bool aNullParent = true) override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // WebIDL + void GetAlt(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::alt, aValue); } + void SetAlt(const nsAString& aAlt, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::alt, aAlt, aError); + } + + void GetCoords(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::coords, aValue); } + void SetCoords(const nsAString& aCoords, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::coords, aCoords, aError); + } + + // argument type nsAString for HTMLImageMapAccessible + void GetShape(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::shape, aValue); } + void SetShape(const nsAString& aShape, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::shape, aShape, aError); + } + + // argument type nsAString for nsContextMenuInfo + void GetHref(nsAString& aValue) { + GetURIAttr(nsGkAtoms::href, nullptr, aValue); + } + void SetHref(const nsAString& aHref, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::href, aHref, aError); + } + + void GetTarget(DOMString& aValue); + void SetTarget(const nsAString& aTarget, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::target, aTarget, aError); + } + + void GetDownload(DOMString& aValue) { + GetHTMLAttr(nsGkAtoms::download, aValue); + } + void SetDownload(const nsAString& aDownload, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::download, aDownload, aError); + } + + void GetPing(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::ping, aValue); } + + void SetPing(const nsAString& aPing, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::ping, aPing, aError); + } + + void GetRel(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::rel, aValue); } + + void SetRel(const nsAString& aRel, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::rel, aRel, aError); + } + nsDOMTokenList* RelList(); + + void SetReferrerPolicy(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::referrerpolicy, aValue, rv); + } + void GetReferrerPolicy(nsAString& aReferrer) { + GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrer); + } + + // The Link::GetOrigin is OK for us + + // Link::Link::GetProtocol is OK for us + // Link::Link::SetProtocol is OK for us + + // The Link::GetUsername is OK for us + // The Link::SetUsername is OK for us + + // The Link::GetPassword is OK for us + // The Link::SetPassword is OK for us + + // Link::Link::GetHost is OK for us + // Link::Link::SetHost is OK for us + + // Link::Link::GetHostname is OK for us + // Link::Link::SetHostname is OK for us + + // Link::Link::GetPort is OK for us + // Link::Link::SetPort is OK for us + + // Link::Link::GetPathname is OK for us + // Link::Link::SetPathname is OK for us + + // Link::Link::GetSearch is OK for us + // Link::Link::SetSearch is OK for us + + // Link::Link::GetHash is OK for us + // Link::Link::SetHash is OK for us + + // The Link::GetSearchParams is OK for us + + bool NoHref() const { return GetBoolAttr(nsGkAtoms::nohref); } + + void SetNoHref(bool aValue, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::nohref, aValue, aError); + } + + void ToString(nsAString& aSource); + void Stringify(nsAString& aResult) { GetHref(aResult); } + + void NodeInfoChanged(Document* aOldDoc) final { + ClearHasPendingLinkUpdate(); + nsGenericHTMLElement::NodeInfoChanged(aOldDoc); + } + + protected: + virtual ~HTMLAreaElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) override; + + RefPtr<nsDOMTokenList> mRelList; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_HTMLAreaElement_h */ diff --git a/dom/html/HTMLAudioElement.cpp b/dom/html/HTMLAudioElement.cpp new file mode 100644 index 0000000000..766df668b8 --- /dev/null +++ b/dom/html/HTMLAudioElement.cpp @@ -0,0 +1,109 @@ +/* -*- 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/HTMLAudioElement.h" +#include "mozilla/dom/HTMLAudioElementBinding.h" +#include "nsError.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "mozilla/dom/Document.h" +#include "jsfriendapi.h" +#include "nsContentUtils.h" +#include "nsJSUtils.h" +#include "AudioSampleFormat.h" +#include <algorithm> +#include "nsComponentManagerUtils.h" +#include "nsIHttpChannel.h" +#include "mozilla/dom/TimeRanges.h" +#include "AudioStream.h" + +nsGenericHTMLElement* NS_NewHTMLAudioElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + mozilla::dom::HTMLAudioElement* element = + new (nim) mozilla::dom::HTMLAudioElement(nodeInfo.forget()); + element->Init(); + return element; +} + +namespace mozilla::dom { + +nsresult HTMLAudioElement::Clone(mozilla::dom::NodeInfo* aNodeInfo, + nsINode** aResult) const { + *aResult = nullptr; + RefPtr<mozilla::dom::NodeInfo> ni(aNodeInfo); + auto* nim = ni->NodeInfoManager(); + HTMLAudioElement* it = new (nim) HTMLAudioElement(ni.forget()); + it->Init(); + nsCOMPtr<nsINode> kungFuDeathGrip = it; + nsresult rv = const_cast<HTMLAudioElement*>(this)->CopyInnerTo(it); + if (NS_SUCCEEDED(rv)) { + kungFuDeathGrip.swap(*aResult); + } + return rv; +} + +HTMLAudioElement::HTMLAudioElement(already_AddRefed<NodeInfo>&& aNodeInfo) + : HTMLMediaElement(std::move(aNodeInfo)) { + DecoderDoctorLogger::LogConstruction(this); +} + +HTMLAudioElement::~HTMLAudioElement() { + DecoderDoctorLogger::LogDestruction(this); +} + +bool HTMLAudioElement::IsInteractiveHTMLContent() const { + return HasAttr(nsGkAtoms::controls) || + HTMLMediaElement::IsInteractiveHTMLContent(); +} + +already_AddRefed<HTMLAudioElement> HTMLAudioElement::Audio( + const GlobalObject& aGlobal, const Optional<nsAString>& aSrc, + ErrorResult& aRv) { + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports()); + Document* doc; + if (!win || !(doc = win->GetExtantDoc())) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<mozilla::dom::NodeInfo> nodeInfo = doc->NodeInfoManager()->GetNodeInfo( + nsGkAtoms::audio, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE); + + RefPtr<HTMLAudioElement> audio = + static_cast<HTMLAudioElement*>(NS_NewHTMLAudioElement(nodeInfo.forget())); + audio->SetHTMLAttr(nsGkAtoms::preload, u"auto"_ns, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (aSrc.WasPassed()) { + audio->SetSrc(aSrc.Value(), aRv); + } + + return audio.forget(); +} + +nsresult HTMLAudioElement::SetAcceptHeader(nsIHttpChannel* aChannel) { + nsAutoCString value( + "audio/webm," + "audio/ogg," + "audio/wav," + "audio/*;q=0.9," + "application/ogg;q=0.7," + "video/*;q=0.6,*/*;q=0.5"); + + return aChannel->SetRequestHeader("Accept"_ns, value, false); +} + +JSObject* HTMLAudioElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLAudioElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLAudioElement.h b/dom/html/HTMLAudioElement.h new file mode 100644 index 0000000000..75f7105c6f --- /dev/null +++ b/dom/html/HTMLAudioElement.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLAudioElement_h +#define mozilla_dom_HTMLAudioElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/TypedArray.h" + +typedef uint16_t nsMediaNetworkState; +typedef uint16_t nsMediaReadyState; + +namespace mozilla::dom { + +class HTMLAudioElement final : public HTMLMediaElement { + public: + typedef mozilla::dom::NodeInfo NodeInfo; + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLAudioElement, audio) + + explicit HTMLAudioElement(already_AddRefed<NodeInfo>&& aNodeInfo); + + // Element + virtual bool IsInteractiveHTMLContent() const override; + + // nsIDOMHTMLMediaElement + using HTMLMediaElement::GetPaused; + + virtual nsresult Clone(NodeInfo*, nsINode** aResult) const override; + virtual nsresult SetAcceptHeader(nsIHttpChannel* aChannel) override; + + // WebIDL + + static already_AddRefed<HTMLAudioElement> Audio( + const GlobalObject& aGlobal, const Optional<nsAString>& aSrc, + ErrorResult& aRv); + + protected: + virtual ~HTMLAudioElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLAudioElement_h diff --git a/dom/html/HTMLBRElement.cpp b/dom/html/HTMLBRElement.cpp new file mode 100644 index 0000000000..5145c9ea3b --- /dev/null +++ b/dom/html/HTMLBRElement.cpp @@ -0,0 +1,76 @@ +/* -*- 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/HTMLBRElement.h" +#include "mozilla/dom/HTMLBRElementBinding.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsAttrValueInlines.h" +#include "nsStyleConsts.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(BR) + +namespace mozilla::dom { + +HTMLBRElement::HTMLBRElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLBRElement::~HTMLBRElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLBRElement) + +static const nsAttrValue::EnumTable kClearTable[] = { + {"left", StyleClear::Left}, + {"right", StyleClear::Right}, + {"all", StyleClear::Both}, + {"both", StyleClear::Both}, + {nullptr, 0}}; + +bool HTMLBRElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aAttribute == nsGkAtoms::clear && aNamespaceID == kNameSpaceID_None) { + return aResult.ParseEnumValue(aValue, kClearTable, false); + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLBRElement::MapAttributesIntoRule(MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_clear)) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::clear); + if (value && value->Type() == nsAttrValue::eEnum) { + aBuilder.SetKeywordValue(eCSSProperty_clear, value->GetEnumValue()); + } + } + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLBRElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = {{nsGkAtoms::clear}, + {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLBRElement::GetAttributeMappingFunction() const { + return &MapAttributesIntoRule; +} + +JSObject* HTMLBRElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLBRElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLBRElement.h b/dom/html/HTMLBRElement.h new file mode 100644 index 0000000000..e98210dcd2 --- /dev/null +++ b/dom/html/HTMLBRElement.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLBRElement_h +#define mozilla_dom_HTMLBRElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" + +namespace mozilla::dom { + +#define BR_ELEMENT_FLAG_BIT(n_) \ + NODE_FLAG_BIT(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + (n_)) + +// BR element specific bits +enum { + // NS_PADDING_FOR_EMPTY_EDITOR is set if the <br> element is created by + // editor for placing caret at proper position in empty editor. + NS_PADDING_FOR_EMPTY_EDITOR = BR_ELEMENT_FLAG_BIT(0), + + // NS_PADDING_FOR_EMPTY_LAST_LINE is set if the <br> element is created by + // editor for placing caret at proper position for making empty last line + // in a block or <textarea> element visible. + NS_PADDING_FOR_EMPTY_LAST_LINE = BR_ELEMENT_FLAG_BIT(1), +}; + +ASSERT_NODE_FLAGS_SPACE(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 2); + +class HTMLBRElement final : public nsGenericHTMLElement { + public: + explicit HTMLBRElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLBRElement, br) + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + bool Clear() const { return GetBoolAttr(nsGkAtoms::clear); } + void SetClear(const nsAString& aClear, ErrorResult& aError) { + return SetHTMLAttr(nsGkAtoms::clear, aClear, aError); + } + void GetClear(DOMString& aClear) const { + return GetHTMLAttr(nsGkAtoms::clear, aClear); + } + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + bool IsPaddingForEmptyEditor() const { + return HasFlag(NS_PADDING_FOR_EMPTY_EDITOR); + } + bool IsPaddingForEmptyLastLine() const { + return HasFlag(NS_PADDING_FOR_EMPTY_LAST_LINE); + } + + private: + virtual ~HTMLBRElement(); + + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/html/HTMLBodyElement.cpp b/dom/html/HTMLBodyElement.cpp new file mode 100644 index 0000000000..9fcf3e0eb6 --- /dev/null +++ b/dom/html/HTMLBodyElement.cpp @@ -0,0 +1,329 @@ +/* -*- 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 "HTMLBodyElement.h" +#include "mozilla/dom/HTMLBodyElementBinding.h" + +#include "mozilla/AttributeStyles.h" +#include "mozilla/EditorBase.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/TextEditor.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/Document.h" +#include "nsAttrValueInlines.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsPresContext.h" +#include "DocumentInlines.h" +#include "nsDocShell.h" +#include "nsIDocShell.h" +#include "nsGlobalWindowInner.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Body) + +namespace mozilla::dom { + +//---------------------------------------------------------------------- + +HTMLBodyElement::~HTMLBodyElement() = default; + +JSObject* HTMLBodyElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLBodyElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_ELEMENT_CLONE(HTMLBodyElement) + +bool HTMLBodyElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::bgcolor || aAttribute == nsGkAtoms::text || + aAttribute == nsGkAtoms::link || aAttribute == nsGkAtoms::alink || + aAttribute == nsGkAtoms::vlink) { + return aResult.ParseColor(aValue); + } + if (aAttribute == nsGkAtoms::marginwidth || + aAttribute == nsGkAtoms::marginheight || + aAttribute == nsGkAtoms::topmargin || + aAttribute == nsGkAtoms::bottommargin || + aAttribute == nsGkAtoms::leftmargin || + aAttribute == nsGkAtoms::rightmargin) { + return aResult.ParseNonNegativeIntValue(aValue); + } + } + + return nsGenericHTMLElement::ParseBackgroundAttribute( + aNamespaceID, aAttribute, aValue, aResult) || + nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLBodyElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + // This is the one place where we try to set the same property + // multiple times in presentation attributes. Servo does not support + // querying if a property is set (because that is O(n) behavior + // in ServoSpecifiedValues). Instead, we use the below values to keep + // track of whether we have already set a property, and if so, what value + // we set it to (which is used when handling margin + // attributes from the containing frame element) + + int32_t bodyMarginWidth = -1; + int32_t bodyMarginHeight = -1; + int32_t bodyTopMargin = -1; + int32_t bodyBottomMargin = -1; + int32_t bodyLeftMargin = -1; + int32_t bodyRightMargin = -1; + + const nsAttrValue* value; + // if marginwidth/marginheight are set, reflect them as 'margin' + value = aBuilder.GetAttr(nsGkAtoms::marginwidth); + if (value && value->Type() == nsAttrValue::eInteger) { + bodyMarginWidth = value->GetIntegerValue(); + if (bodyMarginWidth < 0) { + bodyMarginWidth = 0; + } + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_left, + (float)bodyMarginWidth); + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_right, + (float)bodyMarginWidth); + } + + value = aBuilder.GetAttr(nsGkAtoms::marginheight); + if (value && value->Type() == nsAttrValue::eInteger) { + bodyMarginHeight = value->GetIntegerValue(); + if (bodyMarginHeight < 0) { + bodyMarginHeight = 0; + } + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_top, + (float)bodyMarginHeight); + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_bottom, + (float)bodyMarginHeight); + } + + // topmargin (IE-attribute) + if (bodyMarginHeight == -1) { + value = aBuilder.GetAttr(nsGkAtoms::topmargin); + if (value && value->Type() == nsAttrValue::eInteger) { + bodyTopMargin = value->GetIntegerValue(); + if (bodyTopMargin < 0) { + bodyTopMargin = 0; + } + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_top, + (float)bodyTopMargin); + } + } + // bottommargin (IE-attribute) + + if (bodyMarginHeight == -1) { + value = aBuilder.GetAttr(nsGkAtoms::bottommargin); + if (value && value->Type() == nsAttrValue::eInteger) { + bodyBottomMargin = value->GetIntegerValue(); + if (bodyBottomMargin < 0) { + bodyBottomMargin = 0; + } + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_bottom, + (float)bodyBottomMargin); + } + } + + // leftmargin (IE-attribute) + if (bodyMarginWidth == -1) { + value = aBuilder.GetAttr(nsGkAtoms::leftmargin); + if (value && value->Type() == nsAttrValue::eInteger) { + bodyLeftMargin = value->GetIntegerValue(); + if (bodyLeftMargin < 0) { + bodyLeftMargin = 0; + } + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_left, + (float)bodyLeftMargin); + } + } + // rightmargin (IE-attribute) + if (bodyMarginWidth == -1) { + value = aBuilder.GetAttr(nsGkAtoms::rightmargin); + if (value && value->Type() == nsAttrValue::eInteger) { + bodyRightMargin = value->GetIntegerValue(); + if (bodyRightMargin < 0) { + bodyRightMargin = 0; + } + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_right, + (float)bodyRightMargin); + } + } + + // if marginwidth or marginheight is set in the <frame> and not set in the + // <body> reflect them as margin in the <body> + if (bodyMarginWidth == -1 || bodyMarginHeight == -1) { + if (nsDocShell* ds = nsDocShell::Cast(aBuilder.Document().GetDocShell())) { + CSSIntSize margins = ds->GetFrameMargins(); + int32_t frameMarginWidth = margins.width; + int32_t frameMarginHeight = margins.height; + + if (bodyMarginWidth == -1 && frameMarginWidth >= 0) { + if (bodyLeftMargin == -1) { + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_left, + (float)frameMarginWidth); + } + if (bodyRightMargin == -1) { + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_right, + (float)frameMarginWidth); + } + } + + if (bodyMarginHeight == -1 && frameMarginHeight >= 0) { + if (bodyTopMargin == -1) { + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_top, + (float)frameMarginHeight); + } + if (bodyBottomMargin == -1) { + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_bottom, + (float)frameMarginHeight); + } + } + } + } + + // When display if first asked for, go ahead and get our colors set up. + if (AttributeStyles* attrStyles = aBuilder.Document().GetAttributeStyles()) { + nscolor color; + value = aBuilder.GetAttr(nsGkAtoms::link); + if (value && value->GetColorValue(color)) { + attrStyles->SetLinkColor(color); + } + + value = aBuilder.GetAttr(nsGkAtoms::alink); + if (value && value->GetColorValue(color)) { + attrStyles->SetActiveLinkColor(color); + } + + value = aBuilder.GetAttr(nsGkAtoms::vlink); + if (value && value->GetColorValue(color)) { + attrStyles->SetVisitedLinkColor(color); + } + } + + if (!aBuilder.PropertyIsSet(eCSSProperty_color)) { + // color: color + nscolor color; + value = aBuilder.GetAttr(nsGkAtoms::text); + if (value && value->GetColorValue(color)) { + aBuilder.SetColorValue(eCSSProperty_color, color); + } + } + + nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +nsMapRuleToAttributesFunc HTMLBodyElement::GetAttributeMappingFunction() const { + return &MapAttributesIntoRule; +} + +NS_IMETHODIMP_(bool) +HTMLBodyElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::link}, + {nsGkAtoms::vlink}, + {nsGkAtoms::alink}, + {nsGkAtoms::text}, + {nsGkAtoms::marginwidth}, + {nsGkAtoms::marginheight}, + {nsGkAtoms::topmargin}, + {nsGkAtoms::rightmargin}, + {nsGkAtoms::bottommargin}, + {nsGkAtoms::leftmargin}, + {nullptr}, + }; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + sBackgroundAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +already_AddRefed<EditorBase> HTMLBodyElement::GetAssociatedEditor() { + MOZ_ASSERT(!GetTextEditorInternal()); + + // Make sure this is the actual body of the document + if (this != OwnerDoc()->GetBodyElement()) { + return nullptr; + } + + // For designmode, try to get document's editor + nsPresContext* presContext = GetPresContext(eForComposedDoc); + if (!presContext) { + return nullptr; + } + + nsCOMPtr<nsIDocShell> docShell = presContext->GetDocShell(); + if (!docShell) { + return nullptr; + } + + RefPtr<HTMLEditor> htmlEditor = docShell->GetHTMLEditor(); + return htmlEditor.forget(); +} + +bool HTMLBodyElement::IsEventAttributeNameInternal(nsAtom* aName) { + return nsContentUtils::IsEventAttributeName( + aName, EventNameType_HTML | EventNameType_HTMLBodyOrFramesetOnly); +} + +nsresult HTMLBodyElement::BindToTree(BindContext& aContext, nsINode& aParent) { + mAttrs.MarkAsPendingPresAttributeEvaluation(); + return nsGenericHTMLElement::BindToTree(aContext, aParent); +} + +void HTMLBodyElement::FrameMarginsChanged() { + MOZ_ASSERT(IsInComposedDoc()); + if (IsPendingMappedAttributeEvaluation()) { + return; + } + if (mAttrs.MarkAsPendingPresAttributeEvaluation()) { + OwnerDoc()->ScheduleForPresAttrEvaluation(this); + } +} + +#define EVENT(name_, id_, type_, \ + struct_) /* nothing; handled by the superclass */ +// nsGenericHTMLElement::GetOnError returns +// already_AddRefed<EventHandlerNonNull> while other getters return +// EventHandlerNonNull*, so allow passing in the type to use here. +#define WINDOW_EVENT_HELPER(name_, type_) \ + type_* HTMLBodyElement::GetOn##name_() { \ + if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + return globalWin->GetOn##name_(); \ + } \ + return nullptr; \ + } \ + void HTMLBodyElement::SetOn##name_(type_* handler) { \ + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \ + if (!win) { \ + return; \ + } \ + \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + return globalWin->SetOn##name_(handler); \ + } +#define WINDOW_EVENT(name_, id_, type_, struct_) \ + WINDOW_EVENT_HELPER(name_, EventHandlerNonNull) +#define BEFOREUNLOAD_EVENT(name_, id_, type_, struct_) \ + WINDOW_EVENT_HELPER(name_, OnBeforeUnloadEventHandlerNonNull) +#include "mozilla/EventNameList.h" // IWYU pragma: keep +#undef BEFOREUNLOAD_EVENT +#undef WINDOW_EVENT +#undef WINDOW_EVENT_HELPER +#undef EVENT + +} // namespace mozilla::dom diff --git a/dom/html/HTMLBodyElement.h b/dom/html/HTMLBodyElement.h new file mode 100644 index 0000000000..3c836f2ed0 --- /dev/null +++ b/dom/html/HTMLBodyElement.h @@ -0,0 +1,117 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef HTMLBodyElement_h___ +#define HTMLBodyElement_h___ + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla { + +class EditorBase; + +namespace dom { + +class OnBeforeUnloadEventHandlerNonNull; + +class HTMLBodyElement final : public nsGenericHTMLElement { + public: + using Element::GetText; + + explicit HTMLBodyElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLBodyElement, nsGenericHTMLElement) + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLBodyElement, body); + + // Event listener stuff; we need to declare only the ones we need to + // forward to window that don't come from nsIDOMHTMLBodyElement. +#define EVENT(name_, id_, type_, struct_) /* nothing; handled by the shim */ +#define WINDOW_EVENT_HELPER(name_, type_) \ + type_* GetOn##name_(); \ + void SetOn##name_(type_* handler); +#define WINDOW_EVENT(name_, id_, type_, struct_) \ + WINDOW_EVENT_HELPER(name_, EventHandlerNonNull) +#define BEFOREUNLOAD_EVENT(name_, id_, type_, struct_) \ + WINDOW_EVENT_HELPER(name_, OnBeforeUnloadEventHandlerNonNull) +#include "mozilla/EventNameList.h" // IWYU pragma: keep +#undef BEFOREUNLOAD_EVENT +#undef WINDOW_EVENT +#undef WINDOW_EVENT_HELPER +#undef EVENT + + void GetText(nsAString& aText) { GetHTMLAttr(nsGkAtoms::text, aText); } + void SetText(const nsAString& aText) { SetHTMLAttr(nsGkAtoms::text, aText); } + void SetText(const nsAString& aText, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::text, aText, aError); + } + void GetLink(nsAString& aLink) { GetHTMLAttr(nsGkAtoms::link, aLink); } + void SetLink(const nsAString& aLink) { SetHTMLAttr(nsGkAtoms::link, aLink); } + void SetLink(const nsAString& aLink, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::link, aLink, aError); + } + void GetVLink(nsAString& aVLink) { GetHTMLAttr(nsGkAtoms::vlink, aVLink); } + void SetVLink(const nsAString& aVLink) { + SetHTMLAttr(nsGkAtoms::vlink, aVLink); + } + void SetVLink(const nsAString& aVLink, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::vlink, aVLink, aError); + } + void GetALink(nsAString& aALink) { GetHTMLAttr(nsGkAtoms::alink, aALink); } + void SetALink(const nsAString& aALink) { + SetHTMLAttr(nsGkAtoms::alink, aALink); + } + void SetALink(const nsAString& aALink, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::alink, aALink, aError); + } + void GetBgColor(nsAString& aBgColor) { + GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor); + } + void SetBgColor(const nsAString& aBgColor) { + SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor); + } + void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError); + } + void GetBackground(DOMString& aBackground) { + GetHTMLAttr(nsGkAtoms::background, aBackground); + } + void GetBackground(nsAString& aBackground) { + GetHTMLAttr(nsGkAtoms::background, aBackground); + } + void SetBackground(const nsAString& aBackground, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::background, aBackground, aError); + } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + already_AddRefed<EditorBase> GetAssociatedEditor() override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + bool IsEventAttributeNameInternal(nsAtom* aName) override; + nsresult BindToTree(BindContext&, nsINode& aParent) override; + + void FrameMarginsChanged(); + + protected: + virtual ~HTMLBodyElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace dom +} // namespace mozilla + +#endif /* HTMLBodyElement_h___ */ diff --git a/dom/html/HTMLButtonElement.cpp b/dom/html/HTMLButtonElement.cpp new file mode 100644 index 0000000000..7aa7428a26 --- /dev/null +++ b/dom/html/HTMLButtonElement.cpp @@ -0,0 +1,438 @@ +/* -*- 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/HTMLButtonElement.h" + +#include "HTMLFormSubmissionConstants.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/HTMLButtonElementBinding.h" +#include "nsAttrValueInlines.h" +#include "nsIContentInlines.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsPresContext.h" +#include "nsIFormControl.h" +#include "nsIFrame.h" +#include "nsIFormControlFrame.h" +#include "mozilla/dom/Document.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/TextEvents.h" +#include "nsUnicharUtils.h" +#include "nsLayoutUtils.h" +#include "mozilla/PresState.h" +#include "nsError.h" +#include "nsFocusManager.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "mozAutoDocUpdate.h" + +#define NS_IN_SUBMIT_CLICK (1 << 0) +#define NS_OUTER_ACTIVATE_EVENT (1 << 1) + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Button) + +namespace mozilla::dom { + +static const nsAttrValue::EnumTable kButtonTypeTable[] = { + {"button", FormControlType::ButtonButton}, + {"reset", FormControlType::ButtonReset}, + {"submit", FormControlType::ButtonSubmit}, + {nullptr, 0}}; + +// Default type is 'submit'. +static const nsAttrValue::EnumTable* kButtonDefaultType = &kButtonTypeTable[2]; + +// Construction, destruction +HTMLButtonElement::HTMLButtonElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLFormControlElementWithState( + std::move(aNodeInfo), aFromParser, + FormControlType(kButtonDefaultType->value)), + mDisabledChanged(false), + mInInternalActivate(false), + mInhibitStateRestoration(aFromParser & FROM_PARSER_FRAGMENT) { + // Set up our default state: enabled + AddStatesSilently(ElementState::ENABLED); +} + +HTMLButtonElement::~HTMLButtonElement() = default; + +// nsISupports + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLButtonElement, + nsGenericHTMLFormControlElementWithState, + mValidity) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED( + HTMLButtonElement, nsGenericHTMLFormControlElementWithState, + nsIConstraintValidation) + +void HTMLButtonElement::SetCustomValidity(const nsAString& aError) { + ConstraintValidation::SetCustomValidity(aError); + UpdateValidityElementStates(true); +} + +void HTMLButtonElement::UpdateBarredFromConstraintValidation() { + SetBarredFromConstraintValidation( + mType == FormControlType::ButtonButton || + mType == FormControlType::ButtonReset || + HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR) || IsDisabled()); +} + +void HTMLButtonElement::FieldSetDisabledChanged(bool aNotify) { + // FieldSetDisabledChanged *has* to be called *before* + // UpdateBarredFromConstraintValidation, because the latter depends on our + // disabled state. + nsGenericHTMLFormControlElementWithState::FieldSetDisabledChanged(aNotify); + + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(aNotify); +} + +NS_IMPL_ELEMENT_CLONE(HTMLButtonElement) + +void HTMLButtonElement::GetFormEnctype(nsAString& aFormEncType) { + GetEnumAttr(nsGkAtoms::formenctype, "", kFormDefaultEnctype->tag, + aFormEncType); +} + +void HTMLButtonElement::GetFormMethod(nsAString& aFormMethod) { + GetEnumAttr(nsGkAtoms::formmethod, "", kFormDefaultMethod->tag, aFormMethod); +} + +void HTMLButtonElement::GetType(nsAString& aType) { + GetEnumAttr(nsGkAtoms::type, kButtonDefaultType->tag, aType); +} + +int32_t HTMLButtonElement::TabIndexDefault() { return 0; } + +bool HTMLButtonElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable( + aWithMouse, aIsFocusable, aTabIndex)) { + return true; + } + + *aIsFocusable = IsFormControlDefaultFocusable(aWithMouse) && !IsDisabled(); + + return false; +} + +bool HTMLButtonElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::type) { + return aResult.ParseEnumValue(aValue, kButtonTypeTable, false, + kButtonDefaultType); + } + + if (aAttribute == nsGkAtoms::formmethod) { + return aResult.ParseEnumValue(aValue, kFormMethodTable, false); + } + if (aAttribute == nsGkAtoms::formenctype) { + return aResult.ParseEnumValue(aValue, kFormEnctypeTable, false); + } + } + + return nsGenericHTMLFormControlElementWithState::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +bool HTMLButtonElement::IsDisabledForEvents(WidgetEvent* aEvent) { + nsIFormControlFrame* formControlFrame = GetFormControlFrame(false); + nsIFrame* formFrame = do_QueryFrame(formControlFrame); + return IsElementDisabledForEvents(aEvent, formFrame); +} + +void HTMLButtonElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + aVisitor.mCanHandle = false; + + if (IsDisabledForEvents(aVisitor.mEvent)) { + return; + } + + // Track whether we're in the outermost Dispatch invocation that will + // cause activation of the input. That is, if we're a click event, or a + // DOMActivate that was dispatched directly, this will be set, but if we're + // a DOMActivate dispatched from click handling, it will not be set. + WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); + bool outerActivateEvent = + ((mouseEvent && mouseEvent->IsLeftClickEvent()) || + (aVisitor.mEvent->mMessage == eLegacyDOMActivate && + !mInInternalActivate && aVisitor.mEvent->mOriginalTarget == this)); + + if (outerActivateEvent) { + aVisitor.mItemFlags |= NS_OUTER_ACTIVATE_EVENT; + aVisitor.mWantsActivationBehavior = true; + } + + nsGenericHTMLElement::GetEventTargetParent(aVisitor); +} + +void HTMLButtonElement::LegacyPreActivationBehavior( + EventChainVisitor& aVisitor) { + // out-of-spec legacy pre-activation behavior needed because of bug 1803805 + if (mType == FormControlType::ButtonSubmit && mForm) { + aVisitor.mItemFlags |= NS_IN_SUBMIT_CLICK; + aVisitor.mItemData = static_cast<Element*>(mForm); + // tell the form that we are about to enter a click handler. + // that means that if there are scripted submissions, the + // latest one will be deferred until after the exit point of the handler. + mForm->OnSubmitClickBegin(this); + } +} + +nsresult HTMLButtonElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + nsresult rv = NS_OK; + if (!aVisitor.mPresContext) { + return rv; + } + + if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) { + WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); + if (mouseEvent && mouseEvent->IsLeftClickEvent() && + OwnerDoc()->MayHaveDOMActivateListeners()) { + // DOMActive event should be trusted since the activation is actually + // occurred even if the cause is an untrusted click event. + InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent); + actEvent.mDetail = 1; + + if (RefPtr<PresShell> presShell = aVisitor.mPresContext->GetPresShell()) { + nsEventStatus status = nsEventStatus_eIgnore; + mInInternalActivate = true; + presShell->HandleDOMEventWithTarget(this, &actEvent, &status); + mInInternalActivate = false; + + // If activate is cancelled, we must do the same as when click is + // cancelled (revert the checkbox to its original value). + if (status == nsEventStatus_eConsumeNoDefault) { + aVisitor.mEventStatus = status; + } + } + } + } + + if (nsEventStatus_eIgnore == aVisitor.mEventStatus) { + WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent(); + if (keyEvent && keyEvent->IsTrusted()) { + HandleKeyboardActivation(aVisitor); + } + + // Bug 1459231: Temporarily needed till links respect activation target + // Then also remove NS_OUTER_ACTIVATE_EVENT + if ((aVisitor.mItemFlags & NS_OUTER_ACTIVATE_EVENT) && mForm && + (mType == FormControlType::ButtonReset || + mType == FormControlType::ButtonSubmit)) { + aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; + } + } + + return rv; +} + +void EndSubmitClick(EventChainVisitor& aVisitor) { + if ((aVisitor.mItemFlags & NS_IN_SUBMIT_CLICK)) { + nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mItemData)); + RefPtr<HTMLFormElement> form = HTMLFormElement::FromNodeOrNull(content); + MOZ_ASSERT(form); + // Tell the form that we are about to exit a click handler, + // so the form knows not to defer subsequent submissions. + // The pending ones that were created during the handler + // will be flushed or forgotten. + form->OnSubmitClickEnd(); + // Tell the form to flush a possible pending submission. + // the reason is that the script returned false (the event was + // not ignored) so if there is a stored submission, it needs to + // be submitted immediatelly. + // Note, NS_IN_SUBMIT_CLICK is set only when we're in outer activate event. + form->FlushPendingSubmission(); + } +} + +void HTMLButtonElement::ActivationBehavior(EventChainPostVisitor& aVisitor) { + if (!aVisitor.mPresContext) { + // Should check whether EndSubmitClick is needed here. + return; + } + + if (!IsDisabled()) { + if (mForm) { + // Hold a strong ref while dispatching + RefPtr<mozilla::dom::HTMLFormElement> form(mForm); + if (mType == FormControlType::ButtonReset) { + form->MaybeReset(this); + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } else if (mType == FormControlType::ButtonSubmit) { + form->MaybeSubmit(this); + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + // https://html.spec.whatwg.org/multipage/form-elements.html#attr-button-type-button-state + // NS_FORM_BUTTON_BUTTON do nothing. + } + if (!GetInvokeTargetElement()) { + HandlePopoverTargetAction(); + } else { + HandleInvokeTargetAction(); + } + } + + EndSubmitClick(aVisitor); +} + +void HTMLButtonElement::LegacyCanceledActivationBehavior( + EventChainPostVisitor& aVisitor) { + // still need to end submission, see bug 1803805 + // e.g. when parent element of button has event handler preventing default + // legacy canceled instead of activation behavior will be run + EndSubmitClick(aVisitor); +} + +nsresult HTMLButtonElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = + nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(false); + + return NS_OK; +} + +void HTMLButtonElement::UnbindFromTree(bool aNullParent) { + nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent); + + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(false); +} + +NS_IMETHODIMP +HTMLButtonElement::Reset() { return NS_OK; } + +NS_IMETHODIMP +HTMLButtonElement::SubmitNamesValues(FormData* aFormData) { + // + // We only submit if we were the button pressed + // + if (aFormData->GetSubmitterElement() != this) { + return NS_OK; + } + + // + // Get the name (if no name, no submit) + // + nsAutoString name; + GetHTMLAttr(nsGkAtoms::name, name); + if (name.IsEmpty()) { + return NS_OK; + } + + // + // Get the value + // + nsAutoString value; + GetHTMLAttr(nsGkAtoms::value, value); + + // + // Submit + // + return aFormData->AddNameValuePair(name, value); +} + +void HTMLButtonElement::DoneCreatingElement() { + if (!mInhibitStateRestoration) { + GenerateStateKey(); + RestoreFormControlState(); + } +} + +void HTMLButtonElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aNotify && aName == nsGkAtoms::disabled && + aNameSpaceID == kNameSpaceID_None) { + mDisabledChanged = true; + } + + return nsGenericHTMLFormControlElementWithState::BeforeSetAttr( + aNameSpaceID, aName, aValue, aNotify); +} + +void HTMLButtonElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::type) { + if (aValue) { + mType = FormControlType(aValue->GetEnumValue()); + } else { + mType = FormControlType(kButtonDefaultType->value); + } + } + + if (aName == nsGkAtoms::type || aName == nsGkAtoms::disabled) { + if (aName == nsGkAtoms::disabled) { + // This *has* to be called *before* validity state check because + // UpdateBarredFromConstraintValidation depends on our disabled state. + UpdateDisabledState(aNotify); + } + + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(aNotify); + } + } + + return nsGenericHTMLFormControlElementWithState::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLButtonElement::SaveState() { + if (!mDisabledChanged) { + return; + } + + PresState* state = GetPrimaryPresState(); + if (state) { + // We do not want to save the real disabled state but the disabled + // attribute. + state->disabled() = HasAttr(nsGkAtoms::disabled); + state->disabledSet() = true; + } +} + +bool HTMLButtonElement::RestoreState(PresState* aState) { + if (aState && aState->disabledSet() && !aState->disabled()) { + SetDisabled(false, IgnoreErrors()); + } + return false; +} + +void HTMLButtonElement::UpdateValidityElementStates(bool aNotify) { + AutoStateChangeNotifier notifier(*this, aNotify); + RemoveStatesSilently(ElementState::VALIDITY_STATES); + if (!IsCandidateForConstraintValidation()) { + return; + } + if (IsValid()) { + AddStatesSilently(ElementState::VALID | ElementState::USER_VALID); + } else { + AddStatesSilently(ElementState::INVALID | ElementState::USER_INVALID); + } +} + +JSObject* HTMLButtonElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLButtonElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLButtonElement.h b/dom/html/HTMLButtonElement.h new file mode 100644 index 0000000000..57bc51c05c --- /dev/null +++ b/dom/html/HTMLButtonElement.h @@ -0,0 +1,149 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLButtonElement_h +#define mozilla_dom_HTMLButtonElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/ConstraintValidation.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla { +class EventChainPostVisitor; +class EventChainPreVisitor; +namespace dom { +class FormData; + +class HTMLButtonElement final : public nsGenericHTMLFormControlElementWithState, + public ConstraintValidation { + public: + using ConstraintValidation::GetValidationMessage; + + explicit HTMLButtonElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser = NOT_FROM_PARSER); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + HTMLButtonElement, nsGenericHTMLFormControlElementWithState) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + int32_t TabIndexDefault() override; + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLButtonElement, button) + + // Element + bool IsInteractiveHTMLContent() const override { return true; } + + // nsGenericHTMLFormElement + void SaveState() override; + bool RestoreState(PresState* aState) override; + + // overriden nsIFormControl methods + NS_IMETHOD Reset() override; + NS_IMETHOD SubmitNamesValues(FormData* aFormData) override; + + void FieldSetDisabledChanged(bool aNotify) override; + + // EventTarget + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + void LegacyPreActivationBehavior(EventChainVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT + void ActivationBehavior(EventChainPostVisitor& aVisitor) override; + void LegacyCanceledActivationBehavior( + EventChainPostVisitor& aVisitor) override; + + // nsINode + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + // nsIContent + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + void DoneCreatingElement() override; + + void UpdateBarredFromConstraintValidation(); + void UpdateValidityElementStates(bool aNotify); + /** + * Called when an attribute is about to be changed + */ + void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + /** + * Called when an attribute has just been changed + */ + void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + // nsGenericHTMLElement + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + bool IsDisabledForEvents(WidgetEvent* aEvent) override; + + // WebIDL + bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); } + void SetDisabled(bool aDisabled, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::disabled, aDisabled, aError); + } + // GetFormAction implemented in superclass + void SetFormAction(const nsAString& aFormAction, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::formaction, aFormAction, aRv); + } + void GetFormEnctype(nsAString& aFormEncType); + void SetFormEnctype(const nsAString& aFormEnctype, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::formenctype, aFormEnctype, aRv); + } + void GetFormMethod(nsAString& aFormMethod); + void SetFormMethod(const nsAString& aFormMethod, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::formmethod, aFormMethod, aRv); + } + bool FormNoValidate() const { return GetBoolAttr(nsGkAtoms::formnovalidate); } + void SetFormNoValidate(bool aFormNoValidate, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::formnovalidate, aFormNoValidate, aError); + } + void GetFormTarget(DOMString& aFormTarget) { + GetHTMLAttr(nsGkAtoms::formtarget, aFormTarget); + } + void SetFormTarget(const nsAString& aFormTarget, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::formtarget, aFormTarget, aRv); + } + void GetName(DOMString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); } + void SetName(const nsAString& aName, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aName, aRv); + } + void GetType(nsAString& aType); + void SetType(const nsAString& aType, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::type, aType, aRv); + } + void GetValue(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::value, aValue); } + void SetValue(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::value, aValue, aRv); + } + + // Override SetCustomValidity so we update our state properly when it's called + // via bindings. + void SetCustomValidity(const nsAString& aError); + + protected: + virtual ~HTMLButtonElement(); + + bool mDisabledChanged : 1; + bool mInInternalActivate : 1; + bool mInhibitStateRestoration : 1; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_HTMLButtonElement_h diff --git a/dom/html/HTMLCanvasElement.cpp b/dom/html/HTMLCanvasElement.cpp new file mode 100644 index 0000000000..93a7bb3787 --- /dev/null +++ b/dom/html/HTMLCanvasElement.cpp @@ -0,0 +1,1443 @@ +/* -*- 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/HTMLCanvasElement.h" + +#include "ImageEncoder.h" +#include "jsapi.h" +#include "jsfriendapi.h" +#include "MediaTrackGraph.h" +#include "mozilla/Assertions.h" +#include "mozilla/Base64.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/CanvasCaptureMediaStream.h" +#include "mozilla/dom/CanvasRenderingContext2D.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/GeneratePlaceholderCanvasData.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/HTMLCanvasElementBinding.h" +#include "mozilla/dom/VideoStreamTrack.h" +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/dom/OffscreenCanvas.h" +#include "mozilla/dom/OffscreenCanvasDisplayHelper.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/gfx/Rect.h" +#include "mozilla/layers/CanvasRenderer.h" +#include "mozilla/layers/WebRenderCanvasRenderer.h" +#include "mozilla/layers/WebRenderUserData.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/Telemetry.h" +#include "mozilla/webgpu/CanvasContext.h" +#include "nsAttrValueInlines.h" +#include "nsContentUtils.h" +#include "nsDisplayList.h" +#include "nsDOMJSUtils.h" +#include "nsITimer.h" +#include "nsJSUtils.h" +#include "nsLayoutUtils.h" +#include "nsMathUtils.h" +#include "nsNetUtil.h" +#include "nsRefreshDriver.h" +#include "nsStreamUtils.h" +#include "ActiveLayerTracker.h" +#include "CanvasUtils.h" +#include "VRManagerChild.h" +#include "ClientWebGLContext.h" +#include "WindowRenderer.h" + +using namespace mozilla::layers; +using namespace mozilla::gfx; + +NS_IMPL_NS_NEW_HTML_ELEMENT(Canvas) + +namespace mozilla::dom { + +class RequestedFrameRefreshObserver : public nsARefreshObserver { + NS_INLINE_DECL_REFCOUNTING(RequestedFrameRefreshObserver, override) + + public: + RequestedFrameRefreshObserver(HTMLCanvasElement* const aOwningElement, + nsRefreshDriver* aRefreshDriver, + bool aReturnPlaceholderData) + : mRegistered(false), + mWatching(false), + mReturnPlaceholderData(aReturnPlaceholderData), + mOwningElement(aOwningElement), + mRefreshDriver(aRefreshDriver), + mWatchManager(this, AbstractThread::MainThread()), + mPendingThrottledCapture(false) { + MOZ_ASSERT(mOwningElement); + } + + static already_AddRefed<DataSourceSurface> CopySurface( + const RefPtr<SourceSurface>& aSurface, bool aReturnPlaceholderData) { + RefPtr<DataSourceSurface> data = aSurface->GetDataSurface(); + if (!data) { + return nullptr; + } + + DataSourceSurface::ScopedMap read(data, DataSourceSurface::READ); + if (!read.IsMapped()) { + return nullptr; + } + + RefPtr<DataSourceSurface> copy = Factory::CreateDataSourceSurfaceWithStride( + data->GetSize(), data->GetFormat(), read.GetStride()); + if (!copy) { + return nullptr; + } + + DataSourceSurface::ScopedMap write(copy, DataSourceSurface::WRITE); + if (!write.IsMapped()) { + return nullptr; + } + + MOZ_ASSERT(read.GetStride() == write.GetStride()); + MOZ_ASSERT(data->GetSize() == copy->GetSize()); + MOZ_ASSERT(data->GetFormat() == copy->GetFormat()); + + if (aReturnPlaceholderData) { + auto size = write.GetStride() * copy->GetSize().height; + auto* data = write.GetData(); + GeneratePlaceholderCanvasData(size, data); + } else { + memcpy(write.GetData(), read.GetData(), + write.GetStride() * copy->GetSize().height); + } + + return copy.forget(); + } + + void SetReturnPlaceholderData(bool aReturnPlaceholderData) { + mReturnPlaceholderData = aReturnPlaceholderData; + } + + void NotifyCaptureStateChange() { + if (mPendingThrottledCapture) { + return; + } + + if (!mOwningElement) { + return; + } + + Watchable<FrameCaptureState>* captureState = + mOwningElement->GetFrameCaptureState(); + if (!captureState) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: No capture state"_ns); + return; + } + + if (captureState->Ref() == FrameCaptureState::CLEAN) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: CLEAN"_ns); + return; + } + + if (!mRefreshDriver) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: no refresh driver"_ns); + return; + } + + if (!mRefreshDriver->IsThrottled()) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: not throttled"_ns); + return; + } + + TimeStamp now = TimeStamp::Now(); + TimeStamp next = + mLastCaptureTime.IsNull() + ? now + : mLastCaptureTime + TimeDuration::FromMilliseconds( + nsRefreshDriver::DefaultInterval()); + if (mLastCaptureTime.IsNull() || next <= now) { + AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "CaptureFrame direct while throttled"_ns); + CaptureFrame(now); + return; + } + + nsCString str; + if (profiler_thread_is_being_profiled_for_markers()) { + str.AppendPrintf("Delaying CaptureFrame by %.2fms", + (next - now).ToMilliseconds()); + } + AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, str); + + mPendingThrottledCapture = true; + AbstractThread::MainThread()->DelayedDispatch( + NS_NewRunnableFunction( + __func__, + [this, self = RefPtr<RequestedFrameRefreshObserver>(this), next] { + mPendingThrottledCapture = false; + AUTO_PROFILER_MARKER_TEXT( + "Canvas CaptureStream", MEDIA_RT, {}, + "CaptureFrame after delay while throttled"_ns); + CaptureFrame(next); + }), + // next >= now, so this is a guard for (next - now) flooring to 0. + std::max<uint32_t>( + 1, static_cast<uint32_t>((next - now).ToMilliseconds()))); + } + + void WillRefresh(TimeStamp aTime) override { + AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "CaptureFrame by refresh driver"_ns); + + CaptureFrame(aTime); + } + + void CaptureFrame(TimeStamp aTime) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mOwningElement) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: no owning element"_ns); + return; + } + + if (mOwningElement->IsWriteOnly()) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: write only"_ns); + return; + } + + if (auto* captureStateWatchable = mOwningElement->GetFrameCaptureState(); + captureStateWatchable && + *captureStateWatchable == FrameCaptureState::CLEAN) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: CLEAN"_ns); + return; + } + + // Mark the context already now, since if the frame capture state is DIRTY + // and we catch an early return below (not marking it CLEAN), the next draw + // will not trigger a capture state change from the + // Watchable<FrameCaptureState>. + mOwningElement->MarkContextCleanForFrameCapture(); + + mOwningElement->ProcessDestroyedFrameListeners(); + + if (!mOwningElement->IsFrameCaptureRequested(aTime)) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: no capture requested"_ns); + return; + } + + RefPtr<SourceSurface> snapshot; + { + AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "GetSnapshot"_ns); + snapshot = mOwningElement->GetSurfaceSnapshot(nullptr); + if (!snapshot) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: snapshot failed"_ns); + return; + } + } + + RefPtr<DataSourceSurface> copy; + { + AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "CopySurface"_ns); + copy = CopySurface(snapshot, mReturnPlaceholderData); + if (!copy) { + PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, + "Abort: copy failed"_ns); + return; + } + } + + nsCString str; + if (profiler_thread_is_being_profiled_for_markers()) { + TimeDuration sinceLast = + aTime - (mLastCaptureTime.IsNull() ? aTime : mLastCaptureTime); + str.AppendPrintf("Forwarding captured frame %.2fms after last", + sinceLast.ToMilliseconds()); + } + AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, str); + + if (!mLastCaptureTime.IsNull() && aTime <= mLastCaptureTime) { + aTime = mLastCaptureTime + TimeDuration::FromMilliseconds(1); + } + mLastCaptureTime = aTime; + + mOwningElement->SetFrameCapture(copy.forget(), aTime); + } + + void DetachFromRefreshDriver() { + MOZ_ASSERT(mOwningElement); + MOZ_ASSERT(mRefreshDriver); + + Unregister(); + mRefreshDriver = nullptr; + mWatchManager.Shutdown(); + } + + bool IsRegisteredAndWatching() { return mRegistered && mWatching; } + + void Register() { + if (!mRegistered) { + MOZ_ASSERT(mRefreshDriver); + if (mRefreshDriver) { + mRefreshDriver->AddRefreshObserver(this, FlushType::Display, + "Canvas frame capture listeners"); + mRegistered = true; + } + } + + if (mWatching) { + return; + } + + if (!mOwningElement) { + return; + } + + if (Watchable<FrameCaptureState>* captureState = + mOwningElement->GetFrameCaptureState()) { + mWatchManager.Watch( + *captureState, + &RequestedFrameRefreshObserver::NotifyCaptureStateChange); + mWatching = true; + } + } + + void Unregister() { + if (mRegistered) { + MOZ_ASSERT(mRefreshDriver); + if (mRefreshDriver) { + mRefreshDriver->RemoveRefreshObserver(this, FlushType::Display); + mRegistered = false; + } + } + + if (!mWatching) { + return; + } + + if (!mOwningElement) { + return; + } + + if (Watchable<FrameCaptureState>* captureState = + mOwningElement->GetFrameCaptureState()) { + mWatchManager.Unwatch( + *captureState, + &RequestedFrameRefreshObserver::NotifyCaptureStateChange); + mWatching = false; + } + } + + private: + virtual ~RequestedFrameRefreshObserver() { + MOZ_ASSERT(!mRefreshDriver); + MOZ_ASSERT(!mRegistered); + MOZ_ASSERT(!mWatching); + } + + bool mRegistered; + bool mWatching; + bool mReturnPlaceholderData; + const WeakPtr<HTMLCanvasElement> mOwningElement; + RefPtr<nsRefreshDriver> mRefreshDriver; + WatchManager<RequestedFrameRefreshObserver> mWatchManager; + TimeStamp mLastCaptureTime; + bool mPendingThrottledCapture; +}; + +// --------------------------------------------------------------------------- + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HTMLCanvasPrintState, mCanvas, mContext, + mCallback) + +HTMLCanvasPrintState::HTMLCanvasPrintState( + HTMLCanvasElement* aCanvas, nsICanvasRenderingContextInternal* aContext, + nsITimerCallback* aCallback) + : mIsDone(false), + mPendingNotify(false), + mCanvas(aCanvas), + mContext(aContext), + mCallback(aCallback) {} + +HTMLCanvasPrintState::~HTMLCanvasPrintState() = default; + +/* virtual */ +JSObject* HTMLCanvasPrintState::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MozCanvasPrintState_Binding::Wrap(aCx, this, aGivenProto); +} + +nsISupports* HTMLCanvasPrintState::Context() const { return mContext; } + +void HTMLCanvasPrintState::Done() { + if (!mPendingNotify && !mIsDone) { + // The canvas needs to be invalidated for printing reftests on linux to + // work. + if (mCanvas) { + mCanvas->InvalidateCanvas(); + } + RefPtr<nsRunnableMethod<HTMLCanvasPrintState>> doneEvent = + NewRunnableMethod("dom::HTMLCanvasPrintState::NotifyDone", this, + &HTMLCanvasPrintState::NotifyDone); + if (NS_SUCCEEDED(NS_DispatchToCurrentThread(doneEvent))) { + mPendingNotify = true; + } + } +} + +void HTMLCanvasPrintState::NotifyDone() { + mIsDone = true; + mPendingNotify = false; + if (mCallback) { + mCallback->Notify(nullptr); + } +} + +// --------------------------------------------------------------------------- + +HTMLCanvasElementObserver::HTMLCanvasElementObserver( + HTMLCanvasElement* aElement) + : mElement(aElement) { + RegisterObserverEvents(); +} + +HTMLCanvasElementObserver::~HTMLCanvasElementObserver() { Destroy(); } + +void HTMLCanvasElementObserver::Destroy() { + UnregisterObserverEvents(); + mElement = nullptr; +} + +void HTMLCanvasElementObserver::RegisterObserverEvents() { + if (!mElement) { + return; + } + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + MOZ_ASSERT(observerService); + + if (observerService) { + observerService->AddObserver(this, "memory-pressure", false); + observerService->AddObserver(this, "canvas-device-reset", false); + } +} + +void HTMLCanvasElementObserver::UnregisterObserverEvents() { + if (!mElement) { + return; + } + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + // Do not assert on observerService here. This might be triggered by + // the cycle collector at a late enough time, that XPCOM services are + // no longer available. See bug 1029504. + if (observerService) { + observerService->RemoveObserver(this, "memory-pressure"); + observerService->RemoveObserver(this, "canvas-device-reset"); + } +} + +NS_IMETHODIMP +HTMLCanvasElementObserver::Observe(nsISupports*, const char* aTopic, + const char16_t*) { + if (!mElement) { + return NS_OK; + } + + if (strcmp(aTopic, "memory-pressure") == 0) { + mElement->OnMemoryPressure(); + } else if (strcmp(aTopic, "canvas-device-reset") == 0) { + mElement->OnDeviceReset(); + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(HTMLCanvasElementObserver, nsIObserver) + +// --------------------------------------------------------------------------- + +HTMLCanvasElement::HTMLCanvasElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mResetLayer(true), + mMaybeModified(false), + mWriteOnly(false) {} + +HTMLCanvasElement::~HTMLCanvasElement() { Destroy(); } + +void HTMLCanvasElement::Destroy() { + if (mOffscreenDisplay) { + mOffscreenDisplay->DestroyElement(); + mOffscreenDisplay = nullptr; + mImageContainer = nullptr; + } + + if (mContextObserver) { + mContextObserver->Destroy(); + mContextObserver = nullptr; + } + + ResetPrintCallback(); + if (mRequestedFrameRefreshObserver) { + mRequestedFrameRefreshObserver->DetachFromRefreshDriver(); + mRequestedFrameRefreshObserver = nullptr; + } +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLCanvasElement) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLCanvasElement, + nsGenericHTMLElement) + tmp->Destroy(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCurrentContext, mPrintCallback, mPrintState, + mOriginalCanvas, mOffscreenCanvas) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLCanvasElement, + nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCurrentContext, mPrintCallback, + mPrintState, mOriginalCanvas, + mOffscreenCanvas) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLCanvasElement, + nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLCanvasElement) + +/* virtual */ +JSObject* HTMLCanvasElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLCanvasElement_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<nsICanvasRenderingContextInternal> +HTMLCanvasElement::CreateContext(CanvasContextType aContextType) { + // Note that the compositor backend will be LAYERS_NONE if there is no widget. + RefPtr<nsICanvasRenderingContextInternal> ret = + CreateContextHelper(aContextType, GetCompositorBackendType()); + if (NS_WARN_IF(!ret)) { + return nullptr; + } + + // Add Observer for webgl canvas. + if (aContextType == CanvasContextType::WebGL1 || + aContextType == CanvasContextType::WebGL2 || + aContextType == CanvasContextType::Canvas2D) { + if (!mContextObserver) { + mContextObserver = new HTMLCanvasElementObserver(this); + } + } + + ret->SetCanvasElement(this); + return ret.forget(); +} + +nsresult HTMLCanvasElement::UpdateContext( + JSContext* aCx, JS::Handle<JS::Value> aNewContextOptions, + ErrorResult& aRvForDictionaryInit) { + nsresult rv = CanvasRenderingContextHelper::UpdateContext( + aCx, aNewContextOptions, aRvForDictionaryInit); + + if (NS_FAILED(rv)) { + return rv; + } + + // If we have a mRequestedFrameRefreshObserver that wasn't fully registered, + // retry that now. + if (mRequestedFrameRefreshObserver.get() && + !mRequestedFrameRefreshObserver->IsRegisteredAndWatching()) { + mRequestedFrameRefreshObserver->Register(); + } + + return NS_OK; +} + +nsIntSize HTMLCanvasElement::GetWidthHeight() { + nsIntSize size(DEFAULT_CANVAS_WIDTH, DEFAULT_CANVAS_HEIGHT); + const nsAttrValue* value; + + if ((value = GetParsedAttr(nsGkAtoms::width)) && + value->Type() == nsAttrValue::eInteger) { + size.width = value->GetIntegerValue(); + } + + if ((value = GetParsedAttr(nsGkAtoms::height)) && + value->Type() == nsAttrValue::eInteger) { + size.height = value->GetIntegerValue(); + } + + MOZ_ASSERT(size.width >= 0 && size.height >= 0, + "we should've required <canvas> width/height attrs to be " + "unsigned (non-negative) values"); + + return size; +} + +void HTMLCanvasElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, aNotify); + + return nsGenericHTMLElement::AfterSetAttr( + aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLCanvasElement::OnAttrSetButNotChanged( + int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, aNotify); + + return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName, + aValue, aNotify); +} + +void HTMLCanvasElement::AfterMaybeChangeAttr(int32_t aNamespaceID, + nsAtom* aName, bool aNotify) { + if (mCurrentContext && aNamespaceID == kNameSpaceID_None && + (aName == nsGkAtoms::width || aName == nsGkAtoms::height || + aName == nsGkAtoms::moz_opaque)) { + ErrorResult dummy; + UpdateContext(nullptr, JS::NullHandleValue, dummy); + } +} + +void HTMLCanvasElement::HandlePrintCallback(nsPresContext* aPresContext) { + // Only call the print callback here if 1) we're in a print testing mode or + // print preview mode, 2) the canvas has a print callback and 3) the callback + // hasn't already been called. For real printing the callback is handled in + // nsPageSequenceFrame::PrePrintNextSheet. + if ((aPresContext->Type() == nsPresContext::eContext_PageLayout || + aPresContext->Type() == nsPresContext::eContext_PrintPreview) && + !mPrintState && GetMozPrintCallback()) { + DispatchPrintCallback(nullptr); + } +} + +nsresult HTMLCanvasElement::DispatchPrintCallback(nsITimerCallback* aCallback) { + // For print reftests the context may not be initialized yet, so get a context + // so mCurrentContext is set. + if (!mCurrentContext) { + nsresult rv; + nsCOMPtr<nsISupports> context; + rv = GetContext(u"2d"_ns, getter_AddRefs(context)); + NS_ENSURE_SUCCESS(rv, rv); + } + mPrintState = new HTMLCanvasPrintState(this, mCurrentContext, aCallback); + + RefPtr<nsRunnableMethod<HTMLCanvasElement>> renderEvent = + NewRunnableMethod("dom::HTMLCanvasElement::CallPrintCallback", this, + &HTMLCanvasElement::CallPrintCallback); + return OwnerDoc()->Dispatch(renderEvent.forget()); +} + +void HTMLCanvasElement::CallPrintCallback() { + AUTO_PROFILER_MARKER_TEXT("HTMLCanvasElement Printing", LAYOUT_Printing, {}, + "HTMLCanvasElement::CallPrintCallback"_ns); + if (!mPrintState) { + // `mPrintState` might have been destroyed by cancelling the previous + // printing (especially the canvas frame destruction) during processing + // event loops in the printing. + return; + } + RefPtr<PrintCallback> callback = GetMozPrintCallback(); + RefPtr<HTMLCanvasPrintState> state = mPrintState; + callback->Call(*state); +} + +void HTMLCanvasElement::ResetPrintCallback() { + if (mPrintState) { + mPrintState = nullptr; + } +} + +bool HTMLCanvasElement::IsPrintCallbackDone() { + if (mPrintState == nullptr) { + return true; + } + + return mPrintState->mIsDone; +} + +HTMLCanvasElement* HTMLCanvasElement::GetOriginalCanvas() { + return mOriginalCanvas ? mOriginalCanvas.get() : this; +} + +nsresult HTMLCanvasElement::CopyInnerTo(HTMLCanvasElement* aDest) { + nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest); + NS_ENSURE_SUCCESS(rv, rv); + Document* destDoc = aDest->OwnerDoc(); + if (destDoc->IsStaticDocument()) { + // The Firefox print preview code can create a static clone from an + // existing static clone, so we may not be the original 'canvas' element. + aDest->mOriginalCanvas = GetOriginalCanvas(); + + if (GetMozPrintCallback()) { + destDoc->SetHasPrintCallbacks(); + } + + // We make sure that the canvas is not zero sized since that would cause + // the DrawImage call below to return an error, which would cause printing + // to fail. + nsIntSize size = GetWidthHeight(); + if (size.height > 0 && size.width > 0) { + nsCOMPtr<nsISupports> cxt; + aDest->GetContext(u"2d"_ns, getter_AddRefs(cxt)); + RefPtr<CanvasRenderingContext2D> context2d = + static_cast<CanvasRenderingContext2D*>(cxt.get()); + if (context2d && !mPrintCallback) { + CanvasImageSource source; + source.SetAsHTMLCanvasElement() = this; + ErrorResult err; + context2d->DrawImage(source, 0.0, 0.0, err); + rv = err.StealNSResult(); + } + } + } + return rv; +} + +nsChangeHint HTMLCanvasElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + nsChangeHint retval = + nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType); + if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height) { + retval |= NS_STYLE_HINT_REFLOW; + } else if (aAttribute == nsGkAtoms::moz_opaque) { + retval |= NS_STYLE_HINT_VISUAL; + } + return retval; +} + +void HTMLCanvasElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + MapAspectRatioInto(aBuilder); + MapCommonAttributesInto(aBuilder); +} + +nsMapRuleToAttributesFunc HTMLCanvasElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +NS_IMETHODIMP_(bool) +HTMLCanvasElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::width}, {nsGkAtoms::height}, {nullptr}}; + static const MappedAttributeEntry* const map[] = {attributes, + sCommonAttributeMap}; + return FindAttributeDependence(aAttribute, map); +} + +bool HTMLCanvasElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height)) { + return aResult.ParseNonNegativeIntValue(aValue); + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLCanvasElement::ToDataURL(JSContext* aCx, const nsAString& aType, + JS::Handle<JS::Value> aParams, + nsAString& aDataURL, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + // mWriteOnly check is redundant, but optimizes for the common case. + if (mWriteOnly && !CallerCanRead(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsresult rv = ToDataURLImpl(aCx, aSubjectPrincipal, aType, aParams, aDataURL); + if (NS_FAILED(rv)) { + aDataURL.AssignLiteral("data:,"); + } +} + +void HTMLCanvasElement::SetMozPrintCallback(PrintCallback* aCallback) { + mPrintCallback = aCallback; +} + +PrintCallback* HTMLCanvasElement::GetMozPrintCallback() const { + if (mOriginalCanvas) { + return mOriginalCanvas->GetMozPrintCallback(); + } + return mPrintCallback; +} + +static uint32_t sCaptureSourceId = 0; +class CanvasCaptureTrackSource : public MediaStreamTrackSource { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CanvasCaptureTrackSource, + MediaStreamTrackSource) + + CanvasCaptureTrackSource(nsIPrincipal* aPrincipal, + CanvasCaptureMediaStream* aCaptureStream) + : MediaStreamTrackSource( + aPrincipal, nsString(), + TrackingId(TrackingId::Source::Canvas, sCaptureSourceId++, + TrackingId::TrackAcrossProcesses::Yes)), + mCaptureStream(aCaptureStream) {} + + MediaSourceEnum GetMediaSource() const override { + return MediaSourceEnum::Other; + } + + bool HasAlpha() const override { + if (!mCaptureStream || !mCaptureStream->Canvas()) { + // In cycle-collection + return false; + } + return !mCaptureStream->Canvas()->GetIsOpaque(); + } + + void Stop() override { + if (!mCaptureStream) { + return; + } + + mCaptureStream->StopCapture(); + } + + void Disable() override {} + + void Enable() override {} + + private: + virtual ~CanvasCaptureTrackSource() = default; + + RefPtr<CanvasCaptureMediaStream> mCaptureStream; +}; + +NS_IMPL_ADDREF_INHERITED(CanvasCaptureTrackSource, MediaStreamTrackSource) +NS_IMPL_RELEASE_INHERITED(CanvasCaptureTrackSource, MediaStreamTrackSource) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanvasCaptureTrackSource) +NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource) +NS_IMPL_CYCLE_COLLECTION_INHERITED(CanvasCaptureTrackSource, + MediaStreamTrackSource, mCaptureStream) + +already_AddRefed<CanvasCaptureMediaStream> HTMLCanvasElement::CaptureStream( + const Optional<double>& aFrameRate, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + if (IsWriteOnly()) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return nullptr; + } + + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (!window) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + auto stream = MakeRefPtr<CanvasCaptureMediaStream>(window, this); + + nsCOMPtr<nsIPrincipal> principal = NodePrincipal(); + nsresult rv = stream->Init(aFrameRate, principal); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return nullptr; + } + + RefPtr<MediaStreamTrack> track = + new VideoStreamTrack(window, stream->GetSourceStream(), + new CanvasCaptureTrackSource(principal, stream)); + stream->AddTrackInternal(track); + + // Check site-specific permission and display prompt if appropriate. + // If no permission, arrange for the frame capture listener to return + // all-white, opaque image data. + bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed( + OwnerDoc(), nsContentUtils::GetCurrentJSContext(), aSubjectPrincipal); + + rv = RegisterFrameCaptureListener(stream->FrameCaptureListener(), + usePlaceholder); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return nullptr; + } + + return stream.forget(); +} + +nsresult HTMLCanvasElement::ExtractData(JSContext* aCx, + nsIPrincipal& aSubjectPrincipal, + nsAString& aType, + const nsAString& aOptions, + nsIInputStream** aStream) { + // Check site-specific permission and display prompt if appropriate. + // If no permission, return all-white, opaque image data. + bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed( + OwnerDoc(), aCx, aSubjectPrincipal); + + if (!usePlaceholder) { + auto size = GetWidthHeight(); + CanvasContextType type = GetCurrentContextType(); + CanvasFeatureUsage featureUsage = CanvasFeatureUsage::None; + if (type == CanvasContextType::Canvas2D) { + if (auto ctx = + static_cast<CanvasRenderingContext2D*>(GetCurrentContext())) { + featureUsage = ctx->FeatureUsage(); + } + } + + CanvasUsage usage(size, type, featureUsage); + OwnerDoc()->RecordCanvasUsage(usage); + } + + return ImageEncoder::ExtractData(aType, aOptions, GetSize(), usePlaceholder, + mCurrentContext, mOffscreenDisplay, aStream); +} + +nsresult HTMLCanvasElement::ToDataURLImpl(JSContext* aCx, + nsIPrincipal& aSubjectPrincipal, + const nsAString& aMimeType, + const JS::Value& aEncoderOptions, + nsAString& aDataURL) { + nsIntSize size = GetWidthHeight(); + if (size.height == 0 || size.width == 0) { + aDataURL = u"data:,"_ns; + return NS_OK; + } + + nsAutoString type; + nsContentUtils::ASCIIToLower(aMimeType, type); + + nsAutoString params; + bool usingCustomParseOptions; + nsresult rv = + ParseParams(aCx, type, aEncoderOptions, params, &usingCustomParseOptions); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIInputStream> stream; + rv = + ExtractData(aCx, aSubjectPrincipal, type, params, getter_AddRefs(stream)); + + // If there are unrecognized custom parse options, we should fall back to + // the default values for the encoder without any options at all. + if (rv == NS_ERROR_INVALID_ARG && usingCustomParseOptions) { + rv = ExtractData(aCx, aSubjectPrincipal, type, u""_ns, + getter_AddRefs(stream)); + } + + NS_ENSURE_SUCCESS(rv, rv); + + // build data URL string + aDataURL = u"data:"_ns + type + u";base64,"_ns; + + uint64_t count; + rv = stream->Available(&count); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(count <= UINT32_MAX, NS_ERROR_FILE_TOO_BIG); + + return Base64EncodeInputStream(stream, aDataURL, (uint32_t)count, + aDataURL.Length()); +} + +UniquePtr<uint8_t[]> HTMLCanvasElement::GetImageBuffer( + int32_t* aOutFormat, gfx::IntSize* aOutImageSize) { + if (mCurrentContext) { + return mCurrentContext->GetImageBuffer(aOutFormat, aOutImageSize); + } + if (mOffscreenDisplay) { + return mOffscreenDisplay->GetImageBuffer(aOutFormat, aOutImageSize); + } + return nullptr; +} + +void HTMLCanvasElement::ToBlob(JSContext* aCx, BlobCallback& aCallback, + const nsAString& aType, + JS::Handle<JS::Value> aParams, + nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv) { + // mWriteOnly check is redundant, but optimizes for the common case. + if (mWriteOnly && !CallerCanRead(aSubjectPrincipal)) { + aRv.Throw(NS_ERROR_DOM_SECURITY_ERR); + return; + } + + nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject(); + MOZ_ASSERT(global); + + nsIntSize elemSize = GetWidthHeight(); + if (elemSize.width == 0 || elemSize.height == 0) { + // According to spec, blob should return null if either its horizontal + // dimension or its vertical dimension is zero. See link below. + // https://html.spec.whatwg.org/multipage/scripting.html#dom-canvas-toblob + OwnerDoc()->Dispatch(NewRunnableMethod<Blob*, const char*>( + "dom::HTMLCanvasElement::ToBlob", &aCallback, + static_cast<void (BlobCallback::*)(Blob*, const char*)>( + &BlobCallback::Call), + nullptr, nullptr)); + return; + } + + // Check site-specific permission and display prompt if appropriate. + // If no permission, return all-white, opaque image data. + bool usePlaceholder = !CanvasUtils::IsImageExtractionAllowed( + OwnerDoc(), aCx, aSubjectPrincipal); + CanvasRenderingContextHelper::ToBlob(aCx, global, aCallback, aType, aParams, + usePlaceholder, aRv); +} + +OffscreenCanvas* HTMLCanvasElement::TransferControlToOffscreen( + ErrorResult& aRv) { + if (mCurrentContext || mOffscreenCanvas) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + MOZ_ASSERT(!mOffscreenDisplay); + + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + if (!win) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + LayersBackend backend = LayersBackend::LAYERS_NONE; + TextureType textureType = TextureType::Unknown; + nsIWidget* docWidget = nsContentUtils::WidgetForDocument(OwnerDoc()); + if (docWidget) { + WindowRenderer* renderer = docWidget->GetWindowRenderer(); + if (renderer) { + backend = renderer->GetCompositorBackendType(); + textureType = TexTypeForWebgl(renderer->AsKnowsCompositor()); + } + } + + nsIntSize sz = GetWidthHeight(); + mOffscreenDisplay = + MakeRefPtr<OffscreenCanvasDisplayHelper>(this, sz.width, sz.height); + mOffscreenCanvas = + new OffscreenCanvas(win->AsGlobal(), sz.width, sz.height, backend, + textureType, do_AddRef(mOffscreenDisplay)); + if (mWriteOnly) { + mOffscreenCanvas->SetWriteOnly(mExpandedReader); + } + + if (!mContextObserver) { + mContextObserver = new HTMLCanvasElementObserver(this); + } + + return mOffscreenCanvas; +} + +nsresult HTMLCanvasElement::GetContext(const nsAString& aContextId, + nsISupports** aContext) { + ErrorResult rv; + mMaybeModified = true; // For FirstContentfulPaint + *aContext = GetContext(nullptr, aContextId, JS::NullHandleValue, rv).take(); + return rv.StealNSResult(); +} + +already_AddRefed<nsISupports> HTMLCanvasElement::GetContext( + JSContext* aCx, const nsAString& aContextId, + JS::Handle<JS::Value> aContextOptions, ErrorResult& aRv) { + if (mOffscreenCanvas) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + mMaybeModified = true; // For FirstContentfulPaint + return CanvasRenderingContextHelper::GetOrCreateContext( + aCx, aContextId, + aContextOptions.isObject() ? aContextOptions : JS::NullHandleValue, aRv); +} + +nsIntSize HTMLCanvasElement::GetSize() { return GetWidthHeight(); } + +bool HTMLCanvasElement::IsWriteOnly() const { return mWriteOnly; } + +void HTMLCanvasElement::SetWriteOnly( + nsIPrincipal* aExpandedReader /* = nullptr */) { + mExpandedReader = aExpandedReader; + mWriteOnly = true; + if (mOffscreenCanvas) { + mOffscreenCanvas->SetWriteOnly(aExpandedReader); + } +} + +bool HTMLCanvasElement::CallerCanRead(nsIPrincipal& aPrincipal) const { + if (!mWriteOnly) { + return true; + } + + // If mExpandedReader is set, this canvas was tainted only by + // mExpandedReader's resources. So allow reading if the subject + // principal subsumes mExpandedReader. + if (mExpandedReader && aPrincipal.Subsumes(mExpandedReader)) { + return true; + } + + return nsContentUtils::PrincipalHasPermission(aPrincipal, + nsGkAtoms::all_urlsPermission); +} + +void HTMLCanvasElement::SetWidth(uint32_t aWidth, ErrorResult& aRv) { + if (mOffscreenCanvas) { + aRv.ThrowInvalidStateError( + "Cannot set width of placeholder canvas transferred to " + "OffscreenCanvas."); + return; + } + + SetUnsignedIntAttr(nsGkAtoms::width, aWidth, DEFAULT_CANVAS_WIDTH, aRv); +} + +void HTMLCanvasElement::SetHeight(uint32_t aHeight, ErrorResult& aRv) { + if (mOffscreenCanvas) { + aRv.ThrowInvalidStateError( + "Cannot set height of placeholder canvas transferred to " + "OffscreenCanvas."); + return; + } + + SetUnsignedIntAttr(nsGkAtoms::height, aHeight, DEFAULT_CANVAS_HEIGHT, aRv); +} + +void HTMLCanvasElement::SetSize(const nsIntSize& aSize, ErrorResult& aRv) { + if (mOffscreenCanvas) { + aRv.ThrowInvalidStateError( + "Cannot set width of placeholder canvas transferred to " + "OffscreenCanvas."); + return; + } + + if (NS_WARN_IF(aSize.IsEmpty())) { + aRv.ThrowRangeError("Canvas size is empty, must be non-empty."); + return; + } + + SetUnsignedIntAttr(nsGkAtoms::width, aSize.width, DEFAULT_CANVAS_WIDTH, aRv); + MOZ_ASSERT(!aRv.Failed()); + SetUnsignedIntAttr(nsGkAtoms::height, aSize.height, DEFAULT_CANVAS_HEIGHT, + aRv); + MOZ_ASSERT(!aRv.Failed()); +} + +void HTMLCanvasElement::FlushOffscreenCanvas() { + if (mOffscreenDisplay) { + mOffscreenDisplay->FlushForDisplay(); + } +} + +void HTMLCanvasElement::InvalidateCanvasPlaceholder(uint32_t aWidth, + uint32_t aHeight) { + ErrorResult rv; + SetUnsignedIntAttr(nsGkAtoms::width, aWidth, DEFAULT_CANVAS_WIDTH, rv); + MOZ_ASSERT(!rv.Failed()); + SetUnsignedIntAttr(nsGkAtoms::height, aHeight, DEFAULT_CANVAS_HEIGHT, rv); + MOZ_ASSERT(!rv.Failed()); +} + +void HTMLCanvasElement::InvalidateCanvasContent(const gfx::Rect* damageRect) { + // Cache the current ImageContainer to avoid contention on the mutex. + if (mOffscreenDisplay) { + mImageContainer = mOffscreenDisplay->GetImageContainer(); + } + + // We don't need to flush anything here; if there's no frame or if + // we plan to reframe we don't need to invalidate it anyway. + nsIFrame* frame = GetPrimaryFrame(); + if (!frame) return; + + // When using layers-free WebRender, we cannot invalidate the layer (because + // there isn't one). Instead, we mark the CanvasRenderer dirty and scheduling + // an empty transaction which is effectively equivalent. + CanvasRenderer* renderer = nullptr; + const auto key = static_cast<uint32_t>(DisplayItemType::TYPE_CANVAS); + RefPtr<WebRenderCanvasData> data = + GetWebRenderUserData<WebRenderCanvasData>(frame, key); + if (data) { + renderer = data->GetCanvasRenderer(); + } + + if (renderer) { + renderer->SetDirty(); + frame->SchedulePaint(nsIFrame::PAINT_COMPOSITE_ONLY); + } else { + if (damageRect) { + nsIntSize size = GetWidthHeight(); + if (size.width != 0 && size.height != 0) { + gfx::IntRect invalRect = gfx::IntRect::Truncate(*damageRect); + frame->InvalidateLayer(DisplayItemType::TYPE_CANVAS, &invalRect); + } + } else { + frame->InvalidateLayer(DisplayItemType::TYPE_CANVAS); + } + + // This path is taken in two situations: + // 1) WebRender is enabled and has not yet processed a display list. + // 2) WebRender is disabled and layer invalidation failed. + // In both cases, schedule a full paint to properly update canvas. + frame->SchedulePaint(nsIFrame::PAINT_DEFAULT, false); + } + + /* + * Treat canvas invalidations as animation activity for JS. Frequently + * invalidating a canvas will feed into heuristics and cause JIT code to be + * kept around longer, for smoother animations. + */ + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + + if (win) { + if (JSObject* obj = win->AsGlobal()->GetGlobalJSObject()) { + js::NotifyAnimationActivity(obj); + } + } +} + +void HTMLCanvasElement::InvalidateCanvas() { + // We don't need to flush anything here; if there's no frame or if + // we plan to reframe we don't need to invalidate it anyway. + nsIFrame* frame = GetPrimaryFrame(); + if (!frame) return; + + frame->InvalidateFrame(); +} + +bool HTMLCanvasElement::GetIsOpaque() { + if (mCurrentContext) { + return mCurrentContext->GetIsOpaque(); + } + + return GetOpaqueAttr(); +} + +bool HTMLCanvasElement::GetOpaqueAttr() { + return HasAttr(nsGkAtoms::moz_opaque); +} + +CanvasContextType HTMLCanvasElement::GetCurrentContextType() { + if (mCurrentContextType == CanvasContextType::NoContext && + mOffscreenDisplay) { + mCurrentContextType = mOffscreenDisplay->GetContextType(); + } + return mCurrentContextType; +} + +already_AddRefed<Image> HTMLCanvasElement::GetAsImage() { + if (mOffscreenDisplay) { + return mOffscreenDisplay->GetAsImage(); + } + + if (mCurrentContext) { + return mCurrentContext->GetAsImage(); + } + + return nullptr; +} + +bool HTMLCanvasElement::UpdateWebRenderCanvasData( + nsDisplayListBuilder* aBuilder, WebRenderCanvasData* aCanvasData) { + MOZ_ASSERT(!mOffscreenDisplay); + + if (mCurrentContext) { + return mCurrentContext->UpdateWebRenderCanvasData(aBuilder, aCanvasData); + } + + // Clear CanvasRenderer of WebRenderCanvasData + aCanvasData->ClearCanvasRenderer(); + return false; +} + +bool HTMLCanvasElement::InitializeCanvasRenderer(nsDisplayListBuilder* aBuilder, + CanvasRenderer* aRenderer) { + MOZ_ASSERT(!mOffscreenDisplay); + + if (mCurrentContext) { + return mCurrentContext->InitializeCanvasRenderer(aBuilder, aRenderer); + } + + return false; +} + +void HTMLCanvasElement::MarkContextClean() { + if (!mCurrentContext) return; + + mCurrentContext->MarkContextClean(); +} + +void HTMLCanvasElement::MarkContextCleanForFrameCapture() { + if (!mCurrentContext) return; + + mCurrentContext->MarkContextCleanForFrameCapture(); +} + +Watchable<FrameCaptureState>* HTMLCanvasElement::GetFrameCaptureState() { + if (!mCurrentContext) { + return nullptr; + } + return mCurrentContext->GetFrameCaptureState(); +} + +nsresult HTMLCanvasElement::RegisterFrameCaptureListener( + FrameCaptureListener* aListener, bool aReturnPlaceholderData) { + WeakPtr<FrameCaptureListener> listener = aListener; + + if (mRequestedFrameListeners.Contains(listener)) { + return NS_OK; + } + + if (!mRequestedFrameRefreshObserver) { + Document* doc = OwnerDoc(); + if (!doc) { + return NS_ERROR_FAILURE; + } + + PresShell* shell = nsContentUtils::FindPresShellForDocument(doc); + if (!shell) { + return NS_ERROR_FAILURE; + } + + nsPresContext* context = shell->GetPresContext(); + if (!context) { + return NS_ERROR_FAILURE; + } + + context = context->GetRootPresContext(); + if (!context) { + return NS_ERROR_FAILURE; + } + + nsRefreshDriver* driver = context->RefreshDriver(); + if (!driver) { + return NS_ERROR_FAILURE; + } + + mRequestedFrameRefreshObserver = + new RequestedFrameRefreshObserver(this, driver, aReturnPlaceholderData); + } else { + mRequestedFrameRefreshObserver->SetReturnPlaceholderData( + aReturnPlaceholderData); + } + + mRequestedFrameListeners.AppendElement(listener); + mRequestedFrameRefreshObserver->Register(); + return NS_OK; +} + +bool HTMLCanvasElement::IsFrameCaptureRequested(const TimeStamp& aTime) const { + for (WeakPtr<FrameCaptureListener> listener : mRequestedFrameListeners) { + if (!listener) { + continue; + } + + if (listener->FrameCaptureRequested(aTime)) { + return true; + } + } + return false; +} + +void HTMLCanvasElement::ProcessDestroyedFrameListeners() { + // Remove destroyed listeners from the list. + mRequestedFrameListeners.RemoveElementsBy( + [](const auto& weakListener) { return !weakListener; }); + + if (mRequestedFrameListeners.IsEmpty()) { + mRequestedFrameRefreshObserver->Unregister(); + } +} + +void HTMLCanvasElement::SetFrameCapture( + already_AddRefed<SourceSurface> aSurface, const TimeStamp& aTime) { + RefPtr<SourceSurface> surface = aSurface; + RefPtr<SourceSurfaceImage> image = + new SourceSurfaceImage(surface->GetSize(), surface); + + for (WeakPtr<FrameCaptureListener> listener : mRequestedFrameListeners) { + if (!listener) { + continue; + } + + RefPtr<Image> imageRefCopy = image.get(); + listener->NewFrame(imageRefCopy.forget(), aTime); + } +} + +already_AddRefed<SourceSurface> HTMLCanvasElement::GetSurfaceSnapshot( + gfxAlphaType* const aOutAlphaType, DrawTarget* aTarget) { + if (mCurrentContext) { + return mCurrentContext->GetOptimizedSnapshot(aTarget, aOutAlphaType); + } else if (mOffscreenDisplay) { + return mOffscreenDisplay->GetSurfaceSnapshot(); + } + return nullptr; +} + +layers::LayersBackend HTMLCanvasElement::GetCompositorBackendType() const { + nsIWidget* docWidget = nsContentUtils::WidgetForDocument(OwnerDoc()); + if (docWidget) { + WindowRenderer* renderer = docWidget->GetWindowRenderer(); + if (renderer) { + return renderer->GetCompositorBackendType(); + } + } + + return LayersBackend::LAYERS_NONE; +} + +void HTMLCanvasElement::OnMemoryPressure() { + // FIXME(aosmond): We need to implement memory pressure handling for + // OffscreenCanvas when it is on worker threads. See bug 1746260. + + if (mCurrentContext) { + mCurrentContext->OnMemoryPressure(); + } +} + +void HTMLCanvasElement::OnDeviceReset() { + if (!mOffscreenCanvas && mCurrentContext) { + mCurrentContext->ResetBitmap(); + } +} + +ClientWebGLContext* HTMLCanvasElement::GetWebGLContext() { + if (GetCurrentContextType() != CanvasContextType::WebGL1 && + GetCurrentContextType() != CanvasContextType::WebGL2) { + return nullptr; + } + + return static_cast<ClientWebGLContext*>(GetCurrentContext()); +} + +webgpu::CanvasContext* HTMLCanvasElement::GetWebGPUContext() { + if (GetCurrentContextType() != CanvasContextType::WebGPU) { + return nullptr; + } + + return static_cast<webgpu::CanvasContext*>(GetCurrentContext()); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLCanvasElement.h b/dom/html/HTMLCanvasElement.h new file mode 100644 index 0000000000..586a43fedc --- /dev/null +++ b/dom/html/HTMLCanvasElement.h @@ -0,0 +1,447 @@ +/* -*- 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/. */ +#if !defined(mozilla_dom_HTMLCanvasElement_h) +# define mozilla_dom_HTMLCanvasElement_h + +# include "mozilla/Attributes.h" +# include "mozilla/StateWatching.h" +# include "mozilla/WeakPtr.h" +# include "nsIDOMEventListener.h" +# include "nsIObserver.h" +# include "nsGenericHTMLElement.h" +# include "nsGkAtoms.h" +# include "nsSize.h" +# include "nsError.h" + +# include "mozilla/dom/CanvasRenderingContextHelper.h" +# include "mozilla/gfx/Rect.h" +# include "mozilla/layers/LayersTypes.h" + +class nsICanvasRenderingContextInternal; +class nsIInputStream; +class nsITimerCallback; +enum class gfxAlphaType; +enum class FrameCaptureState : uint8_t; + +namespace mozilla { + +class nsDisplayListBuilder; +class ClientWebGLContext; + +namespace layers { +class CanvasRenderer; +class Image; +class ImageContainer; +class Layer; +class LayerManager; +class OOPCanvasRenderer; +class SharedSurfaceTextureClient; +class WebRenderCanvasData; +} // namespace layers +namespace gfx { +class DrawTarget; +class SourceSurface; +class VRLayerChild; +} // namespace gfx +namespace webgpu { +class CanvasContext; +} // namespace webgpu + +namespace dom { +class BlobCallback; +class CanvasCaptureMediaStream; +class File; +class HTMLCanvasPrintState; +class OffscreenCanvas; +class OffscreenCanvasDisplayHelper; +class PrintCallback; +class PWebGLChild; +class RequestedFrameRefreshObserver; + +// Listen visibilitychange and memory-pressure event and inform +// context when event is fired. +class HTMLCanvasElementObserver final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + explicit HTMLCanvasElementObserver(HTMLCanvasElement* aElement); + void Destroy(); + + void RegisterObserverEvents(); + void UnregisterObserverEvents(); + + private: + ~HTMLCanvasElementObserver(); + + HTMLCanvasElement* mElement; +}; + +/* + * FrameCaptureListener is used by captureStream() as a way of getting video + * frames from the canvas. On a refresh driver tick after something has been + * drawn to the canvas since the last such tick, all registered + * FrameCaptureListeners that report true for FrameCaptureRequested() will be + * given a copy of the just-painted canvas. + * All FrameCaptureListeners get the same copy. + */ +class FrameCaptureListener : public SupportsWeakPtr { + public: + FrameCaptureListener() = default; + + /* + * Indicates to the canvas whether or not this listener has requested a frame. + */ + virtual bool FrameCaptureRequested(const TimeStamp& aTime) const = 0; + + /* + * Interface through which new video frames will be provided while + * `mFrameCaptureRequested` is `true`. + */ + virtual void NewFrame(already_AddRefed<layers::Image> aImage, + const TimeStamp& aTime) = 0; + + protected: + virtual ~FrameCaptureListener() = default; +}; + +class HTMLCanvasElement final : public nsGenericHTMLElement, + public CanvasRenderingContextHelper, + public SupportsWeakPtr { + enum { DEFAULT_CANVAS_WIDTH = 300, DEFAULT_CANVAS_HEIGHT = 150 }; + + typedef layers::CanvasRenderer CanvasRenderer; + typedef layers::LayerManager LayerManager; + typedef layers::WebRenderCanvasData WebRenderCanvasData; + + public: + explicit HTMLCanvasElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLCanvasElement, canvas) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + // CC + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLCanvasElement, + nsGenericHTMLElement) + + // WebIDL + uint32_t Height() { + return GetUnsignedIntAttr(nsGkAtoms::height, DEFAULT_CANVAS_HEIGHT); + } + uint32_t Width() { + return GetUnsignedIntAttr(nsGkAtoms::width, DEFAULT_CANVAS_WIDTH); + } + void SetHeight(uint32_t aHeight, ErrorResult& aRv); + void SetWidth(uint32_t aWidth, ErrorResult& aRv); + + already_AddRefed<nsISupports> GetContext( + JSContext* aCx, const nsAString& aContextId, + JS::Handle<JS::Value> aContextOptions, ErrorResult& aRv); + + void ToDataURL(JSContext* aCx, const nsAString& aType, + JS::Handle<JS::Value> aParams, nsAString& aDataURL, + nsIPrincipal& aSubjectPrincipal, ErrorResult& aRv); + + void ToBlob(JSContext* aCx, BlobCallback& aCallback, const nsAString& aType, + JS::Handle<JS::Value> aParams, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + OffscreenCanvas* TransferControlToOffscreen(ErrorResult& aRv); + + bool MozOpaque() const { return GetBoolAttr(nsGkAtoms::moz_opaque); } + void SetMozOpaque(bool aValue, ErrorResult& aRv) { + if (mOffscreenCanvas) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + SetHTMLBoolAttr(nsGkAtoms::moz_opaque, aValue, aRv); + } + PrintCallback* GetMozPrintCallback() const; + void SetMozPrintCallback(PrintCallback* aCallback); + + already_AddRefed<CanvasCaptureMediaStream> CaptureStream( + const Optional<double>& aFrameRate, nsIPrincipal& aSubjectPrincipal, + ErrorResult& aRv); + + /** + * Get the size in pixels of this canvas element + */ + nsIntSize GetSize(); + + /** + * Set the size in pixels of this canvas element. + */ + void SetSize(const nsIntSize& aSize, ErrorResult& aRv); + + /** + * Determine whether the canvas is write-only. + */ + bool IsWriteOnly() const; + + /** + * Force the canvas to be write-only, except for readers from + * a specific extension's content script expanded principal, if + * available. + */ + void SetWriteOnly(nsIPrincipal* aExpandedReader = nullptr); + + /** + * Notify the placeholder offscreen canvas of an updated size. + */ + void InvalidateCanvasPlaceholder(uint32_t aWidth, uint32_t aHeight); + + /** + * Notify that some canvas content has changed and the window may + * need to be updated. aDamageRect is in canvas coordinates. + */ + void InvalidateCanvasContent(const mozilla::gfx::Rect* aDamageRect); + /* + * Notify that we need to repaint the entire canvas, including updating of + * the layer tree. + */ + void InvalidateCanvas(); + + nsICanvasRenderingContextInternal* GetCurrentContext() { + return mCurrentContext; + } + + /* + * Returns true if the canvas context content is guaranteed to be opaque + * across its entire area. + */ + bool GetIsOpaque(); + virtual bool GetOpaqueAttr() override; + + /** + * Retrieve a snapshot of the internal surface, returning the alpha type if + * requested. An optional target may be supplied for which the snapshot will + * be optimized for, if possible. + */ + virtual already_AddRefed<gfx::SourceSurface> GetSurfaceSnapshot( + gfxAlphaType* aOutAlphaType = nullptr, + gfx::DrawTarget* aTarget = nullptr); + + /* + * Register a FrameCaptureListener with this canvas. + * The canvas hooks into the RefreshDriver while there are + * FrameCaptureListeners registered. + * The registered FrameCaptureListeners are stored as WeakPtrs, thus it's the + * caller's responsibility to keep them alive. Once a registered + * FrameCaptureListener is destroyed it will be automatically deregistered. + */ + nsresult RegisterFrameCaptureListener(FrameCaptureListener* aListener, + bool aReturnPlaceholderData); + + /* + * Returns true when there is at least one registered FrameCaptureListener + * that has requested a frame capture. + */ + bool IsFrameCaptureRequested(const TimeStamp& aTime) const; + + /* + * Processes destroyed FrameCaptureListeners and removes them if necessary. + * Should there be none left, the FrameRefreshObserver will be unregistered. + */ + void ProcessDestroyedFrameListeners(); + + /* + * Called by the RefreshDriver hook when a frame has been captured. + * Makes a copy of the provided surface and hands it to all + * FrameCaptureListeners having requested frame capture. + */ + void SetFrameCapture(already_AddRefed<gfx::SourceSurface> aSurface, + const TimeStamp& aTime); + + virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + nsresult CopyInnerTo(HTMLCanvasElement* aDest); + + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); + + /* + * Helpers called by various users of Canvas + */ + + already_AddRefed<layers::Image> GetAsImage(); + bool UpdateWebRenderCanvasData(nsDisplayListBuilder* aBuilder, + WebRenderCanvasData* aCanvasData); + bool InitializeCanvasRenderer(nsDisplayListBuilder* aBuilder, + CanvasRenderer* aRenderer); + + // Call this whenever we need future changes to the canvas + // to trigger fresh invalidation requests. This needs to be called + // whenever we render the canvas contents to the screen, or whenever we + // take a snapshot of the canvas that needs to be "live" (e.g. -moz-element). + void MarkContextClean(); + + // Call this after capturing a frame, so we can avoid unnecessary surface + // copies for future frames when no drawing has occurred. + void MarkContextCleanForFrameCapture(); + + // Returns non-null when the current context supports captureStream(). + // The FrameCaptureState gets set to DIRTY when something is drawn. + Watchable<FrameCaptureState>* GetFrameCaptureState(); + + nsresult GetContext(const nsAString& aContextId, nsISupports** aContext); + + layers::LayersBackend GetCompositorBackendType() const; + + void OnMemoryPressure(); + void OnDeviceReset(); + + already_AddRefed<layers::SharedSurfaceTextureClient> GetVRFrame(); + void ClearVRFrame(); + + bool MaybeModified() const { return mMaybeModified; }; + + protected: + virtual ~HTMLCanvasElement(); + void Destroy(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual nsIntSize GetWidthHeight() override; + + virtual already_AddRefed<nsICanvasRenderingContextInternal> CreateContext( + CanvasContextType aContextType) override; + + nsresult UpdateContext(JSContext* aCx, + JS::Handle<JS::Value> aNewContextOptions, + ErrorResult& aRvForDictionaryInit) override; + + nsresult ExtractData(JSContext* aCx, nsIPrincipal& aSubjectPrincipal, + nsAString& aType, const nsAString& aOptions, + nsIInputStream** aStream); + nsresult ToDataURLImpl(JSContext* aCx, nsIPrincipal& aSubjectPrincipal, + const nsAString& aMimeType, + const JS::Value& aEncoderOptions, nsAString& aDataURL); + + UniquePtr<uint8_t[]> GetImageBuffer(int32_t* aOutFormat, + gfx::IntSize* aOutImageSize) override; + + MOZ_CAN_RUN_SCRIPT void CallPrintCallback(); + + virtual void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) override; + virtual void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) override; + + public: + ClientWebGLContext* GetWebGLContext(); + webgpu::CanvasContext* GetWebGPUContext(); + + bool IsOffscreen() const { return !!mOffscreenCanvas; } + OffscreenCanvas* GetOffscreenCanvas() const { return mOffscreenCanvas; } + void FlushOffscreenCanvas(); + + layers::ImageContainer* GetImageContainer() const { return mImageContainer; } + + protected: + bool mResetLayer; + bool mMaybeModified; // we fetched the context, so we may have written to the + // canvas + RefPtr<HTMLCanvasElement> mOriginalCanvas; + RefPtr<PrintCallback> mPrintCallback; + RefPtr<HTMLCanvasPrintState> mPrintState; + nsTArray<WeakPtr<FrameCaptureListener>> mRequestedFrameListeners; + RefPtr<RequestedFrameRefreshObserver> mRequestedFrameRefreshObserver; + RefPtr<OffscreenCanvas> mOffscreenCanvas; + RefPtr<OffscreenCanvasDisplayHelper> mOffscreenDisplay; + RefPtr<layers::ImageContainer> mImageContainer; + RefPtr<HTMLCanvasElementObserver> mContextObserver; + + public: + // Record whether this canvas should be write-only or not. + // We set this when script paints an image from a different origin. + // We also transitively set it when script paints a canvas which + // is itself write-only. + bool mWriteOnly; + + // When this canvas is (only) tainted by an image from an extension + // content script, allow reads from the same extension afterwards. + RefPtr<nsIPrincipal> mExpandedReader; + + // Determines if the caller should be able to read the content. + bool CallerCanRead(nsIPrincipal& aPrincipal) const; + + bool IsPrintCallbackDone(); + + void HandlePrintCallback(nsPresContext*); + + nsresult DispatchPrintCallback(nsITimerCallback* aCallback); + + void ResetPrintCallback(); + + HTMLCanvasElement* GetOriginalCanvas(); + + CanvasContextType GetCurrentContextType(); + + private: + /** + * This function is called by AfterSetAttr and OnAttrSetButNotChanged. + * This function will be called by AfterSetAttr whether the attribute is being + * set or unset. + * + * @param aNamespaceID the namespace of the attr being set + * @param aName the localname of the attribute being set + * @param aNotify Whether we plan to notify document observers. + */ + void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify); +}; + +class HTMLCanvasPrintState final : public nsWrapperCache { + public: + HTMLCanvasPrintState(HTMLCanvasElement* aCanvas, + nsICanvasRenderingContextInternal* aContext, + nsITimerCallback* aCallback); + + nsISupports* Context() const; + + void Done(); + + void NotifyDone(); + + bool mIsDone; + + NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(HTMLCanvasPrintState) + NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(HTMLCanvasPrintState) + + virtual JSObject* WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) override; + + HTMLCanvasElement* GetParentObject() { return mCanvas; } + + private: + ~HTMLCanvasPrintState(); + bool mPendingNotify; + + protected: + RefPtr<HTMLCanvasElement> mCanvas; + nsCOMPtr<nsICanvasRenderingContextInternal> mContext; + nsCOMPtr<nsITimerCallback> mCallback; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_HTMLCanvasElement_h */ diff --git a/dom/html/HTMLDNSPrefetch.cpp b/dom/html/HTMLDNSPrefetch.cpp new file mode 100644 index 0000000000..a4043195fe --- /dev/null +++ b/dom/html/HTMLDNSPrefetch.cpp @@ -0,0 +1,647 @@ +/* -*- 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 "HTMLDNSPrefetch.h" + +#include "base/basictypes.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLLinkElement.h" +#include "mozilla/dom/HTMLAnchorElement.h" +#include "mozilla/net/NeckoCommon.h" +#include "mozilla/net/NeckoChild.h" +#include "mozilla/OriginAttributes.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "nsURLHelper.h" + +#include "nsCOMPtr.h" +#include "nsString.h" + +#include "nsNetUtil.h" +#include "nsNetCID.h" +#include "nsIProtocolHandler.h" + +#include "nsIDNSListener.h" +#include "nsIWebProgressListener.h" +#include "nsIWebProgress.h" +#include "nsIDNSRecord.h" +#include "nsIDNSService.h" +#include "nsICancelable.h" +#include "nsGkAtoms.h" +#include "mozilla/dom/Document.h" +#include "nsThreadUtils.h" +#include "nsITimer.h" +#include "nsIObserverService.h" + +#include "mozilla/Components.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_network.h" + +using namespace mozilla::net; + +namespace mozilla::dom { + +class NoOpDNSListener final : public nsIDNSListener { + // This class exists to give a safe callback no-op DNSListener + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIDNSLISTENER + + NoOpDNSListener() = default; + + private: + ~NoOpDNSListener() = default; +}; + +NS_IMPL_ISUPPORTS(NoOpDNSListener, nsIDNSListener) + +NS_IMETHODIMP +NoOpDNSListener::OnLookupComplete(nsICancelable* request, nsIDNSRecord* rec, + nsresult status) { + return NS_OK; +} + +// This is just a (size) optimization and could be avoided by storing the +// SupportsDNSPrefetch pointer of the element in the prefetch queue, but given +// we need this for GetURIForDNSPrefetch... +static SupportsDNSPrefetch& ToSupportsDNSPrefetch(Element& aElement) { + if (auto* link = HTMLLinkElement::FromNode(aElement)) { + return *link; + } + auto* anchor = HTMLAnchorElement::FromNode(aElement); + MOZ_DIAGNOSTIC_ASSERT(anchor); + return *anchor; +} + +nsIURI* SupportsDNSPrefetch::GetURIForDNSPrefetch(Element& aElement) { + MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == this); + if (auto* link = HTMLLinkElement::FromNode(aElement)) { + return link->GetURI(); + } + auto* anchor = HTMLAnchorElement::FromNode(aElement); + MOZ_DIAGNOSTIC_ASSERT(anchor); + return anchor->GetURI(); +} + +class DeferredDNSPrefetches final : public nsIWebProgressListener, + public nsSupportsWeakReference, + public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIOBSERVER + + DeferredDNSPrefetches(); + + void Activate(); + nsresult Add(nsIDNSService::DNSFlags flags, SupportsDNSPrefetch&, Element&); + + void RemoveUnboundLinks(); + + private: + ~DeferredDNSPrefetches(); + void Flush(); + + void SubmitQueue(); + void SubmitQueueEntry(Element&, nsIDNSService::DNSFlags aFlags); + + uint16_t mHead; + uint16_t mTail; + uint32_t mActiveLoaderCount; + + nsCOMPtr<nsITimer> mTimer; + bool mTimerArmed; + static void Tick(nsITimer* aTimer, void* aClosure); + + static const int sMaxDeferred = 512; // keep power of 2 for masking + static const int sMaxDeferredMask = (sMaxDeferred - 1); + + struct deferred_entry { + nsIDNSService::DNSFlags mFlags; + // SupportsDNSPrefetch clears this raw pointer in Destroyed(). + Element* mElement; + } mEntries[sMaxDeferred]; +}; + +static NS_DEFINE_CID(kDNSServiceCID, NS_DNSSERVICE_CID); +static bool sInitialized = false; +static nsIDNSService* sDNSService = nullptr; +static DeferredDNSPrefetches* sPrefetches = nullptr; +static NoOpDNSListener* sDNSListener = nullptr; + +nsresult HTMLDNSPrefetch::Initialize() { + if (sInitialized) { + NS_WARNING("Initialize() called twice"); + return NS_OK; + } + + sPrefetches = new DeferredDNSPrefetches(); + NS_ADDREF(sPrefetches); + + sDNSListener = new NoOpDNSListener(); + NS_ADDREF(sDNSListener); + + sPrefetches->Activate(); + + if (IsNeckoChild()) NeckoChild::InitNeckoChild(); + + sInitialized = true; + return NS_OK; +} + +nsresult HTMLDNSPrefetch::Shutdown() { + if (!sInitialized) { + NS_WARNING("Not Initialized"); + return NS_OK; + } + sInitialized = false; + NS_IF_RELEASE(sDNSService); + NS_IF_RELEASE(sPrefetches); + NS_IF_RELEASE(sDNSListener); + + return NS_OK; +} + +static bool EnsureDNSService() { + if (sDNSService) { + return true; + } + + NS_IF_RELEASE(sDNSService); + nsresult rv; + rv = CallGetService(kDNSServiceCID, &sDNSService); + if (NS_FAILED(rv)) { + return false; + } + + return !!sDNSService; +} + +bool HTMLDNSPrefetch::IsAllowed(Document* aDocument) { + // There is no need to do prefetch on non UI scenarios such as XMLHttpRequest. + return aDocument->IsDNSPrefetchAllowed() && aDocument->GetWindow(); +} + +static nsIDNSService::DNSFlags GetDNSFlagsFromElement(Element& aElement) { + nsIChannel* channel = aElement.OwnerDoc()->GetChannel(); + if (!channel) { + return nsIDNSService::RESOLVE_DEFAULT_FLAGS; + } + return nsIDNSService::GetFlagsFromTRRMode(channel->GetTRRMode()); +} + +nsIDNSService::DNSFlags HTMLDNSPrefetch::PriorityToDNSServiceFlags( + Priority aPriority) { + switch (aPriority) { + case Priority::Low: + return nsIDNSService::RESOLVE_PRIORITY_LOW; + case Priority::Medium: + return nsIDNSService::RESOLVE_PRIORITY_MEDIUM; + case Priority::High: + return nsIDNSService::RESOLVE_DEFAULT_FLAGS; + } + MOZ_ASSERT_UNREACHABLE("Unknown priority"); + return nsIDNSService::RESOLVE_DEFAULT_FLAGS; +} + +nsresult HTMLDNSPrefetch::Prefetch(SupportsDNSPrefetch& aSupports, + Element& aElement, Priority aPriority) { + MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == &aSupports); + if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) { + return NS_ERROR_NOT_AVAILABLE; + } + return sPrefetches->Add( + GetDNSFlagsFromElement(aElement) | PriorityToDNSServiceFlags(aPriority), + aSupports, aElement); +} + +nsresult HTMLDNSPrefetch::Prefetch( + const nsAString& hostname, bool isHttps, + const OriginAttributes& aPartitionedPrincipalOriginAttributes, + nsIDNSService::DNSFlags flags) { + if (IsNeckoChild()) { + // We need to check IsEmpty() because net_IsValidHostName() + // considers empty strings to be valid hostnames + if (!hostname.IsEmpty() && + net_IsValidHostName(NS_ConvertUTF16toUTF8(hostname))) { + // during shutdown gNeckoChild might be null + if (gNeckoChild) { + gNeckoChild->SendHTMLDNSPrefetch( + hostname, isHttps, aPartitionedPrincipalOriginAttributes, flags); + } + } + return NS_OK; + } + + if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) + return NS_ERROR_NOT_AVAILABLE; + + nsCOMPtr<nsICancelable> tmpOutstanding; + nsresult rv = sDNSService->AsyncResolveNative( + NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_DEFAULT, + flags | nsIDNSService::RESOLVE_SPECULATE, nullptr, sDNSListener, nullptr, + aPartitionedPrincipalOriginAttributes, getter_AddRefs(tmpOutstanding)); + if (NS_FAILED(rv)) { + return rv; + } + + if (StaticPrefs::network_dns_upgrade_with_https_rr() || + StaticPrefs::network_dns_use_https_rr_as_altsvc()) { + Unused << sDNSService->AsyncResolveNative( + NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_HTTPSSVC, + flags | nsIDNSService::RESOLVE_SPECULATE, nullptr, sDNSListener, + nullptr, aPartitionedPrincipalOriginAttributes, + getter_AddRefs(tmpOutstanding)); + } + + return NS_OK; +} + +nsresult HTMLDNSPrefetch::Prefetch( + const nsAString& hostname, bool isHttps, + const OriginAttributes& aPartitionedPrincipalOriginAttributes, + nsIRequest::TRRMode aMode, Priority aPriority) { + return Prefetch(hostname, isHttps, aPartitionedPrincipalOriginAttributes, + nsIDNSService::GetFlagsFromTRRMode(aMode) | + PriorityToDNSServiceFlags(aPriority)); +} + +nsresult HTMLDNSPrefetch::CancelPrefetch(SupportsDNSPrefetch& aSupports, + Element& aElement, Priority aPriority, + nsresult aReason) { + MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == &aSupports); + + if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsIDNSService::DNSFlags flags = + GetDNSFlagsFromElement(aElement) | PriorityToDNSServiceFlags(aPriority); + + nsIURI* uri = aSupports.GetURIForDNSPrefetch(aElement); + if (!uri) { + return NS_OK; + } + + nsAutoCString hostname; + uri->GetAsciiHost(hostname); + + nsAutoString protocol; + bool isHttps = uri->SchemeIs("https"); + + OriginAttributes oa; + StoragePrincipalHelper::GetOriginAttributesForNetworkState( + aElement.OwnerDoc(), oa); + + return CancelPrefetch(NS_ConvertUTF8toUTF16(hostname), isHttps, oa, flags, + aReason); +} + +nsresult HTMLDNSPrefetch::CancelPrefetch( + const nsAString& hostname, bool isHttps, + const OriginAttributes& aPartitionedPrincipalOriginAttributes, + nsIDNSService::DNSFlags flags, nsresult aReason) { + // Forward this request to Necko Parent if we're a child process + if (IsNeckoChild()) { + // We need to check IsEmpty() because net_IsValidHostName() + // considers empty strings to be valid hostnames + if (!hostname.IsEmpty() && + net_IsValidHostName(NS_ConvertUTF16toUTF8(hostname))) { + // during shutdown gNeckoChild might be null + if (gNeckoChild) { + gNeckoChild->SendCancelHTMLDNSPrefetch( + hostname, isHttps, aPartitionedPrincipalOriginAttributes, flags, + aReason); + } + } + return NS_OK; + } + + if (!(sInitialized && sPrefetches && sDNSListener) || !EnsureDNSService()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Forward cancellation to DNS service + nsresult rv = sDNSService->CancelAsyncResolveNative( + NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_DEFAULT, + flags | nsIDNSService::RESOLVE_SPECULATE, + nullptr, // AdditionalInfo + sDNSListener, aReason, aPartitionedPrincipalOriginAttributes); + + if (StaticPrefs::network_dns_upgrade_with_https_rr() || + StaticPrefs::network_dns_use_https_rr_as_altsvc()) { + Unused << sDNSService->CancelAsyncResolveNative( + NS_ConvertUTF16toUTF8(hostname), nsIDNSService::RESOLVE_TYPE_HTTPSSVC, + flags | nsIDNSService::RESOLVE_SPECULATE, + nullptr, // AdditionalInfo + sDNSListener, aReason, aPartitionedPrincipalOriginAttributes); + } + return rv; +} + +nsresult HTMLDNSPrefetch::CancelPrefetch( + const nsAString& hostname, bool isHttps, + const OriginAttributes& aPartitionedPrincipalOriginAttributes, + nsIRequest::TRRMode aTRRMode, Priority aPriority, nsresult aReason) { + return CancelPrefetch(hostname, isHttps, + aPartitionedPrincipalOriginAttributes, + nsIDNSService::GetFlagsFromTRRMode(aTRRMode) | + PriorityToDNSServiceFlags(aPriority), + aReason); +} + +void HTMLDNSPrefetch::ElementDestroyed(Element& aElement, + SupportsDNSPrefetch& aSupports) { + MOZ_ASSERT(&ToSupportsDNSPrefetch(aElement) == &aSupports); + MOZ_ASSERT(aSupports.IsInDNSPrefetch()); + if (sPrefetches) { + // Clean up all the possible links at once. + sPrefetches->RemoveUnboundLinks(); + } +} + +void SupportsDNSPrefetch::TryDNSPrefetch(Element& aOwner) { + MOZ_ASSERT(aOwner.IsInComposedDoc()); + if (HTMLDNSPrefetch::IsAllowed(aOwner.OwnerDoc())) { + HTMLDNSPrefetch::Prefetch(*this, aOwner, HTMLDNSPrefetch::Priority::Low); + } +} + +void SupportsDNSPrefetch::CancelDNSPrefetch(Element& aOwner) { + // If prefetch was deferred, clear flag and move on + if (mDNSPrefetchDeferred) { + mDNSPrefetchDeferred = false; + // Else if prefetch was requested, clear flag and send cancellation + } else if (mDNSPrefetchRequested) { + mDNSPrefetchRequested = false; + // Possible that hostname could have changed since binding, but since this + // covers common cases, most DNS prefetch requests will be canceled + HTMLDNSPrefetch::CancelPrefetch( + *this, aOwner, HTMLDNSPrefetch::Priority::Low, NS_ERROR_ABORT); + } +} + +DeferredDNSPrefetches::DeferredDNSPrefetches() + : mHead(0), mTail(0), mActiveLoaderCount(0), mTimerArmed(false) { + mTimer = NS_NewTimer(); +} + +DeferredDNSPrefetches::~DeferredDNSPrefetches() { + if (mTimerArmed) { + mTimerArmed = false; + mTimer->Cancel(); + } + + Flush(); +} + +NS_IMPL_ISUPPORTS(DeferredDNSPrefetches, nsIWebProgressListener, + nsISupportsWeakReference, nsIObserver) + +void DeferredDNSPrefetches::Flush() { + for (; mHead != mTail; mTail = (mTail + 1) & sMaxDeferredMask) { + Element* element = mEntries[mTail].mElement; + if (element) { + ToSupportsDNSPrefetch(*element).ClearIsInDNSPrefetch(); + } + mEntries[mTail].mElement = nullptr; + } +} + +nsresult DeferredDNSPrefetches::Add(nsIDNSService::DNSFlags flags, + SupportsDNSPrefetch& aSupports, + Element& aElement) { + // The FIFO has no lock, so it can only be accessed on main thread + NS_ASSERTION(NS_IsMainThread(), + "DeferredDNSPrefetches::Add must be on main thread"); + + aSupports.DNSPrefetchRequestDeferred(); + + if (((mHead + 1) & sMaxDeferredMask) == mTail) { + return NS_ERROR_DNS_LOOKUP_QUEUE_FULL; + } + + aSupports.SetIsInDNSPrefetch(); + mEntries[mHead].mFlags = flags; + mEntries[mHead].mElement = &aElement; + mHead = (mHead + 1) & sMaxDeferredMask; + + if (!mActiveLoaderCount && !mTimerArmed && mTimer) { + mTimerArmed = true; + mTimer->InitWithNamedFuncCallback( + Tick, this, 2000, nsITimer::TYPE_ONE_SHOT, + "HTMLDNSPrefetch::DeferredDNSPrefetches::Tick"); + } + + return NS_OK; +} + +void DeferredDNSPrefetches::SubmitQueue() { + NS_ASSERTION(NS_IsMainThread(), + "DeferredDNSPrefetches::SubmitQueue must be on main thread"); + if (!EnsureDNSService()) { + return; + } + + for (; mHead != mTail; mTail = (mTail + 1) & sMaxDeferredMask) { + Element* element = mEntries[mTail].mElement; + if (!element) { + continue; + } + SubmitQueueEntry(*element, mEntries[mTail].mFlags); + mEntries[mTail].mElement = nullptr; + } + + if (mTimerArmed) { + mTimerArmed = false; + mTimer->Cancel(); + } +} + +void DeferredDNSPrefetches::SubmitQueueEntry(Element& aElement, + nsIDNSService::DNSFlags aFlags) { + auto& supports = ToSupportsDNSPrefetch(aElement); + supports.ClearIsInDNSPrefetch(); + + // Only prefetch here if request was deferred and deferral not cancelled + if (!supports.IsDNSPrefetchRequestDeferred()) { + return; + } + + nsIURI* uri = supports.GetURIForDNSPrefetch(aElement); + if (!uri) { + return; + } + + nsAutoCString hostName; + uri->GetAsciiHost(hostName); + if (hostName.IsEmpty()) { + return; + } + + bool isLocalResource = false; + nsresult rv = NS_URIChainHasFlags( + uri, nsIProtocolHandler::URI_IS_LOCAL_RESOURCE, &isLocalResource); + if (NS_FAILED(rv) || isLocalResource) { + return; + } + + OriginAttributes oa; + StoragePrincipalHelper::GetOriginAttributesForNetworkState( + aElement.OwnerDoc(), oa); + + bool isHttps = uri->SchemeIs("https"); + + if (IsNeckoChild()) { + // during shutdown gNeckoChild might be null + if (gNeckoChild) { + gNeckoChild->SendHTMLDNSPrefetch(NS_ConvertUTF8toUTF16(hostName), isHttps, + oa, mEntries[mTail].mFlags); + } + } else { + nsCOMPtr<nsICancelable> tmpOutstanding; + + rv = sDNSService->AsyncResolveNative( + hostName, nsIDNSService::RESOLVE_TYPE_DEFAULT, + mEntries[mTail].mFlags | nsIDNSService::RESOLVE_SPECULATE, nullptr, + sDNSListener, nullptr, oa, getter_AddRefs(tmpOutstanding)); + if (NS_FAILED(rv)) { + return; + } + + // Fetch HTTPS RR if needed. + if (StaticPrefs::network_dns_upgrade_with_https_rr() || + StaticPrefs::network_dns_use_https_rr_as_altsvc()) { + sDNSService->AsyncResolveNative( + hostName, nsIDNSService::RESOLVE_TYPE_HTTPSSVC, + mEntries[mTail].mFlags | nsIDNSService::RESOLVE_SPECULATE, nullptr, + sDNSListener, nullptr, oa, getter_AddRefs(tmpOutstanding)); + } + } + + // Tell element that deferred prefetch was requested. + supports.DNSPrefetchRequestStarted(); +} + +void DeferredDNSPrefetches::Activate() { + // Register as an observer for the document loader + nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service(); + if (progress) + progress->AddProgressListener(this, nsIWebProgress::NOTIFY_STATE_DOCUMENT); + + // Register as an observer for xpcom shutdown events so we can drop any + // element refs + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) + observerService->AddObserver(this, "xpcom-shutdown", true); +} + +void DeferredDNSPrefetches::RemoveUnboundLinks() { + uint16_t tail = mTail; + while (mHead != tail) { + Element* element = mEntries[tail].mElement; + if (element && !element->IsInComposedDoc()) { + ToSupportsDNSPrefetch(*element).ClearIsInDNSPrefetch(); + mEntries[tail].mElement = nullptr; + } + tail = (tail + 1) & sMaxDeferredMask; + } +} + +// nsITimer related method + +void DeferredDNSPrefetches::Tick(nsITimer* aTimer, void* aClosure) { + auto* self = static_cast<DeferredDNSPrefetches*>(aClosure); + + NS_ASSERTION(NS_IsMainThread(), + "DeferredDNSPrefetches::Tick must be on main thread"); + NS_ASSERTION(self->mTimerArmed, "Timer is not armed"); + + self->mTimerArmed = false; + + // If the queue is not submitted here because there are outstanding pages + // being loaded, there is no need to rearm the timer as the queue will be + // submtited when those loads complete. + if (!self->mActiveLoaderCount) { + self->SubmitQueue(); + } +} + +//////////// nsIWebProgressListener methods + +NS_IMETHODIMP +DeferredDNSPrefetches::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t progressStateFlags, + nsresult aStatus) { + // The FIFO has no lock, so it can only be accessed on main thread + NS_ASSERTION(NS_IsMainThread(), + "DeferredDNSPrefetches::OnStateChange must be on main thread"); + + if (progressStateFlags & STATE_IS_DOCUMENT) { + if (progressStateFlags & STATE_STOP) { + // Initialization may have missed a STATE_START notification, so do + // not go negative + if (mActiveLoaderCount) mActiveLoaderCount--; + + if (!mActiveLoaderCount) { + SubmitQueue(); + } + } else if (progressStateFlags & STATE_START) + mActiveLoaderCount++; + } + + return NS_OK; +} + +NS_IMETHODIMP +DeferredDNSPrefetches::OnProgressChange(nsIWebProgress* aProgress, + nsIRequest* aRequest, + int32_t curSelfProgress, + int32_t maxSelfProgress, + int32_t curTotalProgress, + int32_t maxTotalProgress) { + return NS_OK; +} + +NS_IMETHODIMP +DeferredDNSPrefetches::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* location, + uint32_t aFlags) { + return NS_OK; +} + +NS_IMETHODIMP +DeferredDNSPrefetches::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + return NS_OK; +} + +NS_IMETHODIMP +DeferredDNSPrefetches::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aState) { + return NS_OK; +} + +NS_IMETHODIMP +DeferredDNSPrefetches::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + return NS_OK; +} + +//////////// nsIObserver method + +NS_IMETHODIMP +DeferredDNSPrefetches::Observe(nsISupports* subject, const char* topic, + const char16_t* data) { + if (!strcmp(topic, "xpcom-shutdown")) Flush(); + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLDNSPrefetch.h b/dom/html/HTMLDNSPrefetch.h new file mode 100644 index 0000000000..5820a6ecb2 --- /dev/null +++ b/dom/html/HTMLDNSPrefetch.h @@ -0,0 +1,147 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLDNSPrefetch_h___ +#define mozilla_dom_HTMLDNSPrefetch_h___ + +#include "nsCOMPtr.h" +#include "nsIRequest.h" +#include "nsString.h" +#include "nsIDNSService.h" + +class nsITimer; +class nsIURI; +namespace mozilla { + +class OriginAttributes; + +namespace net { +class NeckoParent; +} // namespace net + +namespace dom { +class Document; +class Element; + +class SupportsDNSPrefetch; + +class HTMLDNSPrefetch { + public: + // The required aDocument parameter is the context requesting the prefetch - + // under certain circumstances (e.g. headers, or security context) associated + // with the context the prefetch will not be performed. + static bool IsAllowed(Document* aDocument); + + static nsresult Initialize(); + static nsresult Shutdown(); + + // Call one of the Prefetch* methods to start the lookup. + // + // The URI versions will defer DNS lookup until pageload is + // complete, while the string versions submit the lookup to + // the DNS system immediately. The URI version is somewhat lighter + // weight, but its request is also more likely to be dropped due to a + // full queue and it may only be used from the main thread. + // + // If you are planning to use the methods with the OriginAttributes param, be + // sure that you pass a partitioned one. See StoragePrincipalHelper.h to know + // more. + + enum class Priority { + Low, + Medium, + High, + }; + static nsresult Prefetch(SupportsDNSPrefetch&, Element&, Priority); + static nsresult Prefetch( + const nsAString& host, bool isHttps, + const OriginAttributes& aPartitionedPrincipalOriginAttributes, + nsIRequest::TRRMode aTRRMode, Priority); + static nsresult CancelPrefetch( + const nsAString& host, bool isHttps, + const OriginAttributes& aPartitionedPrincipalOriginAttributes, + nsIRequest::TRRMode aTRRMode, Priority, nsresult aReason); + static nsresult CancelPrefetch(SupportsDNSPrefetch&, Element&, Priority, + nsresult aReason); + static void ElementDestroyed(Element&, SupportsDNSPrefetch&); + + private: + static nsIDNSService::DNSFlags PriorityToDNSServiceFlags(Priority); + + static nsresult Prefetch( + const nsAString& host, bool isHttps, + const OriginAttributes& aPartitionedPrincipalOriginAttributes, + nsIDNSService::DNSFlags flags); + static nsresult CancelPrefetch( + const nsAString& hostname, bool isHttps, + const OriginAttributes& aPartitionedPrincipalOriginAttributes, + nsIDNSService::DNSFlags flags, nsresult aReason); + + friend class net::NeckoParent; +}; + +// Elements that support DNS prefetch are expected to subclass this. +class SupportsDNSPrefetch { + public: + bool IsInDNSPrefetch() { return mInDNSPrefetch; } + void SetIsInDNSPrefetch() { mInDNSPrefetch = true; } + void ClearIsInDNSPrefetch() { mInDNSPrefetch = false; } + + void DNSPrefetchRequestStarted() { + mDNSPrefetchDeferred = false; + mDNSPrefetchRequested = true; + } + + void DNSPrefetchRequestDeferred() { + mDNSPrefetchDeferred = true; + mDNSPrefetchRequested = false; + } + + bool IsDNSPrefetchRequestDeferred() const { return mDNSPrefetchDeferred; } + + // This could be a virtual function or something like that, but that would + // cause our subclasses to grow by two pointers, rather than just 1 byte at + // most. + nsIURI* GetURIForDNSPrefetch(Element& aOwner); + + protected: + SupportsDNSPrefetch() + : mInDNSPrefetch(false), + mDNSPrefetchRequested(false), + mDNSPrefetchDeferred(false), + mDestroyedCalled(false) {} + + void CancelDNSPrefetch(Element&); + void TryDNSPrefetch(Element&); + + // This MUST be called on the destructor of the Element subclass. + // Our own destructor ensures that. + void Destroyed(Element& aOwner) { + MOZ_DIAGNOSTIC_ASSERT(!mDestroyedCalled, + "Multiple calls to SupportsDNSPrefetch::Destroyed?"); + mDestroyedCalled = true; + if (mInDNSPrefetch) { + HTMLDNSPrefetch::ElementDestroyed(aOwner, *this); + } + } + + ~SupportsDNSPrefetch() { + MOZ_DIAGNOSTIC_ASSERT(mDestroyedCalled, + "Need to call SupportsDNSPrefetch::Destroyed " + "from the owner element"); + } + + private: + bool mInDNSPrefetch : 1; + bool mDNSPrefetchRequested : 1; + bool mDNSPrefetchDeferred : 1; + bool mDestroyedCalled : 1; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/html/HTMLDataElement.cpp b/dom/html/HTMLDataElement.cpp new file mode 100644 index 0000000000..9693990e39 --- /dev/null +++ b/dom/html/HTMLDataElement.cpp @@ -0,0 +1,28 @@ +/* -*- 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 "HTMLDataElement.h" +#include "mozilla/dom/HTMLDataElementBinding.h" +#include "nsGenericHTMLElement.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Data) + +namespace mozilla::dom { + +HTMLDataElement::HTMLDataElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLDataElement::~HTMLDataElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLDataElement) + +JSObject* HTMLDataElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLDataElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLDataElement.h b/dom/html/HTMLDataElement.h new file mode 100644 index 0000000000..7b5b4b12c7 --- /dev/null +++ b/dom/html/HTMLDataElement.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLDataElement_h +#define mozilla_dom_HTMLDataElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" + +namespace mozilla::dom { + +class HTMLDataElement final : public nsGenericHTMLElement { + public: + explicit HTMLDataElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // HTMLDataElement WebIDL + void GetValue(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::value, aValue); } + + void SetValue(const nsAString& aValue, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::value, aValue, aError); + } + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLDataElement(); + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLDataElement_h diff --git a/dom/html/HTMLDataListElement.cpp b/dom/html/HTMLDataListElement.cpp new file mode 100644 index 0000000000..4d89967775 --- /dev/null +++ b/dom/html/HTMLDataListElement.cpp @@ -0,0 +1,37 @@ +/* -*- 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 "HTMLDataListElement.h" +#include "mozilla/dom/HTMLDataListElementBinding.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(DataList) + +namespace mozilla::dom { + +HTMLDataListElement::~HTMLDataListElement() { + MOZ_ASSERT(HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR)); +} + +JSObject* HTMLDataListElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLDataListElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLDataListElement, nsGenericHTMLElement, + mOptions) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLDataListElement, + nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLDataListElement) + +bool HTMLDataListElement::MatchOptions(Element* aElement, int32_t aNamespaceID, + nsAtom* aAtom, void* aData) { + return aElement->NodeInfo()->Equals(nsGkAtoms::option, kNameSpaceID_XHTML) && + !aElement->HasAttr(nsGkAtoms::disabled); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLDataListElement.h b/dom/html/HTMLDataListElement.h new file mode 100644 index 0000000000..a5a3fe4997 --- /dev/null +++ b/dom/html/HTMLDataListElement.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef HTMLDataListElement_h___ +#define HTMLDataListElement_h___ + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsContentList.h" + +namespace mozilla::dom { + +class HTMLDataListElement final : public nsGenericHTMLElement { + public: + explicit HTMLDataListElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetFlags(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR); + } + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLDataListElement, datalist) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + nsContentList* Options() { + if (!mOptions) { + mOptions = new nsContentList(this, MatchOptions, nullptr, nullptr, true); + } + + return mOptions; + } + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // This function is used to generate the nsContentList (option elements). + static bool MatchOptions(Element* aElement, int32_t aNamespaceID, + nsAtom* aAtom, void* aData); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLDataListElement, + nsGenericHTMLElement) + protected: + virtual ~HTMLDataListElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // <option>'s list inside the datalist element. + RefPtr<nsContentList> mOptions; +}; + +} // namespace mozilla::dom + +#endif /* HTMLDataListElement_h___ */ diff --git a/dom/html/HTMLDetailsElement.cpp b/dom/html/HTMLDetailsElement.cpp new file mode 100644 index 0000000000..c8e9c11c4f --- /dev/null +++ b/dom/html/HTMLDetailsElement.cpp @@ -0,0 +1,162 @@ +/* -*- 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/HTMLDetailsElement.h" + +#include "mozilla/dom/HTMLDetailsElementBinding.h" +#include "mozilla/dom/HTMLSummaryElement.h" +#include "mozilla/dom/ShadowRoot.h" +#include "mozilla/ScopeExit.h" +#include "nsContentUtils.h" +#include "nsTextNode.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Details) + +namespace mozilla::dom { + +HTMLDetailsElement::~HTMLDetailsElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLDetailsElement) + +HTMLDetailsElement::HTMLDetailsElement(already_AddRefed<NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetupShadowTree(); +} + +HTMLSummaryElement* HTMLDetailsElement::GetFirstSummary() const { + // XXX: Bug 1245032: Might want to cache the first summary element. + for (nsIContent* child = nsINode::GetFirstChild(); child; + child = child->GetNextSibling()) { + if (auto* summary = HTMLSummaryElement::FromNode(child)) { + return summary; + } + } + return nullptr; +} + +void HTMLDetailsElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::open) { + bool wasOpen = !!aOldValue; + bool isOpen = !!aValue; + if (wasOpen != isOpen) { + auto stringForState = [](bool aOpen) { + return aOpen ? u"open"_ns : u"closed"_ns; + }; + nsAutoString oldState; + if (mToggleEventDispatcher) { + oldState.Truncate(); + static_cast<ToggleEvent*>(mToggleEventDispatcher->mEvent.get()) + ->GetOldState(oldState); + mToggleEventDispatcher->Cancel(); + } else { + oldState.Assign(stringForState(wasOpen)); + } + RefPtr<ToggleEvent> toggleEvent = CreateToggleEvent( + u"toggle"_ns, oldState, stringForState(isOpen), Cancelable::eNo); + mToggleEventDispatcher = + new AsyncEventDispatcher(this, toggleEvent.forget()); + mToggleEventDispatcher->PostDOMEvent(); + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLDetailsElement::SetupShadowTree() { + const bool kNotify = false; + AttachAndSetUAShadowRoot(NotifyUAWidgetSetup::No); + RefPtr<ShadowRoot> sr = GetShadowRoot(); + if (NS_WARN_IF(!sr)) { + return; + } + + nsNodeInfoManager* nim = OwnerDoc()->NodeInfoManager(); + RefPtr<NodeInfo> slotNodeInfo = nim->GetNodeInfo( + nsGkAtoms::slot, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + { + RefPtr<NodeInfo> linkNodeInfo = nim->GetNodeInfo( + nsGkAtoms::link, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + RefPtr<nsGenericHTMLElement> link = + NS_NewHTMLLinkElement(linkNodeInfo.forget()); + if (NS_WARN_IF(!link)) { + return; + } + link->SetAttr(nsGkAtoms::rel, u"stylesheet"_ns, IgnoreErrors()); + link->SetAttr(nsGkAtoms::href, + u"resource://content-accessible/details.css"_ns, + IgnoreErrors()); + sr->AppendChildTo(link, kNotify, IgnoreErrors()); + } + { + RefPtr<nsGenericHTMLElement> slot = + NS_NewHTMLSlotElement(do_AddRef(slotNodeInfo)); + if (NS_WARN_IF(!slot)) { + return; + } + slot->SetAttr(kNameSpaceID_None, nsGkAtoms::name, + u"internal-main-summary"_ns, kNotify); + sr->AppendChildTo(slot, kNotify, IgnoreErrors()); + + RefPtr<NodeInfo> summaryNodeInfo = nim->GetNodeInfo( + nsGkAtoms::summary, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + RefPtr<nsGenericHTMLElement> summary = + NS_NewHTMLSummaryElement(summaryNodeInfo.forget()); + if (NS_WARN_IF(!summary)) { + return; + } + + nsAutoString defaultSummaryText; + nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "DefaultSummary", defaultSummaryText); + RefPtr<nsTextNode> description = new (nim) nsTextNode(nim); + description->SetText(defaultSummaryText, kNotify); + summary->AppendChildTo(description, kNotify, IgnoreErrors()); + + slot->AppendChildTo(summary, kNotify, IgnoreErrors()); + } + { + RefPtr<nsGenericHTMLElement> slot = + NS_NewHTMLSlotElement(slotNodeInfo.forget()); + if (NS_WARN_IF(!slot)) { + return; + } + sr->AppendChildTo(slot, kNotify, IgnoreErrors()); + } +} + +void HTMLDetailsElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) { + if (mToggleEventDispatcher == aEvent) { + mToggleEventDispatcher = nullptr; + } +} + +JSObject* HTMLDetailsElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLDetailsElement_Binding::Wrap(aCx, this, aGivenProto); +} + +void HTMLDetailsElement::HandleInvokeInternal(nsAtom* aAction, + ErrorResult& aRv) { + if (nsContentUtils::EqualsIgnoreASCIICase(aAction, nsGkAtoms::_auto) || + nsContentUtils::EqualsIgnoreASCIICase(aAction, nsGkAtoms::toggle)) { + ToggleOpen(); + } else if (nsContentUtils::EqualsIgnoreASCIICase(aAction, nsGkAtoms::close)) { + if (Open()) { + SetOpen(false, IgnoreErrors()); + } + } else if (nsContentUtils::EqualsIgnoreASCIICase(aAction, nsGkAtoms::open)) { + if (!Open()) { + SetOpen(true, IgnoreErrors()); + } + } +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLDetailsElement.h b/dom/html/HTMLDetailsElement.h new file mode 100644 index 0000000000..2c7ed56d98 --- /dev/null +++ b/dom/html/HTMLDetailsElement.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLDetailsElement_h +#define mozilla_dom_HTMLDetailsElement_h + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLSummaryElement; + +// HTMLDetailsElement implements the <details> tag, which is used as a +// disclosure widget from which the user can obtain additional information or +// controls. Please see the spec for more information. +// https://html.spec.whatwg.org/multipage/forms.html#the-details-element +// +class HTMLDetailsElement final : public nsGenericHTMLElement { + public: + using NodeInfo = mozilla::dom::NodeInfo; + + explicit HTMLDetailsElement(already_AddRefed<NodeInfo>&& aNodeInfo); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLDetailsElement, details) + + HTMLSummaryElement* GetFirstSummary() const; + + nsresult Clone(NodeInfo* aNodeInfo, nsINode** aResult) const override; + + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + + bool IsInteractiveHTMLContent() const override { return true; } + + // HTMLDetailsElement WebIDL + bool Open() const { return GetBoolAttr(nsGkAtoms::open); } + + void SetOpen(bool aOpen, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::open, aOpen, aError); + } + + void ToggleOpen() { SetOpen(!Open(), IgnoreErrors()); } + + virtual void AsyncEventRunning(AsyncEventDispatcher* aEvent) override; + + void HandleInvokeInternal(nsAtom* aAction, ErrorResult& aRv) override; + + protected: + virtual ~HTMLDetailsElement(); + void SetupShadowTree(); + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + RefPtr<AsyncEventDispatcher> mToggleEventDispatcher; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLDetailsElement_h */ diff --git a/dom/html/HTMLDialogElement.cpp b/dom/html/HTMLDialogElement.cpp new file mode 100644 index 0000000000..6d4bda0392 --- /dev/null +++ b/dom/html/HTMLDialogElement.cpp @@ -0,0 +1,200 @@ +/* -*- 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/HTMLDialogElement.h" +#include "mozilla/dom/ElementBinding.h" +#include "mozilla/dom/HTMLDialogElementBinding.h" + +#include "nsContentUtils.h" +#include "nsFocusManager.h" +#include "nsIFrame.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Dialog) + +namespace mozilla::dom { + +HTMLDialogElement::~HTMLDialogElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLDialogElement) + +void HTMLDialogElement::Close( + const mozilla::dom::Optional<nsAString>& aReturnValue) { + if (!Open()) { + return; + } + if (aReturnValue.WasPassed()) { + SetReturnValue(aReturnValue.Value()); + } + + SetOpen(false, IgnoreErrors()); + + RemoveFromTopLayerIfNeeded(); + + RefPtr<Element> previouslyFocusedElement = + do_QueryReferent(mPreviouslyFocusedElement); + + if (previouslyFocusedElement) { + mPreviouslyFocusedElement = nullptr; + + FocusOptions options; + options.mPreventScroll = true; + previouslyFocusedElement->Focus(options, CallerType::NonSystem, + IgnoredErrorResult()); + } + + RefPtr<AsyncEventDispatcher> eventDispatcher = + new AsyncEventDispatcher(this, u"close"_ns, CanBubble::eNo); + eventDispatcher->PostDOMEvent(); +} + +void HTMLDialogElement::Show(ErrorResult& aError) { + if (Open()) { + if (!IsInTopLayer()) { + return; + } + return aError.ThrowInvalidStateError( + "Cannot call show() on an open modal dialog."); + } + + SetOpen(true, IgnoreErrors()); + + StorePreviouslyFocusedElement(); + + RefPtr<nsINode> hideUntil = GetTopmostPopoverAncestor(nullptr, false); + if (!hideUntil) { + hideUntil = OwnerDoc(); + } + + OwnerDoc()->HideAllPopoversUntil(*hideUntil, false, true); + FocusDialog(); +} + +bool HTMLDialogElement::IsInTopLayer() const { + return State().HasState(ElementState::MODAL); +} + +void HTMLDialogElement::AddToTopLayerIfNeeded() { + MOZ_ASSERT(IsInComposedDoc()); + if (IsInTopLayer()) { + return; + } + + OwnerDoc()->AddModalDialog(*this); +} + +void HTMLDialogElement::RemoveFromTopLayerIfNeeded() { + if (!IsInTopLayer()) { + return; + } + OwnerDoc()->RemoveModalDialog(*this); +} + +void HTMLDialogElement::StorePreviouslyFocusedElement() { + if (Element* element = nsFocusManager::GetFocusedElementStatic()) { + if (NS_SUCCEEDED(nsContentUtils::CheckSameOrigin(this, element))) { + mPreviouslyFocusedElement = do_GetWeakReference(element); + } + } else if (Document* doc = GetComposedDoc()) { + // Looks like there's a discrepancy sometimes when focus is moved + // to a different in-process window. + if (nsIContent* unretargetedFocus = doc->GetUnretargetedFocusedContent()) { + mPreviouslyFocusedElement = do_GetWeakReference(unretargetedFocus); + } + } +} + +void HTMLDialogElement::UnbindFromTree(bool aNullParent) { + RemoveFromTopLayerIfNeeded(); + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +void HTMLDialogElement::ShowModal(ErrorResult& aError) { + if (Open()) { + if (IsInTopLayer()) { + return; + } + return aError.ThrowInvalidStateError( + "Cannot call showModal() on an open non-modal dialog."); + } + + if (!IsInComposedDoc()) { + return aError.ThrowInvalidStateError("Dialog element is not connected"); + } + + if (IsPopoverOpen()) { + return aError.ThrowInvalidStateError( + "Dialog element is already an open popover."); + } + + AddToTopLayerIfNeeded(); + + SetOpen(true, aError); + + StorePreviouslyFocusedElement(); + + RefPtr<nsINode> hideUntil = GetTopmostPopoverAncestor(nullptr, false); + if (!hideUntil) { + hideUntil = OwnerDoc(); + } + + OwnerDoc()->HideAllPopoversUntil(*hideUntil, false, true); + FocusDialog(); + + aError.SuppressException(); +} + +void HTMLDialogElement::FocusDialog() { + // 1) If subject is inert, return. + // 2) Let control be the first descendant element of subject, in tree + // order, that is not inert and has the autofocus attribute specified. + RefPtr<Document> doc = OwnerDoc(); + if (IsInComposedDoc()) { + doc->FlushPendingNotifications(FlushType::Frames); + } + + RefPtr<Element> control = HasAttr(nsGkAtoms::autofocus) + ? this + : GetFocusDelegate(false /* aWithMouse */); + + // If there isn't one of those either, then let control be subject. + if (!control) { + control = this; + } + + FocusCandidate(control, IsInTopLayer()); +} + +int32_t HTMLDialogElement::TabIndexDefault() { return 0; } + +void HTMLDialogElement::QueueCancelDialog() { + // queues an element task on the user interaction task source + OwnerDoc()->Dispatch( + NewRunnableMethod("HTMLDialogElement::RunCancelDialogSteps", this, + &HTMLDialogElement::RunCancelDialogSteps)); +} + +void HTMLDialogElement::RunCancelDialogSteps() { + // 1) Let close be the result of firing an event named cancel at dialog, with + // the cancelable attribute initialized to true. + bool defaultAction = true; + nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, u"cancel"_ns, + CanBubble::eNo, Cancelable::eYes, + &defaultAction); + + // 2) If close is true and dialog has an open attribute, then close the dialog + // with no return value. + if (defaultAction) { + Optional<nsAString> retValue; + Close(retValue); + } +} + +JSObject* HTMLDialogElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLDialogElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLDialogElement.h b/dom/html/HTMLDialogElement.h new file mode 100644 index 0000000000..556e86b891 --- /dev/null +++ b/dom/html/HTMLDialogElement.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef HTMLDialogElement_h +#define HTMLDialogElement_h + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" + +namespace mozilla::dom { + +class HTMLDialogElement final : public nsGenericHTMLElement { + public: + explicit HTMLDialogElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mPreviouslyFocusedElement(nullptr) {} + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLDialogElement, dialog) + + nsresult Clone(dom::NodeInfo* aNodeInfo, nsINode** aResult) const override; + + bool Open() const { return GetBoolAttr(nsGkAtoms::open); } + void SetOpen(bool aOpen, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::open, aOpen, aError); + } + + void GetReturnValue(nsAString& aReturnValue) { aReturnValue = mReturnValue; } + void SetReturnValue(const nsAString& aReturnValue) { + mReturnValue = aReturnValue; + } + + void UnbindFromTree(bool aNullParent = true) override; + + void Close(const mozilla::dom::Optional<nsAString>& aReturnValue); + MOZ_CAN_RUN_SCRIPT void Show(ErrorResult& aError); + MOZ_CAN_RUN_SCRIPT void ShowModal(ErrorResult& aError); + + bool IsInTopLayer() const; + void QueueCancelDialog(); + void RunCancelDialogSteps(); + + MOZ_CAN_RUN_SCRIPT_BOUNDARY void FocusDialog(); + + int32_t TabIndexDefault() override; + + nsString mReturnValue; + + protected: + virtual ~HTMLDialogElement(); + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + void AddToTopLayerIfNeeded(); + void RemoveFromTopLayerIfNeeded(); + void StorePreviouslyFocusedElement(); + + nsWeakPtr mPreviouslyFocusedElement; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/html/HTMLDivElement.cpp b/dom/html/HTMLDivElement.cpp new file mode 100644 index 0000000000..9752dfe50a --- /dev/null +++ b/dom/html/HTMLDivElement.cpp @@ -0,0 +1,54 @@ +/* -*- 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 "HTMLDivElement.h" +#include "nsGenericHTMLElement.h" +#include "nsStyleConsts.h" +#include "mozilla/dom/HTMLDivElementBinding.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Div) + +namespace mozilla::dom { + +HTMLDivElement::~HTMLDivElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLDivElement) + +JSObject* HTMLDivElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::HTMLDivElement_Binding::Wrap(aCx, this, aGivenProto); +} + +bool HTMLDivElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::align) { + return ParseDivAlignValue(aValue, aResult); + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLDivElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + MapDivAlignAttributeInto(aBuilder); + MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLDivElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = {sDivAlignAttributeMap, + sCommonAttributeMap}; + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLDivElement::GetAttributeMappingFunction() const { + return &MapAttributesIntoRule; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLDivElement.h b/dom/html/HTMLDivElement.h new file mode 100644 index 0000000000..d61b78ce06 --- /dev/null +++ b/dom/html/HTMLDivElement.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef HTMLDivElement_h___ +#define HTMLDivElement_h___ + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLDivElement final : public nsGenericHTMLElement { + public: + explicit HTMLDivElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::div), + "HTMLDivElement should be a div"); + } + + void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } + void SetAlign(const nsAString& aAlign, mozilla::ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLDivElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif /* HTMLDivElement_h___ */ diff --git a/dom/html/HTMLElement.cpp b/dom/html/HTMLElement.cpp new file mode 100644 index 0000000000..ff9c786c55 --- /dev/null +++ b/dom/html/HTMLElement.cpp @@ -0,0 +1,467 @@ +/* -*- 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/HTMLElement.h" + +#include "mozilla/EventDispatcher.h" +#include "mozilla/PresState.h" +#include "mozilla/dom/CustomElementRegistry.h" +#include "mozilla/dom/ElementInternalsBinding.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/FromParser.h" +#include "mozilla/dom/HTMLElementBinding.h" +#include "nsContentUtils.h" +#include "nsGenericHTMLElement.h" +#include "nsILayoutHistoryState.h" + +namespace mozilla::dom { + +HTMLElement::HTMLElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLFormElement(std::move(aNodeInfo)) { + if (NodeInfo()->Equals(nsGkAtoms::bdi)) { + AddStatesSilently(ElementState::HAS_DIR_ATTR_LIKE_AUTO); + } + + InhibitRestoration(!(aFromParser & FROM_PARSER_NETWORK)); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLElement, nsGenericHTMLFormElement) + +// QueryInterface implementation for HTMLElement + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLElement) + NS_INTERFACE_MAP_ENTRY_TEAROFF(nsIFormControl, GetElementInternals()) + NS_INTERFACE_MAP_ENTRY_TEAROFF(nsIConstraintValidation, GetElementInternals()) +NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLFormElement) + +NS_IMPL_ADDREF_INHERITED(HTMLElement, nsGenericHTMLFormElement) +NS_IMPL_RELEASE_INHERITED(HTMLElement, nsGenericHTMLFormElement) + +NS_IMPL_ELEMENT_CLONE(HTMLElement) + +JSObject* HTMLElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::HTMLElement_Binding::Wrap(aCx, this, aGivenProto); +} + +void HTMLElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + if (IsDisabledForEvents(aVisitor.mEvent)) { + // Do not process any DOM events if the element is disabled + aVisitor.mCanHandle = false; + return; + } + + nsGenericHTMLFormElement::GetEventTargetParent(aVisitor); +} + +nsINode* HTMLElement::GetScopeChainParent() const { + if (IsFormAssociatedCustomElements()) { + auto* form = GetFormInternal(); + if (form) { + return form; + } + } + return nsGenericHTMLFormElement::GetScopeChainParent(); +} + +nsresult HTMLElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLFormElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(false); + return rv; +} + +void HTMLElement::UnbindFromTree(bool aNullParent) { + nsGenericHTMLFormElement::UnbindFromTree(aNullParent); + + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(false); +} + +void HTMLElement::DoneCreatingElement() { + if (MOZ_UNLIKELY(IsFormAssociatedElement())) { + MaybeRestoreFormAssociatedCustomElementState(); + } +} + +void HTMLElement::SaveState() { + if (MOZ_LIKELY(!IsFormAssociatedElement())) { + return; + } + + auto* internals = GetElementInternals(); + + nsCString stateKey = internals->GetStateKey(); + if (stateKey.IsEmpty()) { + return; + } + + nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(false); + if (!history) { + return; + } + + // Get the pres state for this key, if it doesn't exist, create one. + PresState* result = history->GetState(stateKey); + if (!result) { + UniquePtr<PresState> newState = NewPresState(); + result = newState.get(); + history->AddState(stateKey, std::move(newState)); + } + + const auto& state = internals->GetFormState(); + const auto& value = internals->GetFormSubmissionValue(); + result->contentData() = CustomElementTuple( + nsContentUtils::ConvertToCustomElementFormValue(value), + nsContentUtils::ConvertToCustomElementFormValue(state)); +} + +void HTMLElement::MaybeRestoreFormAssociatedCustomElementState() { + MOZ_ASSERT(IsFormAssociatedElement()); + + if (HasFlag(HTML_ELEMENT_INHIBIT_RESTORATION)) { + return; + } + + auto* internals = GetElementInternals(); + if (internals->GetStateKey().IsEmpty()) { + Document* doc = GetUncomposedDoc(); + nsCString stateKey; + nsContentUtils::GenerateStateKey(this, doc, stateKey); + internals->SetStateKey(std::move(stateKey)); + + RestoreFormAssociatedCustomElementState(); + } +} + +void HTMLElement::RestoreFormAssociatedCustomElementState() { + MOZ_ASSERT(IsFormAssociatedElement()); + + auto* internals = GetElementInternals(); + + const nsCString& stateKey = internals->GetStateKey(); + if (stateKey.IsEmpty()) { + return; + } + nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(true); + if (!history) { + return; + } + PresState* result = history->GetState(stateKey); + if (!result) { + return; + } + auto& content = result->contentData(); + if (content.type() != PresContentData::TCustomElementTuple) { + return; + } + + auto& ce = content.get_CustomElementTuple(); + nsCOMPtr<nsIGlobalObject> global = GetOwnerDocument()->GetOwnerGlobal(); + internals->RestoreFormValue( + nsContentUtils::ExtractFormAssociatedCustomElementValue(global, + ce.value()), + nsContentUtils::ExtractFormAssociatedCustomElementValue(global, + ce.state())); +} + +void HTMLElement::InhibitRestoration(bool aShouldInhibit) { + if (aShouldInhibit) { + SetFlags(HTML_ELEMENT_INHIBIT_RESTORATION); + } else { + UnsetFlags(HTML_ELEMENT_INHIBIT_RESTORATION); + } +} + +void HTMLElement::SetCustomElementDefinition( + CustomElementDefinition* aDefinition) { + nsGenericHTMLFormElement::SetCustomElementDefinition(aDefinition); + // Always create an ElementInternal for form-associated custom element as the + // Form related implementation lives in ElementInternal which implements + // nsIFormControl. It is okay for the attachElementInternal API as there is a + // separated flag for whether attachElementInternal is called. + if (aDefinition && !aDefinition->IsCustomBuiltIn() && + aDefinition->mFormAssociated) { + CustomElementData* data = GetCustomElementData(); + MOZ_ASSERT(data); + auto* internals = data->GetOrCreateElementInternals(this); + + // This is for the case that script constructs a custom element directly, + // e.g. via new MyCustomElement(), where the upgrade steps won't be ran to + // update the disabled state in UpdateFormOwner(). + if (data->mState == CustomElementData::State::eCustom) { + UpdateDisabledState(true); + } else if (!HasFlag(HTML_ELEMENT_INHIBIT_RESTORATION)) { + internals->InitializeControlNumber(); + } + } +} + +// https://html.spec.whatwg.org/commit-snapshots/53bc3803433e1c817918b83e8a84f3db900031dd/#dom-attachinternals +already_AddRefed<ElementInternals> HTMLElement::AttachInternals( + ErrorResult& aRv) { + CustomElementData* ceData = GetCustomElementData(); + + // 1. If element's is value is not null, then throw a "NotSupportedError" + // DOMException. + if (nsAtom* isAtom = ceData ? ceData->GetIs(this) : nullptr) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Cannot attach ElementInternals to a customized built-in element " + "'%s'", + NS_ConvertUTF16toUTF8(isAtom->GetUTF16String()).get())); + return nullptr; + } + + // 2. Let definition be the result of looking up a custom element definition + // given element's node document, its namespace, its local name, and null + // as is value. + nsAtom* nameAtom = NodeInfo()->NameAtom(); + CustomElementDefinition* definition = nullptr; + if (ceData) { + definition = ceData->GetCustomElementDefinition(); + + // If the definition is null, the element possible hasn't yet upgraded. + // Fallback to use LookupCustomElementDefinition to find its definition. + if (!definition) { + definition = nsContentUtils::LookupCustomElementDefinition( + NodeInfo()->GetDocument(), nameAtom, NodeInfo()->NamespaceID(), + ceData->GetCustomElementType()); + } + } + + // 3. If definition is null, then throw an "NotSupportedError" DOMException. + if (!definition) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "Cannot attach ElementInternals to a non-custom element '%s'", + NS_ConvertUTF16toUTF8(nameAtom->GetUTF16String()).get())); + return nullptr; + } + + // 4. If definition's disable internals is true, then throw a + // "NotSupportedError" DOMException. + if (definition->mDisableInternals) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "AttachInternal() to '%s' is disabled by disabledFeatures", + NS_ConvertUTF16toUTF8(nameAtom->GetUTF16String()).get())); + return nullptr; + } + + // If this is not a custom element, i.e. ceData is nullptr, we are unable to + // find a definition and should return earlier above. + MOZ_ASSERT(ceData); + + // 5. If element's attached internals is true, then throw an + // "NotSupportedError" DOMException. + if (ceData->HasAttachedInternals()) { + aRv.ThrowNotSupportedError(nsPrintfCString( + "AttachInternals() has already been called from '%s'", + NS_ConvertUTF16toUTF8(nameAtom->GetUTF16String()).get())); + return nullptr; + } + + // 6. If element's custom element state is not "precustomized" or "custom", + // then throw a "NotSupportedError" DOMException. + if (ceData->mState != CustomElementData::State::ePrecustomized && + ceData->mState != CustomElementData::State::eCustom) { + aRv.ThrowNotSupportedError( + R"(Custom element state is not "precustomized" or "custom".)"); + return nullptr; + } + + // 7. Set element's attached internals to true. + ceData->AttachedInternals(); + + // 8. Create a new ElementInternals instance targeting element, and return it. + return do_AddRef(ceData->GetOrCreateElementInternals(this)); +} + +void HTMLElement::AfterClearForm(bool aUnbindOrDelete) { + // No need to enqueue formAssociated callback if we aren't releasing or + // unbinding from tree, UpdateFormOwner() will handle it. + if (aUnbindOrDelete) { + MOZ_ASSERT(IsFormAssociatedElement()); + nsContentUtils::EnqueueLifecycleCallback( + ElementCallbackType::eFormAssociated, this, {}); + } +} + +void HTMLElement::UpdateFormOwner() { + MOZ_ASSERT(IsFormAssociatedElement()); + + // If @form is set, the element *has* to be in a composed document, + // otherwise it wouldn't be possible to find an element with the + // corresponding id. If @form isn't set, the element *has* to have a parent, + // otherwise it wouldn't be possible to find a form ancestor. We should not + // call UpdateFormOwner if none of these conditions are fulfilled. + if (HasAttr(nsGkAtoms::form) ? IsInComposedDoc() : !!GetParent()) { + UpdateFormOwner(true, nullptr); + } + UpdateFieldSet(true); + UpdateDisabledState(true); + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(true); + + MaybeRestoreFormAssociatedCustomElementState(); +} + +bool HTMLElement::IsDisabledForEvents(WidgetEvent* aEvent) { + if (IsFormAssociatedElement()) { + return IsElementDisabledForEvents(aEvent, GetPrimaryFrame()); + } + + return false; +} + +void HTMLElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && + (aName == nsGkAtoms::disabled || aName == nsGkAtoms::readonly)) { + if (aName == nsGkAtoms::disabled) { + // This *has* to be called *before* validity state check because + // UpdateBarredFromConstraintValidation depend on our disabled state. + UpdateDisabledState(aNotify); + } + if (aName == nsGkAtoms::readonly && !!aValue != !!aOldValue) { + UpdateReadOnlyState(aNotify); + } + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(aNotify); + } + + return nsGenericHTMLFormElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLElement::UpdateValidityElementStates(bool aNotify) { + AutoStateChangeNotifier notifier(*this, aNotify); + RemoveStatesSilently(ElementState::VALIDITY_STATES); + ElementInternals* internals = GetElementInternals(); + if (!internals || !internals->IsCandidateForConstraintValidation()) { + return; + } + if (internals->IsValid()) { + AddStatesSilently(ElementState::VALID | ElementState::USER_VALID); + } else { + AddStatesSilently(ElementState::INVALID | ElementState::USER_INVALID); + } +} + +void HTMLElement::SetFormInternal(HTMLFormElement* aForm, bool aBindToTree) { + ElementInternals* internals = GetElementInternals(); + MOZ_ASSERT(internals); + internals->SetForm(aForm); +} + +HTMLFormElement* HTMLElement::GetFormInternal() const { + ElementInternals* internals = GetElementInternals(); + MOZ_ASSERT(internals); + return internals->GetForm(); +} + +void HTMLElement::SetFieldSetInternal(HTMLFieldSetElement* aFieldset) { + ElementInternals* internals = GetElementInternals(); + MOZ_ASSERT(internals); + internals->SetFieldSet(aFieldset); +} + +HTMLFieldSetElement* HTMLElement::GetFieldSetInternal() const { + ElementInternals* internals = GetElementInternals(); + MOZ_ASSERT(internals); + return internals->GetFieldSet(); +} + +bool HTMLElement::CanBeDisabled() const { return IsFormAssociatedElement(); } + +bool HTMLElement::DoesReadOnlyApply() const { + return IsFormAssociatedElement(); +} + +void HTMLElement::UpdateDisabledState(bool aNotify) { + bool oldState = IsDisabled(); + nsGenericHTMLFormElement::UpdateDisabledState(aNotify); + if (oldState != IsDisabled()) { + MOZ_ASSERT(IsFormAssociatedElement()); + LifecycleCallbackArgs args; + args.mDisabled = !oldState; + nsContentUtils::EnqueueLifecycleCallback(ElementCallbackType::eFormDisabled, + this, args); + } +} + +void HTMLElement::UpdateFormOwner(bool aBindToTree, Element* aFormIdElement) { + HTMLFormElement* oldForm = GetFormInternal(); + nsGenericHTMLFormElement::UpdateFormOwner(aBindToTree, aFormIdElement); + HTMLFormElement* newForm = GetFormInternal(); + if (newForm != oldForm) { + LifecycleCallbackArgs args; + args.mForm = newForm; + nsContentUtils::EnqueueLifecycleCallback( + ElementCallbackType::eFormAssociated, this, args); + } +} + +bool HTMLElement::IsFormAssociatedElement() const { + CustomElementData* data = GetCustomElementData(); + return data && data->IsFormAssociated(); +} + +void HTMLElement::FieldSetDisabledChanged(bool aNotify) { + // This *has* to be called *before* UpdateBarredFromConstraintValidation + // because this function depend on our disabled state. + nsGenericHTMLFormElement::FieldSetDisabledChanged(aNotify); + + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(aNotify); +} + +ElementInternals* HTMLElement::GetElementInternals() const { + CustomElementData* data = GetCustomElementData(); + if (!data || !data->IsFormAssociated()) { + // If the element is not a form associated custom element, it should not be + // able to be QueryInterfaced to nsIFormControl and could not perform + // the form operation, either, so we return nullptr here. + return nullptr; + } + + return data->GetElementInternals(); +} + +void HTMLElement::UpdateBarredFromConstraintValidation() { + CustomElementData* data = GetCustomElementData(); + if (data && data->IsFormAssociated()) { + ElementInternals* internals = data->GetElementInternals(); + MOZ_ASSERT(internals); + internals->UpdateBarredFromConstraintValidation(); + } +} + +} // namespace mozilla::dom + +// Here, we expand 'NS_IMPL_NS_NEW_HTML_ELEMENT()' by hand. +// (Calling the macro directly (with no args) produces compiler warnings.) +nsGenericHTMLElement* NS_NewHTMLElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) mozilla::dom::HTMLElement(nodeInfo.forget(), aFromParser); +} + +// Distinct from the above in order to have function pointer that compared +// unequal to a function pointer to the above. +nsGenericHTMLElement* NS_NewCustomElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) mozilla::dom::HTMLElement(nodeInfo.forget(), aFromParser); +} diff --git a/dom/html/HTMLElement.h b/dom/html/HTMLElement.h new file mode 100644 index 0000000000..8fbbc6f2b9 --- /dev/null +++ b/dom/html/HTMLElement.h @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLElement_h +#define mozilla_dom_HTMLElement_h + +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLElement final : public nsGenericHTMLFormElement { + public: + explicit HTMLElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser = NOT_FROM_PARSER); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLElement, + nsGenericHTMLFormElement) + + // EventTarget + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + + // nsINode + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + nsINode* GetScopeChainParent() const override; + + // nsIContent + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + void DoneCreatingElement() override; + + // Element + void SetCustomElementDefinition( + CustomElementDefinition* aDefinition) override; + bool IsLabelable() const override { return IsFormAssociatedElement(); } + + // nsGenericHTMLElement + // https://html.spec.whatwg.org/multipage/custom-elements.html#dom-attachinternals + already_AddRefed<mozilla::dom::ElementInternals> AttachInternals( + ErrorResult& aRv) override; + bool IsDisabledForEvents(WidgetEvent* aEvent) override; + + // nsGenericHTMLFormElement + bool IsFormAssociatedElement() const override; + void AfterClearForm(bool aUnbindOrDelete) override; + void FieldSetDisabledChanged(bool aNotify) override; + void SaveState() override; + void UpdateValidityElementStates(bool aNotify); + + void UpdateFormOwner(); + + void MaybeRestoreFormAssociatedCustomElementState(); + + void InhibitRestoration(bool aShouldInhibit); + + protected: + virtual ~HTMLElement() = default; + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Element + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + + // nsGenericHTMLFormElement + void SetFormInternal(HTMLFormElement* aForm, bool aBindToTree) override; + HTMLFormElement* GetFormInternal() const override; + void SetFieldSetInternal(HTMLFieldSetElement* aFieldset) override; + HTMLFieldSetElement* GetFieldSetInternal() const override; + bool CanBeDisabled() const override; + bool DoesReadOnlyApply() const override; + void UpdateDisabledState(bool aNotify) override; + void UpdateFormOwner(bool aBindToTree, Element* aFormIdElement) override; + + void UpdateBarredFromConstraintValidation(); + + ElementInternals* GetElementInternals() const; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void RestoreFormAssociatedCustomElementState(); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLElement_h diff --git a/dom/html/HTMLEmbedElement.cpp b/dom/html/HTMLEmbedElement.cpp new file mode 100644 index 0000000000..34d79defbf --- /dev/null +++ b/dom/html/HTMLEmbedElement.cpp @@ -0,0 +1,244 @@ +/* -*- 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/BindContext.h" +#include "mozilla/dom/HTMLEmbedElement.h" +#include "mozilla/dom/HTMLEmbedElementBinding.h" +#include "mozilla/dom/ElementInlines.h" + +#include "mozilla/dom/Document.h" +#include "nsObjectLoadingContent.h" +#include "nsThreadUtils.h" +#include "nsIWidget.h" +#include "nsContentUtils.h" +#include "nsFrameLoader.h" +#ifdef XP_MACOSX +# include "mozilla/EventDispatcher.h" +# include "mozilla/dom/Event.h" +#endif + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Embed) + +namespace mozilla::dom { + +HTMLEmbedElement::HTMLEmbedElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetIsNetworkCreated(aFromParser == FROM_PARSER_NETWORK); +} + +HTMLEmbedElement::~HTMLEmbedElement() = default; + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLEmbedElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLEmbedElement, + nsGenericHTMLElement) + nsObjectLoadingContent::Traverse(tmp, cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLEmbedElement, + nsGenericHTMLElement) + nsObjectLoadingContent::Unlink(tmp); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED( + HTMLEmbedElement, nsGenericHTMLElement, nsIRequestObserver, + nsIStreamListener, nsFrameLoaderOwner, nsIObjectLoadingContent, + nsIChannelEventSink) + +NS_IMPL_ELEMENT_CLONE(HTMLEmbedElement) + +nsresult HTMLEmbedElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInComposedDoc()) { + void (HTMLEmbedElement::*start)() = &HTMLEmbedElement::StartObjectLoad; + nsContentUtils::AddScriptRunner( + NewRunnableMethod("dom::HTMLEmbedElement::BindToTree", this, start)); + } + + return NS_OK; +} + +void HTMLEmbedElement::UnbindFromTree(bool aNullParent) { + nsObjectLoadingContent::UnbindFromTree(aNullParent); + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +void HTMLEmbedElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aValue) { + AfterMaybeChangeAttr(aNamespaceID, aName, aNotify); + } + + if (aNamespaceID == kNameSpaceID_None && + aName == nsGkAtoms::allowfullscreen && mFrameLoader) { + if (auto* bc = mFrameLoader->GetExtantBrowsingContext()) { + MOZ_ALWAYS_SUCCEEDS(bc->SetFullscreenAllowedByOwner(AllowFullscreen())); + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLEmbedElement::OnAttrSetButNotChanged(int32_t aNamespaceID, + nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, aNotify); + return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName, + aValue, aNotify); +} + +void HTMLEmbedElement::AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, + bool aNotify) { + if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::src) { + return; + } + // If aNotify is false, we are coming from the parser or some such place; + // we'll get bound after all the attributes have been set, so we'll do the + // object load from BindToTree. + // Skip the LoadObject call in that case. + // We also don't want to start loading the object when we're not yet in + // a document, just in case that the caller wants to set additional + // attributes before inserting the node into the document. + if (!aNotify || !IsInComposedDoc() || BlockEmbedOrObjectContentLoading()) { + return; + } + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "HTMLEmbedElement::LoadObject", + [self = RefPtr<HTMLEmbedElement>(this), aNotify]() { + if (self->IsInComposedDoc()) { + self->LoadObject(aNotify, true); + } + })); +} + +int32_t HTMLEmbedElement::TabIndexDefault() { + // Only when we loaded a sub-document, <embed> should be tabbable by default + // because it's a navigable containers mentioned in 6.6.3 The tabindex + // attribute in the standard (see "If the value is null" section). + // https://html.spec.whatwg.org/#the-tabindex-attribute + // Otherwise, the default tab-index of <embed> is expected as -1 in a WPT: + // https://searchfox.org/mozilla-central/rev/7d98e651953f3135d91e98fa6d33efa131aec7ea/testing/web-platform/tests/html/interaction/focus/sequential-focus-navigation-and-the-tabindex-attribute/tabindex-getter.html#63 + return Type() == ObjectType::Document ? 0 : -1; +} + +bool HTMLEmbedElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + // Has non-plugin content: let the plugin decide what to do in terms of + // internal focus from mouse clicks + if (aTabIndex) { + *aTabIndex = TabIndex(); + } + + *aIsFocusable = true; + + // Let the plugin decide, so override. + return true; +} + +bool HTMLEmbedElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::align) { + return ParseAlignValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::hspace || aAttribute == nsGkAtoms::vspace) { + return aResult.ParseHTMLDimension(aValue); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +static void MapAttributesIntoRuleBase(MappedDeclarationsBuilder& aBuilder) { + nsGenericHTMLElement::MapImageMarginAttributeInto(aBuilder); + nsGenericHTMLElement::MapImageSizeAttributesInto(aBuilder); + nsGenericHTMLElement::MapImageAlignAttributeInto(aBuilder); +} + +static void MapAttributesIntoRuleExceptHidden( + MappedDeclarationsBuilder& aBuilder) { + MapAttributesIntoRuleBase(aBuilder); + nsGenericHTMLElement::MapCommonAttributesIntoExceptHidden(aBuilder); +} + +void HTMLEmbedElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + MapAttributesIntoRuleBase(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLEmbedElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = { + sCommonAttributeMap, + sImageMarginSizeAttributeMap, + sImageBorderAttributeMap, + sImageAlignAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLEmbedElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRuleExceptHidden; +} + +void HTMLEmbedElement::StartObjectLoad(bool aNotify, bool aForceLoad) { + // BindToTree can call us asynchronously, and we may be removed from the tree + // in the interim + if (!IsInComposedDoc() || !OwnerDoc()->IsActive() || + BlockEmbedOrObjectContentLoading()) { + return; + } + + LoadObject(aNotify, aForceLoad); + SetIsNetworkCreated(false); +} + +uint32_t HTMLEmbedElement::GetCapabilities() const { + return eAllowPluginSkipChannel | eSupportImages | eSupportDocuments; +} + +void HTMLEmbedElement::DestroyContent() { + nsObjectLoadingContent::Destroy(); + nsGenericHTMLElement::DestroyContent(); +} + +nsresult HTMLEmbedElement::CopyInnerTo(HTMLEmbedElement* aDest) { + nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest); + NS_ENSURE_SUCCESS(rv, rv); + + if (aDest->OwnerDoc()->IsStaticDocument()) { + CreateStaticClone(aDest); + } + + return rv; +} + +JSObject* HTMLEmbedElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLEmbedElement_Binding::Wrap(aCx, this, aGivenProto); +} + +nsContentPolicyType HTMLEmbedElement::GetContentPolicyType() const { + return nsIContentPolicy::TYPE_INTERNAL_EMBED; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLEmbedElement.h b/dom/html/HTMLEmbedElement.h new file mode 100644 index 0000000000..0cb82c0baa --- /dev/null +++ b/dom/html/HTMLEmbedElement.h @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLEmbedElement_h +#define mozilla_dom_HTMLEmbedElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsObjectLoadingContent.h" +#include "nsGkAtoms.h" +#include "nsError.h" + +namespace mozilla::dom { + +class HTMLEmbedElement final : public nsGenericHTMLElement, + public nsObjectLoadingContent { + public: + explicit HTMLEmbedElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLEmbedElement, embed) + + bool AllowFullscreen() const { + // We don't need to check prefixed attributes because Flash does not support + // them. + return IsRewrittenYoutubeEmbed() && GetBoolAttr(nsGkAtoms::allowfullscreen); + } + + // nsObjectLoadingContent + const Element* AsElement() const final { return this; } + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + + int32_t TabIndexDefault() override; + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + void DestroyContent() override; + + // nsObjectLoadingContent + uint32_t GetCapabilities() const override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + nsresult CopyInnerTo(HTMLEmbedElement* aDest); + + void StartObjectLoad() { StartObjectLoad(true, false); } + + virtual bool IsInteractiveHTMLContent() const override { return true; } + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLEmbedElement, + nsGenericHTMLElement) + + // WebIDL <embed> api + void GetAlign(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::align, aValue); } + void SetAlign(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::align, aValue, aRv); + } + void GetHeight(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::height, aValue); } + void SetHeight(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::height, aValue, aRv); + } + void GetName(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); } + void SetName(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aValue, aRv); + } + void GetWidth(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::width, aValue); } + void SetWidth(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::width, aValue, aRv); + } + // WebIDL <embed> api + void GetSrc(DOMString& aValue) { + GetURIAttr(nsGkAtoms::src, nullptr, aValue); + } + void SetSrc(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::src, aValue, aRv); + } + void GetType(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::type, aValue); } + void SetType(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::type, aValue, aRv); + } + Document* GetSVGDocument(nsIPrincipal& aSubjectPrincipal) { + return GetContentDocument(aSubjectPrincipal); + } + + /** + * Calls LoadObject with the correct arguments to start the plugin load. + */ + void StartObjectLoad(bool aNotify, bool aForceLoad); + + protected: + void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) override; + + private: + ~HTMLEmbedElement(); + + nsContentPolicyType GetContentPolicyType() const override; + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); + + /** + * This function is called by AfterSetAttr and OnAttrSetButNotChanged. + * It will not be called if the value is being unset. + * + * @param aNamespaceID the namespace of the attr being set + * @param aName the localname of the attribute being set + * @param aNotify Whether we plan to notify document observers. + */ + void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLEmbedElement_h diff --git a/dom/html/HTMLFieldSetElement.cpp b/dom/html/HTMLFieldSetElement.cpp new file mode 100644 index 0000000000..828bed9c8d --- /dev/null +++ b/dom/html/HTMLFieldSetElement.cpp @@ -0,0 +1,314 @@ +/* -*- 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/BasicEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/Maybe.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/CustomElementRegistry.h" +#include "mozilla/dom/HTMLFieldSetElement.h" +#include "mozilla/dom/HTMLFieldSetElementBinding.h" +#include "nsContentList.h" +#include "nsQueryObject.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(FieldSet) + +namespace mozilla::dom { + +HTMLFieldSetElement::HTMLFieldSetElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLFormControlElement(std::move(aNodeInfo), + FormControlType::Fieldset), + mElements(nullptr), + mFirstLegend(nullptr), + mInvalidElementsCount(0) { + // <fieldset> is always barred from constraint validation. + SetBarredFromConstraintValidation(true); + + // We start out enabled and valid. + AddStatesSilently(ElementState::ENABLED | ElementState::VALID); +} + +HTMLFieldSetElement::~HTMLFieldSetElement() { + uint32_t length = mDependentElements.Length(); + for (uint32_t i = 0; i < length; ++i) { + mDependentElements[i]->ForgetFieldSet(this); + } +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLFieldSetElement, + nsGenericHTMLFormControlElement, mValidity, + mElements) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLFieldSetElement, + nsGenericHTMLFormControlElement, + nsIConstraintValidation) + +NS_IMPL_ELEMENT_CLONE(HTMLFieldSetElement) + +bool HTMLFieldSetElement::IsDisabledForEvents(WidgetEvent* aEvent) { + if (StaticPrefs::dom_forms_fieldset_disable_only_descendants_enabled()) { + return false; + } + return IsElementDisabledForEvents(aEvent, nullptr); +} + +// nsIContent +void HTMLFieldSetElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + // Do not process any DOM events if the element is disabled. + aVisitor.mCanHandle = false; + if (IsDisabledForEvents(aVisitor.mEvent)) { + return; + } + + nsGenericHTMLFormControlElement::GetEventTargetParent(aVisitor); +} + +void HTMLFieldSetElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::disabled) { + // This *has* to be called *before* calling FieldSetDisabledChanged on our + // controls, as they may depend on our disabled state. + UpdateDisabledState(aNotify); + } + + return nsGenericHTMLFormControlElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLFieldSetElement::GetType(nsAString& aType) const { + aType.AssignLiteral("fieldset"); +} + +/* static */ +bool HTMLFieldSetElement::MatchListedElements(Element* aElement, + int32_t aNamespaceID, + nsAtom* aAtom, void* aData) { + nsCOMPtr<nsIFormControl> formControl = do_QueryInterface(aElement); + return formControl; +} + +nsIHTMLCollection* HTMLFieldSetElement::Elements() { + if (!mElements) { + mElements = + new nsContentList(this, MatchListedElements, nullptr, nullptr, true); + } + + return mElements; +} + +// nsIFormControl + +nsresult HTMLFieldSetElement::Reset() { return NS_OK; } + +void HTMLFieldSetElement::InsertChildBefore(nsIContent* aChild, + nsIContent* aBeforeThis, + bool aNotify, ErrorResult& aRv) { + bool firstLegendHasChanged = false; + + if (aChild->IsHTMLElement(nsGkAtoms::legend)) { + if (!mFirstLegend) { + mFirstLegend = aChild; + // We do not want to notify the first time mFirstElement is set. + } else { + // If mFirstLegend is before aIndex, we do not change it. + // Otherwise, mFirstLegend is now aChild. + const Maybe<uint32_t> indexOfRef = + aBeforeThis ? ComputeIndexOf(aBeforeThis) : Some(GetChildCount()); + const Maybe<uint32_t> indexOfFirstLegend = ComputeIndexOf(mFirstLegend); + if ((indexOfRef.isSome() && indexOfFirstLegend.isSome() && + *indexOfRef <= *indexOfFirstLegend) || + // XXX Keep the odd traditional behavior for now. + indexOfRef.isNothing()) { + mFirstLegend = aChild; + firstLegendHasChanged = true; + } + } + } + + nsGenericHTMLFormControlElement::InsertChildBefore(aChild, aBeforeThis, + aNotify, aRv); + if (aRv.Failed()) { + return; + } + + if (firstLegendHasChanged) { + NotifyElementsForFirstLegendChange(aNotify); + } +} + +void HTMLFieldSetElement::RemoveChildNode(nsIContent* aKid, bool aNotify) { + bool firstLegendHasChanged = false; + + if (mFirstLegend && aKid == mFirstLegend) { + // If we are removing the first legend we have to found another one. + nsIContent* child = mFirstLegend->GetNextSibling(); + mFirstLegend = nullptr; + firstLegendHasChanged = true; + + for (; child; child = child->GetNextSibling()) { + if (child->IsHTMLElement(nsGkAtoms::legend)) { + mFirstLegend = child; + break; + } + } + } + + nsGenericHTMLFormControlElement::RemoveChildNode(aKid, aNotify); + + if (firstLegendHasChanged) { + NotifyElementsForFirstLegendChange(aNotify); + } +} + +void HTMLFieldSetElement::AddElement(nsGenericHTMLFormElement* aElement) { + mDependentElements.AppendElement(aElement); + + // If the element that we are adding aElement is a fieldset, then all the + // invalid elements in aElement are also invalid elements of this. + HTMLFieldSetElement* fieldSet = FromNode(aElement); + if (fieldSet) { + for (int32_t i = 0; i < fieldSet->mInvalidElementsCount; i++) { + UpdateValidity(false); + } + return; + } + + // If the element is a form-associated custom element, adding element might be + // caused by FACE upgrade which won't trigger mutation observer, so mark + // mElements dirty manually here. + CustomElementData* data = aElement->GetCustomElementData(); + if (data && data->IsFormAssociated() && mElements) { + mElements->SetDirty(); + } + + // We need to update the validity of the fieldset. + nsCOMPtr<nsIConstraintValidation> cvElmt = do_QueryObject(aElement); + if (cvElmt && cvElmt->IsCandidateForConstraintValidation() && + !cvElmt->IsValid()) { + UpdateValidity(false); + } + +#if DEBUG + int32_t debugInvalidElementsCount = 0; + for (uint32_t i = 0; i < mDependentElements.Length(); i++) { + HTMLFieldSetElement* fieldSet = FromNode(mDependentElements[i]); + if (fieldSet) { + debugInvalidElementsCount += fieldSet->mInvalidElementsCount; + continue; + } + nsCOMPtr<nsIConstraintValidation> cvElmt = + do_QueryObject(mDependentElements[i]); + if (cvElmt && cvElmt->IsCandidateForConstraintValidation() && + !(cvElmt->IsValid())) { + debugInvalidElementsCount += 1; + } + } + MOZ_ASSERT(debugInvalidElementsCount == mInvalidElementsCount); +#endif +} + +void HTMLFieldSetElement::RemoveElement(nsGenericHTMLFormElement* aElement) { + mDependentElements.RemoveElement(aElement); + + // If the element that we are removing aElement is a fieldset, then all the + // invalid elements in aElement are also removed from this. + HTMLFieldSetElement* fieldSet = FromNode(aElement); + if (fieldSet) { + for (int32_t i = 0; i < fieldSet->mInvalidElementsCount; i++) { + UpdateValidity(true); + } + return; + } + + // We need to update the validity of the fieldset. + nsCOMPtr<nsIConstraintValidation> cvElmt = do_QueryObject(aElement); + if (cvElmt && cvElmt->IsCandidateForConstraintValidation() && + !cvElmt->IsValid()) { + UpdateValidity(true); + } + +#if DEBUG + int32_t debugInvalidElementsCount = 0; + for (uint32_t i = 0; i < mDependentElements.Length(); i++) { + HTMLFieldSetElement* fieldSet = FromNode(mDependentElements[i]); + if (fieldSet) { + debugInvalidElementsCount += fieldSet->mInvalidElementsCount; + continue; + } + nsCOMPtr<nsIConstraintValidation> cvElmt = + do_QueryObject(mDependentElements[i]); + if (cvElmt && cvElmt->IsCandidateForConstraintValidation() && + !(cvElmt->IsValid())) { + debugInvalidElementsCount += 1; + } + } + MOZ_ASSERT(debugInvalidElementsCount == mInvalidElementsCount); +#endif +} + +void HTMLFieldSetElement::UpdateDisabledState(bool aNotify) { + nsGenericHTMLFormControlElement::UpdateDisabledState(aNotify); + + for (nsGenericHTMLFormElement* element : mDependentElements) { + element->FieldSetDisabledChanged(aNotify); + } +} + +void HTMLFieldSetElement::NotifyElementsForFirstLegendChange(bool aNotify) { + /** + * NOTE: this could be optimized if only call when the fieldset is currently + * disabled. + * This should also make sure that mElements is set when we happen to be here. + * However, this method shouldn't be called very often in normal use cases. + */ + if (!mElements) { + mElements = + new nsContentList(this, MatchListedElements, nullptr, nullptr, true); + } + + uint32_t length = mElements->Length(true); + for (uint32_t i = 0; i < length; ++i) { + static_cast<nsGenericHTMLFormElement*>(mElements->Item(i)) + ->FieldSetFirstLegendChanged(aNotify); + } +} + +void HTMLFieldSetElement::UpdateValidity(bool aElementValidity) { + if (aElementValidity) { + --mInvalidElementsCount; + } else { + ++mInvalidElementsCount; + } + + MOZ_ASSERT(mInvalidElementsCount >= 0); + + // The fieldset validity has just changed if: + // - there are no more invalid elements ; + // - or there is one invalid elmement and an element just became invalid. + if (!mInvalidElementsCount || + (mInvalidElementsCount == 1 && !aElementValidity)) { + AutoStateChangeNotifier notifier(*this, true); + RemoveStatesSilently(ElementState::VALID | ElementState::INVALID); + AddStatesSilently(mInvalidElementsCount ? ElementState::INVALID + : ElementState::VALID); + } + + // We should propagate the change to the fieldset parent chain. + if (mFieldSet) { + mFieldSet->UpdateValidity(aElementValidity); + } +} + +JSObject* HTMLFieldSetElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLFieldSetElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLFieldSetElement.h b/dom/html/HTMLFieldSetElement.h new file mode 100644 index 0000000000..4b592937b2 --- /dev/null +++ b/dom/html/HTMLFieldSetElement.h @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLFieldSetElement_h +#define mozilla_dom_HTMLFieldSetElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/ConstraintValidation.h" +#include "mozilla/dom/ValidityState.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla { +class ErrorResult; +class EventChainPreVisitor; +namespace dom { +class FormData; + +class HTMLFieldSetElement final : public nsGenericHTMLFormControlElement, + public ConstraintValidation { + public: + using ConstraintValidation::GetValidationMessage; + using ConstraintValidation::SetCustomValidity; + + explicit HTMLFieldSetElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLFieldSetElement, fieldset) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + // nsINode + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // nsIContent + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + void InsertChildBefore(nsIContent* aChild, nsIContent* aBeforeThis, + bool aNotify, ErrorResult& aRv) override; + void RemoveChildNode(nsIContent* aKid, bool aNotify) override; + + // nsGenericHTMLElement + bool IsDisabledForEvents(WidgetEvent* aEvent) override; + + // nsIFormControl + NS_IMETHOD Reset() override; + NS_IMETHOD SubmitNamesValues(FormData* aFormData) override { return NS_OK; } + + const nsIContent* GetFirstLegend() const { return mFirstLegend; } + + void AddElement(nsGenericHTMLFormElement* aElement); + + void RemoveElement(nsGenericHTMLFormElement* aElement); + + // nsGenericHTMLFormElement + void UpdateDisabledState(bool aNotify) override; + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLFieldSetElement, + nsGenericHTMLFormControlElement) + + // WebIDL + bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); } + void SetDisabled(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::disabled, aValue, aRv); + } + + void GetName(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); } + + void SetName(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aValue, aRv); + } + + void GetType(nsAString& aType) const; + + nsIHTMLCollection* Elements(); + + // XPCOM WillValidate is OK for us + + // XPCOM Validity is OK for us + + // XPCOM GetValidationMessage is OK for us + + // XPCOM CheckValidity is OK for us + + // XPCOM SetCustomValidity is OK for us + + /* + * This method will update the fieldset's validity. This method has to be + * called by fieldset elements whenever their validity state or status + * regarding constraint validation changes. + * + * @note If an element becomes barred from constraint validation, it has to + * be considered as valid. + * + * @param aElementValidityState the new validity state of the element + */ + void UpdateValidity(bool aElementValidityState); + + protected: + virtual ~HTMLFieldSetElement(); + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + /** + * Notify all elements (in mElements) that the first legend of the fieldset + * has now changed. + */ + void NotifyElementsForFirstLegendChange(bool aNotify); + + // This function is used to generate the nsContentList (listed form elements). + static bool MatchListedElements(Element* aElement, int32_t aNamespaceID, + nsAtom* aAtom, void* aData); + + // listed form controls elements. + RefPtr<nsContentList> mElements; + + // List of elements which have this fieldset as first fieldset ancestor. + nsTArray<nsGenericHTMLFormElement*> mDependentElements; + + nsIContent* mFirstLegend; + + /** + * Number of invalid and candidate for constraint validation + * elements in the fieldSet the last time UpdateValidity has been called. + * + * @note Should only be used by UpdateValidity() + */ + int32_t mInvalidElementsCount; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_HTMLFieldSetElement_h */ diff --git a/dom/html/HTMLFontElement.cpp b/dom/html/HTMLFontElement.cpp new file mode 100644 index 0000000000..fb57407a87 --- /dev/null +++ b/dom/html/HTMLFontElement.cpp @@ -0,0 +1,106 @@ +/* -*- 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 "HTMLFontElement.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLFontElementBinding.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsAttrValueInlines.h" +#include "nsContentUtils.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Font) + +namespace mozilla::dom { + +HTMLFontElement::~HTMLFontElement() = default; + +JSObject* HTMLFontElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLFontElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_ELEMENT_CLONE(HTMLFontElement) + +bool HTMLFontElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::size) { + int32_t size = nsContentUtils::ParseLegacyFontSize(aValue); + if (size) { + aResult.SetTo(size, &aValue); + return true; + } + return false; + } + if (aAttribute == nsGkAtoms::color) { + return aResult.ParseColor(aValue); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLFontElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + // face: string list + if (!aBuilder.PropertyIsSet(eCSSProperty_font_family)) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::face); + if (value && value->Type() == nsAttrValue::eString && + !value->IsEmptyString()) { + aBuilder.SetFontFamily(NS_ConvertUTF16toUTF8(value->GetStringValue())); + } + } + // size: int + if (!aBuilder.PropertyIsSet(eCSSProperty_font_size)) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::size); + if (value && value->Type() == nsAttrValue::eInteger) { + aBuilder.SetKeywordValue(eCSSProperty_font_size, + value->GetIntegerValue()); + } + } + if (!aBuilder.PropertyIsSet(eCSSProperty_color)) { + // color: color + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::color); + nscolor color; + if (value && value->GetColorValue(color)) { + aBuilder.SetColorValue(eCSSProperty_color, color); + } + } + if (aBuilder.Document().GetCompatibilityMode() == eCompatibility_NavQuirks) { + // Make <a><font color="red">text</font></a> give the text a red underline + // in quirks mode. The StyleTextDecorationLine_COLOR_OVERRIDE flag only + // affects quirks mode rendering. + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::color); + nscolor color; + if (value && value->GetColorValue(color)) { + aBuilder.SetTextDecorationColorOverride(); + } + } + + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLFontElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::face}, {nsGkAtoms::size}, {nsGkAtoms::color}, {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLFontElement::GetAttributeMappingFunction() const { + return &MapAttributesIntoRule; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLFontElement.h b/dom/html/HTMLFontElement.h new file mode 100644 index 0000000000..ddaa0e8944 --- /dev/null +++ b/dom/html/HTMLFontElement.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef HTMLFontElement_h___ +#define HTMLFontElement_h___ + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLFontElement final : public nsGenericHTMLElement { + public: + explicit HTMLFontElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + void GetColor(DOMString& aColor) { GetHTMLAttr(nsGkAtoms::color, aColor); } + void SetColor(const nsAString& aColor, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::color, aColor, aError); + } + void GetFace(DOMString& aFace) { GetHTMLAttr(nsGkAtoms::face, aFace); } + void SetFace(const nsAString& aFace, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::face, aFace, aError); + } + void GetSize(DOMString& aSize) { GetHTMLAttr(nsGkAtoms::size, aSize); } + void SetSize(const nsAString& aSize, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::size, aSize, aError); + } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLFontElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif /* HTMLFontElement_h___ */ diff --git a/dom/html/HTMLFormControlsCollection.cpp b/dom/html/HTMLFormControlsCollection.cpp new file mode 100644 index 0000000000..aa10daceeb --- /dev/null +++ b/dom/html/HTMLFormControlsCollection.cpp @@ -0,0 +1,305 @@ +/* -*- 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/HTMLFormControlsCollection.h" + +#include "mozilla/FlushType.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLFormControlsCollectionBinding.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "nsGenericHTMLElement.h" // nsGenericHTMLFormElement +#include "nsQueryObject.h" +#include "nsIFormControl.h" +#include "RadioNodeList.h" +#include "jsfriendapi.h" + +namespace mozilla::dom { + +/* static */ +bool HTMLFormControlsCollection::ShouldBeInElements( + nsIFormControl* aFormControl) { + // For backwards compatibility (with 4.x and IE) we must not add + // <input type=image> elements to the list of form controls in a + // form. + + switch (aFormControl->ControlType()) { + case FormControlType::ButtonButton: + case FormControlType::ButtonReset: + case FormControlType::ButtonSubmit: + case FormControlType::InputButton: + case FormControlType::InputCheckbox: + case FormControlType::InputColor: + case FormControlType::InputEmail: + case FormControlType::InputFile: + case FormControlType::InputHidden: + case FormControlType::InputReset: + case FormControlType::InputPassword: + case FormControlType::InputRadio: + case FormControlType::InputSearch: + case FormControlType::InputSubmit: + case FormControlType::InputText: + case FormControlType::InputTel: + case FormControlType::InputUrl: + case FormControlType::InputNumber: + case FormControlType::InputRange: + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputMonth: + case FormControlType::InputWeek: + case FormControlType::InputDatetimeLocal: + case FormControlType::Select: + case FormControlType::Textarea: + case FormControlType::Fieldset: + case FormControlType::Object: + case FormControlType::Output: + case FormControlType::FormAssociatedCustomElement: + return true; + + // These form control types are not supposed to end up in the + // form.elements array + // XXXbz maybe we should just return aType != InputImage or something + // instead of the big switch? + case FormControlType::InputImage: + break; + } + return false; +} + +HTMLFormControlsCollection::HTMLFormControlsCollection(HTMLFormElement* aForm) + : mForm(aForm), + mNameLookupTable(HTMLFormElement::FORM_CONTROL_LIST_HASHTABLE_LENGTH) {} + +HTMLFormControlsCollection::~HTMLFormControlsCollection() { + mForm = nullptr; + Clear(); +} + +void HTMLFormControlsCollection::DropFormReference() { + mForm = nullptr; + Clear(); +} + +void HTMLFormControlsCollection::Clear() { + // Null out childrens' pointer to me. No refcounting here + for (nsGenericHTMLFormElement* element : Reversed(mElements.AsList())) { + nsCOMPtr<nsIFormControl> formControl = do_QueryObject(element); + MOZ_ASSERT(formControl); + formControl->ClearForm(false, false); + } + mElements.Clear(); + + for (nsGenericHTMLFormElement* element : Reversed(mNotInElements.AsList())) { + nsCOMPtr<nsIFormControl> formControl = do_QueryObject(element); + MOZ_ASSERT(formControl); + formControl->ClearForm(false, false); + } + mNotInElements.Clear(); + + mNameLookupTable.Clear(); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLFormControlsCollection) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HTMLFormControlsCollection) + // Note: We intentionally don't set tmp->mForm to nullptr here, since doing + // so may result in crashes because of inconsistent null-checking after the + // object gets unlinked. + tmp->Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(HTMLFormControlsCollection) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNameLookupTable) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(HTMLFormControlsCollection) + NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +// XPConnect interface list for HTMLFormControlsCollection +NS_INTERFACE_TABLE_HEAD(HTMLFormControlsCollection) + NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY + NS_INTERFACE_TABLE(HTMLFormControlsCollection, nsIHTMLCollection) + NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(HTMLFormControlsCollection) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLFormControlsCollection) +NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLFormControlsCollection) + +// nsIHTMLCollection interfac + +uint32_t HTMLFormControlsCollection::Length() { return mElements->Length(); } + +nsISupports* HTMLFormControlsCollection::NamedItemInternal( + const nsAString& aName) { + return mNameLookupTable.GetWeak(aName); +} + +nsresult HTMLFormControlsCollection::AddElementToTable( + nsGenericHTMLFormElement* aChild, const nsAString& aName) { + nsCOMPtr<nsIFormControl> formControl = do_QueryObject(aChild); + MOZ_ASSERT(formControl); + if (!ShouldBeInElements(formControl)) { + return NS_OK; + } + + return mForm->AddElementToTableInternal(mNameLookupTable, aChild, aName); +} + +nsresult HTMLFormControlsCollection::IndexOfContent(nsIContent* aContent, + int32_t* aIndex) { + // Note -- not a DOM method; callers should handle flushing themselves + + NS_ENSURE_ARG_POINTER(aIndex); + *aIndex = mElements->IndexOf(aContent); + return NS_OK; +} + +nsresult HTMLFormControlsCollection::RemoveElementFromTable( + nsGenericHTMLFormElement* aChild, const nsAString& aName) { + nsCOMPtr<nsIFormControl> formControl = do_QueryObject(aChild); + MOZ_ASSERT(formControl); + if (!ShouldBeInElements(formControl)) { + return NS_OK; + } + + return mForm->RemoveElementFromTableInternal(mNameLookupTable, aChild, aName); +} + +nsresult HTMLFormControlsCollection::GetSortedControls( + nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls) const { +#ifdef DEBUG + HTMLFormElement::AssertDocumentOrder(mElements, mForm); + HTMLFormElement::AssertDocumentOrder(mNotInElements, mForm); +#endif + + aControls.Clear(); + + // Merge the elements list and the not in elements list. Both lists are + // already sorted. + uint32_t elementsLen = mElements->Length(); + uint32_t notInElementsLen = mNotInElements->Length(); + aControls.SetCapacity(elementsLen + notInElementsLen); + + uint32_t elementsIdx = 0; + uint32_t notInElementsIdx = 0; + + while (elementsIdx < elementsLen || notInElementsIdx < notInElementsLen) { + // Check whether we're done with mElements + if (elementsIdx == elementsLen) { + NS_ASSERTION(notInElementsIdx < notInElementsLen, + "Should have remaining not-in-elements"); + // Append the remaining mNotInElements elements + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + aControls.AppendElements(mNotInElements->Elements() + notInElementsIdx, + notInElementsLen - notInElementsIdx); + break; + } + // Check whether we're done with mNotInElements + if (notInElementsIdx == notInElementsLen) { + NS_ASSERTION(elementsIdx < elementsLen, + "Should have remaining in-elements"); + // Append the remaining mElements elements + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + aControls.AppendElements(mElements->Elements() + elementsIdx, + elementsLen - elementsIdx); + break; + } + // Both lists have elements left. + NS_ASSERTION(mElements->ElementAt(elementsIdx) && + mNotInElements->ElementAt(notInElementsIdx), + "Should have remaining elements"); + // Determine which of the two elements should be ordered + // first and add it to the end of the list. + nsGenericHTMLFormElement* elementToAdd; + if (nsContentUtils::CompareTreePosition<TreeKind::DOM>( + mElements->ElementAt(elementsIdx), + mNotInElements->ElementAt(notInElementsIdx), mForm) < 0) { + elementToAdd = mElements->ElementAt(elementsIdx); + ++elementsIdx; + } else { + elementToAdd = mNotInElements->ElementAt(notInElementsIdx); + ++notInElementsIdx; + } + // Add the first element to the list. + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + aControls.AppendElement(elementToAdd); + } + + NS_ASSERTION(aControls.Length() == elementsLen + notInElementsLen, + "Not all form controls were added to the sorted list"); +#ifdef DEBUG + HTMLFormElement::AssertDocumentOrder(aControls, mForm); +#endif + + return NS_OK; +} + +Element* HTMLFormControlsCollection::GetElementAt(uint32_t aIndex) { + return mElements->SafeElementAt(aIndex, nullptr); +} + +/* virtual */ +nsINode* HTMLFormControlsCollection::GetParentObject() { return mForm; } + +/* virtual */ +Element* HTMLFormControlsCollection::GetFirstNamedElement( + const nsAString& aName, bool& aFound) { + Nullable<OwningRadioNodeListOrElement> maybeResult; + NamedGetter(aName, aFound, maybeResult); + if (!aFound) { + return nullptr; + } + MOZ_ASSERT(!maybeResult.IsNull()); + const OwningRadioNodeListOrElement& result = maybeResult.Value(); + if (result.IsElement()) { + return result.GetAsElement().get(); + } + if (result.IsRadioNodeList()) { + RadioNodeList& nodelist = result.GetAsRadioNodeList(); + return nodelist.Item(0)->AsElement(); + } + MOZ_ASSERT_UNREACHABLE("Should only have Elements and NodeLists here."); + return nullptr; +} + +void HTMLFormControlsCollection::NamedGetter( + const nsAString& aName, bool& aFound, + Nullable<OwningRadioNodeListOrElement>& aResult) { + nsISupports* item = NamedItemInternal(aName); + if (!item) { + aFound = false; + return; + } + aFound = true; + if (nsCOMPtr<Element> element = do_QueryInterface(item)) { + aResult.SetValue().SetAsElement() = element; + return; + } + if (nsCOMPtr<RadioNodeList> nodelist = do_QueryInterface(item)) { + aResult.SetValue().SetAsRadioNodeList() = nodelist; + return; + } + MOZ_ASSERT_UNREACHABLE("Should only have Elements and NodeLists here."); +} + +void HTMLFormControlsCollection::GetSupportedNames(nsTArray<nsString>& aNames) { + // Just enumerate mNameLookupTable. This won't guarantee order, but + // that's OK, because the HTML5 spec doesn't define an order for + // this enumeration. + AppendToArray(aNames, mNameLookupTable.Keys()); +} + +/* virtual */ +JSObject* HTMLFormControlsCollection::WrapObject( + JSContext* aCx, JS::Handle<JSObject*> aGivenProto) { + return HTMLFormControlsCollection_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLFormControlsCollection.h b/dom/html/HTMLFormControlsCollection.h new file mode 100644 index 0000000000..367c230b84 --- /dev/null +++ b/dom/html/HTMLFormControlsCollection.h @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLFormControlsCollection_h +#define mozilla_dom_HTMLFormControlsCollection_h + +#include "nsIHTMLCollection.h" +#include "nsInterfaceHashtable.h" +#include "mozilla/dom/TreeOrderedArray.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" + +class nsGenericHTMLFormElement; +class nsIContent; +class nsIFormControl; +template <class T> +class RefPtr; + +namespace mozilla::dom { +class Element; +class HTMLFormElement; +class HTMLImageElement; +class OwningRadioNodeListOrElement; +template <typename> +struct Nullable; + +class HTMLFormControlsCollection final : public nsIHTMLCollection, + public nsWrapperCache { + public: + explicit HTMLFormControlsCollection(HTMLFormElement* aForm); + + void DropFormReference(); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + virtual uint32_t Length() override; + virtual Element* GetElementAt(uint32_t index) override; + virtual nsINode* GetParentObject() override; + + virtual Element* GetFirstNamedElement(const nsAString& aName, + bool& aFound) override; + + void NamedGetter(const nsAString& aName, bool& aFound, + Nullable<OwningRadioNodeListOrElement>& aResult); + void NamedItem(const nsAString& aName, + Nullable<OwningRadioNodeListOrElement>& aResult) { + bool dummy; + NamedGetter(aName, dummy, aResult); + } + virtual void GetSupportedNames(nsTArray<nsString>& aNames) override; + + nsresult AddElementToTable(nsGenericHTMLFormElement* aChild, + const nsAString& aName); + nsresult AddImageElementToTable(HTMLImageElement* aChild, + const nsAString& aName); + nsresult RemoveElementFromTable(nsGenericHTMLFormElement* aChild, + const nsAString& aName); + nsresult IndexOfContent(nsIContent* aContent, int32_t* aIndex); + + nsISupports* NamedItemInternal(const nsAString& aName); + + /** + * Create a sorted list of form control elements. This list is sorted + * in document order and contains the controls in the mElements and + * mNotInElements list. This function does not add references to the + * elements. + * + * @param aControls The list of sorted controls[out]. + * @return NS_OK or NS_ERROR_OUT_OF_MEMORY. + */ + nsresult GetSortedControls( + nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls) const; + + // nsWrapperCache + using nsWrapperCache::GetWrapper; + using nsWrapperCache::GetWrapperPreserveColor; + using nsWrapperCache::PreserveWrapper; + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + virtual ~HTMLFormControlsCollection(); + virtual JSObject* GetWrapperPreserveColorInternal() override { + return nsWrapperCache::GetWrapperPreserveColor(); + } + virtual void PreserveWrapperInternal( + nsISupports* aScriptObjectHolder) override { + nsWrapperCache::PreserveWrapper(aScriptObjectHolder); + } + + public: + static bool ShouldBeInElements(nsIFormControl* aFormControl); + + HTMLFormElement* mForm; // WEAK - the form owns me + + // Holds WEAK references - bug 36639 + // NOTE(emilio): These are not guaranteed to be descendants of mForm, because + // of the form attribute, though that's likely. + TreeOrderedArray<nsGenericHTMLFormElement*> mElements; + + // This array holds on to all form controls that are not contained + // in mElements (form.elements in JS, see ShouldBeInFormControl()). + // This is needed to properly clean up the bi-directional references + // (both weak and strong) between the form and its form controls. + TreeOrderedArray<nsGenericHTMLFormElement*> mNotInElements; + + NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(HTMLFormControlsCollection) + + protected: + // Drop all our references to the form elements + void Clear(); + + // A map from an ID or NAME attribute to the form control(s), this + // hash holds strong references either to the named form control, or + // to a list of named form controls, in the case where this hash + // holds on to a list of named form controls the list has weak + // references to the form control. + + nsInterfaceHashtable<nsStringHashKey, nsISupports> mNameLookupTable; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLFormControlsCollection_h diff --git a/dom/html/HTMLFormElement.cpp b/dom/html/HTMLFormElement.cpp new file mode 100644 index 0000000000..46f2c2a735 --- /dev/null +++ b/dom/html/HTMLFormElement.cpp @@ -0,0 +1,2054 @@ +/* -*- 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/HTMLFormElement.h" + +#include <utility> + +#include "Attr.h" +#include "jsapi.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/BinarySearch.h" +#include "mozilla/Components.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/PresShell.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CustomEvent.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLFormControlsCollection.h" +#include "mozilla/dom/HTMLFormElementBinding.h" +#include "mozilla/dom/TreeOrderedArrayInlines.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/dom/nsMixedContentBlocker.h" +#include "nsCOMArray.h" +#include "nsContentList.h" +#include "nsContentUtils.h" +#include "nsDOMAttributeMap.h" +#include "nsDocShell.h" +#include "nsDocShellLoadState.h" +#include "nsError.h" +#include "nsFocusManager.h" +#include "nsGkAtoms.h" +#include "nsHTMLDocument.h" +#include "nsIFormControlFrame.h" +#include "nsInterfaceHashtable.h" +#include "nsPresContext.h" +#include "nsQueryObject.h" +#include "nsStyleConsts.h" +#include "nsTArray.h" + +// form submission +#include "HTMLFormSubmissionConstants.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/FormDataEvent.h" +#include "mozilla/dom/SubmitEvent.h" +#include "mozilla/Telemetry.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_prompts.h" +#include "nsCategoryManagerUtils.h" +#include "nsIContentInlines.h" +#include "nsISimpleEnumerator.h" +#include "nsRange.h" +#include "nsIScriptError.h" +#include "nsIScriptSecurityManager.h" +#include "nsNetUtil.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIDocShell.h" +#include "nsIPromptService.h" +#include "nsISecurityUITelemetry.h" +#include "nsIStringBundle.h" + +// radio buttons +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLButtonElement.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "nsIRadioVisitor.h" +#include "RadioNodeList.h" + +#include "nsLayoutUtils.h" + +#include "mozAutoDocUpdate.h" +#include "nsIHTMLCollection.h" + +#include "nsIConstraintValidation.h" + +#include "nsSandboxFlags.h" + +#include "mozilla/dom/HTMLAnchorElement.h" + +// images +#include "mozilla/dom/HTMLImageElement.h" +#include "mozilla/dom/HTMLButtonElement.h" + +// construction, destruction +NS_IMPL_NS_NEW_HTML_ELEMENT(Form) + +namespace mozilla::dom { + +static const uint8_t NS_FORM_AUTOCOMPLETE_ON = 1; +static const uint8_t NS_FORM_AUTOCOMPLETE_OFF = 0; + +static const nsAttrValue::EnumTable kFormAutocompleteTable[] = { + {"on", NS_FORM_AUTOCOMPLETE_ON}, + {"off", NS_FORM_AUTOCOMPLETE_OFF}, + {nullptr, 0}}; +// Default autocomplete value is 'on'. +static const nsAttrValue::EnumTable* kFormDefaultAutocomplete = + &kFormAutocompleteTable[0]; + +HTMLFormElement::HTMLFormElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mControls(new HTMLFormControlsCollection(this)), + mPendingSubmission(nullptr), + mDefaultSubmitElement(nullptr), + mFirstSubmitInElements(nullptr), + mFirstSubmitNotInElements(nullptr), + mImageNameLookupTable(FORM_CONTROL_LIST_HASHTABLE_LENGTH), + mPastNameLookupTable(FORM_CONTROL_LIST_HASHTABLE_LENGTH), + mSubmitPopupState(PopupBlocker::openAbused), + mInvalidElementsCount(0), + mFormNumber(-1), + mGeneratingSubmit(false), + mGeneratingReset(false), + mDeferSubmission(false), + mNotifiedObservers(false), + mNotifiedObserversResult(false), + mIsConstructingEntryList(false), + mIsFiringSubmissionEvents(false) { + // We start out valid. + AddStatesSilently(ElementState::VALID); +} + +HTMLFormElement::~HTMLFormElement() { + if (mControls) { + mControls->DropFormReference(); + } + + Clear(); +} + +// nsISupports + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLFormElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLFormElement, + nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControls) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mImageNameLookupTable) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPastNameLookupTable) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTargetContext) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLFormElement, + nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRelList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTargetContext) + tmp->Clear(); + tmp->mExpandoAndGeneration.OwnerUnlinked(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLFormElement, + nsGenericHTMLElement) + +// EventTarget +void HTMLFormElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) { + if (aEvent->mEventType == u"DOMFormHasPassword"_ns) { + mHasPendingPasswordEvent = false; + } else if (aEvent->mEventType == u"DOMFormHasPossibleUsername"_ns) { + mHasPendingPossibleUsernameEvent = false; + } +} + +nsDOMTokenList* HTMLFormElement::RelList() { + if (!mRelList) { + mRelList = + new nsDOMTokenList(this, nsGkAtoms::rel, sAnchorAndFormRelValues); + } + return mRelList; +} + +NS_IMPL_ELEMENT_CLONE(HTMLFormElement) + +HTMLFormControlsCollection* HTMLFormElement::Elements() { return mControls; } + +void HTMLFormElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::action || aName == nsGkAtoms::target) { + // Don't forget we've notified the password manager already if the + // page sets the action/target in the during submit. (bug 343182) + bool notifiedObservers = mNotifiedObservers; + ForgetCurrentSubmission(); + mNotifiedObservers = notifiedObservers; + } + } + + return nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue, + aNotify); +} + +void HTMLFormElement::GetAutocomplete(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::autocomplete, kFormDefaultAutocomplete->tag, aValue); +} + +void HTMLFormElement::GetEnctype(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::enctype, kFormDefaultEnctype->tag, aValue); +} + +void HTMLFormElement::GetMethod(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::method, kFormDefaultMethod->tag, aValue); +} + +void HTMLFormElement::ReportInvalidUnfocusableElements() { + RefPtr<nsFocusManager> focusManager = nsFocusManager::GetFocusManager(); + MOZ_ASSERT(focusManager); + + // This shouldn't be called recursively, so use a rather large value + // for the preallocated buffer. + AutoTArray<RefPtr<nsGenericHTMLFormElement>, 100> sortedControls; + if (NS_FAILED(mControls->GetSortedControls(sortedControls))) { + return; + } + + for (auto& _e : sortedControls) { + // MOZ_CAN_RUN_SCRIPT requires explicit copy, Bug 1620312 + RefPtr<nsGenericHTMLFormElement> element = _e; + bool isFocusable = false; + focusManager->ElementIsFocusable(element, 0, &isFocusable); + if (!isFocusable) { + nsTArray<nsString> params; + nsAutoCString messageName("InvalidFormControlUnfocusable"); + + if (Attr* nameAttr = element->GetAttributes()->GetNamedItem(u"name"_ns)) { + nsAutoString name; + nameAttr->GetValue(name); + params.AppendElement(name); + messageName = "InvalidNamedFormControlUnfocusable"; + } + + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, "DOM"_ns, element->GetOwnerDocument(), + nsContentUtils::eDOM_PROPERTIES, messageName.get(), params, + element->GetBaseURI()); + } + } +} + +// https://html.spec.whatwg.org/multipage/forms.html#concept-form-submit +void HTMLFormElement::MaybeSubmit(Element* aSubmitter) { +#ifdef DEBUG + if (aSubmitter) { + nsCOMPtr<nsIFormControl> fc = do_QueryInterface(aSubmitter); + MOZ_ASSERT(fc); + MOZ_ASSERT(fc->IsSubmitControl(), "aSubmitter is not a submit control?"); + } +#endif + + // 1-4 of + // https://html.spec.whatwg.org/multipage/forms.html#concept-form-submit + Document* doc = GetComposedDoc(); + if (mIsConstructingEntryList || !doc || + (doc->GetSandboxFlags() & SANDBOXED_FORMS)) { + return; + } + + // 5.1. If form's firing submission events is true, then return. + if (mIsFiringSubmissionEvents) { + return; + } + + // 5.2. Set form's firing submission events to true. + AutoRestore<bool> resetFiringSubmissionEventsFlag(mIsFiringSubmissionEvents); + mIsFiringSubmissionEvents = true; + + // Flag elements as user-interacted. + // FIXME: Should be specified, see: + // https://github.com/whatwg/html/issues/10066 + { + for (nsGenericHTMLFormElement* el : mControls->mElements.AsList()) { + el->SetUserInteracted(true); + } + for (nsGenericHTMLFormElement* el : mControls->mNotInElements.AsList()) { + el->SetUserInteracted(true); + } + } + + // 5.3. If the submitter element's no-validate state is false, then + // interactively validate the constraints of form and examine the result. + // If the result is negative (i.e., the constraint validation concluded + // that there were invalid fields and probably informed the user of this) + bool noValidateState = + HasAttr(nsGkAtoms::novalidate) || + (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formnovalidate)); + if (!noValidateState && !CheckValidFormSubmission()) { + ReportInvalidUnfocusableElements(); + return; + } + + RefPtr<PresShell> presShell = doc->GetPresShell(); + if (!presShell) { + // We need the nsPresContext for dispatching the submit event. In some + // rare cases we need to flush notifications to force creation of the + // nsPresContext here (for example when a script calls form.requestSubmit() + // from script early during page load). We only flush the notifications + // if the PresShell hasn't been created yet, to limit the performance + // impact. + doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames); + presShell = doc->GetPresShell(); + } + + // If |PresShell::Destroy| has been called due to handling the event the pres + // context will return a null pres shell. See bug 125624. Using presShell to + // dispatch the event. It makes sure that event is not handled if the window + // is being destroyed. + if (presShell) { + SubmitEventInit init; + init.mBubbles = true; + init.mCancelable = true; + init.mSubmitter = + aSubmitter ? nsGenericHTMLElement::FromNode(aSubmitter) : nullptr; + RefPtr<SubmitEvent> event = + SubmitEvent::Constructor(this, u"submit"_ns, init); + event->SetTrusted(true); + nsEventStatus status = nsEventStatus_eIgnore; + presShell->HandleDOMEventWithTarget(this, event, &status); + } +} + +void HTMLFormElement::MaybeReset(Element* aSubmitter) { + // If |PresShell::Destroy| has been called due to handling the event the pres + // context will return a null pres shell. See bug 125624. Using presShell to + // dispatch the event. It makes sure that event is not handled if the window + // is being destroyed. + if (RefPtr<PresShell> presShell = OwnerDoc()->GetPresShell()) { + InternalFormEvent event(true, eFormReset); + event.mOriginator = aSubmitter; + nsEventStatus status = nsEventStatus_eIgnore; + presShell->HandleDOMEventWithTarget(this, &event, &status); + } +} + +void HTMLFormElement::Submit(ErrorResult& aRv) { aRv = DoSubmit(); } + +// https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit +void HTMLFormElement::RequestSubmit(nsGenericHTMLElement* aSubmitter, + ErrorResult& aRv) { + // 1. If submitter is not null, then: + if (aSubmitter) { + nsCOMPtr<nsIFormControl> fc = do_QueryObject(aSubmitter); + + // 1.1. If submitter is not a submit button, then throw a TypeError. + if (!fc || !fc->IsSubmitControl()) { + aRv.ThrowTypeError("The submitter is not a submit button."); + return; + } + + // 1.2. If submitter's form owner is not this form element, then throw a + // "NotFoundError" DOMException. + if (fc->GetForm() != this) { + aRv.ThrowNotFoundError("The submitter is not owned by this form."); + return; + } + } + + // 2. Otherwise, set submitter to this form element. + // 3. Submit this form element, from submitter. + MaybeSubmit(aSubmitter); +} + +void HTMLFormElement::Reset() { + InternalFormEvent event(true, eFormReset); + EventDispatcher::Dispatch(this, nullptr, &event); +} + +bool HTMLFormElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::method) { + return aResult.ParseEnumValue(aValue, kFormMethodTable, false); + } + if (aAttribute == nsGkAtoms::enctype) { + return aResult.ParseEnumValue(aValue, kFormEnctypeTable, false); + } + if (aAttribute == nsGkAtoms::autocomplete) { + return aResult.ParseEnumValue(aValue, kFormAutocompleteTable, false); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +nsresult HTMLFormElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInUncomposedDoc() && aContext.OwnerDoc().IsHTMLOrXHTML()) { + aContext.OwnerDoc().AsHTMLDocument()->AddedForm(); + } + + return rv; +} + +template <typename T> +static void MarkOrphans(const nsTArray<T*>& aArray) { + uint32_t length = aArray.Length(); + for (uint32_t i = 0; i < length; ++i) { + aArray[i]->SetFlags(MAYBE_ORPHAN_FORM_ELEMENT); + } +} + +static void CollectOrphans(nsINode* aRemovalRoot, + const nsTArray<nsGenericHTMLFormElement*>& aArray +#ifdef DEBUG + , + HTMLFormElement* aThisForm +#endif +) { + // Put a script blocker around all the notifications we're about to do. + nsAutoScriptBlocker scriptBlocker; + + // Walk backwards so that if we remove elements we can just keep iterating + uint32_t length = aArray.Length(); + for (uint32_t i = length; i > 0; --i) { + nsGenericHTMLFormElement* node = aArray[i - 1]; + + // Now if MAYBE_ORPHAN_FORM_ELEMENT is not set, that would mean that the + // node is in fact a descendant of the form and hence should stay in the + // form. If it _is_ set, then we need to check whether the node is a + // descendant of aRemovalRoot. If it is, we leave it in the form. +#ifdef DEBUG + bool removed = false; +#endif + if (node->HasFlag(MAYBE_ORPHAN_FORM_ELEMENT)) { + node->UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT); + if (!node->IsInclusiveDescendantOf(aRemovalRoot)) { + nsCOMPtr<nsIFormControl> fc = do_QueryInterface(node); + MOZ_ASSERT(fc); + fc->ClearForm(true, false); +#ifdef DEBUG + removed = true; +#endif + } + } + +#ifdef DEBUG + if (!removed) { + nsCOMPtr<nsIFormControl> fc = do_QueryInterface(node); + MOZ_ASSERT(fc); + HTMLFormElement* form = fc->GetForm(); + NS_ASSERTION(form == aThisForm, "How did that happen?"); + } +#endif /* DEBUG */ + } +} + +static void CollectOrphans(nsINode* aRemovalRoot, + const nsTArray<HTMLImageElement*>& aArray +#ifdef DEBUG + , + HTMLFormElement* aThisForm +#endif +) { + // Walk backwards so that if we remove elements we can just keep iterating + uint32_t length = aArray.Length(); + for (uint32_t i = length; i > 0; --i) { + HTMLImageElement* node = aArray[i - 1]; + + // Now if MAYBE_ORPHAN_FORM_ELEMENT is not set, that would mean that the + // node is in fact a descendant of the form and hence should stay in the + // form. If it _is_ set, then we need to check whether the node is a + // descendant of aRemovalRoot. If it is, we leave it in the form. +#ifdef DEBUG + bool removed = false; +#endif + if (node->HasFlag(MAYBE_ORPHAN_FORM_ELEMENT)) { + node->UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT); + if (!node->IsInclusiveDescendantOf(aRemovalRoot)) { + node->ClearForm(true); + +#ifdef DEBUG + removed = true; +#endif + } + } + +#ifdef DEBUG + if (!removed) { + HTMLFormElement* form = node->GetForm(); + NS_ASSERTION(form == aThisForm, "How did that happen?"); + } +#endif /* DEBUG */ + } +} + +void HTMLFormElement::UnbindFromTree(bool aNullParent) { + MaybeFireFormRemoved(); + + // Note, this is explicitly using uncomposed doc, since we count + // only forms in document. + RefPtr<Document> oldDocument = GetUncomposedDoc(); + + // Mark all of our controls as maybe being orphans + MarkOrphans(mControls->mElements.AsList()); + MarkOrphans(mControls->mNotInElements.AsList()); + MarkOrphans(mImageElements.AsList()); + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + nsINode* ancestor = this; + nsINode* cur; + do { + cur = ancestor->GetParentNode(); + if (!cur) { + break; + } + ancestor = cur; + } while (true); + + CollectOrphans(ancestor, mControls->mElements +#ifdef DEBUG + , + this +#endif + ); + CollectOrphans(ancestor, mControls->mNotInElements +#ifdef DEBUG + , + this +#endif + ); + CollectOrphans(ancestor, mImageElements +#ifdef DEBUG + , + this +#endif + ); + + if (oldDocument && oldDocument->IsHTMLOrXHTML()) { + oldDocument->AsHTMLDocument()->RemovedForm(); + } + ForgetCurrentSubmission(); +} + +static bool CanSubmit(WidgetEvent& aEvent) { + // According to the UI events spec section "Trusted events", we shouldn't + // trigger UA default action with an untrusted event except click. + // However, there are still some sites depending on sending untrusted event + // to submit form, see Bug 1370630. + return !StaticPrefs::dom_forms_submit_trusted_event_only() || + aEvent.IsTrusted(); +} + +void HTMLFormElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + aVisitor.mWantsWillHandleEvent = true; + if (aVisitor.mEvent->mOriginalTarget == static_cast<nsIContent*>(this) && + CanSubmit(*aVisitor.mEvent)) { + uint32_t msg = aVisitor.mEvent->mMessage; + if (msg == eFormSubmit) { + if (mGeneratingSubmit) { + aVisitor.mCanHandle = false; + return; + } + mGeneratingSubmit = true; + + // XXXedgar, the untrusted event would trigger form submission, in this + // case, form need to handle defer flag and flushing pending submission by + // itself. This could be removed after Bug 1370630. + if (!aVisitor.mEvent->IsTrusted()) { + // let the form know that it needs to defer the submission, + // that means that if there are scripted submissions, the + // latest one will be deferred until after the exit point of the + // handler. + mDeferSubmission = true; + } + } else if (msg == eFormReset) { + if (mGeneratingReset) { + aVisitor.mCanHandle = false; + return; + } + mGeneratingReset = true; + } + } + nsGenericHTMLElement::GetEventTargetParent(aVisitor); +} + +void HTMLFormElement::WillHandleEvent(EventChainPostVisitor& aVisitor) { + // If this is the bubble stage and there is a nested form below us which + // received a submit event we do *not* want to handle the submit event + // for this form too. + if ((aVisitor.mEvent->mMessage == eFormSubmit || + aVisitor.mEvent->mMessage == eFormReset) && + aVisitor.mEvent->mFlags.mInBubblingPhase && + aVisitor.mEvent->mOriginalTarget != static_cast<nsIContent*>(this)) { + aVisitor.mEvent->StopPropagation(); + } +} + +nsresult HTMLFormElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + if (aVisitor.mEvent->mOriginalTarget == static_cast<nsIContent*>(this) && + CanSubmit(*aVisitor.mEvent)) { + EventMessage msg = aVisitor.mEvent->mMessage; + if (aVisitor.mEventStatus == nsEventStatus_eIgnore) { + switch (msg) { + case eFormReset: { + DoReset(); + break; + } + case eFormSubmit: { + if (!aVisitor.mEvent->IsTrusted()) { + // Warning about the form submission is from untrusted event. + OwnerDoc()->WarnOnceAbout( + DeprecatedOperations::eFormSubmissionUntrustedEvent); + } + RefPtr<Event> event = aVisitor.mDOMEvent; + DoSubmit(event); + break; + } + default: + break; + } + } + + // XXXedgar, the untrusted event would trigger form submission, in this + // case, form need to handle defer flag and flushing pending submission by + // itself. This could be removed after Bug 1370630. + if (msg == eFormSubmit && !aVisitor.mEvent->IsTrusted()) { + // let the form know not to defer subsequent submissions + mDeferSubmission = false; + // tell the form to flush a possible pending submission. + FlushPendingSubmission(); + } + + if (msg == eFormSubmit) { + mGeneratingSubmit = false; + } else if (msg == eFormReset) { + mGeneratingReset = false; + } + } + return NS_OK; +} + +nsresult HTMLFormElement::DoReset() { + // Make sure the presentation is up-to-date + Document* doc = GetComposedDoc(); + if (doc) { + doc->FlushPendingNotifications(FlushType::ContentAndNotify); + } + + // JBK walk the elements[] array instead of form frame controls - bug 34297 + uint32_t numElements = mControls->Length(); + for (uint32_t elementX = 0; elementX < numElements; ++elementX) { + // Hold strong ref in case the reset does something weird + nsCOMPtr<nsIFormControl> controlNode = do_QueryInterface( + mControls->mElements->SafeElementAt(elementX, nullptr)); + if (controlNode) { + controlNode->Reset(); + } + } + + return NS_OK; +} + +#define NS_ENSURE_SUBMIT_SUCCESS(rv) \ + if (NS_FAILED(rv)) { \ + ForgetCurrentSubmission(); \ + return rv; \ + } + +nsresult HTMLFormElement::DoSubmit(Event* aEvent) { + Document* doc = GetComposedDoc(); + NS_ASSERTION(doc, "Should never get here without a current doc"); + + // Make sure the presentation is up-to-date + if (doc) { + doc->FlushPendingNotifications(FlushType::ContentAndNotify); + } + + // Don't submit if we're not in a document or if we're in + // a sandboxed frame and form submit is disabled. + if (mIsConstructingEntryList || !doc || + (doc->GetSandboxFlags() & SANDBOXED_FORMS)) { + return NS_OK; + } + + if (IsSubmitting()) { + NS_WARNING("Preventing double form submission"); + // XXX Should this return an error? + return NS_OK; + } + + mTargetContext = nullptr; + mCurrentLoadId = Nothing(); + + UniquePtr<HTMLFormSubmission> submission; + + // + // prepare the submission object + // + nsresult rv = BuildSubmission(getter_Transfers(submission), aEvent); + + // Don't raise an error if form cannot navigate. + if (rv == NS_ERROR_NOT_AVAILABLE) { + return NS_OK; + } + + NS_ENSURE_SUCCESS(rv, rv); + + // XXXbz if the script global is that for an sXBL/XBL2 doc, it won't + // be a window... + nsPIDOMWindowOuter* window = OwnerDoc()->GetWindow(); + if (window) { + mSubmitPopupState = PopupBlocker::GetPopupControlState(); + } else { + mSubmitPopupState = PopupBlocker::openAbused; + } + + // + // perform the submission + // + if (!submission) { +#ifdef DEBUG + HTMLDialogElement* dialog = nullptr; + for (nsIContent* parent = GetParent(); parent; + parent = parent->GetParent()) { + dialog = HTMLDialogElement::FromNodeOrNull(parent); + if (dialog) { + break; + } + } + MOZ_ASSERT(!dialog || !dialog->Open()); +#endif + return NS_OK; + } + + if (DialogFormSubmission* dialogSubmission = + submission->GetAsDialogSubmission()) { + return SubmitDialog(dialogSubmission); + } + + if (mDeferSubmission) { + // we are in an event handler, JS submitted so we have to + // defer this submission. let's remember it and return + // without submitting + mPendingSubmission = std::move(submission); + return NS_OK; + } + + return SubmitSubmission(submission.get()); +} + +nsresult HTMLFormElement::BuildSubmission(HTMLFormSubmission** aFormSubmission, + Event* aEvent) { + // Get the submitter element + nsGenericHTMLElement* submitter = nullptr; + if (aEvent) { + SubmitEvent* submitEvent = aEvent->AsSubmitEvent(); + if (submitEvent) { + submitter = submitEvent->GetSubmitter(); + } + } + + nsresult rv; + + // + // Walk over the form elements and call SubmitNamesValues() on them to get + // their data. + // + auto encoding = GetSubmitEncoding()->OutputEncoding(); + RefPtr<FormData> formData = + new FormData(GetOwnerGlobal(), encoding, submitter); + rv = ConstructEntryList(formData); + NS_ENSURE_SUBMIT_SUCCESS(rv); + + // Step 9. If form cannot navigate, then return. + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm + if (!GetComposedDoc()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // + // Get the submission object + // + rv = HTMLFormSubmission::GetFromForm(this, submitter, encoding, + aFormSubmission); + NS_ENSURE_SUBMIT_SUCCESS(rv); + + // + // Dump the data into the submission object + // + if (!(*aFormSubmission)->GetAsDialogSubmission()) { + rv = formData->CopySubmissionDataTo(*aFormSubmission); + NS_ENSURE_SUBMIT_SUCCESS(rv); + } + + return NS_OK; +} + +nsresult HTMLFormElement::SubmitSubmission( + HTMLFormSubmission* aFormSubmission) { + MOZ_ASSERT(!mDeferSubmission); + MOZ_ASSERT(!mPendingSubmission); + + nsCOMPtr<nsIURI> actionURI = aFormSubmission->GetActionURL(); + if (!actionURI) { + return NS_OK; + } + + // If there is no link handler, then we won't actually be able to submit. + Document* doc = GetComposedDoc(); + RefPtr<nsDocShell> container = + doc ? nsDocShell::Cast(doc->GetDocShell()) : nullptr; + if (!container || IsEditable()) { + return NS_OK; + } + + // javascript URIs are not really submissions; they just call a function. + // Also, they may synchronously call submit(), and we want them to be able to + // do so while still disallowing other double submissions. (Bug 139798) + // Note that any other URI types that are of equivalent type should also be + // added here. + // XXXbz this is a mess. The real issue here is that nsJSChannel sets the + // LOAD_BACKGROUND flag, so doesn't notify us, compounded by the fact that + // the JS executes before we forget the submission in OnStateChange on + // STATE_STOP. As a result, we have to make sure that we simply pretend + // we're not submitting when submitting to a JS URL. That's kinda bogus, but + // there we are. + bool schemeIsJavaScript = actionURI->SchemeIs("javascript"); + + // + // Notify observers of submit + // + nsresult rv; + bool cancelSubmit = false; + if (mNotifiedObservers) { + cancelSubmit = mNotifiedObserversResult; + } else { + rv = NotifySubmitObservers(actionURI, &cancelSubmit, true); + NS_ENSURE_SUBMIT_SUCCESS(rv); + } + + if (cancelSubmit) { + return NS_OK; + } + + cancelSubmit = false; + rv = NotifySubmitObservers(actionURI, &cancelSubmit, false); + NS_ENSURE_SUBMIT_SUCCESS(rv); + + if (cancelSubmit) { + return NS_OK; + } + + // + // Submit + // + uint64_t currentLoadId = 0; + + { + AutoPopupStatePusher popupStatePusher(mSubmitPopupState); + + AutoHandlingUserInputStatePusher userInpStatePusher( + aFormSubmission->IsInitiatedFromUserInput()); + + nsCOMPtr<nsIInputStream> postDataStream; + rv = aFormSubmission->GetEncodedSubmission( + actionURI, getter_AddRefs(postDataStream), actionURI); + NS_ENSURE_SUBMIT_SUCCESS(rv); + + nsAutoString target; + aFormSubmission->GetTarget(target); + + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(actionURI); + loadState->SetTarget(target); + loadState->SetPostDataStream(postDataStream); + loadState->SetFirstParty(true); + loadState->SetIsFormSubmission(true); + loadState->SetTriggeringPrincipal(NodePrincipal()); + loadState->SetPrincipalToInherit(NodePrincipal()); + loadState->SetCsp(GetCsp()); + loadState->SetAllowFocusMove(UserActivation::IsHandlingUserInput()); + + nsCOMPtr<nsIPrincipal> nodePrincipal = NodePrincipal(); + rv = container->OnLinkClickSync(this, loadState, false, nodePrincipal); + NS_ENSURE_SUBMIT_SUCCESS(rv); + + mTargetContext = loadState->TargetBrowsingContext().GetMaybeDiscarded(); + currentLoadId = loadState->GetLoadIdentifier(); + } + + // Even if the submit succeeds, it's possible for there to be no + // browsing context; for example, if it's to a named anchor within + // the same page the submit will not really do anything. + if (mTargetContext && !mTargetContext->IsDiscarded() && !schemeIsJavaScript) { + mCurrentLoadId = Some(currentLoadId); + } else { + ForgetCurrentSubmission(); + } + + return rv; +} + +// https://html.spec.whatwg.org/#concept-form-submit step 11 +nsresult HTMLFormElement::SubmitDialog(DialogFormSubmission* aFormSubmission) { + // Close the dialog subject. If there is a result, let that be the return + // value. + HTMLDialogElement* dialog = aFormSubmission->DialogElement(); + MOZ_ASSERT(dialog); + + Optional<nsAString> retValue; + retValue = &aFormSubmission->ReturnValue(); + dialog->Close(retValue); + + return NS_OK; +} + +nsresult HTMLFormElement::DoSecureToInsecureSubmitCheck(nsIURI* aActionURL, + bool* aCancelSubmit) { + *aCancelSubmit = false; + + if (!StaticPrefs::security_warn_submit_secure_to_insecure()) { + return NS_OK; + } + + // Only ask the user about posting from a secure URI to an insecure URI if + // this element is in the root document. When this is not the case, the mixed + // content blocker will take care of security for us. + if (!OwnerDoc()->IsTopLevelContentDocument()) { + return NS_OK; + } + + if (nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(aActionURL)) { + return NS_OK; + } + + if (nsMixedContentBlocker::URISafeToBeLoadedInSecureContext(aActionURL)) { + return NS_OK; + } + + if (nsMixedContentBlocker::IsPotentiallyTrustworthyOnion(aActionURL)) { + return NS_OK; + } + + nsCOMPtr<nsPIDOMWindowOuter> window = OwnerDoc()->GetWindow(); + if (!window) { + return NS_ERROR_FAILURE; + } + + // Now that we know the action URI is insecure check if we're submitting from + // a secure URI and if so fall thru and prompt user about posting. + if (nsCOMPtr<nsPIDOMWindowInner> innerWindow = OwnerDoc()->GetInnerWindow()) { + if (!innerWindow->IsSecureContext()) { + return NS_OK; + } + } + + // Bug 1351358: While file URIs are considered to be secure contexts we allow + // submitting a form to an insecure URI from a file URI without an alert in an + // attempt to avoid compatibility issues. + if (window->GetDocumentURI()->SchemeIs("file")) { + return NS_OK; + } + + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + if (!docShell) { + return NS_ERROR_FAILURE; + } + + nsresult rv; + nsCOMPtr<nsIPromptService> promptSvc = + do_GetService("@mozilla.org/prompter;1", &rv); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIStringBundle> stringBundle; + nsCOMPtr<nsIStringBundleService> stringBundleService = + mozilla::components::StringBundle::Service(); + if (!stringBundleService) { + return NS_ERROR_FAILURE; + } + rv = stringBundleService->CreateBundle( + "chrome://global/locale/browser.properties", + getter_AddRefs(stringBundle)); + if (NS_FAILED(rv)) { + return rv; + } + nsAutoString title; + nsAutoString message; + nsAutoString cont; + stringBundle->GetStringFromName("formPostSecureToInsecureWarning.title", + title); + stringBundle->GetStringFromName("formPostSecureToInsecureWarning.message", + message); + stringBundle->GetStringFromName("formPostSecureToInsecureWarning.continue", + cont); + int32_t buttonPressed; + bool checkState = + false; // this is unused (ConfirmEx requires this parameter) + rv = promptSvc->ConfirmExBC( + docShell->GetBrowsingContext(), + StaticPrefs::prompts_modalType_insecureFormSubmit(), title.get(), + message.get(), + (nsIPromptService::BUTTON_TITLE_IS_STRING * + nsIPromptService::BUTTON_POS_0) + + (nsIPromptService::BUTTON_TITLE_CANCEL * + nsIPromptService::BUTTON_POS_1), + cont.get(), nullptr, nullptr, nullptr, &checkState, &buttonPressed); + if (NS_FAILED(rv)) { + return rv; + } + *aCancelSubmit = (buttonPressed == 1); + uint32_t telemetryBucket = + nsISecurityUITelemetry::WARNING_CONFIRM_POST_TO_INSECURE_FROM_SECURE; + mozilla::Telemetry::Accumulate(mozilla::Telemetry::SECURITY_UI, + telemetryBucket); + if (!*aCancelSubmit) { + // The user opted to continue, so note that in the next telemetry bucket. + mozilla::Telemetry::Accumulate(mozilla::Telemetry::SECURITY_UI, + telemetryBucket + 1); + } + return NS_OK; +} + +nsresult HTMLFormElement::NotifySubmitObservers(nsIURI* aActionURL, + bool* aCancelSubmit, + bool aEarlyNotify) { + if (!aEarlyNotify) { + nsresult rv = DoSecureToInsecureSubmitCheck(aActionURL, aCancelSubmit); + if (NS_FAILED(rv)) { + return rv; + } + if (*aCancelSubmit) { + return NS_OK; + } + } + + bool defaultAction = true; + nsresult rv = nsContentUtils::DispatchEventOnlyToChrome( + OwnerDoc(), static_cast<nsINode*>(this), + aEarlyNotify ? u"DOMFormBeforeSubmit"_ns : u"DOMFormSubmit"_ns, + CanBubble::eYes, Cancelable::eYes, &defaultAction); + *aCancelSubmit = !defaultAction; + if (*aCancelSubmit) { + return NS_OK; + } + return rv; +} + +nsresult HTMLFormElement::ConstructEntryList(FormData* aFormData) { + MOZ_ASSERT(aFormData, "Must have FormData!"); + if (mIsConstructingEntryList) { + // Step 2.2 of https://xhr.spec.whatwg.org/#dom-formdata. + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + AutoRestore<bool> resetConstructingEntryList(mIsConstructingEntryList); + mIsConstructingEntryList = true; + // This shouldn't be called recursively, so use a rather large value + // for the preallocated buffer. + AutoTArray<RefPtr<nsGenericHTMLFormElement>, 100> sortedControls; + nsresult rv = mControls->GetSortedControls(sortedControls); + NS_ENSURE_SUCCESS(rv, rv); + + // Walk the list of nodes and call SubmitNamesValues() on the controls + for (nsGenericHTMLFormElement* control : sortedControls) { + // Disabled elements don't submit + if (!control->IsDisabled()) { + nsCOMPtr<nsIFormControl> fc = do_QueryInterface(control); + MOZ_ASSERT(fc); + // Tell the control to submit its name/value pairs to the submission + fc->SubmitNamesValues(aFormData); + } + } + + FormDataEventInit init; + init.mBubbles = true; + init.mCancelable = false; + init.mFormData = aFormData; + RefPtr<FormDataEvent> event = + FormDataEvent::Constructor(this, u"formdata"_ns, init); + event->SetTrusted(true); + + EventDispatcher::DispatchDOMEvent(this, nullptr, event, nullptr, nullptr); + + return NS_OK; +} + +NotNull<const Encoding*> HTMLFormElement::GetSubmitEncoding() { + nsAutoString acceptCharsetValue; + GetAttr(nsGkAtoms::acceptcharset, acceptCharsetValue); + + int32_t charsetLen = acceptCharsetValue.Length(); + if (charsetLen > 0) { + int32_t offset = 0; + int32_t spPos = 0; + // get charset from charsets one by one + do { + spPos = acceptCharsetValue.FindChar(char16_t(' '), offset); + int32_t cnt = ((-1 == spPos) ? (charsetLen - offset) : (spPos - offset)); + if (cnt > 0) { + nsAutoString uCharset; + acceptCharsetValue.Mid(uCharset, offset, cnt); + + auto encoding = Encoding::ForLabelNoReplacement(uCharset); + if (encoding) { + return WrapNotNull(encoding); + } + } + offset = spPos + 1; + } while (spPos != -1); + } + // if there are no accept-charset or all the charset are not supported + // Get the charset from document + Document* doc = GetComposedDoc(); + if (doc) { + return doc->GetDocumentCharacterSet(); + } + return UTF_8_ENCODING; +} + +Element* HTMLFormElement::IndexedGetter(uint32_t aIndex, bool& aFound) { + Element* element = mControls->mElements->SafeElementAt(aIndex, nullptr); + aFound = element != nullptr; + return element; +} + +#ifdef DEBUG +/** + * Checks that all form elements are in document order. Asserts if any pair of + * consecutive elements are not in increasing document order. + * + * @param aControls List of form controls to check. + * @param aForm Parent form of the controls. + */ +/* static */ +void HTMLFormElement::AssertDocumentOrder( + const nsTArray<nsGenericHTMLFormElement*>& aControls, nsIContent* aForm) { + // TODO: remove the if directive with bug 598468. + // This is done to prevent asserts in some edge cases. +# if 0 + // Only iterate if aControls is not empty, since otherwise + // |aControls.Length() - 1| will be a very large unsigned number... not what + // we want here. + if (!aControls.IsEmpty()) { + for (uint32_t i = 0; i < aControls.Length() - 1; ++i) { + NS_ASSERTION( + CompareFormControlPosition(aControls[i], aControls[i + 1], aForm) < 0, + "Form controls not ordered correctly"); + } + } +# endif +} + +/** + * Copy of the above function, but with RefPtrs. + * + * @param aControls List of form controls to check. + * @param aForm Parent form of the controls. + */ +/* static */ +void HTMLFormElement::AssertDocumentOrder( + const nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls, + nsIContent* aForm) { + // TODO: remove the if directive with bug 598468. + // This is done to prevent asserts in some edge cases. +# if 0 + // Only iterate if aControls is not empty, since otherwise + // |aControls.Length() - 1| will be a very large unsigned number... not what + // we want here. + if (!aControls.IsEmpty()) { + for (uint32_t i = 0; i < aControls.Length() - 1; ++i) { + NS_ASSERTION( + CompareFormControlPosition(aControls[i], aControls[i + 1], aForm) < 0, + "Form controls not ordered correctly"); + } + } +# endif +} +#endif + +nsresult HTMLFormElement::AddElement(nsGenericHTMLFormElement* aChild, + bool aUpdateValidity, bool aNotify) { + // If an element has a @form, we can assume it *might* be able to not have + // a parent and still be in the form. + NS_ASSERTION(aChild->HasAttr(nsGkAtoms::form) || aChild->GetParent(), + "Form control should have a parent"); + nsCOMPtr<nsIFormControl> fc = do_QueryObject(aChild); + MOZ_ASSERT(fc); + // Determine whether to add the new element to the elements or + // the not-in-elements list. + bool childInElements = HTMLFormControlsCollection::ShouldBeInElements(fc); + TreeOrderedArray<nsGenericHTMLFormElement*>& controlList = + childInElements ? mControls->mElements : mControls->mNotInElements; + + const size_t insertedIndex = controlList.Insert(*aChild, this); + const bool lastElement = controlList->Length() == insertedIndex + 1; + +#ifdef DEBUG + AssertDocumentOrder(controlList, this); +#endif + + auto type = fc->ControlType(); + + // Default submit element handling + if (fc->IsSubmitControl()) { + // Update mDefaultSubmitElement, mFirstSubmitInElements, + // mFirstSubmitNotInElements. + + nsGenericHTMLFormElement** firstSubmitSlot = + childInElements ? &mFirstSubmitInElements : &mFirstSubmitNotInElements; + + // The new child is the new first submit in its list if the firstSubmitSlot + // is currently empty or if the child is before what's currently in the + // slot. Note that if we already have a control in firstSubmitSlot and + // we're appending this element can't possibly replace what's currently in + // the slot. Also note that aChild can't become the mDefaultSubmitElement + // unless it replaces what's in the slot. If it _does_ replace what's in + // the slot, it becomes the default submit if either the default submit is + // what's in the slot or the child is earlier than the default submit. + if (!*firstSubmitSlot || + (!lastElement && nsContentUtils::CompareTreePosition<TreeKind::DOM>( + aChild, *firstSubmitSlot, this) < 0)) { + // Update mDefaultSubmitElement if it's currently in a valid state. + // Valid state means either non-null or null because there are in fact + // no submit elements around. + if ((mDefaultSubmitElement || + (!mFirstSubmitInElements && !mFirstSubmitNotInElements)) && + (*firstSubmitSlot == mDefaultSubmitElement || + nsContentUtils::CompareTreePosition<TreeKind::DOM>( + aChild, mDefaultSubmitElement, this) < 0)) { + SetDefaultSubmitElement(aChild); + } + *firstSubmitSlot = aChild; + } + + MOZ_ASSERT(mDefaultSubmitElement == mFirstSubmitInElements || + mDefaultSubmitElement == mFirstSubmitNotInElements || + !mDefaultSubmitElement, + "What happened here?"); + } + + // If the element is subject to constraint validaton and is invalid, we need + // to update our internal counter. + if (aUpdateValidity) { + nsCOMPtr<nsIConstraintValidation> cvElmt = do_QueryObject(aChild); + if (cvElmt && cvElmt->IsCandidateForConstraintValidation() && + !cvElmt->IsValid()) { + UpdateValidity(false); + } + } + + // Notify the radio button it's been added to a group + // This has to be done _after_ UpdateValidity() call to prevent the element + // being count twice. + if (type == FormControlType::InputRadio) { + RefPtr<HTMLInputElement> radio = static_cast<HTMLInputElement*>(aChild); + radio->AddToRadioGroup(); + } + + return NS_OK; +} + +nsresult HTMLFormElement::AddElementToTable(nsGenericHTMLFormElement* aChild, + const nsAString& aName) { + return mControls->AddElementToTable(aChild, aName); +} + +void HTMLFormElement::SetDefaultSubmitElement( + nsGenericHTMLFormElement* aElement) { + if (mDefaultSubmitElement) { + // It just so happens that a radio button or an <option> can't be our + // default submit element, so we can just blindly remove the bit. + mDefaultSubmitElement->RemoveStates(ElementState::DEFAULT); + } + mDefaultSubmitElement = aElement; + if (mDefaultSubmitElement) { + mDefaultSubmitElement->AddStates(ElementState::DEFAULT); + } +} + +nsresult HTMLFormElement::RemoveElement(nsGenericHTMLFormElement* aChild, + bool aUpdateValidity) { + RemoveElementFromPastNamesMap(aChild); + + // + // Remove it from the radio group if it's a radio button + // + nsresult rv = NS_OK; + nsCOMPtr<nsIFormControl> fc = do_QueryInterface(aChild); + MOZ_ASSERT(fc); + if (fc->ControlType() == FormControlType::InputRadio) { + RefPtr<HTMLInputElement> radio = static_cast<HTMLInputElement*>(aChild); + radio->RemoveFromRadioGroup(); + } + + // Determine whether to remove the child from the elements list + // or the not in elements list. + bool childInElements = HTMLFormControlsCollection::ShouldBeInElements(fc); + TreeOrderedArray<nsGenericHTMLFormElement*>& controls = + childInElements ? mControls->mElements : mControls->mNotInElements; + + // Find the index of the child. This will be used later if necessary + // to find the default submit. + size_t index = controls->IndexOf(aChild); + NS_ENSURE_STATE(index != controls.AsList().NoIndex); + + controls.RemoveElementAt(index); + + // Update our mFirstSubmit* values. + nsGenericHTMLFormElement** firstSubmitSlot = + childInElements ? &mFirstSubmitInElements : &mFirstSubmitNotInElements; + if (aChild == *firstSubmitSlot) { + *firstSubmitSlot = nullptr; + + // We are removing the first submit in this list, find the new first submit + uint32_t length = controls->Length(); + for (uint32_t i = index; i < length; ++i) { + nsCOMPtr<nsIFormControl> currentControl = + do_QueryInterface(controls->ElementAt(i)); + MOZ_ASSERT(currentControl); + if (currentControl->IsSubmitControl()) { + *firstSubmitSlot = controls->ElementAt(i); + break; + } + } + } + + if (aChild == mDefaultSubmitElement) { + // Need to reset mDefaultSubmitElement. Do this asynchronously so + // that we're not doing it while the DOM is in flux. + SetDefaultSubmitElement(nullptr); + nsContentUtils::AddScriptRunner(new RemoveElementRunnable(this)); + + // Note that we don't need to notify on the old default submit (which is + // being removed) because it's either being removed from the DOM or + // changing attributes in a way that makes it responsible for sending its + // own notifications. + } + + // If the element was subject to constraint validation and is invalid, we need + // to update our internal counter. + if (aUpdateValidity) { + nsCOMPtr<nsIConstraintValidation> cvElmt = do_QueryObject(aChild); + if (cvElmt && cvElmt->IsCandidateForConstraintValidation() && + !cvElmt->IsValid()) { + UpdateValidity(true); + } + } + + return rv; +} + +void HTMLFormElement::HandleDefaultSubmitRemoval() { + if (mDefaultSubmitElement) { + // Already got reset somehow; nothing else to do here + return; + } + + nsGenericHTMLFormElement* newDefaultSubmit; + if (!mFirstSubmitNotInElements) { + newDefaultSubmit = mFirstSubmitInElements; + } else if (!mFirstSubmitInElements) { + newDefaultSubmit = mFirstSubmitNotInElements; + } else { + NS_ASSERTION(mFirstSubmitInElements != mFirstSubmitNotInElements, + "How did that happen?"); + // Have both; use the earlier one + newDefaultSubmit = + nsContentUtils::CompareTreePosition<TreeKind::DOM>( + mFirstSubmitInElements, mFirstSubmitNotInElements, this) < 0 + ? mFirstSubmitInElements + : mFirstSubmitNotInElements; + } + SetDefaultSubmitElement(newDefaultSubmit); + + MOZ_ASSERT(mDefaultSubmitElement == mFirstSubmitInElements || + mDefaultSubmitElement == mFirstSubmitNotInElements, + "What happened here?"); +} + +nsresult HTMLFormElement::RemoveElementFromTableInternal( + nsInterfaceHashtable<nsStringHashKey, nsISupports>& aTable, + nsIContent* aChild, const nsAString& aName) { + auto entry = aTable.Lookup(aName); + if (!entry) { + return NS_OK; + } + // Single element in the hash, just remove it if it's the one + // we're trying to remove... + if (entry.Data() == aChild) { + entry.Remove(); + ++mExpandoAndGeneration.generation; + return NS_OK; + } + + nsCOMPtr<nsIContent> content(do_QueryInterface(entry.Data())); + if (content) { + return NS_OK; + } + + // If it's not a content node then it must be a RadioNodeList. + MOZ_ASSERT(nsCOMPtr<RadioNodeList>(do_QueryInterface(entry.Data()))); + auto* list = static_cast<RadioNodeList*>(entry->get()); + + list->RemoveElement(aChild); + + uint32_t length = list->Length(); + + if (!length) { + // If the list is empty we remove if from our hash, this shouldn't + // happen tho + entry.Remove(); + ++mExpandoAndGeneration.generation; + } else if (length == 1) { + // Only one element left, replace the list in the hash with the + // single element. + nsIContent* node = list->Item(0); + if (node) { + entry.Data() = node; + } + } + + return NS_OK; +} + +nsresult HTMLFormElement::RemoveElementFromTable( + nsGenericHTMLFormElement* aElement, const nsAString& aName) { + return mControls->RemoveElementFromTable(aElement, aName); +} + +already_AddRefed<nsISupports> HTMLFormElement::NamedGetter( + const nsAString& aName, bool& aFound) { + aFound = true; + + nsCOMPtr<nsISupports> result = DoResolveName(aName); + if (result) { + AddToPastNamesMap(aName, result); + return result.forget(); + } + + result = mImageNameLookupTable.GetWeak(aName); + if (result) { + AddToPastNamesMap(aName, result); + return result.forget(); + } + + result = mPastNameLookupTable.GetWeak(aName); + if (result) { + return result.forget(); + } + + aFound = false; + return nullptr; +} + +void HTMLFormElement::GetSupportedNames(nsTArray<nsString>& aRetval) { + // TODO https://github.com/whatwg/html/issues/1731 +} + +already_AddRefed<nsISupports> HTMLFormElement::FindNamedItem( + const nsAString& aName, nsWrapperCache** aCache) { + // FIXME Get the wrapper cache from DoResolveName. + + bool found; + nsCOMPtr<nsISupports> result = NamedGetter(aName, found); + if (result) { + *aCache = nullptr; + return result.forget(); + } + + return nullptr; +} + +already_AddRefed<nsISupports> HTMLFormElement::DoResolveName( + const nsAString& aName) { + nsCOMPtr<nsISupports> result = mControls->NamedItemInternal(aName); + return result.forget(); +} + +void HTMLFormElement::OnSubmitClickBegin(Element* aOriginatingElement) { + mDeferSubmission = true; + + // Prepare to run NotifySubmitObservers early before the + // scripts on the page get to modify the form data, possibly + // throwing off any password manager. (bug 257781) + nsCOMPtr<nsIURI> actionURI; + nsresult rv; + + rv = GetActionURL(getter_AddRefs(actionURI), aOriginatingElement); + if (NS_FAILED(rv) || !actionURI) return; + + // Notify observers of submit if the form is valid. + // TODO: checking for mInvalidElementsCount is a temporary fix that should be + // removed with bug 610402. + if (mInvalidElementsCount == 0) { + bool cancelSubmit = false; + rv = NotifySubmitObservers(actionURI, &cancelSubmit, true); + if (NS_SUCCEEDED(rv)) { + mNotifiedObservers = true; + mNotifiedObserversResult = cancelSubmit; + } + } +} + +void HTMLFormElement::OnSubmitClickEnd() { mDeferSubmission = false; } + +void HTMLFormElement::FlushPendingSubmission() { + MOZ_ASSERT(!mDeferSubmission); + + if (mPendingSubmission) { + // Transfer owning reference so that the submission doesn't get deleted + // if we reenter + UniquePtr<HTMLFormSubmission> submission = std::move(mPendingSubmission); + + SubmitSubmission(submission.get()); + } +} + +void HTMLFormElement::GetAction(nsString& aValue) { + if (!GetAttr(nsGkAtoms::action, aValue) || aValue.IsEmpty()) { + Document* document = OwnerDoc(); + nsIURI* docURI = document->GetDocumentURI(); + if (docURI) { + nsAutoCString spec; + nsresult rv = docURI->GetSpec(spec); + if (NS_FAILED(rv)) { + return; + } + + CopyUTF8toUTF16(spec, aValue); + } + } else { + GetURIAttr(nsGkAtoms::action, nullptr, aValue); + } +} + +nsresult HTMLFormElement::GetActionURL(nsIURI** aActionURL, + Element* aOriginatingElement) { + nsresult rv = NS_OK; + + *aActionURL = nullptr; + + // + // Grab the URL string + // + // If the originating element is a submit control and has the formaction + // attribute specified, it should be used. Otherwise, the action attribute + // from the form element should be used. + // + nsAutoString action; + + if (aOriginatingElement && + aOriginatingElement->HasAttr(nsGkAtoms::formaction)) { +#ifdef DEBUG + nsCOMPtr<nsIFormControl> formControl = + do_QueryInterface(aOriginatingElement); + NS_ASSERTION(formControl && formControl->IsSubmitControl(), + "The originating element must be a submit form control!"); +#endif // DEBUG + + HTMLInputElement* inputElement = + HTMLInputElement::FromNode(aOriginatingElement); + if (inputElement) { + inputElement->GetFormAction(action); + } else { + auto buttonElement = HTMLButtonElement::FromNode(aOriginatingElement); + if (buttonElement) { + buttonElement->GetFormAction(action); + } else { + NS_ERROR("Originating element must be an input or button element!"); + return NS_ERROR_UNEXPECTED; + } + } + } else { + GetAction(action); + } + + // + // Form the full action URL + // + + // Get the document to form the URL. + // We'll also need it later to get the DOM window when notifying form submit + // observers (bug 33203) + if (!IsInComposedDoc()) { + return NS_OK; // No doc means don't submit, see Bug 28988 + } + + // Get base URL + Document* document = OwnerDoc(); + nsIURI* docURI = document->GetDocumentURI(); + NS_ENSURE_TRUE(docURI, NS_ERROR_UNEXPECTED); + + // If an action is not specified and we are inside + // a HTML document then reload the URL. This makes us + // compatible with 4.x browsers. + // If we are in some other type of document such as XML or + // XUL, do nothing. This prevents undesirable reloading of + // a document inside XUL. + + nsCOMPtr<nsIURI> actionURL; + if (action.IsEmpty()) { + if (!document->IsHTMLOrXHTML()) { + // Must be a XML, XUL or other non-HTML document type + // so do nothing. + return NS_OK; + } + + actionURL = docURI; + } else { + nsIURI* baseURL = GetBaseURI(); + NS_ASSERTION(baseURL, "No Base URL found in Form Submit!\n"); + if (!baseURL) { + return NS_OK; // No base URL -> exit early, see Bug 30721 + } + rv = NS_NewURI(getter_AddRefs(actionURL), action, nullptr, baseURL); + NS_ENSURE_SUCCESS(rv, rv); + } + + // + // Verify the URL should be reached + // + // Get security manager, check to see if access to action URI is allowed. + // + nsIScriptSecurityManager* securityManager = + nsContentUtils::GetSecurityManager(); + rv = securityManager->CheckLoadURIWithPrincipal( + NodePrincipal(), actionURL, nsIScriptSecurityManager::STANDARD, + OwnerDoc()->InnerWindowID()); + NS_ENSURE_SUCCESS(rv, rv); + + // Potentially the page uses the CSP directive 'upgrade-insecure-requests'. In + // such a case we have to upgrade the action url from http:// to https://. + // The upgrade is only required if the actionURL is http and not a potentially + // trustworthy loopback URI. + bool needsUpgrade = + actionURL->SchemeIs("http") && + !nsMixedContentBlocker::IsPotentiallyTrustworthyLoopbackURL(actionURL) && + document->GetUpgradeInsecureRequests(false); + if (needsUpgrade) { + // let's use the old specification before the upgrade for logging + AutoTArray<nsString, 2> params; + nsAutoCString spec; + rv = actionURL->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF8toUTF16(spec, *params.AppendElement()); + + // upgrade the actionURL from http:// to use https:// + nsCOMPtr<nsIURI> upgradedActionURL; + rv = NS_GetSecureUpgradedURI(actionURL, getter_AddRefs(upgradedActionURL)); + NS_ENSURE_SUCCESS(rv, rv); + actionURL = std::move(upgradedActionURL); + + // let's log a message to the console that we are upgrading a request + nsAutoCString scheme; + rv = actionURL->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, rv); + CopyUTF8toUTF16(scheme, *params.AppendElement()); + + CSP_LogLocalizedStr( + "upgradeInsecureRequest", params, + u""_ns, // aSourceFile + u""_ns, // aScriptSample + 0, // aLineNumber + 1, // aColumnNumber + nsIScriptError::warningFlag, "upgradeInsecureRequest"_ns, + document->InnerWindowID(), + !!document->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId); + } + + // + // Assign to the output + // + actionURL.forget(aActionURL); + + return rv; +} + +nsGenericHTMLFormElement* HTMLFormElement::GetDefaultSubmitElement() const { + MOZ_ASSERT(mDefaultSubmitElement == mFirstSubmitInElements || + mDefaultSubmitElement == mFirstSubmitNotInElements, + "What happened here?"); + + return mDefaultSubmitElement; +} + +bool HTMLFormElement::ImplicitSubmissionIsDisabled() const { + // Input text controls are always in the elements list. + uint32_t numDisablingControlsFound = 0; + uint32_t length = mControls->mElements->Length(); + for (uint32_t i = 0; i < length && numDisablingControlsFound < 2; ++i) { + nsCOMPtr<nsIFormControl> fc = + do_QueryInterface(mControls->mElements->ElementAt(i)); + MOZ_ASSERT(fc); + if (fc->IsSingleLineTextControl(false)) { + numDisablingControlsFound++; + } + } + return numDisablingControlsFound != 1; +} + +bool HTMLFormElement::IsLastActiveElement( + const nsGenericHTMLFormElement* aElement) const { + MOZ_ASSERT(aElement, "Unexpected call"); + + for (auto* element : Reversed(mControls->mElements.AsList())) { + nsCOMPtr<nsIFormControl> fc = do_QueryInterface(element); + MOZ_ASSERT(fc); + // XXX How about date/time control? + if (fc->IsTextControl(false) && !element->IsDisabled()) { + return element == aElement; + } + } + return false; +} + +int32_t HTMLFormElement::Length() { return mControls->Length(); } + +void HTMLFormElement::ForgetCurrentSubmission() { + mNotifiedObservers = false; + mTargetContext = nullptr; + mCurrentLoadId = Nothing(); +} + +bool HTMLFormElement::CheckFormValidity( + nsTArray<RefPtr<Element>>* aInvalidElements) const { + bool ret = true; + + // This shouldn't be called recursively, so use a rather large value + // for the preallocated buffer. + AutoTArray<RefPtr<nsGenericHTMLFormElement>, 100> sortedControls; + if (NS_FAILED(mControls->GetSortedControls(sortedControls))) { + return false; + } + + uint32_t len = sortedControls.Length(); + + for (uint32_t i = 0; i < len; ++i) { + nsCOMPtr<nsIConstraintValidation> cvElmt = + do_QueryObject(sortedControls[i]); + bool defaultAction = true; + if (cvElmt && !cvElmt->CheckValidity(*sortedControls[i], &defaultAction)) { + ret = false; + + // Add all unhandled invalid controls to aInvalidElements if the caller + // requested them. + if (defaultAction && aInvalidElements) { + aInvalidElements->AppendElement(sortedControls[i]); + } + } + } + + return ret; +} + +bool HTMLFormElement::CheckValidFormSubmission() { + /** + * Check for form validity: do not submit a form if there are unhandled + * invalid controls in the form. + * This should not be done if the form has been submitted with .submit(). + * + * NOTE: for the moment, we are also checking that whether the MozInvalidForm + * event gets prevented default so it will prevent blocking form submission if + * the browser does not have implemented a UI yet. + * + * TODO: the check for MozInvalidForm event should be removed later when HTML5 + * Forms will be spread enough and authors will assume forms can't be + * submitted when invalid. See bug 587671. + */ + + NS_ASSERTION(!HasAttr(nsGkAtoms::novalidate), + "We shouldn't be there if novalidate is set!"); + + AutoTArray<RefPtr<Element>, 32> invalidElements; + if (CheckFormValidity(&invalidElements)) { + return true; + } + + AutoJSAPI jsapi; + if (!jsapi.Init(GetOwnerGlobal())) { + return false; + } + JS::Rooted<JS::Value> detail(jsapi.cx()); + if (!ToJSValue(jsapi.cx(), invalidElements, &detail)) { + return false; + } + + RefPtr<CustomEvent> event = + NS_NewDOMCustomEvent(OwnerDoc(), nullptr, nullptr); + event->InitCustomEvent(jsapi.cx(), u"MozInvalidForm"_ns, + /* CanBubble */ true, + /* Cancelable */ true, detail); + event->SetTrusted(true); + event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true; + + DispatchEvent(*event); + + return !event->DefaultPrevented(); +} + +void HTMLFormElement::UpdateValidity(bool aElementValidity) { + if (aElementValidity) { + --mInvalidElementsCount; + } else { + ++mInvalidElementsCount; + } + + NS_ASSERTION(mInvalidElementsCount >= 0, "Something went seriously wrong!"); + + // The form validity has just changed if: + // - there are no more invalid elements ; + // - or there is one invalid elmement and an element just became invalid. + // If we have invalid elements and we used to before as well, do nothing. + if (mInvalidElementsCount && + (mInvalidElementsCount != 1 || aElementValidity)) { + return; + } + + AutoStateChangeNotifier notifier(*this, true); + RemoveStatesSilently(ElementState::VALID | ElementState::INVALID); + AddStatesSilently(mInvalidElementsCount ? ElementState::INVALID + : ElementState::VALID); +} + +int32_t HTMLFormElement::IndexOfContent(nsIContent* aContent) { + int32_t index = 0; + return mControls->IndexOfContent(aContent, &index) == NS_OK ? index : 0; +} + +void HTMLFormElement::Clear() { + for (HTMLImageElement* image : Reversed(mImageElements.AsList())) { + image->ClearForm(false); + } + mImageElements.Clear(); + mImageNameLookupTable.Clear(); + mPastNameLookupTable.Clear(); +} + +namespace { + +struct PositionComparator { + nsIContent* const mElement; + explicit PositionComparator(nsIContent* const aElement) + : mElement(aElement) {} + + int operator()(nsIContent* aElement) const { + if (mElement == aElement) { + return 0; + } + if (nsContentUtils::PositionIsBefore(mElement, aElement)) { + return -1; + } + return 1; + } +}; + +struct RadioNodeListAdaptor { + RadioNodeList* const mList; + explicit RadioNodeListAdaptor(RadioNodeList* aList) : mList(aList) {} + nsIContent* operator[](size_t aIdx) const { return mList->Item(aIdx); } +}; + +} // namespace + +nsresult HTMLFormElement::AddElementToTableInternal( + nsInterfaceHashtable<nsStringHashKey, nsISupports>& aTable, + nsIContent* aChild, const nsAString& aName) { + return aTable.WithEntryHandle(aName, [&](auto&& entry) { + if (!entry) { + // No entry found, add the element + entry.Insert(aChild); + ++mExpandoAndGeneration.generation; + } else { + // Found something in the hash, check its type + nsCOMPtr<nsIContent> content = do_QueryInterface(entry.Data()); + + if (content) { + // Check if the new content is the same as the one we found in the + // hash, if it is then we leave it in the hash as it is, this will + // happen if a form control has both a name and an id with the same + // value + if (content == aChild) { + return NS_OK; + } + + // Found an element, create a list, add the element to the list and put + // the list in the hash + RadioNodeList* list = new RadioNodeList(this); + + // If an element has a @form, we can assume it *might* be able to not + // have a parent and still be in the form. + NS_ASSERTION( + (content->IsElement() && content->AsElement()->HasAttr( + kNameSpaceID_None, nsGkAtoms::form)) || + content->GetParent(), + "Item in list without parent"); + + // Determine the ordering between the new and old element. + bool newFirst = nsContentUtils::PositionIsBefore(aChild, content); + + list->AppendElement(newFirst ? aChild : content.get()); + list->AppendElement(newFirst ? content.get() : aChild); + + nsCOMPtr<nsISupports> listSupports = do_QueryObject(list); + + // Replace the element with the list. + entry.Data() = listSupports; + } else { + // There's already a list in the hash, add the child to the list. + MOZ_ASSERT(nsCOMPtr<RadioNodeList>(do_QueryInterface(entry.Data()))); + auto* list = static_cast<RadioNodeList*>(entry->get()); + + NS_ASSERTION( + list->Length() > 1, + "List should have been converted back to a single element"); + + // Fast-path appends; this check is ok even if the child is + // already in the list, since if it tests true the child would + // have come at the end of the list, and the PositionIsBefore + // will test false. + if (nsContentUtils::PositionIsBefore(list->Item(list->Length() - 1), + aChild)) { + list->AppendElement(aChild); + return NS_OK; + } + + // If a control has a name equal to its id, it could be in the + // list already. + if (list->IndexOf(aChild) != -1) { + return NS_OK; + } + + size_t idx; + DebugOnly<bool> found = + BinarySearchIf(RadioNodeListAdaptor(list), 0, list->Length(), + PositionComparator(aChild), &idx); + MOZ_ASSERT(!found, "should not have found an element"); + + list->InsertElementAt(aChild, idx); + } + } + + return NS_OK; + }); +} + +nsresult HTMLFormElement::AddImageElement(HTMLImageElement* aElement) { + mImageElements.Insert(*aElement, this); + return NS_OK; +} + +nsresult HTMLFormElement::AddImageElementToTable(HTMLImageElement* aChild, + const nsAString& aName) { + return AddElementToTableInternal(mImageNameLookupTable, aChild, aName); +} + +nsresult HTMLFormElement::RemoveImageElement(HTMLImageElement* aElement) { + RemoveElementFromPastNamesMap(aElement); + mImageElements.RemoveElement(*aElement); + return NS_OK; +} + +nsresult HTMLFormElement::RemoveImageElementFromTable( + HTMLImageElement* aElement, const nsAString& aName) { + return RemoveElementFromTableInternal(mImageNameLookupTable, aElement, aName); +} + +void HTMLFormElement::AddToPastNamesMap(const nsAString& aName, + nsISupports* aChild) { + // If candidates contains exactly one node. Add a mapping from name to the + // node in candidates in the form element's past names map, replacing the + // previous entry with the same name, if any. + nsCOMPtr<nsIContent> node = do_QueryInterface(aChild); + if (node) { + mPastNameLookupTable.InsertOrUpdate(aName, ToSupports(node)); + node->SetFlags(MAY_BE_IN_PAST_NAMES_MAP); + } +} + +void HTMLFormElement::RemoveElementFromPastNamesMap(Element* aElement) { + if (!aElement->HasFlag(MAY_BE_IN_PAST_NAMES_MAP)) { + return; + } + + aElement->UnsetFlags(MAY_BE_IN_PAST_NAMES_MAP); + + uint32_t oldCount = mPastNameLookupTable.Count(); + for (auto iter = mPastNameLookupTable.Iter(); !iter.Done(); iter.Next()) { + if (aElement == iter.Data()) { + iter.Remove(); + } + } + if (oldCount != mPastNameLookupTable.Count()) { + ++mExpandoAndGeneration.generation; + } +} + +JSObject* HTMLFormElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLFormElement_Binding::Wrap(aCx, this, aGivenProto); +} + +int32_t HTMLFormElement::GetFormNumberForStateKey() { + if (mFormNumber == -1) { + mFormNumber = OwnerDoc()->GetNextFormNumber(); + } + return mFormNumber; +} + +void HTMLFormElement::NodeInfoChanged(Document* aOldDoc) { + nsGenericHTMLElement::NodeInfoChanged(aOldDoc); + + // When a <form> element is adopted into a new document, we want any state + // keys generated from it to no longer consider this element to be parser + // inserted, and so have state keys based on the position of the <form> + // element in the document, rather than the order it was inserted in. + // + // This is not strictly necessary, since we only ever look at the form number + // for parser inserted form controls, and we do that at the time the form + // control element is inserted into its original document by the parser. + mFormNumber = -1; +} + +bool HTMLFormElement::IsSubmitting() const { + bool loading = mTargetContext && !mTargetContext->IsDiscarded() && + mCurrentLoadId && + mTargetContext->IsLoadingIdentifier(*mCurrentLoadId); + return loading; +} + +void HTMLFormElement::MaybeFireFormRemoved() { + // We want this event to be fired only when the form is removed from the DOM + // tree, not when it is released (ex, tab is closed). So don't fire an event + // when the form doesn't have a docshell. + Document* doc = GetComposedDoc(); + nsIDocShell* container = doc ? doc->GetDocShell() : nullptr; + if (!container) { + return; + } + + // Right now, only the password manager and formautofill listen to the event + // and only listen to it under certain circumstances. So don't fire this event + // unless necessary. + if (!doc->ShouldNotifyFormOrPasswordRemoved()) { + return; + } + + AsyncEventDispatcher::RunDOMEventWhenSafe( + *this, u"DOMFormRemoved"_ns, CanBubble::eNo, ChromeOnlyDispatch::eYes); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLFormElement.h b/dom/html/HTMLFormElement.h new file mode 100644 index 0000000000..68dd627554 --- /dev/null +++ b/dom/html/HTMLFormElement.h @@ -0,0 +1,596 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLFormElement_h +#define mozilla_dom_HTMLFormElement_h + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/PopupBlocker.h" +#include "mozilla/dom/RadioGroupContainer.h" +#include "nsIFormControl.h" +#include "nsGenericHTMLElement.h" +#include "nsThreadUtils.h" +#include "nsInterfaceHashtable.h" +#include "js/friend/DOMProxy.h" // JS::ExpandoAndGeneration + +class nsIMutableArray; +class nsIURI; + +namespace mozilla { +class EventChainPostVisitor; +class EventChainPreVisitor; +namespace dom { +class DialogFormSubmission; +class HTMLFormControlsCollection; +class HTMLFormSubmission; +class HTMLImageElement; +class FormData; + +class HTMLFormElement final : public nsGenericHTMLElement { + friend class HTMLFormControlsCollection; + + public: + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLFormElement, form) + + explicit HTMLFormElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + enum { FORM_CONTROL_LIST_HASHTABLE_LENGTH = 8 }; + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + int32_t IndexOfContent(nsIContent* aContent); + nsGenericHTMLFormElement* GetDefaultSubmitElement() const; + bool IsDefaultSubmitElement(nsGenericHTMLFormElement* aElement) const { + return aElement == mDefaultSubmitElement; + } + + // EventTarget + void AsyncEventRunning(AsyncEventDispatcher* aEvent) override; + + /** Whether we already dispatched a DOMFormHasPassword event or not */ + bool mHasPendingPasswordEvent = false; + /** Whether we already dispatched a DOMFormHasPossibleUsername event or not */ + bool mHasPendingPossibleUsernameEvent = false; + + // nsIContent + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + void WillHandleEvent(EventChainPostVisitor& aVisitor) override; + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + + /** + * Forget all information about the current submission (and the fact that we + * are currently submitting at all). + */ + void ForgetCurrentSubmission(); + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLFormElement, + nsGenericHTMLElement) + + /** + * Remove an element from this form's list of elements + * + * @param aElement the element to remove + * @param aUpdateValidity If true, updates the form validity. + * @return NS_OK if the element was successfully removed. + */ + nsresult RemoveElement(nsGenericHTMLFormElement* aElement, + bool aUpdateValidity); + + /** + * Remove an element from the lookup table maintained by the form. + * We can't fold this method into RemoveElement() because when + * RemoveElement() is called it doesn't know if the element is + * removed because the id attribute has changed, or because the + * name attribute has changed. + * + * @param aElement the element to remove + * @param aName the name or id of the element to remove + * @return NS_OK if the element was successfully removed. + */ + nsresult RemoveElementFromTable(nsGenericHTMLFormElement* aElement, + const nsAString& aName); + + /** + * Add an element to end of this form's list of elements + * + * @param aElement the element to add + * @param aUpdateValidity If true, the form validity will be updated. + * @param aNotify If true, send DocumentObserver notifications as needed. + * @return NS_OK if the element was successfully added + */ + nsresult AddElement(nsGenericHTMLFormElement* aElement, bool aUpdateValidity, + bool aNotify); + + /** + * Add an element to the lookup table maintained by the form. + * + * We can't fold this method into AddElement() because when + * AddElement() is called, the form control has no + * attributes. The name or id attributes of the form control + * are used as a key into the table. + */ + nsresult AddElementToTable(nsGenericHTMLFormElement* aChild, + const nsAString& aName); + + /** + * Remove an image element from this form's list of image elements + * + * @param aElement the image element to remove + * @return NS_OK if the element was successfully removed. + */ + nsresult RemoveImageElement(HTMLImageElement* aElement); + + /** + * Remove an image element from the lookup table maintained by the form. + * We can't fold this method into RemoveImageElement() because when + * RemoveImageElement() is called it doesn't know if the element is + * removed because the id attribute has changed, or because the + * name attribute has changed. + * + * @param aElement the image element to remove + * @param aName the name or id of the element to remove + * @return NS_OK if the element was successfully removed. + */ + nsresult RemoveImageElementFromTable(HTMLImageElement* aElement, + const nsAString& aName); + /** + * Add an image element to the end of this form's list of image elements + * + * @param aElement the element to add + * @return NS_OK if the element was successfully added + */ + nsresult AddImageElement(HTMLImageElement* aElement); + + /** + * Add an image element to the lookup table maintained by the form. + * + * We can't fold this method into AddImageElement() because when + * AddImageElement() is called, the image attributes can change. + * The name or id attributes of the image are used as a key into the table. + */ + nsresult AddImageElementToTable(HTMLImageElement* aChild, + const nsAString& aName); + + /** + * Returns true if implicit submission of this form is disabled. For more + * on implicit submission see: + * + * http://www.whatwg.org/specs/web-apps/current-work/multipage/association-of-controls-and-forms.html#implicit-submission + */ + bool ImplicitSubmissionIsDisabled() const; + + /** + * Check whether a given nsGenericHTMLFormElement is the last single line + * input control that is not disabled. aElement is expected to not be null. + */ + bool IsLastActiveElement(const nsGenericHTMLFormElement* aElement) const; + + /** + * Flag the form to know that a button or image triggered scripted form + * submission. In that case the form will defer the submission until the + * script handler returns and the return value is known. + */ + void OnSubmitClickBegin(Element* aOriginatingElement); + void OnSubmitClickEnd(); + + /** + * This method will update the form validity. + * + * This method has to be called by form elements whenever their validity state + * or status regarding constraint validation changes. + * + * @note This method isn't used for CheckValidity(). + * @note If an element becomes barred from constraint validation, it has to be + * considered as valid. + * + * @param aElementValidityState the new validity state of the element + */ + void UpdateValidity(bool aElementValidityState); + + /** + * This method check the form validity and make invalid form elements send + * invalid event if needed. + * + * @return Whether the form is valid. + * + * @note Do not call this method if novalidate/formnovalidate is used. + * @note This method might disappear with bug 592124, hopefuly. + * @see + * https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#interactively-validate-the-constraints + */ + bool CheckValidFormSubmission(); + + /** + * Contruct the entry list to get their data pumped into the FormData and + * fire a `formdata` event with the entry list in formData attribute. + * <https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#constructing-form-data-set> + * + * @param aFormData the form data object + */ + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult ConstructEntryList(FormData*); + + /** + * Implements form[name]. Returns form controls in this form with the correct + * value of the name attribute. + */ + already_AddRefed<nsISupports> FindNamedItem(const nsAString& aName, + nsWrapperCache** aCache); + + // WebIDL + + void GetAcceptCharset(DOMString& aValue) { + GetHTMLAttr(nsGkAtoms::acceptcharset, aValue); + } + + void SetAcceptCharset(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::acceptcharset, aValue, aRv); + } + + void GetAction(nsString& aValue); + void SetAction(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::action, aValue, aRv); + } + + void GetAutocomplete(nsAString& aValue); + void SetAutocomplete(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::autocomplete, aValue, aRv); + } + + void GetEnctype(nsAString& aValue); + void SetEnctype(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::enctype, aValue, aRv); + } + + void GetEncoding(nsAString& aValue) { GetEnctype(aValue); } + void SetEncoding(const nsAString& aValue, ErrorResult& aRv) { + SetEnctype(aValue, aRv); + } + + void GetMethod(nsAString& aValue); + void SetMethod(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::method, aValue, aRv); + } + + void GetName(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); } + + void SetName(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aValue, aRv); + } + + bool NoValidate() const { return GetBoolAttr(nsGkAtoms::novalidate); } + + void SetNoValidate(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::novalidate, aValue, aRv); + } + + void GetTarget(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::target, aValue); } + + void SetTarget(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::target, aValue, aRv); + } + + void GetRel(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::rel, aValue); } + void SetRel(const nsAString& aRel, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::rel, aRel, aError); + } + nsDOMTokenList* RelList(); + + // it's only out-of-line because the class definition is not available in the + // header + HTMLFormControlsCollection* Elements(); + + int32_t Length(); + + /** + * Check whether submission can proceed for this form then fire submit event. + * This basically implements steps 1-6 (more or less) of + * <https://html.spec.whatwg.org/multipage/forms.html#concept-form-submit>. + * @param aSubmitter If not null, is the "submitter" from that algorithm. + * Therefore it must be a valid submit control. + */ + MOZ_CAN_RUN_SCRIPT void MaybeSubmit(Element* aSubmitter); + MOZ_CAN_RUN_SCRIPT void MaybeReset(Element* aSubmitter); + void Submit(ErrorResult& aRv); + + /** + * Requests to submit the form. Unlike submit(), this method includes + * interactive constraint validation and firing a submit event, + * either of which can cancel submission. + * + * @param aSubmitter The submitter argument can be used to point to a specific + * submit button. + * @param aRv An ErrorResult. + * @see + * https://html.spec.whatwg.org/multipage/forms.html#dom-form-requestsubmit + */ + MOZ_CAN_RUN_SCRIPT void RequestSubmit(nsGenericHTMLElement* aSubmitter, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void Reset(); + + bool CheckValidity() { return CheckFormValidity(nullptr); } + + bool ReportValidity() { return CheckValidFormSubmission(); } + + Element* IndexedGetter(uint32_t aIndex, bool& aFound); + + already_AddRefed<nsISupports> NamedGetter(const nsAString& aName, + bool& aFound); + + void GetSupportedNames(nsTArray<nsString>& aRetval); + +#ifdef DEBUG + static void AssertDocumentOrder( + const nsTArray<nsGenericHTMLFormElement*>& aControls, nsIContent* aForm); + static void AssertDocumentOrder( + const nsTArray<RefPtr<nsGenericHTMLFormElement>>& aControls, + nsIContent* aForm); +#endif + + JS::ExpandoAndGeneration mExpandoAndGeneration; + + protected: + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + class RemoveElementRunnable; + friend class RemoveElementRunnable; + + class RemoveElementRunnable : public Runnable { + public: + explicit RemoveElementRunnable(HTMLFormElement* aForm) + : Runnable("dom::HTMLFormElement::RemoveElementRunnable"), + mForm(aForm) {} + + NS_IMETHOD Run() override { + mForm->HandleDefaultSubmitRemoval(); + return NS_OK; + } + + private: + RefPtr<HTMLFormElement> mForm; + }; + + nsresult DoReset(); + + // Async callback to handle removal of our default submit + void HandleDefaultSubmitRemoval(); + + // + // Submit Helpers + // + // + /** + * Attempt to submit (submission might be deferred) + * + * @param aPresContext the presentation context + * @param aEvent the DOM event that was passed to us for the submit + */ + nsresult DoSubmit(Event* aEvent = nullptr); + + /** + * Prepare the submission object (called by DoSubmit) + * + * @param aFormSubmission the submission object + * @param aEvent the DOM event that was passed to us for the submit + */ + nsresult BuildSubmission(HTMLFormSubmission** aFormSubmission, Event* aEvent); + /** + * Perform the submission (called by DoSubmit and FlushPendingSubmission) + * + * @param aFormSubmission the submission object + */ + nsresult SubmitSubmission(HTMLFormSubmission* aFormSubmission); + + /** + * Submit a form[method=dialog] + * @param aFormSubmission the submission object + */ + nsresult SubmitDialog(DialogFormSubmission* aFormSubmission); + + /** + * Notify any submit observers of the submit. + * + * @param aActionURL the URL being submitted to + * @param aCancelSubmit out param where submit observers can specify that the + * submit should be cancelled. + */ + nsresult NotifySubmitObservers(nsIURI* aActionURL, bool* aCancelSubmit, + bool aEarlyNotify); + + /** + * If this form submission is secure -> insecure, ask the user if they want + * to continue. + * + * @param aActionURL the URL being submitted to + * @param aCancelSubmit out param: will be true if the user wants to cancel + */ + nsresult DoSecureToInsecureSubmitCheck(nsIURI* aActionURL, + bool* aCancelSubmit); + + /** + * Find form controls in this form with the correct value in the name + * attribute. + */ + already_AddRefed<nsISupports> DoResolveName(const nsAString& aName); + + /** + * Check the form validity following this algorithm: + * https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#statically-validate-the-constraints + * + * @param aInvalidElements [out] parameter containing the list of unhandled + * invalid controls. + * + * @return Whether the form is currently valid. + */ + bool CheckFormValidity(nsTArray<RefPtr<Element>>* aInvalidElements) const; + + // Clear the mImageNameLookupTable and mImageElements. + void Clear(); + + // Insert a element into the past names map. + void AddToPastNamesMap(const nsAString& aName, nsISupports* aChild); + + // Remove the given element from the past names map. The element must be an + // nsGenericHTMLFormElement or HTMLImageElement. + void RemoveElementFromPastNamesMap(Element* aElement); + + nsresult AddElementToTableInternal( + nsInterfaceHashtable<nsStringHashKey, nsISupports>& aTable, + nsIContent* aChild, const nsAString& aName); + + nsresult RemoveElementFromTableInternal( + nsInterfaceHashtable<nsStringHashKey, nsISupports>& aTable, + nsIContent* aChild, const nsAString& aName); + + public: + /** + * Flush a possible pending submission. If there was a scripted submission + * triggered by a button or image, the submission was defered. This method + * forces the pending submission to be submitted. (happens when the handler + * returns false or there is an action/target change in the script) + */ + void FlushPendingSubmission(); + + /** + * Get the full URL to submit to. Do not submit if the returned URL is null. + * + * @param aActionURL the full, unadulterated URL you'll be submitting to [OUT] + * @param aOriginatingElement the originating element of the form submission + * [IN] + */ + nsresult GetActionURL(nsIURI** aActionURL, Element* aOriginatingElement); + + // Returns a number for this form that is unique within its owner document. + // This is used by nsContentUtils::GenerateStateKey to identify form controls + // that are inserted into the document by the parser. + int32_t GetFormNumberForStateKey(); + + /** + * Called when we have been cloned and adopted, and the information of the + * node has been changed. + */ + void NodeInfoChanged(Document* aOldDoc) override; + + protected: + // + // Data members + // + /** The list of controls (form.elements as well as stuff not in elements) */ + RefPtr<HTMLFormControlsCollection> mControls; + + /** The pending submission object */ + UniquePtr<HTMLFormSubmission> mPendingSubmission; + + /** The target browsing context, if any. */ + RefPtr<BrowsingContext> mTargetContext; + /** The load identifier for the pending request created for a + * submit, used to be able to block double submits. */ + Maybe<uint64_t> mCurrentLoadId; + + /** The default submit element -- WEAK */ + nsGenericHTMLFormElement* mDefaultSubmitElement; + + /** The first submit element in mElements -- WEAK */ + nsGenericHTMLFormElement* mFirstSubmitInElements; + + /** The first submit element in mNotInElements -- WEAK */ + nsGenericHTMLFormElement* mFirstSubmitNotInElements; + + // This array holds on to all HTMLImageElement(s). + // This is needed to properly clean up the bi-directional references + // (both weak and strong) between the form and its HTMLImageElements. + + // Holds WEAK references + TreeOrderedArray<HTMLImageElement*> mImageElements; + + // A map from an ID or NAME attribute to the HTMLImageElement(s), this + // hash holds strong references either to the named HTMLImageElement, or + // to a list of named HTMLImageElement(s), in the case where this hash + // holds on to a list of named HTMLImageElement(s) the list has weak + // references to the HTMLImageElement. + + nsInterfaceHashtable<nsStringHashKey, nsISupports> mImageNameLookupTable; + + // A map from names to elements that were gotten by those names from this + // form in that past. See "past names map" in the HTML5 specification. + + nsInterfaceHashtable<nsStringHashKey, nsISupports> mPastNameLookupTable; + + /** Keep track of what the popup state was when the submit was initiated */ + PopupBlocker::PopupControlState mSubmitPopupState; + + RefPtr<nsDOMTokenList> mRelList; + + /** + * Number of invalid and candidate for constraint validation elements in the + * form the last time UpdateValidity has been called. + */ + int32_t mInvalidElementsCount; + + // See GetFormNumberForStateKey. + int32_t mFormNumber; + + /** Whether we are currently processing a submit event or not */ + bool mGeneratingSubmit; + /** Whether we are currently processing a reset event or not */ + bool mGeneratingReset; + /** Whether the submission is to be deferred in case a script triggers it */ + bool mDeferSubmission; + /** Whether we notified NS_FORMSUBMIT_SUBJECT listeners already */ + bool mNotifiedObservers; + /** If we notified the listeners early, what was the result? */ + bool mNotifiedObserversResult; + /** + * Whether the submission of this form has been ever prevented because of + * being invalid. + */ + bool mEverTriedInvalidSubmit; + /** Whether we are constructing entry list */ + bool mIsConstructingEntryList; + /** Whether we are firing submission event */ + bool mIsFiringSubmissionEvents; + + private: + bool IsSubmitting() const; + + void SetDefaultSubmitElement(nsGenericHTMLFormElement*); + + NotNull<const Encoding*> GetSubmitEncoding(); + + /** + * Fire an event when the form is removed from the DOM tree. This is now only + * used by the password manager and formautofill. + */ + void MaybeFireFormRemoved(); + + MOZ_CAN_RUN_SCRIPT + void ReportInvalidUnfocusableElements(); + + ~HTMLFormElement(); +}; + +} // namespace dom + +} // namespace mozilla + +#endif // mozilla_dom_HTMLFormElement_h diff --git a/dom/html/HTMLFormSubmission.cpp b/dom/html/HTMLFormSubmission.cpp new file mode 100644 index 0000000000..fa25794274 --- /dev/null +++ b/dom/html/HTMLFormSubmission.cpp @@ -0,0 +1,881 @@ +/* -*- 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 "HTMLFormSubmission.h" +#include "HTMLFormElement.h" +#include "HTMLFormSubmissionConstants.h" +#include "nsCOMPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsGkAtoms.h" +#include "nsIFormControl.h" +#include "nsError.h" +#include "nsGenericHTMLElement.h" +#include "nsAttrValueInlines.h" +#include "nsDirectoryServiceDefs.h" +#include "nsStringStream.h" +#include "nsIURI.h" +#include "nsIURIMutator.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsLinebreakConverter.h" +#include "nsEscape.h" +#include "nsUnicharUtils.h" +#include "nsIMultiplexInputStream.h" +#include "nsIMIMEInputStream.h" +#include "nsIScriptError.h" +#include "nsCExternalHandlerService.h" +#include "nsContentUtils.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/AncestorIterator.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/File.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/RandomNum.h" + +#include <tuple> + +namespace mozilla::dom { + +namespace { + +void SendJSWarning(Document* aDocument, const char* aWarningName, + const nsTArray<nsString>& aWarningArgs) { + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "HTML"_ns, + aDocument, nsContentUtils::eFORMS_PROPERTIES, + aWarningName, aWarningArgs); +} + +void RetrieveFileName(Blob* aBlob, nsAString& aFilename) { + if (!aBlob) { + return; + } + + RefPtr<File> file = aBlob->ToFile(); + if (file) { + file->GetName(aFilename); + } +} + +void RetrieveDirectoryName(Directory* aDirectory, nsAString& aDirname) { + MOZ_ASSERT(aDirectory); + + ErrorResult rv; + aDirectory->GetName(aDirname, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + aDirname.Truncate(); + } +} + +// -------------------------------------------------------------------------- + +class FSURLEncoded : public EncodingFormSubmission { + public: + /** + * @param aEncoding the character encoding of the form + * @param aMethod the method of the submit (either NS_FORM_METHOD_GET or + * NS_FORM_METHOD_POST). + */ + FSURLEncoded(nsIURI* aActionURL, const nsAString& aTarget, + NotNull<const Encoding*> aEncoding, int32_t aMethod, + Document* aDocument, Element* aSubmitter) + : EncodingFormSubmission(aActionURL, aTarget, aEncoding, aSubmitter), + mMethod(aMethod), + mDocument(aDocument), + mWarnedFileControl(false) {} + + virtual nsresult AddNameValuePair(const nsAString& aName, + const nsAString& aValue) override; + + virtual nsresult AddNameBlobPair(const nsAString& aName, + Blob* aBlob) override; + + virtual nsresult AddNameDirectoryPair(const nsAString& aName, + Directory* aDirectory) override; + + virtual nsresult GetEncodedSubmission(nsIURI* aURI, + nsIInputStream** aPostDataStream, + nsCOMPtr<nsIURI>& aOutURI) override; + + protected: + /** + * URL encode a Unicode string by encoding it to bytes, converting linebreaks + * properly, and then escaping many bytes as %xx. + * + * @param aStr the string to encode + * @param aEncoded the encoded string [OUT] + * @throws NS_ERROR_OUT_OF_MEMORY if we run out of memory + */ + nsresult URLEncode(const nsAString& aStr, nsACString& aEncoded); + + private: + /** + * The method of the submit (either NS_FORM_METHOD_GET or + * NS_FORM_METHOD_POST). + */ + int32_t mMethod; + + /** The query string so far (the part after the ?) */ + nsCString mQueryString; + + /** The document whose URI to use when reporting errors */ + nsCOMPtr<Document> mDocument; + + /** Whether or not we have warned about a file control not being submitted */ + bool mWarnedFileControl; +}; + +nsresult FSURLEncoded::AddNameValuePair(const nsAString& aName, + const nsAString& aValue) { + // Encode value + nsCString convValue; + nsresult rv = URLEncode(aValue, convValue); + NS_ENSURE_SUCCESS(rv, rv); + + // Encode name + nsAutoCString convName; + rv = URLEncode(aName, convName); + NS_ENSURE_SUCCESS(rv, rv); + + // Append data to string + if (mQueryString.IsEmpty()) { + mQueryString += convName + "="_ns + convValue; + } else { + mQueryString += "&"_ns + convName + "="_ns + convValue; + } + + return NS_OK; +} + +nsresult FSURLEncoded::AddNameBlobPair(const nsAString& aName, Blob* aBlob) { + if (!mWarnedFileControl) { + SendJSWarning(mDocument, "ForgotFileEnctypeWarning", nsTArray<nsString>()); + mWarnedFileControl = true; + } + + nsAutoString filename; + RetrieveFileName(aBlob, filename); + return AddNameValuePair(aName, filename); +} + +nsresult FSURLEncoded::AddNameDirectoryPair(const nsAString& aName, + Directory* aDirectory) { + // No warning about because Directory objects are never sent via form. + + nsAutoString dirname; + RetrieveDirectoryName(aDirectory, dirname); + return AddNameValuePair(aName, dirname); +} + +void HandleMailtoSubject(nsCString& aPath) { + // Walk through the string and see if we have a subject already. + bool hasSubject = false; + bool hasParams = false; + int32_t paramSep = aPath.FindChar('?'); + while (paramSep != kNotFound && paramSep < (int32_t)aPath.Length()) { + hasParams = true; + + // Get the end of the name at the = op. If it is *after* the next &, + // assume that someone made a parameter without an = in it + int32_t nameEnd = aPath.FindChar('=', paramSep + 1); + int32_t nextParamSep = aPath.FindChar('&', paramSep + 1); + if (nextParamSep == kNotFound) { + nextParamSep = aPath.Length(); + } + + // If the = op is after the &, this parameter is a name without value. + // If there is no = op, same thing. + if (nameEnd == kNotFound || nextParamSep < nameEnd) { + nameEnd = nextParamSep; + } + + if (nameEnd != kNotFound) { + if (Substring(aPath, paramSep + 1, nameEnd - (paramSep + 1)) + .LowerCaseEqualsLiteral("subject")) { + hasSubject = true; + break; + } + } + + paramSep = nextParamSep; + } + + // If there is no subject, append a preformed subject to the mailto line + if (!hasSubject) { + if (hasParams) { + aPath.Append('&'); + } else { + aPath.Append('?'); + } + + // Get the default subject + nsAutoString brandName; + nsresult rv = nsContentUtils::GetLocalizedString( + nsContentUtils::eBRAND_PROPERTIES, "brandShortName", brandName); + if (NS_FAILED(rv)) return; + nsAutoString subjectStr; + rv = nsContentUtils::FormatLocalizedString( + subjectStr, nsContentUtils::eFORMS_PROPERTIES, "DefaultFormSubject", + brandName); + if (NS_FAILED(rv)) return; + aPath.AppendLiteral("subject="); + nsCString subjectStrEscaped; + rv = NS_EscapeURL(NS_ConvertUTF16toUTF8(subjectStr), esc_Query, + subjectStrEscaped, mozilla::fallible); + if (NS_FAILED(rv)) return; + + aPath.Append(subjectStrEscaped); + } +} + +nsresult FSURLEncoded::GetEncodedSubmission(nsIURI* aURI, + nsIInputStream** aPostDataStream, + nsCOMPtr<nsIURI>& aOutURI) { + nsresult rv = NS_OK; + aOutURI = aURI; + + *aPostDataStream = nullptr; + + if (mMethod == NS_FORM_METHOD_POST) { + if (aURI->SchemeIs("mailto")) { + nsAutoCString path; + rv = aURI->GetPathQueryRef(path); + NS_ENSURE_SUCCESS(rv, rv); + + HandleMailtoSubject(path); + + // Append the body to and force-plain-text args to the mailto line + nsAutoCString escapedBody; + if (NS_WARN_IF(!NS_Escape(mQueryString, escapedBody, url_XAlphas))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + path += "&force-plain-text=Y&body="_ns + escapedBody; + + return NS_MutateURI(aURI).SetPathQueryRef(path).Finalize(aOutURI); + } else { + nsCOMPtr<nsIInputStream> dataStream; + rv = NS_NewCStringInputStream(getter_AddRefs(dataStream), + std::move(mQueryString)); + NS_ENSURE_SUCCESS(rv, rv); + mQueryString.Truncate(); + + nsCOMPtr<nsIMIMEInputStream> mimeStream( + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + mimeStream->AddHeader("Content-Type", + "application/x-www-form-urlencoded"); + mimeStream->SetData(dataStream); + + mimeStream.forget(aPostDataStream); + } + + } else { + // Get the full query string + if (aURI->SchemeIs("javascript")) { + return NS_OK; + } + + nsCOMPtr<nsIURL> url = do_QueryInterface(aURI); + if (url) { + // Make sure that we end up with a query component in the URL. If + // mQueryString is empty, nsIURI::SetQuery() will remove the query + // component, which is not what we want. + rv = NS_MutateURI(aURI) + .SetQuery(mQueryString.IsEmpty() ? "?"_ns : mQueryString) + .Finalize(aOutURI); + } else { + nsAutoCString path; + rv = aURI->GetPathQueryRef(path); + NS_ENSURE_SUCCESS(rv, rv); + // Bug 42616: Trim off named anchor and save it to add later + int32_t namedAnchorPos = path.FindChar('#'); + nsAutoCString namedAnchor; + if (kNotFound != namedAnchorPos) { + path.Right(namedAnchor, (path.Length() - namedAnchorPos)); + path.Truncate(namedAnchorPos); + } + + // Chop off old query string (bug 25330, 57333) + // Only do this for GET not POST (bug 41585) + int32_t queryStart = path.FindChar('?'); + if (kNotFound != queryStart) { + path.Truncate(queryStart); + } + + path.Append('?'); + // Bug 42616: Add named anchor to end after query string + path.Append(mQueryString + namedAnchor); + + rv = NS_MutateURI(aURI).SetPathQueryRef(path).Finalize(aOutURI); + } + } + + return rv; +} + +// i18n helper routines +nsresult FSURLEncoded::URLEncode(const nsAString& aStr, nsACString& aEncoded) { + nsAutoCString encodedBuf; + // We encode with eValueEncode because the urlencoded format needs the newline + // normalizations but percent-escapes characters that eNameEncode doesn't, + // so calling NS_Escape would still be needed. + nsresult rv = EncodeVal(aStr, encodedBuf, EncodeType::eValueEncode); + NS_ENSURE_SUCCESS(rv, rv); + + if (NS_WARN_IF(!NS_Escape(encodedBuf, aEncoded, url_XPAlphas))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +} // anonymous namespace + +// -------------------------------------------------------------------------- + +FSMultipartFormData::FSMultipartFormData(nsIURI* aActionURL, + const nsAString& aTarget, + NotNull<const Encoding*> aEncoding, + Element* aSubmitter) + : EncodingFormSubmission(aActionURL, aTarget, aEncoding, aSubmitter) { + mPostData = do_CreateInstance("@mozilla.org/io/multiplex-input-stream;1"); + + nsCOMPtr<nsIInputStream> inputStream = do_QueryInterface(mPostData); + MOZ_ASSERT(SameCOMIdentity(mPostData, inputStream)); + mPostDataStream = inputStream; + + mTotalLength = 0; + + mBoundary.AssignLiteral("---------------------------"); + mBoundary.AppendInt(static_cast<uint32_t>(mozilla::RandomUint64OrDie())); + mBoundary.AppendInt(static_cast<uint32_t>(mozilla::RandomUint64OrDie())); + mBoundary.AppendInt(static_cast<uint32_t>(mozilla::RandomUint64OrDie())); +} + +FSMultipartFormData::~FSMultipartFormData() { + NS_ASSERTION(mPostDataChunk.IsEmpty(), "Left unsubmitted data"); +} + +nsIInputStream* FSMultipartFormData::GetSubmissionBody( + uint64_t* aContentLength) { + // Finish data + mPostDataChunk += "--"_ns + mBoundary + nsLiteralCString("--" CRLF); + + // Add final data input stream + AddPostDataStream(); + + *aContentLength = mTotalLength; + return mPostDataStream; +} + +nsresult FSMultipartFormData::AddNameValuePair(const nsAString& aName, + const nsAString& aValue) { + nsAutoCString encodedVal; + nsresult rv = EncodeVal(aValue, encodedVal, EncodeType::eValueEncode); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString nameStr; + rv = EncodeVal(aName, nameStr, EncodeType::eNameEncode); + NS_ENSURE_SUCCESS(rv, rv); + + // Make MIME block for name/value pair + + mPostDataChunk += "--"_ns + mBoundary + nsLiteralCString(CRLF) + + "Content-Disposition: form-data; name=\""_ns + nameStr + + nsLiteralCString("\"" CRLF CRLF) + encodedVal + + nsLiteralCString(CRLF); + + return NS_OK; +} + +nsresult FSMultipartFormData::AddNameBlobPair(const nsAString& aName, + Blob* aBlob) { + MOZ_ASSERT(aBlob); + + // Encode the control name + nsAutoCString nameStr; + nsresult rv = EncodeVal(aName, nameStr, EncodeType::eNameEncode); + NS_ENSURE_SUCCESS(rv, rv); + + ErrorResult error; + + uint64_t size = 0; + nsAutoCString filename; + nsAutoCString contentType; + nsCOMPtr<nsIInputStream> fileStream; + nsAutoString filename16; + + RefPtr<File> file = aBlob->ToFile(); + if (file) { + nsAutoString relativePath; + file->GetRelativePath(relativePath); + if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() && + !relativePath.IsEmpty()) { + filename16 = relativePath; + } + + if (filename16.IsEmpty()) { + RetrieveFileName(aBlob, filename16); + } + } + + rv = EncodeVal(filename16, filename, EncodeType::eFilenameEncode); + NS_ENSURE_SUCCESS(rv, rv); + + // Get content type + nsAutoString contentType16; + aBlob->GetType(contentType16); + if (contentType16.IsEmpty()) { + contentType16.AssignLiteral("application/octet-stream"); + } + + NS_ConvertUTF16toUTF8 contentType8(contentType16); + int32_t convertedBufLength = 0; + char* convertedBuf = nsLinebreakConverter::ConvertLineBreaks( + contentType8.get(), nsLinebreakConverter::eLinebreakAny, + nsLinebreakConverter::eLinebreakSpace, contentType8.Length(), + &convertedBufLength); + contentType.Adopt(convertedBuf, convertedBufLength); + + // Get input stream + aBlob->CreateInputStream(getter_AddRefs(fileStream), error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + // Get size + size = aBlob->GetSize(error); + if (error.Failed()) { + error.SuppressException(); + fileStream = nullptr; + } + + if (fileStream) { + // Create buffered stream (for efficiency) + nsCOMPtr<nsIInputStream> bufferedStream; + rv = NS_NewBufferedInputStream(getter_AddRefs(bufferedStream), + fileStream.forget(), 8192); + NS_ENSURE_SUCCESS(rv, rv); + + fileStream = bufferedStream; + } + + AddDataChunk(nameStr, filename, contentType, fileStream, size); + return NS_OK; +} + +nsresult FSMultipartFormData::AddNameDirectoryPair(const nsAString& aName, + Directory* aDirectory) { + if (!StaticPrefs::dom_webkitBlink_dirPicker_enabled()) { + return NS_OK; + } + + // Encode the control name + nsAutoCString nameStr; + nsresult rv = EncodeVal(aName, nameStr, EncodeType::eNameEncode); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString dirname; + nsAutoString dirname16; + + ErrorResult error; + nsAutoString path; + aDirectory->GetPath(path, error); + if (NS_WARN_IF(error.Failed())) { + error.SuppressException(); + } else { + dirname16 = path; + } + + if (dirname16.IsEmpty()) { + RetrieveDirectoryName(aDirectory, dirname16); + } + + rv = EncodeVal(dirname16, dirname, EncodeType::eFilenameEncode); + NS_ENSURE_SUCCESS(rv, rv); + + AddDataChunk(nameStr, dirname, "application/octet-stream"_ns, nullptr, 0); + return NS_OK; +} + +void FSMultipartFormData::AddDataChunk(const nsACString& aName, + const nsACString& aFilename, + const nsACString& aContentType, + nsIInputStream* aInputStream, + uint64_t aInputStreamSize) { + // + // Make MIME block for name/value pair + // + // more appropriate than always using binary? + mPostDataChunk += "--"_ns + mBoundary + nsLiteralCString(CRLF); + mPostDataChunk += "Content-Disposition: form-data; name=\""_ns + aName + + "\"; filename=\""_ns + aFilename + + nsLiteralCString("\"" CRLF) + "Content-Type: "_ns + + aContentType + nsLiteralCString(CRLF CRLF); + + // We should not try to append an invalid stream. That will happen for example + // if we try to update a file that actually do not exist. + if (aInputStream) { + // We need to dump the data up to this point into the POST data stream + // here, since we're about to add the file input stream + AddPostDataStream(); + + mPostData->AppendStream(aInputStream); + mTotalLength += aInputStreamSize; + } + + // CRLF after file + mPostDataChunk.AppendLiteral(CRLF); +} + +nsresult FSMultipartFormData::GetEncodedSubmission( + nsIURI* aURI, nsIInputStream** aPostDataStream, nsCOMPtr<nsIURI>& aOutURI) { + nsresult rv; + aOutURI = aURI; + + // Make header + nsCOMPtr<nsIMIMEInputStream> mimeStream = + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString contentType; + GetContentType(contentType); + mimeStream->AddHeader("Content-Type", contentType.get()); + + uint64_t bodySize; + mimeStream->SetData(GetSubmissionBody(&bodySize)); + + mimeStream.forget(aPostDataStream); + + return NS_OK; +} + +nsresult FSMultipartFormData::AddPostDataStream() { + nsresult rv = NS_OK; + + nsCOMPtr<nsIInputStream> postDataChunkStream; + rv = NS_NewCStringInputStream(getter_AddRefs(postDataChunkStream), + mPostDataChunk); + NS_ASSERTION(postDataChunkStream, "Could not open a stream for POST!"); + if (postDataChunkStream) { + mPostData->AppendStream(postDataChunkStream); + mTotalLength += mPostDataChunk.Length(); + } + + mPostDataChunk.Truncate(); + + return rv; +} + +// -------------------------------------------------------------------------- + +namespace { + +class FSTextPlain : public EncodingFormSubmission { + public: + FSTextPlain(nsIURI* aActionURL, const nsAString& aTarget, + NotNull<const Encoding*> aEncoding, Element* aSubmitter) + : EncodingFormSubmission(aActionURL, aTarget, aEncoding, aSubmitter) {} + + virtual nsresult AddNameValuePair(const nsAString& aName, + const nsAString& aValue) override; + + virtual nsresult AddNameBlobPair(const nsAString& aName, + Blob* aBlob) override; + + virtual nsresult AddNameDirectoryPair(const nsAString& aName, + Directory* aDirectory) override; + + virtual nsresult GetEncodedSubmission(nsIURI* aURI, + nsIInputStream** aPostDataStream, + nsCOMPtr<nsIURI>& aOutURI) override; + + private: + nsString mBody; +}; + +nsresult FSTextPlain::AddNameValuePair(const nsAString& aName, + const nsAString& aValue) { + // XXX This won't work well with a name like "a=b" or "a\nb" but I suppose + // text/plain doesn't care about that. Parsers aren't built for escaped + // values so we'll have to live with it. + mBody.Append(aName + u"="_ns + aValue + NS_LITERAL_STRING_FROM_CSTRING(CRLF)); + + return NS_OK; +} + +nsresult FSTextPlain::AddNameBlobPair(const nsAString& aName, Blob* aBlob) { + nsAutoString filename; + RetrieveFileName(aBlob, filename); + AddNameValuePair(aName, filename); + return NS_OK; +} + +nsresult FSTextPlain::AddNameDirectoryPair(const nsAString& aName, + Directory* aDirectory) { + nsAutoString dirname; + RetrieveDirectoryName(aDirectory, dirname); + AddNameValuePair(aName, dirname); + return NS_OK; +} + +nsresult FSTextPlain::GetEncodedSubmission(nsIURI* aURI, + nsIInputStream** aPostDataStream, + nsCOMPtr<nsIURI>& aOutURI) { + nsresult rv = NS_OK; + aOutURI = aURI; + + *aPostDataStream = nullptr; + + // XXX HACK We are using the standard URL mechanism to give the body to the + // mailer instead of passing the post data stream to it, since that sounds + // hard. + if (aURI->SchemeIs("mailto")) { + nsAutoCString path; + rv = aURI->GetPathQueryRef(path); + NS_ENSURE_SUCCESS(rv, rv); + + HandleMailtoSubject(path); + + // Append the body to and force-plain-text args to the mailto line + nsAutoCString escapedBody; + if (NS_WARN_IF(!NS_Escape(NS_ConvertUTF16toUTF8(mBody), escapedBody, + url_XAlphas))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + path += "&force-plain-text=Y&body="_ns + escapedBody; + + rv = NS_MutateURI(aURI).SetPathQueryRef(path).Finalize(aOutURI); + } else { + // Create data stream. + // We use eValueEncode to send the data through the charset encoder and to + // normalize linebreaks to use the "standard net" format (\r\n), but not + // perform any other escaping. This means that names and values which + // contain '=' or newlines are potentially ambiguously encoded, but that is + // how text/plain is specced. + nsCString cbody; + EncodeVal(mBody, cbody, EncodeType::eValueEncode); + + nsCOMPtr<nsIInputStream> bodyStream; + rv = NS_NewCStringInputStream(getter_AddRefs(bodyStream), std::move(cbody)); + if (!bodyStream) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Create mime stream with headers and such + nsCOMPtr<nsIMIMEInputStream> mimeStream = + do_CreateInstance("@mozilla.org/network/mime-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + mimeStream->AddHeader("Content-Type", "text/plain"); + mimeStream->SetData(bodyStream); + mimeStream.forget(aPostDataStream); + } + + return rv; +} + +} // anonymous namespace + +// -------------------------------------------------------------------------- + +HTMLFormSubmission::HTMLFormSubmission( + nsIURI* aActionURL, const nsAString& aTarget, + mozilla::NotNull<const mozilla::Encoding*> aEncoding) + : mActionURL(aActionURL), + mTarget(aTarget), + mEncoding(aEncoding), + mInitiatedFromUserInput(UserActivation::IsHandlingUserInput()) { + MOZ_COUNT_CTOR(HTMLFormSubmission); +} + +EncodingFormSubmission::EncodingFormSubmission( + nsIURI* aActionURL, const nsAString& aTarget, + NotNull<const Encoding*> aEncoding, Element* aSubmitter) + : HTMLFormSubmission(aActionURL, aTarget, aEncoding) { + if (!aEncoding->CanEncodeEverything()) { + nsAutoCString name; + aEncoding->Name(name); + AutoTArray<nsString, 1> args; + CopyUTF8toUTF16(name, *args.AppendElement()); + SendJSWarning(aSubmitter ? aSubmitter->GetOwnerDocument() : nullptr, + "CannotEncodeAllUnicode", args); + } +} + +EncodingFormSubmission::~EncodingFormSubmission() = default; + +// i18n helper routines +nsresult EncodingFormSubmission::EncodeVal(const nsAString& aStr, + nsCString& aOut, + EncodeType aEncodeType) { + nsresult rv; + std::tie(rv, std::ignore) = mEncoding->Encode(aStr, aOut); + if (NS_FAILED(rv)) { + return rv; + } + + if (aEncodeType != EncodeType::eFilenameEncode) { + // Normalize newlines + int32_t convertedBufLength = 0; + char* convertedBuf = nsLinebreakConverter::ConvertLineBreaks( + aOut.get(), nsLinebreakConverter::eLinebreakAny, + nsLinebreakConverter::eLinebreakNet, (int32_t)aOut.Length(), + &convertedBufLength); + aOut.Adopt(convertedBuf, convertedBufLength); + } + + if (aEncodeType != EncodeType::eValueEncode) { + // Percent-escape LF, CR and double quotes. + int32_t offset = 0; + while ((offset = aOut.FindCharInSet("\n\r\"", offset)) != kNotFound) { + if (aOut[offset] == '\n') { + aOut.ReplaceLiteral(offset, 1, "%0A"); + } else if (aOut[offset] == '\r') { + aOut.ReplaceLiteral(offset, 1, "%0D"); + } else if (aOut[offset] == '"') { + aOut.ReplaceLiteral(offset, 1, "%22"); + } else { + MOZ_ASSERT(false); + offset++; + continue; + } + } + } + + return NS_OK; +} + +// -------------------------------------------------------------------------- + +namespace { + +void GetEnumAttr(nsGenericHTMLElement* aContent, nsAtom* atom, + int32_t* aValue) { + const nsAttrValue* value = aContent->GetParsedAttr(atom); + if (value && value->Type() == nsAttrValue::eEnum) { + *aValue = value->GetEnumValue(); + } +} + +} // anonymous namespace + +/* static */ +nsresult HTMLFormSubmission::GetFromForm(HTMLFormElement* aForm, + nsGenericHTMLElement* aSubmitter, + NotNull<const Encoding*>& aEncoding, + HTMLFormSubmission** aFormSubmission) { + // Get all the information necessary to encode the form data + NS_ASSERTION(aForm->GetComposedDoc(), + "Should have doc if we're building submission!"); + + nsresult rv; + + // Get method (default: GET) + int32_t method = NS_FORM_METHOD_GET; + if (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formmethod)) { + GetEnumAttr(aSubmitter, nsGkAtoms::formmethod, &method); + } else { + GetEnumAttr(aForm, nsGkAtoms::method, &method); + } + + if (method == NS_FORM_METHOD_DIALOG) { + HTMLDialogElement* dialog = aForm->FirstAncestorOfType<HTMLDialogElement>(); + + // If there isn't one, do nothing. + if (!dialog) { + return NS_ERROR_FAILURE; + } + + nsAutoString result; + if (aSubmitter) { + aSubmitter->ResultForDialogSubmit(result); + } + *aFormSubmission = new DialogFormSubmission(result, aEncoding, dialog); + return NS_OK; + } + + MOZ_ASSERT(method != NS_FORM_METHOD_DIALOG); + + // Get action + nsCOMPtr<nsIURI> actionURL; + rv = aForm->GetActionURL(getter_AddRefs(actionURL), aSubmitter); + NS_ENSURE_SUCCESS(rv, rv); + + // Check if CSP allows this form-action + nsCOMPtr<nsIContentSecurityPolicy> csp = aForm->GetCsp(); + if (csp) { + bool permitsFormAction = true; + + // form-action is only enforced if explicitly defined in the + // policy - do *not* consult default-src, see: + // http://www.w3.org/TR/CSP2/#directive-default-src + rv = csp->Permits(aForm, nullptr /* nsICSPEventListener */, actionURL, + nsIContentSecurityPolicy::FORM_ACTION_DIRECTIVE, + true /* aSpecific */, true /* aSendViolationReports */, + &permitsFormAction); + NS_ENSURE_SUCCESS(rv, rv); + if (!permitsFormAction) { + return NS_ERROR_CSP_FORM_ACTION_VIOLATION; + } + } + + // Get target + // The target is the submitter element formtarget attribute if the element + // is a submit control and has such an attribute. + // Otherwise, the target is the form owner's target attribute, + // if it has such an attribute. + // Finally, if one of the child nodes of the head element is a base element + // with a target attribute, then the value of the target attribute of the + // first such base element; or, if there is no such element, the empty string. + nsAutoString target; + if (!(aSubmitter && aSubmitter->GetAttr(nsGkAtoms::formtarget, target)) && + !aForm->GetAttr(nsGkAtoms::target, target)) { + aForm->GetBaseTarget(target); + } + + // Get encoding type (default: urlencoded) + int32_t enctype = NS_FORM_ENCTYPE_URLENCODED; + if (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formenctype)) { + GetEnumAttr(aSubmitter, nsGkAtoms::formenctype, &enctype); + } else { + GetEnumAttr(aForm, nsGkAtoms::enctype, &enctype); + } + + // Choose encoder + if (method == NS_FORM_METHOD_POST && enctype == NS_FORM_ENCTYPE_MULTIPART) { + *aFormSubmission = + new FSMultipartFormData(actionURL, target, aEncoding, aSubmitter); + } else if (method == NS_FORM_METHOD_POST && + enctype == NS_FORM_ENCTYPE_TEXTPLAIN) { + *aFormSubmission = + new FSTextPlain(actionURL, target, aEncoding, aSubmitter); + } else { + Document* doc = aForm->OwnerDoc(); + if (enctype == NS_FORM_ENCTYPE_MULTIPART || + enctype == NS_FORM_ENCTYPE_TEXTPLAIN) { + AutoTArray<nsString, 1> args; + nsString& enctypeStr = *args.AppendElement(); + if (aSubmitter && aSubmitter->HasAttr(nsGkAtoms::formenctype)) { + aSubmitter->GetAttr(nsGkAtoms::formenctype, enctypeStr); + } else { + aForm->GetAttr(nsGkAtoms::enctype, enctypeStr); + } + + SendJSWarning(doc, "ForgotPostWarning", args); + } + *aFormSubmission = + new FSURLEncoded(actionURL, target, aEncoding, method, doc, aSubmitter); + } + + return NS_OK; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLFormSubmission.h b/dom/html/HTMLFormSubmission.h new file mode 100644 index 0000000000..7e4d3a0e4e --- /dev/null +++ b/dom/html/HTMLFormSubmission.h @@ -0,0 +1,291 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLFormSubmission_h +#define mozilla_dom_HTMLFormSubmission_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/HTMLDialogElement.h" +#include "nsCOMPtr.h" +#include "mozilla/Encoding.h" +#include "nsString.h" + +class nsIURI; +class nsIInputStream; +class nsGenericHTMLElement; +class nsIMultiplexInputStream; + +namespace mozilla::dom { + +class Blob; +class DialogFormSubmission; +class Directory; +class Element; +class HTMLFormElement; + +/** + * Class for form submissions; encompasses the function to call to submit as + * well as the form submission name/value pairs + */ +class HTMLFormSubmission { + public: + /** + * Get a submission object based on attributes in the form (ENCTYPE and + * METHOD) + * + * @param aForm the form to get a submission object based on + * @param aSubmitter the submitter element (can be null) + * @param aEncoding the submiter element's encoding + * @param aFormSubmission the form submission object (out param) + */ + static nsresult GetFromForm(HTMLFormElement* aForm, + nsGenericHTMLElement* aSubmitter, + NotNull<const Encoding*>& aEncoding, + HTMLFormSubmission** aFormSubmission); + + MOZ_COUNTED_DTOR_VIRTUAL(HTMLFormSubmission) + + /** + * Submit a name/value pair + * + * @param aName the name of the parameter + * @param aValue the value of the parameter + */ + virtual nsresult AddNameValuePair(const nsAString& aName, + const nsAString& aValue) = 0; + + /** + * Submit a name/blob pair + * + * @param aName the name of the parameter + * @param aBlob the blob to submit. The file's name will be used if the Blob + * is actually a File, otherwise 'blob' string is used instead. Must not be + * null. + */ + virtual nsresult AddNameBlobPair(const nsAString& aName, Blob* aBlob) = 0; + + /** + * Submit a name/directory pair + * + * @param aName the name of the parameter + * @param aBlob the directory to submit. + */ + virtual nsresult AddNameDirectoryPair(const nsAString& aName, + Directory* aDirectory) = 0; + + /** + * Given a URI and the current submission, create the final URI and data + * stream that will be submitted. Subclasses *must* implement this. + * + * @param aURI the URI being submitted to [IN] + * @param aPostDataStream a data stream for POST data [OUT] + * @param aOutURI the resulting URI. May be the same as aURI [OUT] + */ + virtual nsresult GetEncodedSubmission(nsIURI* aURI, + nsIInputStream** aPostDataStream, + nsCOMPtr<nsIURI>& aOutURI) = 0; + + /** + * Get the charset that will be used for submission. + */ + void GetCharset(nsACString& aCharset) { mEncoding->Name(aCharset); } + + /** + * Get the action URI that will be used for submission. + */ + nsIURI* GetActionURL() const { return mActionURL; } + + /** + * Get the target that will be used for submission. + */ + void GetTarget(nsAString& aTarget) { aTarget = mTarget; } + + /** + * Return true if this form submission was user-initiated. + */ + bool IsInitiatedFromUserInput() const { return mInitiatedFromUserInput; } + + virtual DialogFormSubmission* GetAsDialogSubmission() { return nullptr; } + + protected: + /** + * Can only be constructed by subclasses. + * + * @param aEncoding the character encoding of the form + */ + HTMLFormSubmission(nsIURI* aActionURL, const nsAString& aTarget, + mozilla::NotNull<const mozilla::Encoding*> aEncoding); + + // The action url. + nsCOMPtr<nsIURI> mActionURL; + + // The target. + nsString mTarget; + + // The character encoding of this form submission + mozilla::NotNull<const mozilla::Encoding*> mEncoding; + + // Keep track of whether this form submission was user-initiated or not + bool mInitiatedFromUserInput; +}; + +class EncodingFormSubmission : public HTMLFormSubmission { + public: + EncodingFormSubmission(nsIURI* aActionURL, const nsAString& aTarget, + mozilla::NotNull<const mozilla::Encoding*> aEncoding, + Element* aSubmitter); + + virtual ~EncodingFormSubmission(); + + // Indicates the type of newline normalization and escaping to perform in + // `EncodeVal`, in addition to encoding the string into bytes. + enum EncodeType { + // Normalizes newlines to CRLF and then escapes for use in + // `Content-Disposition`. (Useful for `multipart/form-data` entry names.) + eNameEncode, + // Escapes for use in `Content-Disposition`. (Useful for + // `multipart/form-data` filenames.) + eFilenameEncode, + // Normalizes newlines to CRLF. + eValueEncode, + }; + + /** + * Encode a Unicode string to bytes, additionally performing escapes or + * normalizations. + * @param aStr the string to encode + * @param aOut the encoded string [OUT] + * @param aEncodeType The type of escapes or normalizations to perform on the + * encoded string. + * @throws an error if UnicodeToNewBytes fails + */ + nsresult EncodeVal(const nsAString& aStr, nsCString& aOut, + EncodeType aEncodeType); +}; + +class DialogFormSubmission final : public HTMLFormSubmission { + public: + DialogFormSubmission(nsAString& aResult, NotNull<const Encoding*> aEncoding, + HTMLDialogElement* aDialogElement) + : HTMLFormSubmission(nullptr, u""_ns, aEncoding), + mDialogElement(aDialogElement), + mReturnValue(aResult) {} + nsresult AddNameValuePair(const nsAString& aName, + const nsAString& aValue) override { + MOZ_CRASH("This method should not be called"); + return NS_OK; + } + + nsresult AddNameBlobPair(const nsAString& aName, Blob* aBlob) override { + MOZ_CRASH("This method should not be called"); + return NS_OK; + } + + nsresult AddNameDirectoryPair(const nsAString& aName, + Directory* aDirectory) override { + MOZ_CRASH("This method should not be called"); + return NS_OK; + } + + nsresult GetEncodedSubmission(nsIURI* aURI, nsIInputStream** aPostDataStream, + nsCOMPtr<nsIURI>& aOutURI) override { + MOZ_CRASH("This method should not be called"); + return NS_OK; + } + + DialogFormSubmission* GetAsDialogSubmission() override { return this; } + + HTMLDialogElement* DialogElement() { return mDialogElement; } + + nsString& ReturnValue() { return mReturnValue; } + + private: + const RefPtr<HTMLDialogElement> mDialogElement; + nsString mReturnValue; +}; + +/** + * Handle multipart/form-data encoding, which does files as well as normal + * inputs. This always does POST. + */ +class FSMultipartFormData : public EncodingFormSubmission { + public: + /** + * @param aEncoding the character encoding of the form + */ + FSMultipartFormData(nsIURI* aActionURL, const nsAString& aTarget, + mozilla::NotNull<const mozilla::Encoding*> aEncoding, + Element* aSubmitter); + ~FSMultipartFormData(); + + virtual nsresult AddNameValuePair(const nsAString& aName, + const nsAString& aValue) override; + + virtual nsresult AddNameBlobPair(const nsAString& aName, + Blob* aBlob) override; + + virtual nsresult AddNameDirectoryPair(const nsAString& aName, + Directory* aDirectory) override; + + virtual nsresult GetEncodedSubmission(nsIURI* aURI, + nsIInputStream** aPostDataStream, + nsCOMPtr<nsIURI>& aOutURI) override; + + void GetContentType(nsACString& aContentType) { + aContentType = "multipart/form-data; boundary="_ns + mBoundary; + } + + nsIInputStream* GetSubmissionBody(uint64_t* aContentLength); + + protected: + /** + * Roll up the data we have so far and add it to the multiplexed data stream. + */ + nsresult AddPostDataStream(); + + private: + void AddDataChunk(const nsACString& aName, const nsACString& aFilename, + const nsACString& aContentType, + nsIInputStream* aInputStream, uint64_t aInputStreamSize); + /** + * The post data stream as it is so far. This is a collection of smaller + * chunks--string streams and file streams interleaved to make one big POST + * stream. + */ + nsCOMPtr<nsIMultiplexInputStream> mPostData; + + /** + * The same stream, but as an nsIInputStream. + * Raw pointers because it is just QI of mInputStream. + */ + nsIInputStream* mPostDataStream; + + /** + * The current string chunk. When a file is hit, the string chunk gets + * wrapped up into an input stream and put into mPostDataStream so that the + * file input stream can then be appended and everything is in the right + * order. Then the string chunk gets appended to again as we process more + * name/value pairs. + */ + nsCString mPostDataChunk; + + /** + * The boundary string to use after each "part" (the boundary that marks the + * end of a value). This is computed randomly and is different for each + * submission. + */ + nsCString mBoundary; + + /** + * The total length in bytes of the streams that make up mPostDataStream + */ + uint64_t mTotalLength; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLFormSubmission_h */ diff --git a/dom/html/HTMLFormSubmissionConstants.h b/dom/html/HTMLFormSubmissionConstants.h new file mode 100644 index 0000000000..c6e2436472 --- /dev/null +++ b/dom/html/HTMLFormSubmissionConstants.h @@ -0,0 +1,36 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLFormSubmissionConstants_h +#define mozilla_dom_HTMLFormSubmissionConstants_h + +#define NS_FORM_METHOD_GET 0 +#define NS_FORM_METHOD_POST 1 +#define NS_FORM_METHOD_DIALOG 2 +#define NS_FORM_ENCTYPE_URLENCODED 0 +#define NS_FORM_ENCTYPE_MULTIPART 1 +#define NS_FORM_ENCTYPE_TEXTPLAIN 2 + +static const nsAttrValue::EnumTable kFormMethodTable[] = { + {"get", NS_FORM_METHOD_GET}, + {"post", NS_FORM_METHOD_POST}, + {"dialog", NS_FORM_METHOD_DIALOG}, + {nullptr, 0}}; + +// Default method is 'get'. +static const nsAttrValue::EnumTable* kFormDefaultMethod = &kFormMethodTable[0]; + +static const nsAttrValue::EnumTable kFormEnctypeTable[] = { + {"multipart/form-data", NS_FORM_ENCTYPE_MULTIPART}, + {"application/x-www-form-urlencoded", NS_FORM_ENCTYPE_URLENCODED}, + {"text/plain", NS_FORM_ENCTYPE_TEXTPLAIN}, + {nullptr, 0}}; + +// Default method is 'application/x-www-form-urlencoded'. +static const nsAttrValue::EnumTable* kFormDefaultEnctype = + &kFormEnctypeTable[1]; + +#endif // mozilla_dom_HTMLFormSubmissionConstants_h diff --git a/dom/html/HTMLFrameElement.cpp b/dom/html/HTMLFrameElement.cpp new file mode 100644 index 0000000000..3e2f145e88 --- /dev/null +++ b/dom/html/HTMLFrameElement.cpp @@ -0,0 +1,54 @@ +/* -*- 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/HTMLFrameElement.h" +#include "mozilla/dom/HTMLFrameElementBinding.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Frame) + +namespace mozilla::dom { + +HTMLFrameElement::HTMLFrameElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLFrameElement(std::move(aNodeInfo), aFromParser) {} + +HTMLFrameElement::~HTMLFrameElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLFrameElement) + +bool HTMLFrameElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::bordercolor) { + return aResult.ParseColor(aValue); + } + if (aAttribute == nsGkAtoms::frameborder) { + return ParseFrameborderValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::marginwidth) { + return aResult.ParseNonNegativeIntValue(aValue); + } + if (aAttribute == nsGkAtoms::marginheight) { + return aResult.ParseNonNegativeIntValue(aValue); + } + if (aAttribute == nsGkAtoms::scrolling) { + return ParseScrollingValue(aValue, aResult); + } + } + + return nsGenericHTMLFrameElement::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +JSObject* HTMLFrameElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLFrameElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLFrameElement.h b/dom/html/HTMLFrameElement.h new file mode 100644 index 0000000000..82e6d57b94 --- /dev/null +++ b/dom/html/HTMLFrameElement.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLFrameElement_h +#define mozilla_dom_HTMLFrameElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLFrameElement.h" +#include "nsGkAtoms.h" + +namespace mozilla::dom { + +class HTMLFrameElement final : public nsGenericHTMLFrameElement { + public: + using nsGenericHTMLFrameElement::SwapFrameLoaders; + + explicit HTMLFrameElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser = NOT_FROM_PARSER); + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLFrameElement, + nsGenericHTMLFrameElement) + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLFrameElement, frame) + + // nsIContent + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // WebIDL API + void GetFrameBorder(DOMString& aFrameBorder) const { + GetHTMLAttr(nsGkAtoms::frameborder, aFrameBorder); + } + void SetFrameBorder(const nsAString& aFrameBorder, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::frameborder, aFrameBorder, aError); + } + + void GetLongDesc(nsAString& aLongDesc) const { + GetURIAttr(nsGkAtoms::longdesc, nullptr, aLongDesc); + } + void SetLongDesc(const nsAString& aLongDesc, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::longdesc, aLongDesc); + } + + void GetMarginHeight(DOMString& aMarginHeight) const { + GetHTMLAttr(nsGkAtoms::marginheight, aMarginHeight); + } + void SetMarginHeight(const nsAString& aMarginHeight, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::marginheight, aMarginHeight, aError); + } + + void GetMarginWidth(DOMString& aMarginWidth) const { + GetHTMLAttr(nsGkAtoms::marginwidth, aMarginWidth); + } + void SetMarginWidth(const nsAString& aMarginWidth, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::marginwidth, aMarginWidth, aError); + } + + void GetName(DOMString& aName) const { GetHTMLAttr(nsGkAtoms::name, aName); } + void SetName(const nsAString& aName, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::name, aName, aError); + } + + bool NoResize() const { return GetBoolAttr(nsGkAtoms::noresize); } + void SetNoResize(bool& aNoResize, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::noresize, aNoResize, aError); + } + + void GetScrolling(DOMString& aScrolling) const { + GetHTMLAttr(nsGkAtoms::scrolling, aScrolling); + } + void SetScrolling(const nsAString& aScrolling, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::scrolling, aScrolling, aError); + } + + void GetSrc(nsString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); } + void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal, + ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aError); + } + + using nsGenericHTMLFrameElement::GetContentDocument; + using nsGenericHTMLFrameElement::GetContentWindow; + + protected: + virtual ~HTMLFrameElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLFrameElement_h diff --git a/dom/html/HTMLFrameSetElement.cpp b/dom/html/HTMLFrameSetElement.cpp new file mode 100644 index 0000000000..d6a794698b --- /dev/null +++ b/dom/html/HTMLFrameSetElement.cpp @@ -0,0 +1,316 @@ +/* -*- 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 "HTMLFrameSetElement.h" +#include "mozilla/Try.h" +#include "mozilla/dom/HTMLFrameSetElementBinding.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/EventHandlerBinding.h" +#include "nsGlobalWindowInner.h" +#include "mozilla/UniquePtrExtensions.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(FrameSet) + +namespace mozilla::dom { + +HTMLFrameSetElement::~HTMLFrameSetElement() = default; + +JSObject* HTMLFrameSetElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLFrameSetElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_ELEMENT_CLONE(HTMLFrameSetElement) + +void HTMLFrameSetElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + bool aNotify) { + /* The main goal here is to see whether the _number_ of rows or + * columns has changed. If it has, we need to reframe; otherwise + * we want to reflow. + * Ideally, the style hint would be changed back to reflow after the reframe + * has been performed. Unfortunately, however, the reframe will be performed + * by the call to MutationObservers::AttributeChanged, which occurs *after* + * AfterSetAttr is called, leaving us with no convenient way of changing the + * value back to reflow afterwards. However, + * MutationObservers::AttributeChanged is effectively the only consumer of + * this value, so as long as we always set the value correctly here, we should + * be fine. + */ + mCurrentRowColHint = NS_STYLE_HINT_REFLOW; + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::rows) { + if (aValue) { + int32_t oldRows = mNumRows; + ParseRowCol(*aValue, mNumRows, &mRowSpecs); + if (mNumRows != oldRows) { + mCurrentRowColHint = nsChangeHint_ReconstructFrame; + } + } + } else if (aName == nsGkAtoms::cols) { + if (aValue) { + int32_t oldCols = mNumCols; + ParseRowCol(*aValue, mNumCols, &mColSpecs); + if (mNumCols != oldCols) { + mCurrentRowColHint = nsChangeHint_ReconstructFrame; + } + } + } + } + + return nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue, + aNotify); +} + +nsresult HTMLFrameSetElement::GetRowSpec(int32_t* aNumValues, + const nsFramesetSpec** aSpecs) { + MOZ_ASSERT(aNumValues, "Must have a pointer to an integer here!"); + MOZ_ASSERT(aSpecs, "Must have a pointer to an array of nsFramesetSpecs"); + *aNumValues = 0; + *aSpecs = nullptr; + + if (!mRowSpecs) { + if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::rows)) { + MOZ_TRY(ParseRowCol(*value, mNumRows, &mRowSpecs)); + } + + if (!mRowSpecs) { // we may not have had an attr or had an empty attr + mRowSpecs = MakeUnique<nsFramesetSpec[]>(1); + mNumRows = 1; + mRowSpecs[0].mUnit = eFramesetUnit_Relative; + mRowSpecs[0].mValue = 1; + } + } + + *aSpecs = mRowSpecs.get(); + *aNumValues = mNumRows; + return NS_OK; +} + +nsresult HTMLFrameSetElement::GetColSpec(int32_t* aNumValues, + const nsFramesetSpec** aSpecs) { + MOZ_ASSERT(aNumValues, "Must have a pointer to an integer here!"); + MOZ_ASSERT(aSpecs, "Must have a pointer to an array of nsFramesetSpecs"); + *aNumValues = 0; + *aSpecs = nullptr; + + if (!mColSpecs) { + if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::cols)) { + MOZ_TRY(ParseRowCol(*value, mNumCols, &mColSpecs)); + } + + if (!mColSpecs) { // we may not have had an attr or had an empty attr + mColSpecs = MakeUnique<nsFramesetSpec[]>(1); + mNumCols = 1; + mColSpecs[0].mUnit = eFramesetUnit_Relative; + mColSpecs[0].mValue = 1; + } + } + + *aSpecs = mColSpecs.get(); + *aNumValues = mNumCols; + return NS_OK; +} + +bool HTMLFrameSetElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::bordercolor) { + return aResult.ParseColor(aValue); + } + if (aAttribute == nsGkAtoms::frameborder) { + return nsGenericHTMLElement::ParseFrameborderValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::border) { + return aResult.ParseIntWithBounds(aValue, 0, 100); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +nsChangeHint HTMLFrameSetElement::GetAttributeChangeHint( + const nsAtom* aAttribute, int32_t aModType) const { + nsChangeHint retval = + nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType); + if (aAttribute == nsGkAtoms::rows || aAttribute == nsGkAtoms::cols) { + retval |= mCurrentRowColHint; + } + return retval; +} + +/** + * Translate a "rows" or "cols" spec into an array of nsFramesetSpecs + */ +nsresult HTMLFrameSetElement::ParseRowCol(const nsAttrValue& aValue, + int32_t& aNumSpecs, + UniquePtr<nsFramesetSpec[]>* aSpecs) { + if (aValue.IsEmptyString()) { + aNumSpecs = 0; + *aSpecs = nullptr; + return NS_OK; + } + + MOZ_ASSERT(aValue.Type() == nsAttrValue::eString); + + static const char16_t sAster('*'); + static const char16_t sPercent('%'); + static const char16_t sComma(','); + + nsAutoString spec(aValue.GetStringValue()); + // remove whitespace (Bug 33699) and quotation marks (bug 224598) + // also remove leading/trailing commas (bug 31482) + spec.StripChars(u" \n\r\t\"\'"); + spec.Trim(","); + + // Count the commas. Don't count more than X commas (bug 576447). + static_assert(NS_MAX_FRAMESET_SPEC_COUNT * sizeof(nsFramesetSpec) < (1 << 30), + "Too many frameset specs allowed to allocate"); + int32_t commaX = spec.FindChar(sComma); + int32_t count = 1; + while (commaX != kNotFound && count < NS_MAX_FRAMESET_SPEC_COUNT) { + count++; + commaX = spec.FindChar(sComma, commaX + 1); + } + + auto specs = MakeUniqueFallible<nsFramesetSpec[]>(count); + if (!specs) { + *aSpecs = nullptr; + aNumSpecs = 0; + return NS_ERROR_OUT_OF_MEMORY; + } + + // Pre-grab the compat mode; we may need it later in the loop. + bool isInQuirks = InNavQuirksMode(OwnerDoc()); + + // Parse each comma separated token + + int32_t start = 0; + int32_t specLen = spec.Length(); + + for (int32_t i = 0; i < count; i++) { + // Find our comma + commaX = spec.FindChar(sComma, start); + NS_ASSERTION(i == count - 1 || commaX != kNotFound, + "Failed to find comma, somehow"); + int32_t end = (commaX == kNotFound) ? specLen : commaX; + + // Note: If end == start then it means that the token has no + // data in it other than a terminating comma (or the end of the spec). + // So default to a fixed width of 0. + specs[i].mUnit = eFramesetUnit_Fixed; + specs[i].mValue = 0; + if (end > start) { + int32_t numberEnd = end; + char16_t ch = spec.CharAt(numberEnd - 1); + if (sAster == ch) { + specs[i].mUnit = eFramesetUnit_Relative; + numberEnd--; + } else if (sPercent == ch) { + specs[i].mUnit = eFramesetUnit_Percent; + numberEnd--; + // check for "*%" + if (numberEnd > start) { + ch = spec.CharAt(numberEnd - 1); + if (sAster == ch) { + specs[i].mUnit = eFramesetUnit_Relative; + numberEnd--; + } + } + } + + // Translate value to an integer + nsAutoString token; + spec.Mid(token, start, numberEnd - start); + + // Treat * as 1* + if ((eFramesetUnit_Relative == specs[i].mUnit) && (0 == token.Length())) { + specs[i].mValue = 1; + } else { + // Otherwise just convert to integer. + nsresult err; + specs[i].mValue = token.ToInteger(&err); + if (NS_FAILED(err)) { + specs[i].mValue = 0; + } + } + + // Treat 0* as 1* in quirks mode (bug 40383) + if (isInQuirks) { + if ((eFramesetUnit_Relative == specs[i].mUnit) && + (0 == specs[i].mValue)) { + specs[i].mValue = 1; + } + } + + // Catch zero and negative frame sizes for Nav compatibility + // Nav resized absolute and relative frames to "1" and + // percent frames to an even percentage of the width + // + // if (isInQuirks && (specs[i].mValue <= 0)) { + // if (eFramesetUnit_Percent == specs[i].mUnit) { + // specs[i].mValue = 100 / count; + // } else { + // specs[i].mValue = 1; + // } + //} else { + + // In standards mode, just set negative sizes to zero + if (specs[i].mValue < 0) { + specs[i].mValue = 0; + } + start = end + 1; + } + } + + aNumSpecs = count; + // Transfer ownership to caller here + *aSpecs = std::move(specs); + + return NS_OK; +} + +bool HTMLFrameSetElement::IsEventAttributeNameInternal(nsAtom* aName) { + return nsContentUtils::IsEventAttributeName( + aName, EventNameType_HTML | EventNameType_HTMLBodyOrFramesetOnly); +} + +#define EVENT(name_, id_, type_, struct_) /* nothing; handled by the shim */ +// nsGenericHTMLElement::GetOnError returns +// already_AddRefed<EventHandlerNonNull> while other getters return +// EventHandlerNonNull*, so allow passing in the type to use here. +#define WINDOW_EVENT_HELPER(name_, type_) \ + type_* HTMLFrameSetElement::GetOn##name_() { \ + if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + return globalWin->GetOn##name_(); \ + } \ + return nullptr; \ + } \ + void HTMLFrameSetElement::SetOn##name_(type_* handler) { \ + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \ + if (!win) { \ + return; \ + } \ + \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + return globalWin->SetOn##name_(handler); \ + } +#define WINDOW_EVENT(name_, id_, type_, struct_) \ + WINDOW_EVENT_HELPER(name_, EventHandlerNonNull) +#define BEFOREUNLOAD_EVENT(name_, id_, type_, struct_) \ + WINDOW_EVENT_HELPER(name_, OnBeforeUnloadEventHandlerNonNull) +#include "mozilla/EventNameList.h" // IWYU pragma: keep +#undef BEFOREUNLOAD_EVENT +#undef WINDOW_EVENT +#undef WINDOW_EVENT_HELPER +#undef EVENT + +} // namespace mozilla::dom diff --git a/dom/html/HTMLFrameSetElement.h b/dom/html/HTMLFrameSetElement.h new file mode 100644 index 0000000000..d0c735621a --- /dev/null +++ b/dom/html/HTMLFrameSetElement.h @@ -0,0 +1,153 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef HTMLFrameSetElement_h +#define HTMLFrameSetElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/UniquePtr.h" +#include "nsGenericHTMLElement.h" + +/** + * The nsFramesetUnit enum is used to denote the type of each entry + * in the row or column spec. + */ +enum nsFramesetUnit { + eFramesetUnit_Fixed = 0, + eFramesetUnit_Percent, + eFramesetUnit_Relative +}; + +/** + * The nsFramesetSpec struct is used to hold a single entry in the + * row or column spec. + */ +struct nsFramesetSpec { + nsFramesetUnit mUnit; + nscoord mValue; +}; + +/** + * The maximum number of entries allowed in the frame set element row + * or column spec. + */ +#define NS_MAX_FRAMESET_SPEC_COUNT 16000 + +//---------------------------------------------------------------------- + +namespace mozilla::dom { + +class OnBeforeUnloadEventHandlerNonNull; + +class HTMLFrameSetElement final : public nsGenericHTMLElement { + public: + explicit HTMLFrameSetElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mNumRows(0), + mNumCols(0), + mCurrentRowColHint(NS_STYLE_HINT_REFLOW) { + SetHasWeirdParserInsertionMode(); + } + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLFrameSetElement, frameset) + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLFrameSetElement, + nsGenericHTMLElement) + + void GetCols(DOMString& aCols) { GetHTMLAttr(nsGkAtoms::cols, aCols); } + void SetCols(const nsAString& aCols, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::cols, aCols, aError); + } + void GetRows(DOMString& aRows) { GetHTMLAttr(nsGkAtoms::rows, aRows); } + void SetRows(const nsAString& aRows, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::rows, aRows, aError); + } + + bool IsEventAttributeNameInternal(nsAtom* aName) override; + + // Event listener stuff; we need to declare only the ones we need to + // forward to window that don't come from nsIDOMHTMLFrameSetElement. +#define EVENT(name_, id_, type_, \ + struct_) /* nothing; handled by the superclass */ +#define WINDOW_EVENT_HELPER(name_, type_) \ + type_* GetOn##name_(); \ + void SetOn##name_(type_* handler); +#define WINDOW_EVENT(name_, id_, type_, struct_) \ + WINDOW_EVENT_HELPER(name_, EventHandlerNonNull) +#define BEFOREUNLOAD_EVENT(name_, id_, type_, struct_) \ + WINDOW_EVENT_HELPER(name_, OnBeforeUnloadEventHandlerNonNull) +#include "mozilla/EventNameList.h" // IWYU pragma: keep +#undef BEFOREUNLOAD_EVENT +#undef WINDOW_EVENT +#undef WINDOW_EVENT_HELPER +#undef EVENT + + /** + * GetRowSpec is used to get the "rows" spec. + * @param out int32_t aNumValues The number of row sizes specified. + * @param out nsFramesetSpec* aSpecs The array of size specifications. + This is _not_ owned by the caller, but by the nsFrameSetElement + implementation. DO NOT DELETE IT. + */ + nsresult GetRowSpec(int32_t* aNumValues, const nsFramesetSpec** aSpecs); + /** + * GetColSpec is used to get the "cols" spec + * @param out int32_t aNumValues The number of row sizes specified. + * @param out nsFramesetSpec* aSpecs The array of size specifications. + This is _not_ owned by the caller, but by the nsFrameSetElement + implementation. DO NOT DELETE IT. + */ + nsresult GetColSpec(int32_t* aNumValues, const nsFramesetSpec** aSpecs); + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLFrameSetElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + + private: + nsresult ParseRowCol(const nsAttrValue& aValue, int32_t& aNumSpecs, + UniquePtr<nsFramesetSpec[]>* aSpecs); + + /** + * The number of size specs in our "rows" attr + */ + int32_t mNumRows; + /** + * The number of size specs in our "cols" attr + */ + int32_t mNumCols; + /** + * The style hint to return for the rows/cols attrs in + * GetAttributeChangeHint + */ + nsChangeHint mCurrentRowColHint; + /** + * The parsed representation of the "rows" attribute + */ + UniquePtr<nsFramesetSpec[]> mRowSpecs; // parsed, non-computed dimensions + /** + * The parsed representation of the "cols" attribute + */ + UniquePtr<nsFramesetSpec[]> mColSpecs; // parsed, non-computed dimensions +}; + +} // namespace mozilla::dom + +#endif // HTMLFrameSetElement_h diff --git a/dom/html/HTMLHRElement.cpp b/dom/html/HTMLHRElement.cpp new file mode 100644 index 0000000000..e433478a87 --- /dev/null +++ b/dom/html/HTMLHRElement.cpp @@ -0,0 +1,193 @@ +/* -*- 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/HTMLHRElement.h" +#include "mozilla/dom/HTMLHRElementBinding.h" + +#include "nsCSSProps.h" +#include "nsStyleConsts.h" +#include "mozilla/MappedDeclarationsBuilder.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(HR) + +namespace mozilla::dom { + +HTMLHRElement::HTMLHRElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLHRElement::~HTMLHRElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLHRElement) + +bool HTMLHRElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + static const nsAttrValue::EnumTable kAlignTable[] = { + {"left", StyleTextAlign::Left}, + {"right", StyleTextAlign::Right}, + {"center", StyleTextAlign::Center}, + {nullptr, 0}}; + + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::width) { + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::size) { + return aResult.ParseIntWithBounds(aValue, 1, 1000); + } + if (aAttribute == nsGkAtoms::align) { + return aResult.ParseEnumValue(aValue, kAlignTable, false); + } + if (aAttribute == nsGkAtoms::color) { + return aResult.ParseColor(aValue); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLHRElement::MapAttributesIntoRule(MappedDeclarationsBuilder& aBuilder) { + bool noshade = false; + + const nsAttrValue* colorValue = aBuilder.GetAttr(nsGkAtoms::color); + nscolor color; + bool colorIsSet = colorValue && colorValue->GetColorValue(color); + + if (colorIsSet) { + noshade = true; + } else { + noshade = !!aBuilder.GetAttr(nsGkAtoms::noshade); + } + + // align: enum + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align); + if (value && value->Type() == nsAttrValue::eEnum) { + // Map align attribute into auto side margins + switch (StyleTextAlign(value->GetEnumValue())) { + case StyleTextAlign::Left: + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_left, 0.0f); + aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_right); + break; + case StyleTextAlign::Right: + aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_left); + aBuilder.SetPixelValueIfUnset(eCSSProperty_margin_right, 0.0f); + break; + case StyleTextAlign::Center: + aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_left); + aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_right); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown <hr align> value"); + break; + } + } + if (!aBuilder.PropertyIsSet(eCSSProperty_height)) { + // size: integer + if (noshade) { + // noshade case: size is set using the border + aBuilder.SetAutoValue(eCSSProperty_height); + } else { + // normal case + // the height includes the top and bottom borders that are initially 1px. + // for size=1, html.css has a special case rule that makes this work by + // removing all but the top border. + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::size); + if (value && value->Type() == nsAttrValue::eInteger) { + aBuilder.SetPixelValue(eCSSProperty_height, + (float)value->GetIntegerValue()); + } // else use default value from html.css + } + } + + // if not noshade, border styles are dealt with by html.css + if (noshade) { + // size: integer + // if a size is set, use half of it per side, otherwise, use 1px per side + float sizePerSide; + bool allSides = true; + value = aBuilder.GetAttr(nsGkAtoms::size); + if (value && value->Type() == nsAttrValue::eInteger) { + sizePerSide = (float)value->GetIntegerValue() / 2.0f; + if (sizePerSide < 1.0f) { + // XXX When the pixel bug is fixed, all the special casing for + // subpixel borders should be removed. + // In the meantime, this makes http://www.microsoft.com/ look right. + sizePerSide = 1.0f; + allSides = false; + } + } else { + sizePerSide = 1.0f; // default to a 2px high line + } + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_top_width, sizePerSide); + if (allSides) { + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_right_width, + sizePerSide); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_bottom_width, + sizePerSide); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_left_width, + sizePerSide); + } + + if (!aBuilder.PropertyIsSet(eCSSProperty_border_top_style)) { + aBuilder.SetKeywordValue(eCSSProperty_border_top_style, + StyleBorderStyle::Solid); + } + if (allSides) { + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_right_style, + StyleBorderStyle::Solid); + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_bottom_style, + StyleBorderStyle::Solid); + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_left_style, + StyleBorderStyle::Solid); + + // If it would be noticeable, set the border radius to + // 10000px on all corners; this triggers the clamping to make + // circular ends. This assumes the <hr> isn't larger than + // that in *both* dimensions. + for (const nsCSSPropertyID* props = + nsCSSProps::SubpropertyEntryFor(eCSSProperty_border_radius); + *props != eCSSProperty_UNKNOWN; ++props) { + aBuilder.SetPixelValueIfUnset(*props, 10000.0f); + } + } + } + // color: a color + // (we got the color attribute earlier) + if (colorIsSet) { + aBuilder.SetColorValueIfUnset(eCSSProperty_color, color); + } + MapWidthAttributeInto(aBuilder); + MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLHRElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::align}, {nsGkAtoms::width}, {nsGkAtoms::size}, + {nsGkAtoms::color}, {nsGkAtoms::noshade}, {nullptr}, + }; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLHRElement::GetAttributeMappingFunction() const { + return &MapAttributesIntoRule; +} + +JSObject* HTMLHRElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLHRElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLHRElement.h b/dom/html/HTMLHRElement.h new file mode 100644 index 0000000000..9b8d33279e --- /dev/null +++ b/dom/html/HTMLHRElement.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLHRElement_h +#define mozilla_dom_HTMLHRElement_h + +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLHRElement final : public nsGenericHTMLElement { + public: + explicit HTMLHRElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLHRElement, nsGenericHTMLElement) + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // WebIDL API + void GetAlign(nsAString& aValue) const { + GetHTMLAttr(nsGkAtoms::align, aValue); + } + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + + void GetColor(nsAString& aValue) const { + GetHTMLAttr(nsGkAtoms::color, aValue); + } + void SetColor(const nsAString& aColor, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::color, aColor, aError); + } + + bool NoShade() const { return GetBoolAttr(nsGkAtoms::noshade); } + void SetNoShade(bool aNoShade, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::noshade, aNoShade, aError); + } + + void GetSize(nsAString& aValue) const { + GetHTMLAttr(nsGkAtoms::size, aValue); + } + void SetSize(const nsAString& aSize, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::size, aSize, aError); + } + + void GetWidth(nsAString& aValue) const { + GetHTMLAttr(nsGkAtoms::width, aValue); + } + void SetWidth(const nsAString& aWidth, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::width, aWidth, aError); + } + + protected: + virtual ~HTMLHRElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLHRElement_h diff --git a/dom/html/HTMLHeadingElement.cpp b/dom/html/HTMLHeadingElement.cpp new file mode 100644 index 0000000000..e3274d44b3 --- /dev/null +++ b/dom/html/HTMLHeadingElement.cpp @@ -0,0 +1,58 @@ +/* -*- 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/HTMLHeadingElement.h" +#include "mozilla/dom/HTMLHeadingElementBinding.h" + +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsGkAtoms.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Heading) + +namespace mozilla::dom { + +HTMLHeadingElement::~HTMLHeadingElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLHeadingElement) + +JSObject* HTMLHeadingElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLHeadingElement_Binding::Wrap(aCx, this, aGivenProto); +} + +bool HTMLHeadingElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aAttribute == nsGkAtoms::align && aNamespaceID == kNameSpaceID_None) { + return ParseDivAlignValue(aValue, aResult); + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLHeadingElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLHeadingElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = {sDivAlignAttributeMap, + sCommonAttributeMap}; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLHeadingElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLHeadingElement.h b/dom/html/HTMLHeadingElement.h new file mode 100644 index 0000000000..43d718b5a3 --- /dev/null +++ b/dom/html/HTMLHeadingElement.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLHeadingElement_h +#define mozilla_dom_HTMLHeadingElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLHeadingElement final : public nsGenericHTMLElement { + public: + explicit HTMLHeadingElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + MOZ_ASSERT(IsHTMLHeadingElement()); + } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + return SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + void GetAlign(DOMString& aAlign) const { + return GetHTMLAttr(nsGkAtoms::align, aAlign); + } + + int32_t AccessibilityLevel() const { + nsAtom* name = NodeInfo()->NameAtom(); + if (name == nsGkAtoms::h1) { + return 1; + } + if (name == nsGkAtoms::h2) { + return 2; + } + if (name == nsGkAtoms::h3) { + return 3; + } + if (name == nsGkAtoms::h4) { + return 4; + } + if (name == nsGkAtoms::h5) { + return 5; + } + MOZ_ASSERT(name == nsGkAtoms::h6); + return 6; + } + + NS_IMPL_FROMNODE_HELPER(HTMLHeadingElement, IsHTMLHeadingElement()) + + protected: + virtual ~HTMLHeadingElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLHeadingElement_h diff --git a/dom/html/HTMLIFrameElement.cpp b/dom/html/HTMLIFrameElement.cpp new file mode 100644 index 0000000000..97363ccbff --- /dev/null +++ b/dom/html/HTMLIFrameElement.cpp @@ -0,0 +1,381 @@ +/* -*- 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/DOMIntersectionObserver.h" +#include "mozilla/dom/HTMLIFrameElement.h" +#include "mozilla/dom/ContentChild.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLIFrameElementBinding.h" +#include "mozilla/dom/FeaturePolicy.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/NullPrincipal.h" +#include "mozilla/StaticPrefs_dom.h" +#include "nsSubDocumentFrame.h" +#include "nsError.h" +#include "nsContentUtils.h" +#include "nsSandboxFlags.h" +#include "nsNetUtil.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(IFrame) + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLIFrameElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLIFrameElement, + nsGenericHTMLFrameElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFeaturePolicy) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSandbox) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLIFrameElement, + nsGenericHTMLFrameElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFeaturePolicy) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSandbox) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ADDREF_INHERITED(HTMLIFrameElement, nsGenericHTMLFrameElement) +NS_IMPL_RELEASE_INHERITED(HTMLIFrameElement, nsGenericHTMLFrameElement) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLIFrameElement) +NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLFrameElement) + +// static +const DOMTokenListSupportedToken HTMLIFrameElement::sSupportedSandboxTokens[] = + { +#define SANDBOX_KEYWORD(string, atom, flags) string, +#include "IframeSandboxKeywordList.h" +#undef SANDBOX_KEYWORD + nullptr}; + +HTMLIFrameElement::HTMLIFrameElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLFrameElement(std::move(aNodeInfo), aFromParser) { + // We always need a featurePolicy, even if not exposed. + mFeaturePolicy = new mozilla::dom::FeaturePolicy(this); + nsCOMPtr<nsIPrincipal> origin = GetFeaturePolicyDefaultOrigin(); + MOZ_ASSERT(origin); + mFeaturePolicy->SetDefaultOrigin(origin); +} + +HTMLIFrameElement::~HTMLIFrameElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLIFrameElement) + +void HTMLIFrameElement::BindToBrowsingContext(BrowsingContext*) { + RefreshFeaturePolicy(true /* parse the feature policy attribute */); +} + +bool HTMLIFrameElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::marginwidth) { + return aResult.ParseNonNegativeIntValue(aValue); + } + if (aAttribute == nsGkAtoms::marginheight) { + return aResult.ParseNonNegativeIntValue(aValue); + } + if (aAttribute == nsGkAtoms::width) { + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::height) { + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::frameborder) { + return ParseFrameborderValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::scrolling) { + return ParseScrollingValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::align) { + return ParseAlignValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::sandbox) { + aResult.ParseAtomArray(aValue); + return true; + } + if (aAttribute == nsGkAtoms::loading) { + return ParseLoadingAttribute(aValue, aResult); + } + } + + return nsGenericHTMLFrameElement::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +void HTMLIFrameElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + // frameborder: 0 | 1 (| NO | YES in quirks mode) + // If frameborder is 0 or No, set border to 0 + // else leave it as the value set in html.css + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::frameborder); + if (value && value->Type() == nsAttrValue::eEnum) { + auto frameborder = static_cast<FrameBorderProperty>(value->GetEnumValue()); + if (FrameBorderProperty::No == frameborder || + FrameBorderProperty::Zero == frameborder) { + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_top_width, 0.0f); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_right_width, 0.0f); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_bottom_width, 0.0f); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_left_width, 0.0f); + } + } + + nsGenericHTMLElement::MapImageSizeAttributesInto(aBuilder); + nsGenericHTMLElement::MapImageAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLIFrameElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::width}, + {nsGkAtoms::height}, + {nsGkAtoms::frameborder}, + {nullptr}, + }; + + static const MappedAttributeEntry* const map[] = { + attributes, + sImageAlignAttributeMap, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLIFrameElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +void HTMLIFrameElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + AfterMaybeChangeAttr(aNameSpaceID, aName, aNotify); + + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::loading) { + if (aValue && Loading(aValue->GetEnumValue()) == Loading::Lazy) { + SetLazyLoading(); + } else if (aOldValue && + Loading(aOldValue->GetEnumValue()) == Loading::Lazy) { + StopLazyLoading(); + } + } + + // If lazy loading and src set, set lazy loading again as we are doing a new + // load (lazy loading is unset after a load is complete). + if ((aName == nsGkAtoms::src || aName == nsGkAtoms::srcdoc) && + LoadingState() == Loading::Lazy) { + SetLazyLoading(); + } + + if (aName == nsGkAtoms::sandbox) { + if (mFrameLoader) { + // If we have an nsFrameLoader, apply the new sandbox flags. + // Since this is called after the setter, the sandbox flags have + // alreay been updated. + mFrameLoader->ApplySandboxFlags(GetSandboxFlags()); + } + } + + if (aName == nsGkAtoms::allow || aName == nsGkAtoms::src || + aName == nsGkAtoms::srcdoc || aName == nsGkAtoms::sandbox) { + RefreshFeaturePolicy(true /* parse the feature policy attribute */); + } else if (aName == nsGkAtoms::allowfullscreen) { + RefreshFeaturePolicy(false /* parse the feature policy attribute */); + } + } + + return nsGenericHTMLFrameElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLIFrameElement::OnAttrSetButNotChanged( + int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, aNotify); + + return nsGenericHTMLFrameElement::OnAttrSetButNotChanged(aNamespaceID, aName, + aValue, aNotify); +} + +void HTMLIFrameElement::AfterMaybeChangeAttr(int32_t aNamespaceID, + nsAtom* aName, bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::srcdoc) { + // Don't propagate errors from LoadSrc. The attribute was successfully + // set/unset, that's what we should reflect. + LoadSrc(); + } + } +} + +uint32_t HTMLIFrameElement::GetSandboxFlags() const { + const nsAttrValue* sandboxAttr = GetParsedAttr(nsGkAtoms::sandbox); + // No sandbox attribute, no sandbox flags. + if (!sandboxAttr) { + return SANDBOXED_NONE; + } + return nsContentUtils::ParseSandboxAttributeToFlags(sandboxAttr); +} + +JSObject* HTMLIFrameElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLIFrameElement_Binding::Wrap(aCx, this, aGivenProto); +} + +mozilla::dom::FeaturePolicy* HTMLIFrameElement::FeaturePolicy() const { + return mFeaturePolicy; +} + +void HTMLIFrameElement::MaybeStoreCrossOriginFeaturePolicy() { + if (!mFrameLoader) { + return; + } + + // If the browsingContext is not ready (because docshell is dead), don't try + // to create one. + if (!mFrameLoader->IsRemoteFrame() && !mFrameLoader->GetExistingDocShell()) { + return; + } + + RefPtr<BrowsingContext> browsingContext = mFrameLoader->GetBrowsingContext(); + + if (!browsingContext || !browsingContext->IsContentSubframe()) { + return; + } + + if (ContentChild* cc = ContentChild::GetSingleton()) { + Unused << cc->SendSetContainerFeaturePolicy(browsingContext, + mFeaturePolicy); + } +} + +already_AddRefed<nsIPrincipal> +HTMLIFrameElement::GetFeaturePolicyDefaultOrigin() const { + nsCOMPtr<nsIPrincipal> principal; + + if (HasAttr(nsGkAtoms::srcdoc)) { + principal = NodePrincipal(); + return principal.forget(); + } + + nsCOMPtr<nsIURI> nodeURI; + if (GetURIAttr(nsGkAtoms::src, nullptr, getter_AddRefs(nodeURI)) && nodeURI) { + principal = BasePrincipal::CreateContentPrincipal( + nodeURI, BasePrincipal::Cast(NodePrincipal())->OriginAttributesRef()); + } + + if (!principal) { + principal = NodePrincipal(); + } + + return principal.forget(); +} + +void HTMLIFrameElement::RefreshFeaturePolicy(bool aParseAllowAttribute) { + if (aParseAllowAttribute) { + mFeaturePolicy->ResetDeclaredPolicy(); + + // The origin can change if 'src' and 'srcdoc' attributes change. + nsCOMPtr<nsIPrincipal> origin = GetFeaturePolicyDefaultOrigin(); + MOZ_ASSERT(origin); + mFeaturePolicy->SetDefaultOrigin(origin); + + nsAutoString allow; + GetAttr(nsGkAtoms::allow, allow); + + if (!allow.IsEmpty()) { + // Set or reset the FeaturePolicy directives. + mFeaturePolicy->SetDeclaredPolicy(OwnerDoc(), allow, NodePrincipal(), + origin); + } + } + + if (AllowFullscreen()) { + mFeaturePolicy->MaybeSetAllowedPolicy(u"fullscreen"_ns); + } + + mFeaturePolicy->InheritPolicy(OwnerDoc()->FeaturePolicy()); + MaybeStoreCrossOriginFeaturePolicy(); +} + +void HTMLIFrameElement::UpdateLazyLoadState() { + // Store current base URI and referrer policy in the lazy load state. + mLazyLoadState.mBaseURI = GetBaseURI(); + mLazyLoadState.mReferrerPolicy = GetReferrerPolicyAsEnum(); +} + +nsresult HTMLIFrameElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + // Update lazy load state on bind to tree again if lazy loading, as the + // loading attribute could be set before others. + if (mLazyLoading) { + UpdateLazyLoadState(); + } + + return nsGenericHTMLFrameElement::BindToTree(aContext, aParent); +} + +void HTMLIFrameElement::SetLazyLoading() { + if (mLazyLoading) { + return; + } + + if (!StaticPrefs::dom_iframe_lazy_loading_enabled()) { + return; + } + + // https://html.spec.whatwg.org/multipage/urls-and-fetching.html#will-lazy-load-element-steps + // "If scripting is disabled for element, then return false." + Document* doc = OwnerDoc(); + if (!doc->IsScriptEnabled() || doc->IsStaticDocument()) { + return; + } + + doc->EnsureLazyLoadObserver().Observe(*this); + mLazyLoading = true; + + UpdateLazyLoadState(); +} + +void HTMLIFrameElement::StopLazyLoading() { + if (!mLazyLoading) { + return; + } + + mLazyLoading = false; + + Document* doc = OwnerDoc(); + if (auto* obs = doc->GetLazyLoadObserver()) { + obs->Unobserve(*this); + } + + LoadSrc(); + + mLazyLoadState.Clear(); + if (nsSubDocumentFrame* ourFrame = do_QueryFrame(GetPrimaryFrame())) { + ourFrame->ResetFrameLoader(nsSubDocumentFrame::RetainPaintData::No); + } +} + +void HTMLIFrameElement::NodeInfoChanged(Document* aOldDoc) { + nsGenericHTMLElement::NodeInfoChanged(aOldDoc); + + if (mLazyLoading) { + aOldDoc->GetLazyLoadObserver()->Unobserve(*this); + mLazyLoading = false; + SetLazyLoading(); + } +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLIFrameElement.h b/dom/html/HTMLIFrameElement.h new file mode 100644 index 0000000000..8a87c02472 --- /dev/null +++ b/dom/html/HTMLIFrameElement.h @@ -0,0 +1,234 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLIFrameElement_h +#define mozilla_dom_HTMLIFrameElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsGenericHTMLFrameElement.h" +#include "nsDOMTokenList.h" + +namespace mozilla::dom { + +class FeaturePolicy; + +class HTMLIFrameElement final : public nsGenericHTMLFrameElement { + public: + explicit HTMLIFrameElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser = NOT_FROM_PARSER); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLIFrameElement, iframe) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLIFrameElement, + nsGenericHTMLFrameElement) + + // Element + virtual bool IsInteractiveHTMLContent() const override { return true; } + + // nsIContent + virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + virtual nsMapRuleToAttributesFunc GetAttributeMappingFunction() + const override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + void NodeInfoChanged(Document* aOldDoc) override; + + void BindToBrowsingContext(BrowsingContext* aBrowsingContext); + + uint32_t GetSandboxFlags() const; + + // Web IDL binding methods + void GetSrc(nsString& aSrc) const { + GetURIAttr(nsGkAtoms::src, nullptr, aSrc); + } + void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal, + ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aError); + } + void GetSrcdoc(DOMString& aSrcdoc) { + GetHTMLAttr(nsGkAtoms::srcdoc, aSrcdoc); + } + void SetSrcdoc(const nsAString& aSrcdoc, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::srcdoc, aSrcdoc, aError); + } + void GetName(DOMString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); } + void SetName(const nsAString& aName, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::name, aName, aError); + } + nsDOMTokenList* Sandbox() { + if (!mSandbox) { + mSandbox = + new nsDOMTokenList(this, nsGkAtoms::sandbox, sSupportedSandboxTokens); + } + return mSandbox; + } + + bool AllowFullscreen() const { + return GetBoolAttr(nsGkAtoms::allowfullscreen); + } + + void SetAllowFullscreen(bool aAllow, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::allowfullscreen, aAllow, aError); + } + + void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); } + void SetWidth(const nsAString& aWidth, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::width, aWidth, aError); + } + void GetHeight(DOMString& aHeight) { + GetHTMLAttr(nsGkAtoms::height, aHeight); + } + void SetHeight(const nsAString& aHeight, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::height, aHeight, aError); + } + using nsGenericHTMLFrameElement::GetContentDocument; + using nsGenericHTMLFrameElement::GetContentWindow; + void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + void GetAllow(DOMString& aAllow) { GetHTMLAttr(nsGkAtoms::allow, aAllow); } + void SetAllow(const nsAString& aAllow, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::allow, aAllow, aError); + } + void GetScrolling(DOMString& aScrolling) { + GetHTMLAttr(nsGkAtoms::scrolling, aScrolling); + } + void SetScrolling(const nsAString& aScrolling, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::scrolling, aScrolling, aError); + } + void GetFrameBorder(DOMString& aFrameBorder) { + GetHTMLAttr(nsGkAtoms::frameborder, aFrameBorder); + } + void SetFrameBorder(const nsAString& aFrameBorder, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::frameborder, aFrameBorder, aError); + } + void GetLongDesc(nsAString& aLongDesc) const { + GetURIAttr(nsGkAtoms::longdesc, nullptr, aLongDesc); + } + void SetLongDesc(const nsAString& aLongDesc, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::longdesc, aLongDesc, aError); + } + void GetMarginWidth(DOMString& aMarginWidth) { + GetHTMLAttr(nsGkAtoms::marginwidth, aMarginWidth); + } + void SetMarginWidth(const nsAString& aMarginWidth, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::marginwidth, aMarginWidth, aError); + } + void GetMarginHeight(DOMString& aMarginHeight) { + GetHTMLAttr(nsGkAtoms::marginheight, aMarginHeight); + } + void SetMarginHeight(const nsAString& aMarginHeight, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::marginheight, aMarginHeight, aError); + } + void SetReferrerPolicy(const nsAString& aReferrer, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::referrerpolicy, aReferrer, aError); + } + void GetReferrerPolicy(nsAString& aReferrer) { + GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrer); + } + Document* GetSVGDocument(nsIPrincipal& aSubjectPrincipal) { + return GetContentDocument(aSubjectPrincipal); + } + bool Mozbrowser() const { return GetBoolAttr(nsGkAtoms::mozbrowser); } + void SetMozbrowser(bool aAllow, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::mozbrowser, aAllow, aError); + } + using nsGenericHTMLFrameElement::SetMozbrowser; + // nsGenericHTMLFrameElement::GetFrameLoader is fine + // nsGenericHTMLFrameElement::GetAppManifestURL is fine + + // The fullscreen flag is set to true only when requestFullscreen is + // explicitly called on this <iframe> element. In case this flag is + // set, the fullscreen state of this element will not be reverted + // automatically when its subdocument exits fullscreen. + bool FullscreenFlag() const { return mFullscreenFlag; } + void SetFullscreenFlag(bool aValue) { mFullscreenFlag = aValue; } + + mozilla::dom::FeaturePolicy* FeaturePolicy() const; + + void SetLoading(const nsAString& aLoading, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::loading, aLoading, aError); + } + + void SetLazyLoading(); + void StopLazyLoading(); + + const LazyLoadFrameResumptionState& GetLazyLoadFrameResumptionState() const { + return mLazyLoadState; + } + + protected: + virtual ~HTMLIFrameElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + virtual void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) override; + nsresult BindToTree(BindContext&, nsINode& aParent) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); + + static const DOMTokenListSupportedToken sSupportedSandboxTokens[]; + + void RefreshFeaturePolicy(bool aParseAllowAttribute); + + // If this iframe has a 'srcdoc' attribute, the document's origin will be + // returned. Otherwise, if this iframe has a 'src' attribute, the origin will + // be the parsing of its value as URL. If the URL is invalid, or 'src' + // attribute doesn't exist, the origin will be the document's origin. + already_AddRefed<nsIPrincipal> GetFeaturePolicyDefaultOrigin() const; + + /** + * This function is called by AfterSetAttr and OnAttrSetButNotChanged. + * This function will be called by AfterSetAttr whether the attribute is being + * set or unset. + * + * @param aNamespaceID the namespace of the attr being set + * @param aName the localname of the attribute being set + * @param aNotify Whether we plan to notify document observers. + */ + void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify); + + /** + * Feature policy inheritance is broken in cross process model, so we may + * have to store feature policy in browsingContext when neccesary. + */ + void MaybeStoreCrossOriginFeaturePolicy(); + + RefPtr<dom::FeaturePolicy> mFeaturePolicy; + RefPtr<nsDOMTokenList> mSandbox; + + /** + * Current lazy load resumption state (base URI and referrer policy). + * https://html.spec.whatwg.org/#lazy-load-resumption-steps + */ + LazyLoadFrameResumptionState mLazyLoadState; + + // Update lazy load state internally + void UpdateLazyLoadState(); +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/html/HTMLImageElement.cpp b/dom/html/HTMLImageElement.cpp new file mode 100644 index 0000000000..1aa23cdea8 --- /dev/null +++ b/dom/html/HTMLImageElement.cpp @@ -0,0 +1,1380 @@ +/* -*- 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/HTMLImageElement.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/HTMLImageElementBinding.h" +#include "mozilla/dom/NameSpaceConstants.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsPresContext.h" +#include "nsSize.h" +#include "mozilla/dom/Document.h" +#include "nsImageFrame.h" +#include "nsIScriptContext.h" +#include "nsContentUtils.h" +#include "nsContainerFrame.h" +#include "nsNodeInfoManager.h" +#include "mozilla/MouseEvents.h" +#include "nsContentPolicyUtils.h" +#include "nsFocusManager.h" +#include "mozilla/dom/DOMIntersectionObserver.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/UserActivation.h" +#include "nsAttrValueOrString.h" +#include "imgLoader.h" +#include "Image.h" + +// Responsive images! +#include "mozilla/dom/HTMLSourceElement.h" +#include "mozilla/dom/ResponsiveImageSelector.h" + +#include "imgINotificationObserver.h" +#include "imgRequestProxy.h" + +#include "mozilla/CycleCollectedJSContext.h" + +#include "mozilla/EventDispatcher.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/Maybe.h" +#include "mozilla/RestyleManager.h" + +#include "nsLayoutUtils.h" + +using namespace mozilla::net; +using mozilla::Maybe; + +NS_IMPL_NS_NEW_HTML_ELEMENT(Image) + +#ifdef DEBUG +// Is aSubject a previous sibling of aNode. +static bool IsPreviousSibling(const nsINode* aSubject, const nsINode* aNode) { + if (aSubject == aNode) { + return false; + } + + nsINode* parent = aSubject->GetParentNode(); + if (parent && parent == aNode->GetParentNode()) { + const Maybe<uint32_t> indexOfSubject = parent->ComputeIndexOf(aSubject); + const Maybe<uint32_t> indexOfNode = parent->ComputeIndexOf(aNode); + if (MOZ_LIKELY(indexOfSubject.isSome() && indexOfNode.isSome())) { + return *indexOfSubject < *indexOfNode; + } + // XXX Keep the odd traditional behavior for now. + return indexOfSubject.isNothing() && indexOfNode.isSome(); + } + + return false; +} +#endif + +namespace mozilla::dom { + +// Calls LoadSelectedImage on host element unless it has been superseded or +// canceled -- this is the synchronous section of "update the image data". +// https://html.spec.whatwg.org/multipage/embedded-content.html#update-the-image-data +class ImageLoadTask final : public MicroTaskRunnable { + public: + ImageLoadTask(HTMLImageElement* aElement, bool aAlwaysLoad, + bool aUseUrgentStartForChannel) + : mElement(aElement), + mAlwaysLoad(aAlwaysLoad), + mUseUrgentStartForChannel(aUseUrgentStartForChannel) { + mDocument = aElement->OwnerDoc(); + mDocument->BlockOnload(); + } + + void Run(AutoSlowOperation& aAso) override { + if (mElement->mPendingImageLoadTask == this) { + mElement->mPendingImageLoadTask = nullptr; + mElement->mUseUrgentStartForChannel = mUseUrgentStartForChannel; + mElement->LoadSelectedImage(true, true, mAlwaysLoad); + } + mDocument->UnblockOnload(false); + } + + bool Suppressed() override { + nsIGlobalObject* global = mElement->GetOwnerGlobal(); + return global && global->IsInSyncOperation(); + } + + bool AlwaysLoad() const { return mAlwaysLoad; } + + private: + ~ImageLoadTask() = default; + RefPtr<HTMLImageElement> mElement; + nsCOMPtr<Document> mDocument; + bool mAlwaysLoad; + + // True if we want to set nsIClassOfService::UrgentStart to the channel to + // get the response ASAP for better user responsiveness. + bool mUseUrgentStartForChannel; +}; + +HTMLImageElement::HTMLImageElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + // We start out broken + AddStatesSilently(ElementState::BROKEN); +} + +HTMLImageElement::~HTMLImageElement() { nsImageLoadingContent::Destroy(); } + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLImageElement, nsGenericHTMLElement, + mResponsiveSelector) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLImageElement, + nsGenericHTMLElement, + nsIImageLoadingContent, + imgINotificationObserver) + +NS_IMPL_ELEMENT_CLONE(HTMLImageElement) + +bool HTMLImageElement::IsInteractiveHTMLContent() const { + return HasAttr(nsGkAtoms::usemap) || + nsGenericHTMLElement::IsInteractiveHTMLContent(); +} + +void HTMLImageElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) { + nsImageLoadingContent::AsyncEventRunning(aEvent); +} + +void HTMLImageElement::GetCurrentSrc(nsAString& aValue) { + nsCOMPtr<nsIURI> currentURI; + GetCurrentURI(getter_AddRefs(currentURI)); + if (currentURI) { + nsAutoCString spec; + currentURI->GetSpec(spec); + CopyUTF8toUTF16(spec, aValue); + } else { + SetDOMStringToNull(aValue); + } +} + +bool HTMLImageElement::Draggable() const { + // images may be dragged unless the draggable attribute is false + return !AttrValueIs(kNameSpaceID_None, nsGkAtoms::draggable, + nsGkAtoms::_false, eIgnoreCase); +} + +bool HTMLImageElement::Complete() { + // It is still not clear what value should img.complete return in various + // cases, see https://github.com/whatwg/html/issues/4884 + + if (!HasAttr(nsGkAtoms::srcset) && !HasNonEmptyAttr(nsGkAtoms::src)) { + return true; + } + + if (!mCurrentRequest || mPendingRequest) { + return false; + } + + uint32_t status; + mCurrentRequest->GetImageStatus(&status); + return (status & + (imgIRequest::STATUS_LOAD_COMPLETE | imgIRequest::STATUS_ERROR)) != 0; +} + +CSSIntPoint HTMLImageElement::GetXY() { + nsIFrame* frame = GetPrimaryFrame(FlushType::Layout); + if (!frame) { + return CSSIntPoint(0, 0); + } + return CSSIntPoint::FromAppUnitsRounded( + frame->GetOffsetTo(frame->PresShell()->GetRootFrame())); +} + +int32_t HTMLImageElement::X() { return GetXY().x; } + +int32_t HTMLImageElement::Y() { return GetXY().y; } + +void HTMLImageElement::GetDecoding(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::decoding, kDecodingTableDefault->tag, aValue); +} + +already_AddRefed<Promise> HTMLImageElement::Decode(ErrorResult& aRv) { + return nsImageLoadingContent::QueueDecodeAsync(aRv); +} + +bool HTMLImageElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::align) { + return ParseAlignValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::crossorigin) { + ParseCORSValue(aValue, aResult); + return true; + } + if (aAttribute == nsGkAtoms::decoding) { + return aResult.ParseEnumValue(aValue, kDecodingTable, + /* aCaseSensitive = */ false, + kDecodingTableDefault); + } + if (aAttribute == nsGkAtoms::loading) { + return ParseLoadingAttribute(aValue, aResult); + } + if (ParseImageAttribute(aAttribute, aValue, aResult)) { + return true; + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLImageElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + MapImageAlignAttributeInto(aBuilder); + MapImageBorderAttributeInto(aBuilder); + MapImageMarginAttributeInto(aBuilder); + MapImageSizeAttributesInto(aBuilder, MapAspectRatio::Yes); + MapCommonAttributesInto(aBuilder); +} + +nsChangeHint HTMLImageElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + nsChangeHint retval = + nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType); + if (aAttribute == nsGkAtoms::usemap || aAttribute == nsGkAtoms::ismap) { + retval |= nsChangeHint_ReconstructFrame; + } else if (aAttribute == nsGkAtoms::alt) { + if (aModType == MutationEvent_Binding::ADDITION || + aModType == MutationEvent_Binding::REMOVAL) { + retval |= nsChangeHint_ReconstructFrame; + } + } + return retval; +} + +NS_IMETHODIMP_(bool) +HTMLImageElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = { + sCommonAttributeMap, sImageMarginSizeAttributeMap, + sImageBorderAttributeMap, sImageAlignAttributeMap}; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLImageElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +void HTMLImageElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && mForm && + (aName == nsGkAtoms::name || aName == nsGkAtoms::id)) { + // remove the image from the hashtable as needed + if (const auto* old = GetParsedAttr(aName); old && !old->IsEmptyString()) { + mForm->RemoveImageElementFromTable( + this, nsDependentAtomString(old->GetAtomValue())); + } + } + + return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue, + aNotify); +} + +void HTMLImageElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID != kNameSpaceID_None) { + return nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName, aValue, + aOldValue, + aMaybeScriptedPrincipal, aNotify); + } + + nsAttrValueOrString attrVal(aValue); + if (aName == nsGkAtoms::src) { + mSrcURI = nullptr; + if (aValue && !aValue->IsEmptyString()) { + StringToURI(attrVal.String(), OwnerDoc(), getter_AddRefs(mSrcURI)); + } + } + + if (aValue) { + AfterMaybeChangeAttr(aNameSpaceID, aName, attrVal, aOldValue, + aMaybeScriptedPrincipal, aNotify); + } + + if (mForm && (aName == nsGkAtoms::name || aName == nsGkAtoms::id) && aValue && + !aValue->IsEmptyString()) { + // add the image to the hashtable as needed + MOZ_ASSERT(aValue->Type() == nsAttrValue::eAtom, + "Expected atom value for name/id"); + mForm->AddImageElementToTable( + this, nsDependentAtomString(aValue->GetAtomValue())); + } + + bool forceReload = false; + + if (aName == nsGkAtoms::loading && !mLoading) { + if (aValue && Loading(aValue->GetEnumValue()) == Loading::Lazy) { + SetLazyLoading(); + } else if (aOldValue && + Loading(aOldValue->GetEnumValue()) == Loading::Lazy) { + StopLazyLoading(StartLoading::Yes); + } + } else if (aName == nsGkAtoms::src && !aValue) { + // NOTE: regular src value changes are handled in AfterMaybeChangeAttr, so + // this only needs to handle unsetting the src attribute. + // Mark channel as urgent-start before load image if the image load is + // initaiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + // AfterMaybeChangeAttr handles setting src since it needs to catch + // img.src = img.src, so we only need to handle the unset case + if (InResponsiveMode()) { + if (mResponsiveSelector && mResponsiveSelector->Content() == this) { + mResponsiveSelector->SetDefaultSource(VoidString()); + } + UpdateSourceSyncAndQueueImageTask(true); + } else { + // Bug 1076583 - We still behave synchronously in the non-responsive case + CancelImageRequests(aNotify); + } + } else if (aName == nsGkAtoms::srcset) { + // Mark channel as urgent-start before load image if the image load is + // initaiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + mSrcsetTriggeringPrincipal = aMaybeScriptedPrincipal; + + PictureSourceSrcsetChanged(this, attrVal.String(), aNotify); + } else if (aName == nsGkAtoms::sizes) { + // Mark channel as urgent-start before load image if the image load is + // initiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + PictureSourceSizesChanged(this, attrVal.String(), aNotify); + } else if (aName == nsGkAtoms::decoding) { + // Request sync or async image decoding. + SetSyncDecodingHint( + aValue && static_cast<ImageDecodingType>(aValue->GetEnumValue()) == + ImageDecodingType::Sync); + } else if (aName == nsGkAtoms::referrerpolicy) { + ReferrerPolicy referrerPolicy = GetReferrerPolicyAsEnum(); + // FIXME(emilio): Why only when not in responsive mode? Also see below for + // aNotify. + forceReload = aNotify && !InResponsiveMode() && + referrerPolicy != ReferrerPolicy::_empty && + referrerPolicy != ReferrerPolicyFromAttr(aOldValue); + } else if (aName == nsGkAtoms::crossorigin) { + // FIXME(emilio): The aNotify bit seems a bit suspicious, but it is useful + // to avoid extra sync loads, specially in non-responsive mode. Ideally we + // can unify the responsive and non-responsive code paths (bug 1076583), and + // simplify this a bit. + forceReload = aNotify && GetCORSMode() != AttrValueToCORSMode(aOldValue); + } + + if (forceReload) { + // Because we load image synchronously in non-responsive-mode, we need to do + // reload after the attribute has been set if the reload is triggered by + // cross origin / referrer policy changing. + // + // Mark channel as urgent-start before load image if the image load is + // initiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + if (InResponsiveMode()) { + // Per spec, full selection runs when this changes, even though + // it doesn't directly affect the source selection + UpdateSourceSyncAndQueueImageTask(true); + } else if (ShouldLoadImage()) { + // Bug 1076583 - We still use the older synchronous algorithm in + // non-responsive mode. Force a new load of the image with the + // new cross origin policy + ForceReload(aNotify, IgnoreErrors()); + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLImageElement::OnAttrSetButNotChanged(int32_t aNamespaceID, + nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, aValue, nullptr, nullptr, aNotify); + return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName, + aValue, aNotify); +} + +void HTMLImageElement::AfterMaybeChangeAttr( + int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue, + const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::src) { + return; + } + + // We need to force our image to reload. This must be done here, not in + // AfterSetAttr or BeforeSetAttr, because we want to do it even if the attr is + // being set to its existing value, which is normally optimized away as a + // no-op. + // + // If we are in responsive mode, we drop the forced reload behavior, + // but still trigger a image load task for img.src = img.src per + // spec. + // + // Both cases handle unsetting src in AfterSetAttr + // Mark channel as urgent-start before load image if the image load is + // initaiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal( + this, aValue.String(), aMaybeScriptedPrincipal); + + if (InResponsiveMode()) { + if (mResponsiveSelector && mResponsiveSelector->Content() == this) { + mResponsiveSelector->SetDefaultSource(mSrcURI, mSrcTriggeringPrincipal); + } + UpdateSourceSyncAndQueueImageTask(true); + } else if (aNotify && ShouldLoadImage()) { + // If aNotify is false, we are coming from the parser or some such place; + // we'll get bound after all the attributes have been set, so we'll do the + // sync image load from BindToTree. Skip the LoadImage call in that case. + + // Note that this sync behavior is partially removed from the spec, bug + // 1076583 + + // A hack to get animations to reset. See bug 594771. + mNewRequestsWillNeedAnimationReset = true; + + // Force image loading here, so that we'll try to load the image from + // network if it's set to be not cacheable. + // Potentially, false could be passed here rather than aNotify since + // UpdateState will be called by SetAttrAndNotify, but there are two + // obstacles to this: 1) LoadImage will end up calling + // UpdateState(aNotify), and we do not want it to call UpdateState(false) + // when aNotify is true, and 2) When this function is called by + // OnAttrSetButNotChanged, SetAttrAndNotify will not subsequently call + // UpdateState. + LoadSelectedImage(/* aForce = */ true, aNotify, + /* aAlwaysLoad = */ true); + + mNewRequestsWillNeedAnimationReset = false; + } +} + +void HTMLImageElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + // We handle image element with attribute ismap in its corresponding frame + // element. Set mMultipleActionsPrevented here to prevent the click event + // trigger the behaviors in Element::PostHandleEventForLinks + WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); + if (mouseEvent && mouseEvent->IsLeftClickEvent() && IsMap()) { + mouseEvent->mFlags.mMultipleActionsPrevented = true; + } + nsGenericHTMLElement::GetEventTargetParent(aVisitor); +} + +nsINode* HTMLImageElement::GetScopeChainParent() const { + if (mForm) { + return mForm; + } + return nsGenericHTMLElement::GetScopeChainParent(); +} + +bool HTMLImageElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + int32_t tabIndex = TabIndex(); + + if (IsInComposedDoc() && FindImageMap()) { + // Use tab index on individual map areas. + *aTabIndex = (sTabFocusModel & eTabFocus_linksMask) ? 0 : -1; + // Image map is not focusable itself, but flag as tabbable + // so that image map areas get walked into. + *aIsFocusable = false; + return false; + } + + // Can be in tab order if tabindex >=0 and form controls are tabbable. + *aTabIndex = (sTabFocusModel & eTabFocus_formElementsMask) ? tabIndex : -1; + *aIsFocusable = IsFormControlDefaultFocusable(aWithMouse) && + (tabIndex >= 0 || GetTabIndexAttrValue().isSome()); + + return false; +} + +nsresult HTMLImageElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + nsImageLoadingContent::BindToTree(aContext, aParent); + + UpdateFormOwner(); + + if (HaveSrcsetOrInPicture()) { + if (IsInComposedDoc() && !mInDocResponsiveContent) { + aContext.OwnerDoc().AddResponsiveContent(this); + mInDocResponsiveContent = true; + } + + // Mark channel as urgent-start before load image if the image load is + // initaiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + // Run selection algorithm when an img element is inserted into a document + // in order to react to changes in the environment. See note of + // https://html.spec.whatwg.org/multipage/embedded-content.html#img-environment-changes + // + // We also do this in PictureSourceAdded() if it is in <picture>, so here + // we only need to do if its parent is not <picture>, even if there is no + // <source>. + if (!IsInPicture()) { + UpdateSourceSyncAndQueueImageTask(false); + } + } else if (!InResponsiveMode() && HasAttr(nsGkAtoms::src)) { + // We skip loading when our attributes were set from parser land, + // so trigger a aForce=false load now to check if things changed. + // This isn't necessary for responsive mode, since creating the + // image load task is asynchronous we don't need to take special + // care to avoid doing so when being filled by the parser. + + // Mark channel as urgent-start before load image if the image load is + // initaiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + // We still act synchronously for the non-responsive case (Bug + // 1076583), but still need to delay if it is unsafe to run + // script. + + // If loading is temporarily disabled, don't even launch MaybeLoadImage. + // Otherwise MaybeLoadImage may run later when someone has reenabled + // loading. + if (LoadingEnabled() && ShouldLoadImage()) { + nsContentUtils::AddScriptRunner( + NewRunnableMethod<bool>("dom::HTMLImageElement::MaybeLoadImage", this, + &HTMLImageElement::MaybeLoadImage, false)); + } + } + + return rv; +} + +void HTMLImageElement::UnbindFromTree(bool aNullParent) { + if (mForm) { + if (aNullParent || !FindAncestorForm(mForm)) { + ClearForm(true); + } else { + UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT); + } + } + + if (mInDocResponsiveContent) { + OwnerDoc()->RemoveResponsiveContent(this); + mInDocResponsiveContent = false; + } + + nsImageLoadingContent::UnbindFromTree(aNullParent); + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +void HTMLImageElement::UpdateFormOwner() { + if (!mForm) { + mForm = FindAncestorForm(); + } + + if (mForm && !HasFlag(ADDED_TO_FORM)) { + // Now we need to add ourselves to the form + nsAutoString nameVal, idVal; + GetAttr(nsGkAtoms::name, nameVal); + GetAttr(nsGkAtoms::id, idVal); + + SetFlags(ADDED_TO_FORM); + + mForm->AddImageElement(this); + + if (!nameVal.IsEmpty()) { + mForm->AddImageElementToTable(this, nameVal); + } + + if (!idVal.IsEmpty()) { + mForm->AddImageElementToTable(this, idVal); + } + } +} + +void HTMLImageElement::MaybeLoadImage(bool aAlwaysForceLoad) { + // Our base URI may have changed, or we may have had responsive parameters + // change while not bound to the tree. However, at this moment, we should have + // updated the responsive source in other places, so we don't have to re-parse + // src/srcset here. Just need to LoadImage. + + // Note, check LoadingEnabled() after LoadImage call. + + LoadSelectedImage(aAlwaysForceLoad, /* aNotify */ true, aAlwaysForceLoad); + + if (!LoadingEnabled()) { + CancelImageRequests(true); + } +} + +void HTMLImageElement::NodeInfoChanged(Document* aOldDoc) { + nsGenericHTMLElement::NodeInfoChanged(aOldDoc); + + // Reparse the URI if needed. Note that we can't check whether we already have + // a parsed URI, because it might be null even if we have a valid src + // attribute, if we tried to parse with a different base. + mSrcURI = nullptr; + nsAutoString src; + if (GetAttr(nsGkAtoms::src, src) && !src.IsEmpty()) { + StringToURI(src, OwnerDoc(), getter_AddRefs(mSrcURI)); + } + + if (mLazyLoading) { + aOldDoc->GetLazyLoadObserver()->Unobserve(*this); + mLazyLoading = false; + SetLazyLoading(); + } + + // Run selection algorithm synchronously when an img element's adopting steps + // are run, in order to react to changes in the environment, per spec, + // https://html.spec.whatwg.org/multipage/images.html#reacting-to-dom-mutations, + // and + // https://html.spec.whatwg.org/multipage/images.html#reacting-to-environment-changes. + if (InResponsiveMode()) { + UpdateResponsiveSource(); + } + + // Force reload image if adoption steps are run. + // If loading is temporarily disabled, don't even launch script runner. + // Otherwise script runner may run later when someone has reenabled loading. + StartLoadingIfNeeded(); +} + +// static +already_AddRefed<HTMLImageElement> HTMLImageElement::Image( + const GlobalObject& aGlobal, const Optional<uint32_t>& aWidth, + const Optional<uint32_t>& aHeight, ErrorResult& aError) { + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports()); + Document* doc; + if (!win || !(doc = win->GetExtantDoc())) { + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<mozilla::dom::NodeInfo> nodeInfo = doc->NodeInfoManager()->GetNodeInfo( + nsGkAtoms::img, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE); + + auto* nim = nodeInfo->NodeInfoManager(); + RefPtr<HTMLImageElement> img = new (nim) HTMLImageElement(nodeInfo.forget()); + + if (aWidth.WasPassed()) { + img->SetWidth(aWidth.Value(), aError); + if (aError.Failed()) { + return nullptr; + } + + if (aHeight.WasPassed()) { + img->SetHeight(aHeight.Value(), aError); + if (aError.Failed()) { + return nullptr; + } + } + } + + return img.forget(); +} + +uint32_t HTMLImageElement::Height() { return GetWidthHeightForImage().height; } + +uint32_t HTMLImageElement::Width() { return GetWidthHeightForImage().width; } + +nsIntSize HTMLImageElement::NaturalSize() { + if (!mCurrentRequest) { + return {}; + } + + nsCOMPtr<imgIContainer> image; + mCurrentRequest->GetImage(getter_AddRefs(image)); + if (!image) { + return {}; + } + + nsIntSize size; + Unused << image->GetHeight(&size.height); + Unused << image->GetWidth(&size.width); + + ImageResolution resolution = image->GetResolution(); + // NOTE(emilio): What we implement here matches the image-set() spec, but it's + // unclear whether this is the right thing to do, see + // https://github.com/whatwg/html/pull/5574#issuecomment-826335244. + if (mResponsiveSelector) { + float density = mResponsiveSelector->GetSelectedImageDensity(); + MOZ_ASSERT(density >= 0.0); + resolution.ScaleBy(density); + } + + resolution.ApplyTo(size.width, size.height); + return size; +} + +nsresult HTMLImageElement::CopyInnerTo(HTMLImageElement* aDest) { + nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest); + if (NS_FAILED(rv)) { + return rv; + } + + // In SetAttr (called from nsGenericHTMLElement::CopyInnerTo), aDest skipped + // doing the image load because we passed in false for aNotify. But we + // really do want it to do the load, so set it up to happen once the cloning + // reaches a stable state. + if (!aDest->InResponsiveMode() && aDest->HasAttr(nsGkAtoms::src) && + aDest->ShouldLoadImage()) { + // Mark channel as urgent-start before load image if the image load is + // initaiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + nsContentUtils::AddScriptRunner( + NewRunnableMethod<bool>("dom::HTMLImageElement::MaybeLoadImage", aDest, + &HTMLImageElement::MaybeLoadImage, false)); + } + + return NS_OK; +} + +CORSMode HTMLImageElement::GetCORSMode() { + return AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin)); +} + +JSObject* HTMLImageElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLImageElement_Binding::Wrap(aCx, this, aGivenProto); +} + +#ifdef DEBUG +HTMLFormElement* HTMLImageElement::GetForm() const { return mForm; } +#endif + +void HTMLImageElement::SetForm(HTMLFormElement* aForm) { + MOZ_ASSERT(aForm, "Don't pass null here"); + NS_ASSERTION(!mForm, + "We don't support switching from one non-null form to another."); + + mForm = aForm; +} + +void HTMLImageElement::ClearForm(bool aRemoveFromForm) { + NS_ASSERTION((mForm != nullptr) == HasFlag(ADDED_TO_FORM), + "Form control should have had flag set correctly"); + + if (!mForm) { + return; + } + + if (aRemoveFromForm) { + nsAutoString nameVal, idVal; + GetAttr(nsGkAtoms::name, nameVal); + GetAttr(nsGkAtoms::id, idVal); + + mForm->RemoveImageElement(this); + + if (!nameVal.IsEmpty()) { + mForm->RemoveImageElementFromTable(this, nameVal); + } + + if (!idVal.IsEmpty()) { + mForm->RemoveImageElementFromTable(this, idVal); + } + } + + UnsetFlags(ADDED_TO_FORM); + mForm = nullptr; +} + +void HTMLImageElement::UpdateSourceSyncAndQueueImageTask( + bool aAlwaysLoad, const HTMLSourceElement* aSkippedSource) { + // Per spec, when updating the image data or reacting to environment + // changes, we always run the full selection (including selecting the source + // element and the best fit image from srcset) even if it doesn't directly + // affect the source selection. + // + // However, in the spec of updating the image data, the selection of image + // source URL is in the asynchronous part (i.e. in a microtask), and so this + // doesn't guarantee that the image style is correct after we flush the style + // synchornously. So here we update the responsive source synchronously always + // to make sure the image source is always up-to-date after each DOM mutation. + // Spec issue: https://github.com/whatwg/html/issues/8207. + const bool changed = UpdateResponsiveSource(aSkippedSource); + + // If loading is temporarily disabled, we don't want to queue tasks + // that may then run when loading is re-enabled. + if (!LoadingEnabled() || !ShouldLoadImage()) { + return; + } + + // Ensure that we don't overwrite a previous load request that requires + // a complete load to occur. + bool alwaysLoad = aAlwaysLoad; + if (mPendingImageLoadTask) { + alwaysLoad = alwaysLoad || mPendingImageLoadTask->AlwaysLoad(); + } + + if (!changed && !alwaysLoad) { + return; + } + + QueueImageLoadTask(alwaysLoad); +} + +bool HTMLImageElement::HaveSrcsetOrInPicture() { + if (HasAttr(nsGkAtoms::srcset)) { + return true; + } + + return IsInPicture(); +} + +bool HTMLImageElement::InResponsiveMode() { + // When we lose srcset or leave a <picture> element, the fallback to img.src + // will happen from the microtask, and we should behave responsively in the + // interim + return mResponsiveSelector || mPendingImageLoadTask || + HaveSrcsetOrInPicture(); +} + +bool HTMLImageElement::SelectedSourceMatchesLast(nsIURI* aSelectedSource) { + // If there was no selected source previously, we don't want to short-circuit + // the load. Similarly for if there is no newly selected source. + if (!mLastSelectedSource || !aSelectedSource) { + return false; + } + bool equal = false; + return NS_SUCCEEDED(mLastSelectedSource->Equals(aSelectedSource, &equal)) && + equal; +} + +nsresult HTMLImageElement::LoadSelectedImage(bool aForce, bool aNotify, + bool aAlwaysLoad) { + // In responsive mode, we have to make sure we ran the full selection algrithm + // before loading the selected image. + // Use this assertion to catch any cases we missed. + MOZ_ASSERT(!UpdateResponsiveSource(), + "The image source should be the same because we update the " + "responsive source synchronously"); + + // The density is default to 1.0 for the src attribute case. + double currentDensity = mResponsiveSelector + ? mResponsiveSelector->GetSelectedImageDensity() + : 1.0; + + nsCOMPtr<nsIURI> selectedSource; + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + ImageLoadType type = eImageLoadType_Normal; + bool hasSrc = false; + if (mResponsiveSelector) { + selectedSource = mResponsiveSelector->GetSelectedImageURL(); + triggeringPrincipal = + mResponsiveSelector->GetSelectedImageTriggeringPrincipal(); + type = eImageLoadType_Imageset; + } else if (mSrcURI || HasAttr(nsGkAtoms::src)) { + hasSrc = true; + if (mSrcURI) { + selectedSource = mSrcURI; + if (HaveSrcsetOrInPicture()) { + // If we have a srcset attribute or are in a <picture> element, we + // always use the Imageset load type, even if we parsed no valid + // responsive sources from either, per spec. + type = eImageLoadType_Imageset; + } + triggeringPrincipal = mSrcTriggeringPrincipal; + } + } + + if (!aAlwaysLoad && SelectedSourceMatchesLast(selectedSource)) { + // Update state when only density may have changed (i.e., the source to load + // hasn't changed, and we don't do any request at all). We need (apart from + // updating our internal state) to tell the image frame because its + // intrinsic size may have changed. + // + // In the case we actually trigger a new load, that load will trigger a call + // to nsImageFrame::NotifyNewCurrentRequest, which takes care of that for + // us. + SetDensity(currentDensity); + return NS_OK; + } + + // Before we actually defer the lazy-loading + if (mLazyLoading) { + if (!selectedSource || + !nsContentUtils::IsImageAvailable(this, selectedSource, + triggeringPrincipal, GetCORSMode())) { + return NS_OK; + } + StopLazyLoading(StartLoading::No); + } + + nsresult rv = NS_ERROR_FAILURE; + + // src triggers an error event on invalid URI, unlike other loads. + if (selectedSource || hasSrc) { + rv = LoadImage(selectedSource, aForce, aNotify, type, triggeringPrincipal); + } + + mLastSelectedSource = selectedSource; + mCurrentDensity = currentDensity; + + if (NS_FAILED(rv)) { + CancelImageRequests(aNotify); + } + return rv; +} + +void HTMLImageElement::PictureSourceSrcsetChanged(nsIContent* aSourceNode, + const nsAString& aNewValue, + bool aNotify) { + MOZ_ASSERT(aSourceNode == this || IsPreviousSibling(aSourceNode, this), + "Should not be getting notifications for non-previous-siblings"); + + nsIContent* currentSrc = + mResponsiveSelector ? mResponsiveSelector->Content() : nullptr; + + if (aSourceNode == currentSrc) { + // We're currently using this node as our responsive selector + // source. + nsCOMPtr<nsIPrincipal> principal; + if (aSourceNode == this) { + principal = mSrcsetTriggeringPrincipal; + } else if (auto* source = HTMLSourceElement::FromNode(aSourceNode)) { + principal = source->GetSrcsetTriggeringPrincipal(); + } + mResponsiveSelector->SetCandidatesFromSourceSet(aNewValue, principal); + } + + if (!mInDocResponsiveContent && IsInComposedDoc()) { + OwnerDoc()->AddResponsiveContent(this); + mInDocResponsiveContent = true; + } + + // This always triggers the image update steps per the spec, even if + // we are not using this source. + UpdateSourceSyncAndQueueImageTask(true); +} + +void HTMLImageElement::PictureSourceSizesChanged(nsIContent* aSourceNode, + const nsAString& aNewValue, + bool aNotify) { + MOZ_ASSERT(aSourceNode == this || IsPreviousSibling(aSourceNode, this), + "Should not be getting notifications for non-previous-siblings"); + + nsIContent* currentSrc = + mResponsiveSelector ? mResponsiveSelector->Content() : nullptr; + + if (aSourceNode == currentSrc) { + // We're currently using this node as our responsive selector + // source. + mResponsiveSelector->SetSizesFromDescriptor(aNewValue); + } + + // This always triggers the image update steps per the spec, even if + // we are not using this source. + UpdateSourceSyncAndQueueImageTask(true); +} + +void HTMLImageElement::PictureSourceMediaOrTypeChanged(nsIContent* aSourceNode, + bool aNotify) { + MOZ_ASSERT(IsPreviousSibling(aSourceNode, this), + "Should not be getting notifications for non-previous-siblings"); + + // This always triggers the image update steps per the spec, even if + // we are not switching to/from this source + UpdateSourceSyncAndQueueImageTask(true); +} + +void HTMLImageElement::PictureSourceDimensionChanged( + HTMLSourceElement* aSourceNode, bool aNotify) { + MOZ_ASSERT(IsPreviousSibling(aSourceNode, this), + "Should not be getting notifications for non-previous-siblings"); + + // "width" and "height" affect the dimension of images, but they don't have + // impact on the selection of <source> elements. In other words, + // UpdateResponsiveSource doesn't change the source, so all we need to do is + // just request restyle. + if (mResponsiveSelector && mResponsiveSelector->Content() == aSourceNode) { + InvalidateAttributeMapping(); + } +} + +void HTMLImageElement::PictureSourceAdded(HTMLSourceElement* aSourceNode) { + MOZ_ASSERT(!aSourceNode || IsPreviousSibling(aSourceNode, this), + "Should not be getting notifications for non-previous-siblings"); + + UpdateSourceSyncAndQueueImageTask(true); +} + +void HTMLImageElement::PictureSourceRemoved(HTMLSourceElement* aSourceNode) { + MOZ_ASSERT(!aSourceNode || IsPreviousSibling(aSourceNode, this), + "Should not be getting notifications for non-previous-siblings"); + + UpdateSourceSyncAndQueueImageTask(true, aSourceNode); +} + +bool HTMLImageElement::UpdateResponsiveSource( + const HTMLSourceElement* aSkippedSource) { + bool hadSelector = !!mResponsiveSelector; + + nsIContent* currentSource = + mResponsiveSelector ? mResponsiveSelector->Content() : nullptr; + + // Walk source nodes previous to ourselves if IsInPicture(). + nsINode* candidateSource = + IsInPicture() ? GetParentElement()->GetFirstChild() : this; + + // Initialize this as nullptr so we don't have to nullify it when runing out + // of siblings without finding ourself, e.g. XBL magic. + RefPtr<ResponsiveImageSelector> newResponsiveSelector = nullptr; + + for (; candidateSource; candidateSource = candidateSource->GetNextSibling()) { + if (aSkippedSource == candidateSource) { + continue; + } + + if (candidateSource == currentSource) { + // found no better source before current, re-run selection on + // that and keep it if it's still usable. + bool changed = mResponsiveSelector->SelectImage(true); + if (mResponsiveSelector->NumCandidates()) { + bool isUsableCandidate = true; + + // an otherwise-usable source element may still have a media query that + // may not match any more. + if (candidateSource->IsHTMLElement(nsGkAtoms::source) && + !SourceElementMatches(candidateSource->AsElement())) { + isUsableCandidate = false; + } + + if (isUsableCandidate) { + // We are still using the current source, but the selected image may + // be changed, so always set the density from the selected image. + SetDensity(mResponsiveSelector->GetSelectedImageDensity()); + return changed; + } + } + + // no longer valid + newResponsiveSelector = nullptr; + if (candidateSource == this) { + // No further possibilities + break; + } + } else if (candidateSource == this) { + // We are the last possible source + newResponsiveSelector = + TryCreateResponsiveSelector(candidateSource->AsElement()); + break; + } else if (auto* source = HTMLSourceElement::FromNode(candidateSource)) { + if (RefPtr<ResponsiveImageSelector> selector = + TryCreateResponsiveSelector(source)) { + newResponsiveSelector = selector.forget(); + // This led to a valid source, stop + break; + } + } + } + + // If we reach this point, either: + // - there was no selector originally, and there is not one now + // - there was no selector originally, and there is one now + // - there was a selector, and there is a different one now + // - there was a selector, and there is not one now + SetResponsiveSelector(std::move(newResponsiveSelector)); + return hadSelector || mResponsiveSelector; +} + +/*static */ +bool HTMLImageElement::SupportedPictureSourceType(const nsAString& aType) { + nsAutoString type; + nsAutoString params; + + nsContentUtils::SplitMimeType(aType, type, params); + if (type.IsEmpty()) { + return true; + } + + return imgLoader::SupportImageWithMimeType( + NS_ConvertUTF16toUTF8(type), AcceptedMimeTypes::IMAGES_AND_DOCUMENTS); +} + +bool HTMLImageElement::SourceElementMatches(Element* aSourceElement) { + MOZ_ASSERT(aSourceElement->IsHTMLElement(nsGkAtoms::source)); + + MOZ_ASSERT(IsInPicture()); + MOZ_ASSERT(IsPreviousSibling(aSourceElement, this)); + + // Check media and type + auto* src = static_cast<HTMLSourceElement*>(aSourceElement); + if (!src->MatchesCurrentMedia()) { + return false; + } + + nsAutoString type; + return !src->GetAttr(nsGkAtoms::type, type) || + SupportedPictureSourceType(type); +} + +already_AddRefed<ResponsiveImageSelector> +HTMLImageElement::TryCreateResponsiveSelector(Element* aSourceElement) { + nsCOMPtr<nsIPrincipal> principal; + + // Skip if this is not a <source> with matching media query + bool isSourceTag = aSourceElement->IsHTMLElement(nsGkAtoms::source); + if (isSourceTag) { + if (!SourceElementMatches(aSourceElement)) { + return nullptr; + } + auto* source = HTMLSourceElement::FromNode(aSourceElement); + principal = source->GetSrcsetTriggeringPrincipal(); + } else if (aSourceElement->IsHTMLElement(nsGkAtoms::img)) { + // Otherwise this is the <img> tag itself + MOZ_ASSERT(aSourceElement == this); + principal = mSrcsetTriggeringPrincipal; + } + + // Skip if has no srcset or an empty srcset + nsString srcset; + if (!aSourceElement->GetAttr(nsGkAtoms::srcset, srcset)) { + return nullptr; + } + + if (srcset.IsEmpty()) { + return nullptr; + } + + // Try to parse + RefPtr<ResponsiveImageSelector> sel = + new ResponsiveImageSelector(aSourceElement); + if (!sel->SetCandidatesFromSourceSet(srcset, principal)) { + // No possible candidates, don't need to bother parsing sizes + return nullptr; + } + + nsAutoString sizes; + aSourceElement->GetAttr(nsGkAtoms::sizes, sizes); + sel->SetSizesFromDescriptor(sizes); + + // If this is the <img> tag, also pull in src as the default source + if (!isSourceTag) { + MOZ_ASSERT(aSourceElement == this); + if (mSrcURI) { + sel->SetDefaultSource(mSrcURI, mSrcTriggeringPrincipal); + } + } + + return sel.forget(); +} + +/* static */ +bool HTMLImageElement::SelectSourceForTagWithAttrs( + Document* aDocument, bool aIsSourceTag, const nsAString& aSrcAttr, + const nsAString& aSrcsetAttr, const nsAString& aSizesAttr, + const nsAString& aTypeAttr, const nsAString& aMediaAttr, + nsAString& aResult) { + MOZ_ASSERT(aIsSourceTag || (aTypeAttr.IsEmpty() && aMediaAttr.IsEmpty()), + "Passing type or media attrs makes no sense without aIsSourceTag"); + MOZ_ASSERT(!aIsSourceTag || aSrcAttr.IsEmpty(), + "Passing aSrcAttr makes no sense with aIsSourceTag set"); + + if (aSrcsetAttr.IsEmpty()) { + if (!aIsSourceTag) { + // For an <img> with no srcset, we would always select the src attr. + aResult.Assign(aSrcAttr); + return true; + } + // Otherwise, a <source> without srcset is never selected + return false; + } + + // Would not consider source tags with unsupported media or type + if (aIsSourceTag && + ((!aMediaAttr.IsVoid() && !HTMLSourceElement::WouldMatchMediaForDocument( + aMediaAttr, aDocument)) || + (!aTypeAttr.IsVoid() && !SupportedPictureSourceType(aTypeAttr)))) { + return false; + } + + // Using srcset or picture <source>, build a responsive selector for this tag. + RefPtr<ResponsiveImageSelector> sel = new ResponsiveImageSelector(aDocument); + + sel->SetCandidatesFromSourceSet(aSrcsetAttr); + if (!aSizesAttr.IsEmpty()) { + sel->SetSizesFromDescriptor(aSizesAttr); + } + if (!aIsSourceTag) { + sel->SetDefaultSource(aSrcAttr); + } + + if (sel->GetSelectedImageURLSpec(aResult)) { + return true; + } + + if (!aIsSourceTag) { + // <img> tag with no match would definitively load nothing. + aResult.Truncate(); + return true; + } + + // <source> tags with no match would leave source yet-undetermined. + return false; +} + +void HTMLImageElement::DestroyContent() { + // Clear mPendingImageLoadTask to avoid running LoadSelectedImage() after + // getting destroyed. + mPendingImageLoadTask = nullptr; + + mResponsiveSelector = nullptr; + + nsImageLoadingContent::Destroy(); + nsGenericHTMLElement::DestroyContent(); +} + +void HTMLImageElement::MediaFeatureValuesChanged() { + UpdateSourceSyncAndQueueImageTask(false); +} + +bool HTMLImageElement::ShouldLoadImage() const { + return OwnerDoc()->ShouldLoadImages(); +} + +void HTMLImageElement::SetLazyLoading() { + if (mLazyLoading) { + return; + } + + // If scripting is disabled don't do lazy load. + // https://whatpr.org/html/3752/images.html#updating-the-image-data + // + // Same for printing. + Document* doc = OwnerDoc(); + if (!doc->IsScriptEnabled() || doc->IsStaticDocument()) { + return; + } + + doc->EnsureLazyLoadObserver().Observe(*this); + mLazyLoading = true; + UpdateImageState(true); +} + +void HTMLImageElement::StartLoadingIfNeeded() { + if (!LoadingEnabled() || !ShouldLoadImage()) { + return; + } + + // Use script runner for the case the adopt is from appendChild. + // Bug 1076583 - We still behave synchronously in the non-responsive case + nsContentUtils::AddScriptRunner( + InResponsiveMode() + ? NewRunnableMethod<bool>("dom::HTMLImageElement::QueueImageLoadTask", + this, &HTMLImageElement::QueueImageLoadTask, + true) + : NewRunnableMethod<bool>("dom::HTMLImageElement::MaybeLoadImage", + this, &HTMLImageElement::MaybeLoadImage, + true)); +} + +void HTMLImageElement::StopLazyLoading(StartLoading aStartLoading) { + if (!mLazyLoading) { + return; + } + mLazyLoading = false; + Document* doc = OwnerDoc(); + if (auto* obs = doc->GetLazyLoadObserver()) { + obs->Unobserve(*this); + } + + if (aStartLoading == StartLoading::Yes) { + StartLoadingIfNeeded(); + } +} + +const StyleLockedDeclarationBlock* +HTMLImageElement::GetMappedAttributesFromSource() const { + if (!IsInPicture() || !mResponsiveSelector) { + return nullptr; + } + + const auto* source = + HTMLSourceElement::FromNodeOrNull(mResponsiveSelector->Content()); + if (!source) { + return nullptr; + } + + MOZ_ASSERT(IsPreviousSibling(source, this), + "Incorrect or out-of-date source"); + return source->GetAttributesMappedForImage(); +} + +void HTMLImageElement::InvalidateAttributeMapping() { + if (!IsInPicture()) { + return; + } + + nsPresContext* presContext = nsContentUtils::GetContextForContent(this); + if (!presContext) { + return; + } + + // Note: Unfortunately, we have to use RESTYLE_SELF, instead of using + // RESTYLE_STYLE_ATTRIBUTE or other ways, to avoid re-selector-match because + // we are using Gecko_GetExtraContentStyleDeclarations() to retrieve the + // extra declaration block from |this|'s width and height attributes, and + // other restyle hints seems not enough. + // FIXME: We may refine this together with the restyle for presentation + // attributes in RestyleManger::AttributeChagned() + presContext->RestyleManager()->PostRestyleEvent( + this, RestyleHint::RESTYLE_SELF, nsChangeHint(0)); +} + +void HTMLImageElement::SetResponsiveSelector( + RefPtr<ResponsiveImageSelector>&& aSource) { + if (mResponsiveSelector == aSource) { + return; + } + + mResponsiveSelector = std::move(aSource); + + // Invalidate the style if needed. + InvalidateAttributeMapping(); + + // Update density. + SetDensity(mResponsiveSelector + ? mResponsiveSelector->GetSelectedImageDensity() + : 1.0); +} + +void HTMLImageElement::SetDensity(double aDensity) { + if (mCurrentDensity == aDensity) { + return; + } + + mCurrentDensity = aDensity; + + // Invalidate the reflow. + if (nsImageFrame* f = do_QueryFrame(GetPrimaryFrame())) { + f->ResponsiveContentDensityChanged(); + } +} + +void HTMLImageElement::QueueImageLoadTask(bool aAlwaysLoad) { + RefPtr<ImageLoadTask> task = + new ImageLoadTask(this, aAlwaysLoad, mUseUrgentStartForChannel); + // The task checks this to determine if it was the last + // queued event, and so earlier tasks are implicitly canceled. + mPendingImageLoadTask = task; + CycleCollectedJSContext::Get()->DispatchToMicroTask(task.forget()); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLImageElement.h b/dom/html/HTMLImageElement.h new file mode 100644 index 0000000000..8e10c6867e --- /dev/null +++ b/dom/html/HTMLImageElement.h @@ -0,0 +1,428 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLImageElement_h +#define mozilla_dom_HTMLImageElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsImageLoadingContent.h" +#include "Units.h" +#include "nsCycleCollectionParticipant.h" + +namespace mozilla { +class EventChainPreVisitor; +namespace dom { + +class ImageLoadTask; + +class ResponsiveImageSelector; +class HTMLImageElement final : public nsGenericHTMLElement, + public nsImageLoadingContent { + friend class HTMLSourceElement; + friend class HTMLPictureElement; + friend class ImageLoadTask; + + public: + explicit HTMLImageElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + static already_AddRefed<HTMLImageElement> Image( + const GlobalObject& aGlobal, const Optional<uint32_t>& aWidth, + const Optional<uint32_t>& aHeight, ErrorResult& aError); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLImageElement, + nsGenericHTMLElement) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + bool Draggable() const override; + + ResponsiveImageSelector* GetResponsiveImageSelector() { + return mResponsiveSelector.get(); + } + + // Element + bool IsInteractiveHTMLContent() const override; + + // EventTarget + void AsyncEventRunning(AsyncEventDispatcher* aEvent) override; + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLImageElement, img) + + // override from nsImageLoadingContent + CORSMode GetCORSMode() override; + + // nsIContent + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + nsINode* GetScopeChainParent() const override; + + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent) override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + void NodeInfoChanged(Document* aOldDoc) override; + + nsresult CopyInnerTo(HTMLImageElement* aDest); + + void MaybeLoadImage(bool aAlwaysForceLoad); + + bool IsMap() { return GetBoolAttr(nsGkAtoms::ismap); } + void SetIsMap(bool aIsMap, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::ismap, aIsMap, aError); + } + MOZ_CAN_RUN_SCRIPT uint32_t Width(); + void SetWidth(uint32_t aWidth, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::width, aWidth, 0, aError); + } + MOZ_CAN_RUN_SCRIPT uint32_t Height(); + void SetHeight(uint32_t aHeight, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::height, aHeight, 0, aError); + } + + nsIntSize NaturalSize(); + uint32_t NaturalHeight() { return NaturalSize().height; } + uint32_t NaturalWidth() { return NaturalSize().width; } + + bool Complete(); + uint32_t Hspace() { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::hspace, 0); + } + void SetHspace(uint32_t aHspace, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::hspace, aHspace, 0, aError); + } + uint32_t Vspace() { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::vspace, 0); + } + void SetVspace(uint32_t aVspace, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::vspace, aVspace, 0, aError); + } + + void GetAlt(nsAString& aAlt) { GetHTMLAttr(nsGkAtoms::alt, aAlt); } + void SetAlt(const nsAString& aAlt, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::alt, aAlt, aError); + } + void GetSrc(nsAString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); } + void SetSrc(const nsAString& aSrc, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::src, aSrc, aError); + } + void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal, + ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aError); + } + void GetSrcset(nsAString& aSrcset) { + GetHTMLAttr(nsGkAtoms::srcset, aSrcset); + } + void SetSrcset(const nsAString& aSrcset, nsIPrincipal* aTriggeringPrincipal, + ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::srcset, aSrcset, aTriggeringPrincipal, aError); + } + void GetCrossOrigin(nsAString& aResult) { + // Null for both missing and invalid defaults is ok, since we + // always parse to an enum value, so we don't need an invalid + // default, and we _want_ the missing default to be null. + GetEnumAttr(nsGkAtoms::crossorigin, nullptr, aResult); + } + void SetCrossOrigin(const nsAString& aCrossOrigin, ErrorResult& aError) { + SetOrRemoveNullableStringAttr(nsGkAtoms::crossorigin, aCrossOrigin, aError); + } + void GetUseMap(nsAString& aUseMap) { + GetHTMLAttr(nsGkAtoms::usemap, aUseMap); + } + void SetUseMap(const nsAString& aUseMap, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::usemap, aUseMap, aError); + } + void GetName(nsAString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); } + void SetName(const nsAString& aName, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::name, aName, aError); + } + void GetAlign(nsAString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + void GetLongDesc(nsAString& aLongDesc) { + GetURIAttr(nsGkAtoms::longdesc, nullptr, aLongDesc); + } + void SetLongDesc(const nsAString& aLongDesc, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::longdesc, aLongDesc, aError); + } + void GetSizes(nsAString& aSizes) { GetHTMLAttr(nsGkAtoms::sizes, aSizes); } + void SetSizes(const nsAString& aSizes, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::sizes, aSizes, aError); + } + void GetCurrentSrc(nsAString& aValue); + void GetBorder(nsAString& aBorder) { + GetHTMLAttr(nsGkAtoms::border, aBorder); + } + void SetBorder(const nsAString& aBorder, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::border, aBorder, aError); + } + void SetReferrerPolicy(const nsAString& aReferrer, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::referrerpolicy, aReferrer, aError); + } + void GetReferrerPolicy(nsAString& aReferrer) { + GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrer); + } + void SetDecoding(const nsAString& aDecoding, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::decoding, aDecoding, aError); + } + void GetDecoding(nsAString& aValue); + + void SetLoading(const nsAString& aLoading, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::loading, aLoading, aError); + } + + bool IsAwaitingLoadOrLazyLoading() const { + return mLazyLoading || mPendingImageLoadTask; + } + + bool IsLazyLoading() const { return mLazyLoading; } + + already_AddRefed<Promise> Decode(ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT int32_t X(); + MOZ_CAN_RUN_SCRIPT int32_t Y(); + void GetLowsrc(nsAString& aLowsrc) { + GetURIAttr(nsGkAtoms::lowsrc, nullptr, aLowsrc); + } + void SetLowsrc(const nsAString& aLowsrc, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::lowsrc, aLowsrc, aError); + } + +#ifdef DEBUG + HTMLFormElement* GetForm() const; +#endif + void SetForm(HTMLFormElement* aForm); + void ClearForm(bool aRemoveFromForm); + + void DestroyContent() override; + + void MediaFeatureValuesChanged(); + + /** + * Given a hypothetical <img> or <source> tag with the given parameters, + * return what URI we would attempt to use, if any. Used by the preloader to + * resolve sources prior to DOM creation. + * + * @param aDocument The document this image would be for, for referencing + * viewport width and DPI/zoom + * @param aIsSourceTag If these parameters are for a <source> tag (as in a + * <picture>) rather than an <img> tag. Note that some attrs are unused + * when this is true an vice versa + * @param aSrcAttr [ignored if aIsSourceTag] The src attr for this image. + * @param aSrcsetAttr The srcset attr for this image/source + * @param aSizesAttr The sizes attr for this image/source + * @param aTypeAttr [ignored if !aIsSourceTag] The type attr for this source. + * Should be a void string to differentiate no type attribute + * from an empty one. + * @param aMediaAttr [ignored if !aIsSourceTag] The media attr for this + * source. Should be a void string to differentiate no + * media attribute from an empty one. + * @param aResult A reference to store the resulting URL spec in if we + * selected a source. This value is not guaranteed to parse to + * a valid URL, merely the URL that the tag would attempt to + * resolve and load (which may be the empty string). This + * parameter is not modified if return value is false. + * @return True if we were able to select a final source, false if further + * sources would be considered. It follows that this always returns + * true if !aIsSourceTag. + * + * Note that the return value may be true with an empty string as the result, + * which implies that the parameters provided describe a tag that would select + * no source. This is distinct from a return of false which implies that + * further <source> or <img> tags would be considered. + */ + static bool SelectSourceForTagWithAttrs( + Document* aDocument, bool aIsSourceTag, const nsAString& aSrcAttr, + const nsAString& aSrcsetAttr, const nsAString& aSizesAttr, + const nsAString& aTypeAttr, const nsAString& aMediaAttr, + nsAString& aResult); + + enum class FromIntersectionObserver : bool { No, Yes }; + enum class StartLoading : bool { No, Yes }; + void StopLazyLoading(StartLoading); + + // This is used when restyling, for retrieving the extra style from the source + // element. + const StyleLockedDeclarationBlock* GetMappedAttributesFromSource() const; + + protected: + virtual ~HTMLImageElement(); + + // Update the responsive source synchronously and queues a task to run + // LoadSelectedImage pending stable state. + // + // Pending Bug 1076583 this is only used by the responsive image + // algorithm (InResponsiveMode()) -- synchronous actions when just + // using img.src will bypass this, and update source and kick off + // image load synchronously. + void UpdateSourceSyncAndQueueImageTask( + bool aAlwaysLoad, const HTMLSourceElement* aSkippedSource = nullptr); + + // True if we have a srcset attribute or a <picture> parent, regardless of if + // any valid responsive sources were parsed from either. + bool HaveSrcsetOrInPicture(); + + // True if we are using the newer image loading algorithm. This will be the + // only mode after Bug 1076583 + bool InResponsiveMode(); + + // True if the given URL equals the last URL that was loaded by this element. + bool SelectedSourceMatchesLast(nsIURI* aSelectedSource); + + // Load the current mResponsiveSelector (responsive mode) or src attr image. + // Note: This doesn't run the full selection for the responsive selector. + nsresult LoadSelectedImage(bool aForce, bool aNotify, bool aAlwaysLoad); + + // True if this string represents a type we would support on <source type> + static bool SupportedPictureSourceType(const nsAString& aType); + + // Update/create/destroy mResponsiveSelector + void PictureSourceSrcsetChanged(nsIContent* aSourceNode, + const nsAString& aNewValue, bool aNotify); + void PictureSourceSizesChanged(nsIContent* aSourceNode, + const nsAString& aNewValue, bool aNotify); + // As we re-run the source selection on these mutations regardless, + // we don't actually care which changed or to what + void PictureSourceMediaOrTypeChanged(nsIContent* aSourceNode, bool aNotify); + + // This is called when we update "width" or "height" attribute of source + // element. + void PictureSourceDimensionChanged(HTMLSourceElement* aSourceNode, + bool aNotify); + + void PictureSourceAdded(HTMLSourceElement* aSourceNode = nullptr); + // This should be called prior to the unbind, such that nextsibling works + void PictureSourceRemoved(HTMLSourceElement* aSourceNode = nullptr); + + // Re-evaluates all source nodes (picture <source>,<img>) and finds + // the best source set for mResponsiveSelector. If a better source + // is found, creates a new selector and feeds the source to it. If + // the current ResponsiveSelector is not changed, runs + // SelectImage(true) to re-evaluate its candidates. + // + // Because keeping the existing selector is the common case (and we + // often do no-op reselections), this does not re-parse values for + // the existing mResponsiveSelector, meaning you need to update its + // parameters as appropriate before calling (or null it out to force + // recreation) + // + // if |aSkippedSource| is non-null, we will skip it when running the + // algorithm. This is used when we need to update the source when we are + // removing the source element. + // + // Returns true if the source has changed, and false otherwise. + bool UpdateResponsiveSource( + const HTMLSourceElement* aSkippedSource = nullptr); + + // Given a <source> node that is a previous sibling *or* ourselves, try to + // create a ResponsiveSelector. + + // If the node's srcset/sizes make for an invalid selector, returns + // nullptr. This does not guarantee the resulting selector matches an image, + // only that it is valid. + already_AddRefed<ResponsiveImageSelector> TryCreateResponsiveSelector( + Element* aSourceElement); + + MOZ_CAN_RUN_SCRIPT CSSIntPoint GetXY(); + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + void UpdateFormOwner(); + + void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) override; + + // Override for nsImageLoadingContent. + nsIContent* AsContent() override { return this; } + + // Created when we're tracking responsive image state + RefPtr<ResponsiveImageSelector> mResponsiveSelector; + + // This is a weak reference that this element and the HTMLFormElement + // cooperate in maintaining. + HTMLFormElement* mForm = nullptr; + + private: + bool SourceElementMatches(Element* aSourceElement); + + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); + /** + * This function is called by AfterSetAttr and OnAttrSetButNotChanged. + * It will not be called if the value is being unset. + * + * @param aNamespaceID the namespace of the attr being set + * @param aName the localname of the attribute being set + * @param aValue the value it's being set to represented as either a string or + * a parsed nsAttrValue. + * @param aOldValue the value previously set. Will be null if no value was + * previously set. This value should only be used when + * aValueMaybeChanged is true; when aValueMaybeChanged is false, + * aOldValue should be considered unreliable. + * @param aNotify Whether we plan to notify document observers. + */ + void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString& aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify); + + bool ShouldLoadImage() const; + + // Set this image as a lazy load image due to loading="lazy". + void SetLazyLoading(); + + void StartLoadingIfNeeded(); + + bool IsInPicture() const { + return GetParentElement() && + GetParentElement()->IsHTMLElement(nsGkAtoms::picture); + } + + void InvalidateAttributeMapping(); + + void SetResponsiveSelector(RefPtr<ResponsiveImageSelector>&& aSource); + void SetDensity(double aDensity); + + // Queue an image load task (via microtask). + void QueueImageLoadTask(bool aAlwaysLoad); + + RefPtr<ImageLoadTask> mPendingImageLoadTask; + nsCOMPtr<nsIURI> mSrcURI; + nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal; + nsCOMPtr<nsIPrincipal> mSrcsetTriggeringPrincipal; + + // Last URL that was attempted to load by this element. + nsCOMPtr<nsIURI> mLastSelectedSource; + // Last pixel density that was selected. + double mCurrentDensity = 1.0; + bool mInDocResponsiveContent = false; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_HTMLImageElement_h */ diff --git a/dom/html/HTMLInputElement.cpp b/dom/html/HTMLInputElement.cpp new file mode 100644 index 0000000000..4e7241ec7a --- /dev/null +++ b/dom/html/HTMLInputElement.cpp @@ -0,0 +1,7407 @@ +/* -*- 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/HTMLInputElement.h" + +#include "mozilla/ArrayUtils.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Components.h" +#include "mozilla/dom/AutocompleteInfoBinding.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/Directory.h" +#include "mozilla/dom/DocumentOrShadowRoot.h" +#include "mozilla/dom/ElementBinding.h" +#include "mozilla/dom/FileSystemUtils.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/GetFilesHelper.h" +#include "mozilla/dom/NumericInputTypes.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/dom/InputType.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/WheelEventBinding.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_signon.h" +#include "mozilla/TextUtils.h" +#include "mozilla/Try.h" +#include "nsAttrValueInlines.h" +#include "nsCRTGlue.h" +#include "nsIFilePicker.h" +#include "nsNetUtil.h" +#include "nsQueryObject.h" + +#include "HTMLDataListElement.h" +#include "HTMLFormSubmissionConstants.h" +#include "mozilla/Telemetry.h" +#include "nsBaseCommandController.h" +#include "nsIStringBundle.h" +#include "nsFocusManager.h" +#include "nsColorControlFrame.h" +#include "nsNumberControlFrame.h" +#include "nsSearchControlFrame.h" +#include "nsPIDOMWindow.h" +#include "nsRepeatService.h" +#include "nsContentCID.h" +#include "mozilla/dom/ProgressEvent.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsPresContext.h" +#include "nsIFormControl.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLDataListElement.h" +#include "mozilla/dom/HTMLOptionElement.h" +#include "nsIFormControlFrame.h" +#include "nsITextControlFrame.h" +#include "nsIFrame.h" +#include "nsRangeFrame.h" +#include "nsError.h" +#include "nsIEditor.h" +#include "nsIPromptCollection.h" + +#include "mozilla/PresState.h" +#include "nsLinebreakConverter.h" //to strip out carriage returns +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsLayoutUtils.h" +#include "nsVariant.h" + +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/InternalMutationEvent.h" +#include "mozilla/TextControlState.h" +#include "mozilla/TextEditor.h" +#include "mozilla/TextEvents.h" +#include "mozilla/TouchEvents.h" + +#include <algorithm> + +// input type=radio +#include "mozilla/dom/RadioGroupContainer.h" +#include "nsIRadioVisitor.h" +#include "nsRadioVisitor.h" + +// input type=file +#include "mozilla/dom/FileSystemEntry.h" +#include "mozilla/dom/FileSystem.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/FileList.h" +#include "nsIFile.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIContentPrefService2.h" +#include "nsIMIMEService.h" +#include "nsIObserverService.h" + +// input type=image +#include "nsImageLoadingContent.h" +#include "imgRequestProxy.h" + +#include "mozAutoDocUpdate.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentUtils.h" +#include "mozilla/dom/DirectionalityUtils.h" + +#include "mozilla/LookAndFeel.h" +#include "mozilla/Preferences.h" +#include "mozilla/MathAlgorithms.h" + +#include <limits> + +#include "nsIColorPicker.h" +#include "nsIStringEnumerator.h" +#include "HTMLSplitOnSpacesTokenizer.h" +#include "nsIMIMEInfo.h" +#include "nsFrameSelection.h" +#include "nsXULControllers.h" + +// input type=date +#include "js/Date.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Input) + +// XXX align=left, hspace, vspace, border? other nav4 attrs + +namespace mozilla::dom { + +// First bits are needed for the control type. +#define NS_OUTER_ACTIVATE_EVENT (1 << 9) +#define NS_ORIGINAL_CHECKED_VALUE (1 << 10) +// (1 << 11 is unused) +#define NS_ORIGINAL_INDETERMINATE_VALUE (1 << 12) +#define NS_PRE_HANDLE_BLUR_EVENT (1 << 13) +#define NS_IN_SUBMIT_CLICK (1 << 15) +#define NS_CONTROL_TYPE(bits) \ + ((bits) & ~(NS_OUTER_ACTIVATE_EVENT | NS_ORIGINAL_CHECKED_VALUE | \ + NS_ORIGINAL_INDETERMINATE_VALUE | NS_PRE_HANDLE_BLUR_EVENT | \ + NS_IN_SUBMIT_CLICK)) + +// whether textfields should be selected once focused: +// -1: no, 1: yes, 0: uninitialized +static int32_t gSelectTextFieldOnFocus; +UploadLastDir* HTMLInputElement::gUploadLastDir; + +static const nsAttrValue::EnumTable kInputTypeTable[] = { + {"button", FormControlType::InputButton}, + {"checkbox", FormControlType::InputCheckbox}, + {"color", FormControlType::InputColor}, + {"date", FormControlType::InputDate}, + {"datetime-local", FormControlType::InputDatetimeLocal}, + {"email", FormControlType::InputEmail}, + {"file", FormControlType::InputFile}, + {"hidden", FormControlType::InputHidden}, + {"reset", FormControlType::InputReset}, + {"image", FormControlType::InputImage}, + {"month", FormControlType::InputMonth}, + {"number", FormControlType::InputNumber}, + {"password", FormControlType::InputPassword}, + {"radio", FormControlType::InputRadio}, + {"range", FormControlType::InputRange}, + {"search", FormControlType::InputSearch}, + {"submit", FormControlType::InputSubmit}, + {"tel", FormControlType::InputTel}, + {"time", FormControlType::InputTime}, + {"url", FormControlType::InputUrl}, + {"week", FormControlType::InputWeek}, + // "text" must be last for ParseAttribute to work right. If you add things + // before it, please update kInputDefaultType. + {"text", FormControlType::InputText}, + {nullptr, 0}}; + +// Default type is 'text'. +static const nsAttrValue::EnumTable* kInputDefaultType = + &kInputTypeTable[ArrayLength(kInputTypeTable) - 2]; + +static const nsAttrValue::EnumTable kCaptureTable[] = { + {"user", nsIFilePicker::captureUser}, + {"environment", nsIFilePicker::captureEnv}, + {"", nsIFilePicker::captureDefault}, + {nullptr, nsIFilePicker::captureNone}}; + +static const nsAttrValue::EnumTable* kCaptureDefault = &kCaptureTable[2]; + +using namespace blink; + +constexpr Decimal HTMLInputElement::kStepScaleFactorDate(86400000_d); +constexpr Decimal HTMLInputElement::kStepScaleFactorNumberRange(1_d); +constexpr Decimal HTMLInputElement::kStepScaleFactorTime(1000_d); +constexpr Decimal HTMLInputElement::kStepScaleFactorMonth(1_d); +constexpr Decimal HTMLInputElement::kStepScaleFactorWeek(7 * 86400000_d); +constexpr Decimal HTMLInputElement::kDefaultStepBase(0_d); +constexpr Decimal HTMLInputElement::kDefaultStepBaseWeek(-259200000_d); +constexpr Decimal HTMLInputElement::kDefaultStep(1_d); +constexpr Decimal HTMLInputElement::kDefaultStepTime(60_d); +constexpr Decimal HTMLInputElement::kStepAny(0_d); + +const double HTMLInputElement::kMinimumYear = 1; +const double HTMLInputElement::kMaximumYear = 275760; +const double HTMLInputElement::kMaximumWeekInMaximumYear = 37; +const double HTMLInputElement::kMaximumDayInMaximumYear = 13; +const double HTMLInputElement::kMaximumMonthInMaximumYear = 9; +const double HTMLInputElement::kMaximumWeekInYear = 53; +const double HTMLInputElement::kMsPerDay = 24 * 60 * 60 * 1000; + +// An helper class for the dispatching of the 'change' event. +// This class is used when the FilePicker finished its task (or when files and +// directories are set by some chrome/test only method). +// The task of this class is to postpone the dispatching of 'change' and 'input' +// events at the end of the exploration of the directories. +class DispatchChangeEventCallback final : public GetFilesCallback { + public: + explicit DispatchChangeEventCallback(HTMLInputElement* aInputElement) + : mInputElement(aInputElement) { + MOZ_ASSERT(aInputElement); + } + + virtual void Callback( + nsresult aStatus, + const FallibleTArray<RefPtr<BlobImpl>>& aBlobImpls) override { + if (!mInputElement->GetOwnerGlobal()) { + return; + } + + nsTArray<OwningFileOrDirectory> array; + for (uint32_t i = 0; i < aBlobImpls.Length(); ++i) { + OwningFileOrDirectory* element = array.AppendElement(); + RefPtr<File> file = + File::Create(mInputElement->GetOwnerGlobal(), aBlobImpls[i]); + if (NS_WARN_IF(!file)) { + return; + } + + element->SetAsFile() = file; + } + + mInputElement->SetFilesOrDirectories(array, true); + Unused << NS_WARN_IF(NS_FAILED(DispatchEvents())); + } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult DispatchEvents() { + RefPtr<HTMLInputElement> inputElement(mInputElement); + nsresult rv = nsContentUtils::DispatchInputEvent(inputElement); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch input event"); + mInputElement->SetUserInteracted(true); + rv = nsContentUtils::DispatchTrustedEvent(mInputElement->OwnerDoc(), + mInputElement, u"change"_ns, + CanBubble::eYes, Cancelable::eNo); + + return rv; + } + + private: + RefPtr<HTMLInputElement> mInputElement; +}; + +struct HTMLInputElement::FileData { + /** + * The value of the input if it is a file input. This is the list of files or + * directories DOM objects used when uploading a file. It is vital that this + * is kept separate from mValue so that it won't be possible to 'leak' the + * value from a text-input to a file-input. Additionally, the logic for this + * value is kept as simple as possible to avoid accidental errors where the + * wrong filename is used. Therefor the list of filenames is always owned by + * this member, never by the frame. Whenever the frame wants to change the + * filename it has to call SetFilesOrDirectories to update this member. + */ + nsTArray<OwningFileOrDirectory> mFilesOrDirectories; + + RefPtr<GetFilesHelper> mGetFilesRecursiveHelper; + RefPtr<GetFilesHelper> mGetFilesNonRecursiveHelper; + + /** + * Hack for bug 1086684: Stash the .value when we're a file picker. + */ + nsString mFirstFilePath; + + RefPtr<FileList> mFileList; + Sequence<RefPtr<FileSystemEntry>> mEntries; + + nsString mStaticDocFileList; + + void ClearGetFilesHelpers() { + if (mGetFilesRecursiveHelper) { + mGetFilesRecursiveHelper->Unlink(); + mGetFilesRecursiveHelper = nullptr; + } + + if (mGetFilesNonRecursiveHelper) { + mGetFilesNonRecursiveHelper->Unlink(); + mGetFilesNonRecursiveHelper = nullptr; + } + } + + // Cycle Collection support. + void Traverse(nsCycleCollectionTraversalCallback& cb) { + FileData* tmp = this; + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFilesOrDirectories) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFileList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEntries) + if (mGetFilesRecursiveHelper) { + mGetFilesRecursiveHelper->Traverse(cb); + } + + if (mGetFilesNonRecursiveHelper) { + mGetFilesNonRecursiveHelper->Traverse(cb); + } + } + + void Unlink() { + FileData* tmp = this; + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFilesOrDirectories) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFileList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEntries) + ClearGetFilesHelpers(); + } +}; + +HTMLInputElement::nsFilePickerShownCallback::nsFilePickerShownCallback( + HTMLInputElement* aInput, nsIFilePicker* aFilePicker) + : mFilePicker(aFilePicker), mInput(aInput) {} + +NS_IMPL_ISUPPORTS(UploadLastDir::ContentPrefCallback, nsIContentPrefCallback2) + +NS_IMETHODIMP +UploadLastDir::ContentPrefCallback::HandleCompletion(uint16_t aReason) { + nsCOMPtr<nsIFile> localFile; + nsAutoString prefStr; + + if (aReason == nsIContentPrefCallback2::COMPLETE_ERROR || !mResult) { + Preferences::GetString("dom.input.fallbackUploadDir", prefStr); + } + + if (prefStr.IsEmpty() && mResult) { + nsCOMPtr<nsIVariant> pref; + mResult->GetValue(getter_AddRefs(pref)); + pref->GetAsAString(prefStr); + } + + if (!prefStr.IsEmpty()) { + localFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID); + if (localFile && NS_WARN_IF(NS_FAILED(localFile->InitWithPath(prefStr)))) { + localFile = nullptr; + } + } + + if (localFile) { + mFilePicker->SetDisplayDirectory(localFile); + } else { + // If no custom directory was set through the pref, default to + // "desktop" directory for each platform. + mFilePicker->SetDisplaySpecialDirectory( + NS_LITERAL_STRING_FROM_CSTRING(NS_OS_DESKTOP_DIR)); + } + + mFilePicker->Open(mFpCallback); + return NS_OK; +} + +NS_IMETHODIMP +UploadLastDir::ContentPrefCallback::HandleResult(nsIContentPref* pref) { + mResult = pref; + return NS_OK; +} + +NS_IMETHODIMP +UploadLastDir::ContentPrefCallback::HandleError(nsresult error) { + // HandleCompletion is always called (even with HandleError was called), + // so we don't need to do anything special here. + return NS_OK; +} + +namespace { + +/** + * This may return nullptr if the DOM File's implementation of + * File::mozFullPathInternal does not successfully return a non-empty + * string that is a valid path. This can happen on Firefox OS, for example, + * where the file picker can create Blobs. + */ +static already_AddRefed<nsIFile> LastUsedDirectory( + const OwningFileOrDirectory& aData) { + if (aData.IsFile()) { + nsAutoString path; + ErrorResult error; + aData.GetAsFile()->GetMozFullPathInternal(path, error); + if (error.Failed() || path.IsEmpty()) { + error.SuppressException(); + return nullptr; + } + + nsCOMPtr<nsIFile> localFile; + nsresult rv = NS_NewLocalFile(path, true, getter_AddRefs(localFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + nsCOMPtr<nsIFile> parentFile; + rv = localFile->GetParent(getter_AddRefs(parentFile)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + return parentFile.forget(); + } + + MOZ_ASSERT(aData.IsDirectory()); + + nsCOMPtr<nsIFile> localFile = aData.GetAsDirectory()->GetInternalNsIFile(); + MOZ_ASSERT(localFile); + + return localFile.forget(); +} + +void GetDOMFileOrDirectoryName(const OwningFileOrDirectory& aData, + nsAString& aName) { + if (aData.IsFile()) { + aData.GetAsFile()->GetName(aName); + } else { + MOZ_ASSERT(aData.IsDirectory()); + ErrorResult rv; + aData.GetAsDirectory()->GetName(aName, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + } +} + +void GetDOMFileOrDirectoryPath(const OwningFileOrDirectory& aData, + nsAString& aPath, ErrorResult& aRv) { + if (aData.IsFile()) { + aData.GetAsFile()->GetMozFullPathInternal(aPath, aRv); + } else { + MOZ_ASSERT(aData.IsDirectory()); + aData.GetAsDirectory()->GetFullRealPath(aPath); + } +} + +} // namespace + +NS_IMETHODIMP +HTMLInputElement::nsFilePickerShownCallback::Done( + nsIFilePicker::ResultCode aResult) { + mInput->PickerClosed(); + + if (aResult == nsIFilePicker::returnCancel) { + RefPtr<HTMLInputElement> inputElement(mInput); + return nsContentUtils::DispatchTrustedEvent( + inputElement->OwnerDoc(), inputElement, u"cancel"_ns, CanBubble::eYes, + Cancelable::eNo); + } + + mInput->OwnerDoc()->NotifyUserGestureActivation(); + + nsIFilePicker::Mode mode; + mFilePicker->GetMode(&mode); + + // Collect new selected filenames + nsTArray<OwningFileOrDirectory> newFilesOrDirectories; + if (mode == nsIFilePicker::modeOpenMultiple) { + nsCOMPtr<nsISimpleEnumerator> iter; + nsresult rv = + mFilePicker->GetDomFileOrDirectoryEnumerator(getter_AddRefs(iter)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!iter) { + return NS_OK; + } + + nsCOMPtr<nsISupports> tmp; + bool hasMore = true; + + while (NS_SUCCEEDED(iter->HasMoreElements(&hasMore)) && hasMore) { + iter->GetNext(getter_AddRefs(tmp)); + RefPtr<Blob> domBlob = do_QueryObject(tmp); + MOZ_ASSERT(domBlob, + "Null file object from FilePicker's file enumerator?"); + if (!domBlob) { + continue; + } + + OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement(); + element->SetAsFile() = domBlob->ToFile(); + } + } else { + MOZ_ASSERT(mode == nsIFilePicker::modeOpen || + mode == nsIFilePicker::modeGetFolder); + nsCOMPtr<nsISupports> tmp; + nsresult rv = mFilePicker->GetDomFileOrDirectory(getter_AddRefs(tmp)); + NS_ENSURE_SUCCESS(rv, rv); + + // Show a prompt to get user confirmation before allowing folder access. + // This is to prevent sites from tricking the user into uploading files. + // See Bug 1338637. + if (mode == nsIFilePicker::modeGetFolder) { + nsCOMPtr<nsIPromptCollection> prompter = + do_GetService("@mozilla.org/embedcomp/prompt-collection;1"); + if (!prompter) { + return NS_ERROR_NOT_AVAILABLE; + } + + bool confirmed = false; + BrowsingContext* bc = mInput->OwnerDoc()->GetBrowsingContext(); + + // Get directory name + RefPtr<Directory> directory = static_cast<Directory*>(tmp.get()); + nsAutoString directoryName; + ErrorResult error; + directory->GetName(directoryName, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + rv = prompter->ConfirmFolderUpload(bc, directoryName, &confirmed); + NS_ENSURE_SUCCESS(rv, rv); + if (!confirmed) { + // User aborted upload + return NS_OK; + } + } + + RefPtr<Blob> blob = do_QueryObject(tmp); + if (blob) { + RefPtr<File> file = blob->ToFile(); + MOZ_ASSERT(file); + + OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement(); + element->SetAsFile() = file; + } else if (tmp) { + RefPtr<Directory> directory = static_cast<Directory*>(tmp.get()); + OwningFileOrDirectory* element = newFilesOrDirectories.AppendElement(); + element->SetAsDirectory() = directory; + } + } + + if (newFilesOrDirectories.IsEmpty()) { + return NS_OK; + } + + // Store the last used directory using the content pref service: + nsCOMPtr<nsIFile> lastUsedDir = LastUsedDirectory(newFilesOrDirectories[0]); + + if (lastUsedDir) { + HTMLInputElement::gUploadLastDir->StoreLastUsedDirectory(mInput->OwnerDoc(), + lastUsedDir); + } + + // The text control frame (if there is one) isn't going to send a change + // event because it will think this is done by a script. + // So, we can safely send one by ourself. + mInput->SetFilesOrDirectories(newFilesOrDirectories, true); + + // mInput(HTMLInputElement) has no scriptGlobalObject, don't create + // DispatchChangeEventCallback + if (!mInput->GetOwnerGlobal()) { + return NS_OK; + } + RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback = + new DispatchChangeEventCallback(mInput); + + if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() && + mInput->HasAttr(nsGkAtoms::webkitdirectory)) { + ErrorResult error; + GetFilesHelper* helper = mInput->GetOrCreateGetFilesHelper(true, error); + if (NS_WARN_IF(error.Failed())) { + return error.StealNSResult(); + } + + helper->AddCallback(dispatchChangeEventCallback); + return NS_OK; + } + + return dispatchChangeEventCallback->DispatchEvents(); +} + +NS_IMPL_ISUPPORTS(HTMLInputElement::nsFilePickerShownCallback, + nsIFilePickerShownCallback) + +class nsColorPickerShownCallback final : public nsIColorPickerShownCallback { + ~nsColorPickerShownCallback() = default; + + public: + nsColorPickerShownCallback(HTMLInputElement* aInput, + nsIColorPicker* aColorPicker) + : mInput(aInput), mColorPicker(aColorPicker), mValueChanged(false) {} + + NS_DECL_ISUPPORTS + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD Update(const nsAString& aColor) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD Done(const nsAString& aColor) override; + + private: + /** + * Updates the internals of the object using aColor as the new value. + * If aTrustedUpdate is true, it will consider that aColor is a new value. + * Otherwise, it will check that aColor is different from the current value. + */ + MOZ_CAN_RUN_SCRIPT + nsresult UpdateInternal(const nsAString& aColor, bool aTrustedUpdate); + + RefPtr<HTMLInputElement> mInput; + nsCOMPtr<nsIColorPicker> mColorPicker; + bool mValueChanged; +}; + +nsresult nsColorPickerShownCallback::UpdateInternal(const nsAString& aColor, + bool aTrustedUpdate) { + bool valueChanged = false; + nsAutoString oldValue; + if (aTrustedUpdate) { + mInput->OwnerDoc()->NotifyUserGestureActivation(); + valueChanged = true; + } else { + mInput->GetValue(oldValue, CallerType::System); + } + + mInput->SetValue(aColor, CallerType::System, IgnoreErrors()); + + if (!aTrustedUpdate) { + nsAutoString newValue; + mInput->GetValue(newValue, CallerType::System); + if (!oldValue.Equals(newValue)) { + valueChanged = true; + } + } + + if (!valueChanged) { + return NS_OK; + } + + mValueChanged = true; + RefPtr<HTMLInputElement> input(mInput); + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(input); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + return NS_OK; +} + +NS_IMETHODIMP +nsColorPickerShownCallback::Update(const nsAString& aColor) { + return UpdateInternal(aColor, true); +} + +NS_IMETHODIMP +nsColorPickerShownCallback::Done(const nsAString& aColor) { + /** + * When Done() is called, we might be at the end of a serie of Update() calls + * in which case mValueChanged is set to true and a change event will have to + * be fired but we might also be in a one shot Done() call situation in which + * case we should fire a change event iif the value actually changed. + * UpdateInternal(bool) is taking care of that logic for us. + */ + nsresult rv = NS_OK; + + mInput->PickerClosed(); + + if (!aColor.IsEmpty()) { + UpdateInternal(aColor, false); + } + + if (mValueChanged) { + mInput->SetUserInteracted(true); + rv = nsContentUtils::DispatchTrustedEvent( + mInput->OwnerDoc(), static_cast<Element*>(mInput.get()), u"change"_ns, + CanBubble::eYes, Cancelable::eNo); + } + + return rv; +} + +NS_IMPL_ISUPPORTS(nsColorPickerShownCallback, nsIColorPickerShownCallback) + +static bool IsPopupBlocked(Document* aDoc) { + if (aDoc->ConsumeTransientUserGestureActivation()) { + return false; + } + + WindowContext* wc = aDoc->GetWindowContext(); + if (wc && wc->CanShowPopup()) { + return false; + } + + nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "DOM"_ns, aDoc, + nsContentUtils::eDOM_PROPERTIES, + "InputPickerBlockedNoUserActivation"); + return true; +} + +nsTArray<nsString> HTMLInputElement::GetColorsFromList() { + RefPtr<HTMLDataListElement> dataList = GetList(); + if (!dataList) { + return {}; + } + + nsTArray<nsString> colors; + + RefPtr<nsContentList> options = dataList->Options(); + uint32_t length = options->Length(true); + for (uint32_t i = 0; i < length; ++i) { + auto* option = HTMLOptionElement::FromNodeOrNull(options->Item(i, false)); + if (!option) { + continue; + } + + nsString value; + option->GetValue(value); + if (IsValidSimpleColor(value)) { + ToLowerCase(value); + colors.AppendElement(value); + } + } + + return colors; +} + +nsresult HTMLInputElement::InitColorPicker() { + MOZ_ASSERT(IsMutable()); + + if (mPickerRunning) { + NS_WARNING("Just one nsIColorPicker is allowed"); + return NS_ERROR_FAILURE; + } + + nsCOMPtr<Document> doc = OwnerDoc(); + + nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow(); + if (!win) { + return NS_ERROR_FAILURE; + } + + if (IsPopupBlocked(doc)) { + return NS_OK; + } + + // Get Loc title + nsAutoString title; + nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "ColorPicker", title); + + nsCOMPtr<nsIColorPicker> colorPicker = + do_CreateInstance("@mozilla.org/colorpicker;1"); + if (!colorPicker) { + return NS_ERROR_FAILURE; + } + + nsAutoString initialValue; + GetNonFileValueInternal(initialValue); + nsTArray<nsString> colors = GetColorsFromList(); + nsresult rv = colorPicker->Init(win, title, initialValue, colors); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIColorPickerShownCallback> callback = + new nsColorPickerShownCallback(this, colorPicker); + + rv = colorPicker->Open(callback); + if (NS_SUCCEEDED(rv)) { + mPickerRunning = true; + } + + return rv; +} + +nsresult HTMLInputElement::InitFilePicker(FilePickerType aType) { + MOZ_ASSERT(IsMutable()); + + if (mPickerRunning) { + NS_WARNING("Just one nsIFilePicker is allowed"); + return NS_ERROR_FAILURE; + } + + // Get parent nsPIDOMWindow object. + nsCOMPtr<Document> doc = OwnerDoc(); + + nsCOMPtr<nsPIDOMWindowOuter> win = doc->GetWindow(); + if (!win) { + return NS_ERROR_FAILURE; + } + + if (IsPopupBlocked(doc)) { + return NS_OK; + } + + // Get Loc title + nsAutoString title; + nsAutoString okButtonLabel; + if (aType == FILE_PICKER_DIRECTORY) { + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "DirectoryUpload", OwnerDoc(), + title); + + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "DirectoryPickerOkButtonLabel", + OwnerDoc(), okButtonLabel); + } else { + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "FileUpload", OwnerDoc(), title); + } + + nsCOMPtr<nsIFilePicker> filePicker = + do_CreateInstance("@mozilla.org/filepicker;1"); + if (!filePicker) return NS_ERROR_FAILURE; + + nsIFilePicker::Mode mode; + + if (aType == FILE_PICKER_DIRECTORY) { + mode = nsIFilePicker::modeGetFolder; + } else if (HasAttr(nsGkAtoms::multiple)) { + mode = nsIFilePicker::modeOpenMultiple; + } else { + mode = nsIFilePicker::modeOpen; + } + + nsresult rv = + filePicker->Init(win, title, mode, OwnerDoc()->GetBrowsingContext()); + NS_ENSURE_SUCCESS(rv, rv); + + if (!okButtonLabel.IsEmpty()) { + filePicker->SetOkButtonLabel(okButtonLabel); + } + + // Native directory pickers ignore file type filters, so we don't spend + // cycles adding them for FILE_PICKER_DIRECTORY. + if (HasAttr(nsGkAtoms::accept) && aType != FILE_PICKER_DIRECTORY) { + SetFilePickerFiltersFromAccept(filePicker); + + if (StaticPrefs::dom_capture_enabled()) { + if (const nsAttrValue* captureVal = GetParsedAttr(nsGkAtoms::capture)) { + filePicker->SetCapture(static_cast<nsIFilePicker::CaptureTarget>( + captureVal->GetEnumValue())); + } + } + } else { + filePicker->AppendFilters(nsIFilePicker::filterAll); + } + + // Set default directory and filename + nsAutoString defaultName; + + const nsTArray<OwningFileOrDirectory>& oldFiles = + GetFilesOrDirectoriesInternal(); + + nsCOMPtr<nsIFilePickerShownCallback> callback = + new HTMLInputElement::nsFilePickerShownCallback(this, filePicker); + + if (!oldFiles.IsEmpty() && aType != FILE_PICKER_DIRECTORY) { + nsAutoString path; + + nsCOMPtr<nsIFile> parentFile = LastUsedDirectory(oldFiles[0]); + if (parentFile) { + filePicker->SetDisplayDirectory(parentFile); + } + + // Unfortunately nsIFilePicker doesn't allow multiple files to be + // default-selected, so only select something by default if exactly + // one file was selected before. + if (oldFiles.Length() == 1) { + nsAutoString leafName; + GetDOMFileOrDirectoryName(oldFiles[0], leafName); + + if (!leafName.IsEmpty()) { + filePicker->SetDefaultString(leafName); + } + } + + rv = filePicker->Open(callback); + if (NS_SUCCEEDED(rv)) { + mPickerRunning = true; + } + + return rv; + } + + HTMLInputElement::gUploadLastDir->FetchDirectoryAndDisplayPicker( + doc, filePicker, callback); + mPickerRunning = true; + return NS_OK; +} + +#define CPS_PREF_NAME u"browser.upload.lastDir"_ns + +NS_IMPL_ISUPPORTS(UploadLastDir, nsIObserver, nsISupportsWeakReference) + +void HTMLInputElement::InitUploadLastDir() { + gUploadLastDir = new UploadLastDir(); + NS_ADDREF(gUploadLastDir); + + nsCOMPtr<nsIObserverService> observerService = services::GetObserverService(); + if (observerService && gUploadLastDir) { + observerService->AddObserver(gUploadLastDir, + "browser:purge-session-history", true); + } +} + +void HTMLInputElement::DestroyUploadLastDir() { NS_IF_RELEASE(gUploadLastDir); } + +nsresult UploadLastDir::FetchDirectoryAndDisplayPicker( + Document* aDoc, nsIFilePicker* aFilePicker, + nsIFilePickerShownCallback* aFpCallback) { + MOZ_ASSERT(aDoc, "aDoc is null"); + MOZ_ASSERT(aFilePicker, "aFilePicker is null"); + MOZ_ASSERT(aFpCallback, "aFpCallback is null"); + + nsIURI* docURI = aDoc->GetDocumentURI(); + MOZ_ASSERT(docURI, "docURI is null"); + + nsCOMPtr<nsILoadContext> loadContext = aDoc->GetLoadContext(); + nsCOMPtr<nsIContentPrefCallback2> prefCallback = + new UploadLastDir::ContentPrefCallback(aFilePicker, aFpCallback); + + // Attempt to get the CPS, if it's not present we'll fallback to use the + // Desktop folder + nsCOMPtr<nsIContentPrefService2> contentPrefService = + do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); + if (!contentPrefService) { + prefCallback->HandleCompletion(nsIContentPrefCallback2::COMPLETE_ERROR); + return NS_OK; + } + + nsAutoCString cstrSpec; + docURI->GetSpec(cstrSpec); + NS_ConvertUTF8toUTF16 spec(cstrSpec); + + contentPrefService->GetByDomainAndName(spec, CPS_PREF_NAME, loadContext, + prefCallback); + return NS_OK; +} + +nsresult UploadLastDir::StoreLastUsedDirectory(Document* aDoc, nsIFile* aDir) { + MOZ_ASSERT(aDoc, "aDoc is null"); + if (!aDir) { + return NS_OK; + } + + nsCOMPtr<nsIURI> docURI = aDoc->GetDocumentURI(); + MOZ_ASSERT(docURI, "docURI is null"); + + // Attempt to get the CPS, if it's not present we'll just return + nsCOMPtr<nsIContentPrefService2> contentPrefService = + do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); + if (!contentPrefService) return NS_ERROR_NOT_AVAILABLE; + + nsAutoCString cstrSpec; + docURI->GetSpec(cstrSpec); + NS_ConvertUTF8toUTF16 spec(cstrSpec); + + // Find the parent of aFile, and store it + nsString unicodePath; + aDir->GetPath(unicodePath); + if (unicodePath.IsEmpty()) // nothing to do + return NS_OK; + RefPtr<nsVariantCC> prefValue = new nsVariantCC(); + prefValue->SetAsAString(unicodePath); + + // Use the document's current load context to ensure that the content pref + // service doesn't persistently store this directory for this domain if the + // user is using private browsing: + nsCOMPtr<nsILoadContext> loadContext = aDoc->GetLoadContext(); + return contentPrefService->Set(spec, CPS_PREF_NAME, prefValue, loadContext, + nullptr); +} + +NS_IMETHODIMP +UploadLastDir::Observe(nsISupports* aSubject, char const* aTopic, + char16_t const* aData) { + if (strcmp(aTopic, "browser:purge-session-history") == 0) { + nsCOMPtr<nsIContentPrefService2> contentPrefService = + do_GetService(NS_CONTENT_PREF_SERVICE_CONTRACTID); + if (contentPrefService) + contentPrefService->RemoveByName(CPS_PREF_NAME, nullptr, nullptr); + } + return NS_OK; +} + +#ifdef ACCESSIBILITY +// Helper method +static nsresult FireEventForAccessibility(HTMLInputElement* aTarget, + EventMessage aEventMessage); +#endif + +// +// construction, destruction +// + +HTMLInputElement::HTMLInputElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser, FromClone aFromClone) + : TextControlElement(std::move(aNodeInfo), aFromParser, + FormControlType(kInputDefaultType->value)), + mAutocompleteAttrState(nsContentUtils::eAutocompleteAttrState_Unknown), + mAutocompleteInfoState(nsContentUtils::eAutocompleteAttrState_Unknown), + mDisabledChanged(false), + mValueChanged(false), + mUserInteracted(false), + mLastValueChangeWasInteractive(false), + mCheckedChanged(false), + mChecked(false), + mHandlingSelectEvent(false), + mShouldInitChecked(false), + mDoneCreating(aFromParser == NOT_FROM_PARSER && + aFromClone == FromClone::No), + mInInternalActivate(false), + mCheckedIsToggled(false), + mIndeterminate(false), + mInhibitRestoration(aFromParser & FROM_PARSER_FRAGMENT), + mHasRange(false), + mIsDraggingRange(false), + mNumberControlSpinnerIsSpinning(false), + mNumberControlSpinnerSpinsUp(false), + mPickerRunning(false), + mIsPreviewEnabled(false), + mHasBeenTypePassword(false), + mHasPatternAttribute(false), + mRadioGroupContainer(nullptr) { + // If size is above 512, mozjemalloc allocates 1kB, see + // memory/build/mozjemalloc.cpp + static_assert(sizeof(HTMLInputElement) <= 512, + "Keep the size of HTMLInputElement under 512 to avoid " + "performance regression!"); + + // We are in a type=text but we create TextControlState lazily. + mInputData.mState = nullptr; + + void* memory = mInputTypeMem; + mInputType = InputType::Create(this, mType, memory); + + if (!gUploadLastDir) HTMLInputElement::InitUploadLastDir(); + + // Set up our default state. By default we're enabled (since we're a control + // type that can be disabled but not actually disabled right now), optional, + // read-write, and valid. Also by default we don't have to show validity UI + // and so forth. + AddStatesSilently(ElementState::ENABLED | ElementState::OPTIONAL_ | + ElementState::VALID | ElementState::VALUE_EMPTY | + ElementState::READWRITE); + RemoveStatesSilently(ElementState::READONLY); + UpdateApzAwareFlag(); +} + +HTMLInputElement::~HTMLInputElement() { + if (mNumberControlSpinnerIsSpinning) { + StopNumberControlSpinnerSpin(eDisallowDispatchingEvents); + } + nsImageLoadingContent::Destroy(); + FreeData(); +} + +void HTMLInputElement::FreeData() { + if (!IsSingleLineTextControl(false)) { + free(mInputData.mValue); + mInputData.mValue = nullptr; + } else if (mInputData.mState) { + // XXX Passing nullptr to UnbindFromFrame doesn't do anything! + UnbindFromFrame(nullptr); + mInputData.mState->Destroy(); + mInputData.mState = nullptr; + } + + if (mInputType) { + mInputType->DropReference(); + mInputType = nullptr; + } +} + +void HTMLInputElement::EnsureEditorState() { + MOZ_ASSERT(IsSingleLineTextControl(false)); + if (!mInputData.mState) { + mInputData.mState = TextControlState::Construct(this); + } +} + +TextControlState* HTMLInputElement::GetEditorState() const { + if (!IsSingleLineTextControl(false)) { + return nullptr; + } + + // We've postponed allocating TextControlState, doing that in a const + // method is fine. + const_cast<HTMLInputElement*>(this)->EnsureEditorState(); + + MOZ_ASSERT(mInputData.mState, + "Single line text controls need to have a state" + " associated with them"); + + return mInputData.mState; +} + +// nsISupports + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLInputElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLInputElement, + TextControlElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControllers) + if (tmp->IsSingleLineTextControl(false) && tmp->mInputData.mState) { + tmp->mInputData.mState->Traverse(cb); + } + + if (tmp->mFileData) { + tmp->mFileData->Traverse(cb); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLInputElement, + TextControlElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mControllers) + if (tmp->IsSingleLineTextControl(false) && tmp->mInputData.mState) { + tmp->mInputData.mState->Unlink(); + } + + if (tmp->mFileData) { + tmp->mFileData->Unlink(); + } + // XXX should unlink more? +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLInputElement, + TextControlElement, + imgINotificationObserver, + nsIImageLoadingContent, + nsIConstraintValidation) + +// nsINode + +nsresult HTMLInputElement::Clone(dom::NodeInfo* aNodeInfo, + nsINode** aResult) const { + *aResult = nullptr; + + RefPtr<HTMLInputElement> it = new (aNodeInfo->NodeInfoManager()) + HTMLInputElement(do_AddRef(aNodeInfo), NOT_FROM_PARSER, FromClone::Yes); + + nsresult rv = const_cast<HTMLInputElement*>(this)->CopyInnerTo(it); + NS_ENSURE_SUCCESS(rv, rv); + + switch (GetValueMode()) { + case VALUE_MODE_VALUE: + if (mValueChanged) { + // We don't have our default value anymore. Set our value on + // the clone. + nsAutoString value; + GetNonFileValueInternal(value); + // SetValueInternal handles setting the VALUE_CHANGED bit for us + if (NS_WARN_IF( + NS_FAILED(rv = it->SetValueInternal( + value, {ValueSetterOption::SetValueChanged})))) { + return rv; + } + } + break; + case VALUE_MODE_FILENAME: + if (it->OwnerDoc()->IsStaticDocument()) { + // We're going to be used in print preview. Since the doc is static + // we can just grab the pretty string and use it as wallpaper + GetDisplayFileName(it->mFileData->mStaticDocFileList); + } else { + it->mFileData->ClearGetFilesHelpers(); + it->mFileData->mFilesOrDirectories.Clear(); + it->mFileData->mFilesOrDirectories.AppendElements( + mFileData->mFilesOrDirectories); + } + break; + case VALUE_MODE_DEFAULT_ON: + case VALUE_MODE_DEFAULT: + break; + } + + if (mCheckedChanged) { + // We no longer have our original checked state. Set our + // checked state on the clone. + it->DoSetChecked(mChecked, false, true); + // Then tell DoneCreatingElement() not to overwrite: + it->mShouldInitChecked = false; + } + + it->mIndeterminate = mIndeterminate; + + it->DoneCreatingElement(); + + it->SetLastValueChangeWasInteractive(mLastValueChangeWasInteractive); + it.forget(aResult); + return NS_OK; +} + +void HTMLInputElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aNotify && aName == nsGkAtoms::disabled) { + mDisabledChanged = true; + } + + // When name or type changes, radio should be removed from radio group. + // If we are not done creating the radio, we also should not do it. + if (mType == FormControlType::InputRadio) { + if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) && + (mForm || mDoneCreating)) { + RemoveFromRadioGroup(); + } else if (aName == nsGkAtoms::required) { + auto* container = GetCurrentRadioGroupContainer(); + + if (container && ((aValue && !HasAttr(aNameSpaceID, aName)) || + (!aValue && HasAttr(aNameSpaceID, aName)))) { + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + container->RadioRequiredWillChange(name, !!aValue); + } + } + } + + if (aName == nsGkAtoms::webkitdirectory) { + Telemetry::Accumulate(Telemetry::WEBKIT_DIRECTORY_USED, true); + } + } + + return nsGenericHTMLFormControlElementWithState::BeforeSetAttr( + aNameSpaceID, aName, aValue, aNotify); +} + +void HTMLInputElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + bool needValidityUpdate = false; + if (aName == nsGkAtoms::src) { + mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal( + this, aValue ? aValue->GetStringValue() : EmptyString(), + aSubjectPrincipal); + if (aNotify && mType == FormControlType::InputImage) { + if (aValue) { + // Mark channel as urgent-start before load image if the image load is + // initiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + LoadImage(aValue->GetStringValue(), true, aNotify, + eImageLoadType_Normal, mSrcTriggeringPrincipal); + } else { + // Null value means the attr got unset; drop the image + CancelImageRequests(aNotify); + } + } + } + + if (aName == nsGkAtoms::value) { + // If the element has a value in value mode, the value content attribute + // is the default value. So if the elements value didn't change from the + // default, we have to re-set it. + if (!mValueChanged && GetValueMode() == VALUE_MODE_VALUE) { + SetDefaultValueAsValue(); + } else if (GetValueMode() == VALUE_MODE_DEFAULT && HasDirAuto()) { + SetAutoDirectionality(aNotify); + } + // GetStepBase() depends on the `value` attribute if `min` is not present, + // even if the value doesn't change. + UpdateStepMismatchValidityState(); + needValidityUpdate = true; + } + + // Checked must be set no matter what type of control it is, since + // mChecked must reflect the new value + if (aName == nsGkAtoms::checked) { + if (IsRadioOrCheckbox()) { + SetStates(ElementState::DEFAULT, !!aValue, aNotify); + } + if (!mCheckedChanged) { + // Delay setting checked if we are creating this element (wait + // until everything is set) + if (!mDoneCreating) { + mShouldInitChecked = true; + } else { + DoSetChecked(!!aValue, aNotify, false); + } + } + needValidityUpdate = true; + } + + if (aName == nsGkAtoms::type) { + FormControlType newType; + if (!aValue) { + // We're now a text input. + newType = FormControlType(kInputDefaultType->value); + } else { + newType = FormControlType(aValue->GetEnumValue()); + } + if (newType != mType) { + HandleTypeChange(newType, aNotify); + needValidityUpdate = true; + } + } + + // When name or type changes, radio should be added to radio group. + // If we are not done creating the radio, we also should not do it. + if ((aName == nsGkAtoms::name || (aName == nsGkAtoms::type && !mForm)) && + mType == FormControlType::InputRadio && (mForm || mDoneCreating)) { + AddToRadioGroup(); + UpdateValueMissingValidityStateForRadio(false); + needValidityUpdate = true; + } + + if (aName == nsGkAtoms::required || aName == nsGkAtoms::disabled || + aName == nsGkAtoms::readonly) { + if (aName == nsGkAtoms::disabled) { + // This *has* to be called *before* validity state check because + // UpdateBarredFromConstraintValidation and + // UpdateValueMissingValidityState depend on our disabled state. + UpdateDisabledState(aNotify); + } + + if (aName == nsGkAtoms::required && DoesRequiredApply()) { + // This *has* to be called *before* UpdateValueMissingValidityState + // because UpdateValueMissingValidityState depends on our required + // state. + UpdateRequiredState(!!aValue, aNotify); + } + + if (aName == nsGkAtoms::readonly && !!aValue != !!aOldValue) { + UpdateReadOnlyState(aNotify); + } + + UpdateValueMissingValidityState(); + + // This *has* to be called *after* validity has changed. + if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) { + UpdateBarredFromConstraintValidation(); + } + needValidityUpdate = true; + } else if (aName == nsGkAtoms::maxlength) { + UpdateTooLongValidityState(); + needValidityUpdate = true; + } else if (aName == nsGkAtoms::minlength) { + UpdateTooShortValidityState(); + needValidityUpdate = true; + } else if (aName == nsGkAtoms::pattern) { + // Although pattern attribute only applies to single line text controls, + // we set this flag for all input types to save having to check the type + // here. + mHasPatternAttribute = !!aValue; + + if (mDoneCreating) { + UpdatePatternMismatchValidityState(); + } + needValidityUpdate = true; + } else if (aName == nsGkAtoms::multiple) { + UpdateTypeMismatchValidityState(); + needValidityUpdate = true; + } else if (aName == nsGkAtoms::max) { + UpdateHasRange(aNotify); + mInputType->MinMaxStepAttrChanged(); + // Validity state must be updated *after* the UpdateValueDueToAttrChange + // call above or else the following assert will not be valid. + // We don't assert the state of underflow during creation since + // DoneCreatingElement sanitizes. + UpdateRangeOverflowValidityState(); + needValidityUpdate = true; + MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange || + !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW), + "HTML5 spec does not allow underflow for type=range"); + } else if (aName == nsGkAtoms::min) { + UpdateHasRange(aNotify); + mInputType->MinMaxStepAttrChanged(); + // See corresponding @max comment + UpdateRangeUnderflowValidityState(); + UpdateStepMismatchValidityState(); + needValidityUpdate = true; + MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange || + !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW), + "HTML5 spec does not allow underflow for type=range"); + } else if (aName == nsGkAtoms::step) { + mInputType->MinMaxStepAttrChanged(); + // See corresponding @max comment + UpdateStepMismatchValidityState(); + needValidityUpdate = true; + MOZ_ASSERT(!mDoneCreating || mType != FormControlType::InputRange || + !GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW), + "HTML5 spec does not allow underflow for type=range"); + } else if (aName == nsGkAtoms::dir && aValue && + aValue->Equals(nsGkAtoms::_auto, eIgnoreCase)) { + SetAutoDirectionality(aNotify); + } else if (aName == nsGkAtoms::lang) { + // FIXME(emilio, bug 1651070): This doesn't account for lang changes on + // ancestors. + if (mType == FormControlType::InputNumber) { + // The validity of our value may have changed based on the locale. + UpdateValidityState(); + needValidityUpdate = true; + } + } else if (aName == nsGkAtoms::autocomplete) { + // Clear the cached @autocomplete attribute and autocompleteInfo state. + mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown; + mAutocompleteInfoState = nsContentUtils::eAutocompleteAttrState_Unknown; + } else if (aName == nsGkAtoms::placeholder) { + // Full addition / removals of the attribute reconstruct right now. + if (nsTextControlFrame* f = do_QueryFrame(GetPrimaryFrame())) { + f->PlaceholderChanged(aOldValue, aValue); + } + UpdatePlaceholderShownState(); + needValidityUpdate = true; + } + + if (CreatesDateTimeWidget()) { + if (aName == nsGkAtoms::value || aName == nsGkAtoms::readonly || + aName == nsGkAtoms::tabindex || aName == nsGkAtoms::required || + aName == nsGkAtoms::disabled) { + // If original target is this and not the inner text control, we should + // pass the focus to the inner text control. + if (Element* dateTimeBoxElement = GetDateTimeBoxElement()) { + AsyncEventDispatcher::RunDOMEventWhenSafe( + *dateTimeBoxElement, + aName == nsGkAtoms::value ? u"MozDateTimeValueChanged"_ns + : u"MozDateTimeAttributeChanged"_ns, + CanBubble::eNo, ChromeOnlyDispatch::eNo); + } + } + } + if (needValidityUpdate) { + UpdateValidityElementStates(aNotify); + } + } + + return nsGenericHTMLFormControlElementWithState::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLInputElement::BeforeSetForm(HTMLFormElement* aForm, bool aBindToTree) { + // No need to remove from radio group if we are just binding to tree. + if (mType == FormControlType::InputRadio && !aBindToTree) { + RemoveFromRadioGroup(); + } + + // Dispatch event when <input> @form is set + if (!aBindToTree) { + MaybeDispatchLoginManagerEvents(aForm); + } +} + +void HTMLInputElement::AfterClearForm(bool aUnbindOrDelete) { + MOZ_ASSERT(!mForm); + + // Do not add back to radio group if we are releasing or unbinding from tree. + if (mType == FormControlType::InputRadio && !aUnbindOrDelete && + !GetCurrentRadioGroupContainer()) { + AddToRadioGroup(); + UpdateValueMissingValidityStateForRadio(false); + } +} + +void HTMLInputElement::ResultForDialogSubmit(nsAString& aResult) { + if (mType == FormControlType::InputImage) { + // Get a property set by the frame to find out where it was clicked. + const auto* lastClickedPoint = + static_cast<CSSIntPoint*>(GetProperty(nsGkAtoms::imageClickedPoint)); + int32_t x, y; + if (lastClickedPoint) { + x = lastClickedPoint->x; + y = lastClickedPoint->y; + } else { + x = y = 0; + } + aResult.AppendInt(x); + aResult.AppendLiteral(","); + aResult.AppendInt(y); + } else { + GetAttr(nsGkAtoms::value, aResult); + } +} + +void HTMLInputElement::GetAutocomplete(nsAString& aValue) { + if (!DoesAutocompleteApply()) { + return; + } + + aValue.Truncate(); + const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete); + + mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute( + attributeVal, aValue, mAutocompleteAttrState); +} + +void HTMLInputElement::GetAutocompleteInfo(Nullable<AutocompleteInfo>& aInfo) { + if (!DoesAutocompleteApply()) { + aInfo.SetNull(); + return; + } + + const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete); + mAutocompleteInfoState = nsContentUtils::SerializeAutocompleteAttribute( + attributeVal, aInfo.SetValue(), mAutocompleteInfoState, true); +} + +void HTMLInputElement::GetCapture(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::capture, kCaptureDefault->tag, aValue); +} + +void HTMLInputElement::GetFormEnctype(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::formenctype, "", kFormDefaultEnctype->tag, aValue); +} + +void HTMLInputElement::GetFormMethod(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::formmethod, "", kFormDefaultMethod->tag, aValue); +} + +void HTMLInputElement::GetType(nsAString& aValue) const { + GetEnumAttr(nsGkAtoms::type, kInputDefaultType->tag, aValue); +} + +int32_t HTMLInputElement::TabIndexDefault() { return 0; } + +uint32_t HTMLInputElement::Height() { + if (mType != FormControlType::InputImage) { + return 0; + } + return GetWidthHeightForImage().height; +} + +void HTMLInputElement::SetIndeterminateInternal(bool aValue, + bool aShouldInvalidate) { + mIndeterminate = aValue; + if (mType != FormControlType::InputCheckbox) { + return; + } + + SetStates(ElementState::INDETERMINATE, aValue); + + if (aShouldInvalidate) { + // Repaint the frame + if (nsIFrame* frame = GetPrimaryFrame()) { + frame->InvalidateFrameSubtree(); + } + } +} + +void HTMLInputElement::SetIndeterminate(bool aValue) { + SetIndeterminateInternal(aValue, true); +} + +uint32_t HTMLInputElement::Width() { + if (mType != FormControlType::InputImage) { + return 0; + } + return GetWidthHeightForImage().width; +} + +bool HTMLInputElement::SanitizesOnValueGetter() const { + // Don't return non-sanitized value for datetime types, email, or number. + return mType == FormControlType::InputEmail || + mType == FormControlType::InputNumber || IsDateTimeInputType(mType); +} + +void HTMLInputElement::GetValue(nsAString& aValue, CallerType aCallerType) { + GetValueInternal(aValue, aCallerType); + + // In the case where we need to sanitize an input value without affecting + // the displayed user's input, we instead sanitize only on .value accesses. + // For the more general case of input elements displaying text that isn't + // their current value, see bug 805049. + if (SanitizesOnValueGetter()) { + SanitizeValue(aValue, SanitizationKind::ForValueGetter); + } +} + +void HTMLInputElement::GetValueInternal(nsAString& aValue, + CallerType aCallerType) const { + if (mType != FormControlType::InputFile) { + GetNonFileValueInternal(aValue); + return; + } + + if (aCallerType == CallerType::System) { + aValue.Assign(mFileData->mFirstFilePath); + return; + } + + if (mFileData->mFilesOrDirectories.IsEmpty()) { + aValue.Truncate(); + return; + } + + nsAutoString file; + GetDOMFileOrDirectoryName(mFileData->mFilesOrDirectories[0], file); + if (file.IsEmpty()) { + aValue.Truncate(); + return; + } + + aValue.AssignLiteral("C:\\fakepath\\"); + aValue.Append(file); +} + +void HTMLInputElement::GetNonFileValueInternal(nsAString& aValue) const { + switch (GetValueMode()) { + case VALUE_MODE_VALUE: + if (IsSingleLineTextControl(false)) { + if (mInputData.mState) { + mInputData.mState->GetValue(aValue, true, /* aForDisplay = */ false); + } else { + // Value hasn't been set yet. + aValue.Truncate(); + } + } else if (!aValue.Assign(mInputData.mValue, fallible)) { + aValue.Truncate(); + } + return; + + case VALUE_MODE_FILENAME: + MOZ_ASSERT_UNREACHABLE("Someone screwed up here"); + // We'll just return empty string if someone does screw up. + aValue.Truncate(); + return; + + case VALUE_MODE_DEFAULT: + // Treat defaultValue as value. + GetAttr(nsGkAtoms::value, aValue); + return; + + case VALUE_MODE_DEFAULT_ON: + // Treat default value as value and returns "on" if no value. + if (!GetAttr(nsGkAtoms::value, aValue)) { + aValue.AssignLiteral("on"); + } + return; + } +} + +void HTMLInputElement::ClearFiles(bool aSetValueChanged) { + nsTArray<OwningFileOrDirectory> data; + SetFilesOrDirectories(data, aSetValueChanged); +} + +int32_t HTMLInputElement::MonthsSinceJan1970(uint32_t aYear, + uint32_t aMonth) const { + return (aYear - 1970) * 12 + aMonth - 1; +} + +/* static */ +Decimal HTMLInputElement::StringToDecimal(const nsAString& aValue) { + if (!IsAscii(aValue)) { + return Decimal::nan(); + } + NS_LossyConvertUTF16toASCII asciiString(aValue); + std::string stdString(asciiString.get(), asciiString.Length()); + auto decimal = Decimal::fromString(stdString); + if (!decimal.isFinite()) { + return Decimal::nan(); + } + // Numbers are considered finite IEEE 754 Double-precision floating point + // values, but decimal supports a bigger range. + static const Decimal maxDouble = + Decimal::fromDouble(std::numeric_limits<double>::max()); + if (decimal < -maxDouble || decimal > maxDouble) { + return Decimal::nan(); + } + return decimal; +} + +Decimal HTMLInputElement::GetValueAsDecimal() const { + nsAutoString stringValue; + GetNonFileValueInternal(stringValue); + Decimal result = mInputType->ConvertStringToNumber(stringValue).mResult; + return result.isFinite() ? result : Decimal::nan(); +} + +void HTMLInputElement::SetValue(const nsAString& aValue, CallerType aCallerType, + ErrorResult& aRv) { + // check security. Note that setting the value to the empty string is always + // OK and gives pages a way to clear a file input if necessary. + if (mType == FormControlType::InputFile) { + if (!aValue.IsEmpty()) { + if (aCallerType != CallerType::System) { + // setting the value of a "FILE" input widget requires + // chrome privilege + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + Sequence<nsString> list; + if (!list.AppendElement(aValue, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + MozSetFileNameArray(list, aRv); + return; + } + ClearFiles(true); + } else { + if (MayFireChangeOnBlur()) { + // If the value has been set by a script, we basically want to keep the + // current change event state. If the element is ready to fire a change + // event, we should keep it that way. Otherwise, we should make sure the + // element will not fire any event because of the script interaction. + // + // NOTE: this is currently quite expensive work (too much string + // manipulation). We should probably optimize that. + nsAutoString currentValue; + GetNonFileValueInternal(currentValue); + + nsresult rv = SetValueInternal( + aValue, ¤tValue, + {ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged, + ValueSetterOption::MoveCursorToEndIfValueChanged}); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + if (mFocusedValue.Equals(currentValue)) { + GetValue(mFocusedValue, aCallerType); + } + } else { + nsresult rv = SetValueInternal( + aValue, + {ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged, + ValueSetterOption::MoveCursorToEndIfValueChanged}); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + } + } +} + +HTMLDataListElement* HTMLInputElement::GetList() const { + nsAutoString dataListId; + GetAttr(nsGkAtoms::list_, dataListId); + if (dataListId.IsEmpty()) { + return nullptr; + } + + DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); + if (!docOrShadow) { + return nullptr; + } + + return HTMLDataListElement::FromNodeOrNull( + docOrShadow->GetElementById(dataListId)); +} + +void HTMLInputElement::SetValue(Decimal aValue, CallerType aCallerType) { + MOZ_ASSERT(!aValue.isInfinity(), "aValue must not be Infinity!"); + + if (aValue.isNaN()) { + SetValue(u""_ns, aCallerType, IgnoreErrors()); + return; + } + + nsAutoString value; + mInputType->ConvertNumberToString(aValue, value); + SetValue(value, aCallerType, IgnoreErrors()); +} + +void HTMLInputElement::GetValueAsDate(JSContext* aCx, + JS::MutableHandle<JSObject*> aObject, + ErrorResult& aRv) { + aObject.set(nullptr); + if (!IsDateTimeInputType(mType)) { + return; + } + + Maybe<JS::ClippedTime> time; + + switch (mType) { + case FormControlType::InputDate: { + uint32_t year, month, day; + nsAutoString value; + GetNonFileValueInternal(value); + if (!ParseDate(value, &year, &month, &day)) { + return; + } + + time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, day))); + break; + } + case FormControlType::InputTime: { + uint32_t millisecond; + nsAutoString value; + GetNonFileValueInternal(value); + if (!ParseTime(value, &millisecond)) { + return; + } + + time.emplace(JS::TimeClip(millisecond)); + MOZ_ASSERT(time->toDouble() == millisecond, + "HTML times are restricted to the day after the epoch and " + "never clip"); + break; + } + case FormControlType::InputMonth: { + uint32_t year, month; + nsAutoString value; + GetNonFileValueInternal(value); + if (!ParseMonth(value, &year, &month)) { + return; + } + + time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, 1))); + break; + } + case FormControlType::InputWeek: { + uint32_t year, week; + nsAutoString value; + GetNonFileValueInternal(value); + if (!ParseWeek(value, &year, &week)) { + return; + } + + double days = DaysSinceEpochFromWeek(year, week); + time.emplace(JS::TimeClip(days * kMsPerDay)); + + break; + } + case FormControlType::InputDatetimeLocal: { + uint32_t year, month, day, timeInMs; + nsAutoString value; + GetNonFileValueInternal(value); + if (!ParseDateTimeLocal(value, &year, &month, &day, &timeInMs)) { + return; + } + + time.emplace(JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs))); + break; + } + default: + break; + } + + if (time) { + aObject.set(JS::NewDateObject(aCx, *time)); + if (!aObject) { + aRv.NoteJSContextException(aCx); + } + return; + } + + MOZ_ASSERT(false, "Unrecognized input type"); + aRv.Throw(NS_ERROR_UNEXPECTED); +} + +void HTMLInputElement::SetValueAsDate(JSContext* aCx, + JS::Handle<JSObject*> aObj, + ErrorResult& aRv) { + if (!IsDateTimeInputType(mType)) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (aObj) { + bool isDate; + if (!JS::ObjectIsDate(aCx, aObj, &isDate)) { + aRv.NoteJSContextException(aCx); + return; + } + if (!isDate) { + aRv.ThrowTypeError("Value being assigned is not a date."); + return; + } + } + + double milliseconds; + if (aObj) { + if (!js::DateGetMsecSinceEpoch(aCx, aObj, &milliseconds)) { + aRv.NoteJSContextException(aCx); + return; + } + } else { + milliseconds = UnspecifiedNaN<double>(); + } + + // At this point we know we're not a file input, so we can just pass "not + // system" as the caller type, since the caller type only matters in the file + // input case. + if (std::isnan(milliseconds)) { + SetValue(u""_ns, CallerType::NonSystem, aRv); + return; + } + + if (mType != FormControlType::InputMonth) { + SetValue(Decimal::fromDouble(milliseconds), CallerType::NonSystem); + return; + } + + // type=month expects the value to be number of months. + double year = JS::YearFromTime(milliseconds); + double month = JS::MonthFromTime(milliseconds); + + if (std::isnan(year) || std::isnan(month)) { + SetValue(u""_ns, CallerType::NonSystem, aRv); + return; + } + + int32_t months = MonthsSinceJan1970(year, month + 1); + SetValue(Decimal(int32_t(months)), CallerType::NonSystem); +} + +void HTMLInputElement::SetValueAsNumber(double aValueAsNumber, + ErrorResult& aRv) { + // TODO: return TypeError when HTMLInputElement is converted to WebIDL, see + // bug 825197. + if (std::isinf(aValueAsNumber)) { + aRv.Throw(NS_ERROR_INVALID_ARG); + return; + } + + if (!DoesValueAsNumberApply()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // At this point we know we're not a file input, so we can just pass "not + // system" as the caller type, since the caller type only matters in the file + // input case. + SetValue(Decimal::fromDouble(aValueAsNumber), CallerType::NonSystem); +} + +Decimal HTMLInputElement::GetMinimum() const { + MOZ_ASSERT( + DoesValueAsNumberApply(), + "GetMinimum() should only be used for types that allow .valueAsNumber"); + + // Only type=range has a default minimum + Decimal defaultMinimum = + mType == FormControlType::InputRange ? Decimal(0) : Decimal::nan(); + + if (!HasAttr(nsGkAtoms::min)) { + return defaultMinimum; + } + + nsAutoString minStr; + GetAttr(nsGkAtoms::min, minStr); + + Decimal min = mInputType->ConvertStringToNumber(minStr).mResult; + return min.isFinite() ? min : defaultMinimum; +} + +Decimal HTMLInputElement::GetMaximum() const { + MOZ_ASSERT( + DoesValueAsNumberApply(), + "GetMaximum() should only be used for types that allow .valueAsNumber"); + + // Only type=range has a default maximum + Decimal defaultMaximum = + mType == FormControlType::InputRange ? Decimal(100) : Decimal::nan(); + + if (!HasAttr(nsGkAtoms::max)) { + return defaultMaximum; + } + + nsAutoString maxStr; + GetAttr(nsGkAtoms::max, maxStr); + + Decimal max = mInputType->ConvertStringToNumber(maxStr).mResult; + return max.isFinite() ? max : defaultMaximum; +} + +Decimal HTMLInputElement::GetStepBase() const { + MOZ_ASSERT(IsDateTimeInputType(mType) || + mType == FormControlType::InputNumber || + mType == FormControlType::InputRange, + "Check that kDefaultStepBase is correct for this new type"); + // Do NOT use GetMinimum here - the spec says to use "the min content + // attribute", not "the minimum". + nsAutoString minStr; + if (GetAttr(nsGkAtoms::min, minStr)) { + Decimal min = mInputType->ConvertStringToNumber(minStr).mResult; + if (min.isFinite()) { + return min; + } + } + + // If @min is not a double, we should use @value. + nsAutoString valueStr; + if (GetAttr(nsGkAtoms::value, valueStr)) { + Decimal value = mInputType->ConvertStringToNumber(valueStr).mResult; + if (value.isFinite()) { + return value; + } + } + + if (mType == FormControlType::InputWeek) { + return kDefaultStepBaseWeek; + } + + return kDefaultStepBase; +} + +nsresult HTMLInputElement::GetValueIfStepped(int32_t aStep, + StepCallerType aCallerType, + Decimal* aNextStep) { + if (!DoStepDownStepUpApply()) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + Decimal stepBase = GetStepBase(); + Decimal step = GetStep(); + if (step == kStepAny) { + if (aCallerType != CALLED_FOR_USER_EVENT) { + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + // Allow the spin buttons and up/down arrow keys to do something sensible: + step = GetDefaultStep(); + } + + Decimal minimum = GetMinimum(); + Decimal maximum = GetMaximum(); + + if (!maximum.isNaN()) { + // "max - (max - stepBase) % step" is the nearest valid value to max. + maximum = maximum - NS_floorModulo(maximum - stepBase, step); + if (!minimum.isNaN()) { + if (minimum > maximum) { + // Either the minimum was greater than the maximum prior to our + // adjustment to align maximum on a step, or else (if we adjusted + // maximum) there is no valid step between minimum and the unadjusted + // maximum. + return NS_OK; + } + } + } + + Decimal value = GetValueAsDecimal(); + bool valueWasNaN = false; + if (value.isNaN()) { + value = Decimal(0); + valueWasNaN = true; + } + Decimal valueBeforeStepping = value; + + Decimal deltaFromStep = NS_floorModulo(value - stepBase, step); + + if (deltaFromStep != Decimal(0)) { + if (aStep > 0) { + value += step - deltaFromStep; // partial step + value += step * Decimal(aStep - 1); // then remaining steps + } else if (aStep < 0) { + value -= deltaFromStep; // partial step + value += step * Decimal(aStep + 1); // then remaining steps + } + } else { + value += step * Decimal(aStep); + } + + if (value < minimum) { + value = minimum; + deltaFromStep = NS_floorModulo(value - stepBase, step); + if (deltaFromStep != Decimal(0)) { + value += step - deltaFromStep; + } + } + if (value > maximum) { + value = maximum; + deltaFromStep = NS_floorModulo(value - stepBase, step); + if (deltaFromStep != Decimal(0)) { + value -= deltaFromStep; + } + } + + if (!valueWasNaN && // value="", resulting in us using "0" + ((aStep > 0 && value < valueBeforeStepping) || + (aStep < 0 && value > valueBeforeStepping))) { + // We don't want step-up to effectively step down, or step-down to + // effectively step up, so return; + return NS_OK; + } + + *aNextStep = value; + return NS_OK; +} + +nsresult HTMLInputElement::ApplyStep(int32_t aStep) { + Decimal nextStep = Decimal::nan(); // unchanged if value will not change + + nsresult rv = GetValueIfStepped(aStep, CALLED_FOR_SCRIPT, &nextStep); + + if (NS_SUCCEEDED(rv) && nextStep.isFinite()) { + // We know we're not a file input, so the caller type does not matter; just + // pass "not system" to be safe. + SetValue(nextStep, CallerType::NonSystem); + } + + return rv; +} + +bool HTMLInputElement::IsDateTimeInputType(FormControlType aType) { + switch (aType) { + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputMonth: + case FormControlType::InputWeek: + case FormControlType::InputDatetimeLocal: + return true; + default: + return false; + } +} + +void HTMLInputElement::MozGetFileNameArray(nsTArray<nsString>& aArray, + ErrorResult& aRv) { + if (NS_WARN_IF(mType != FormControlType::InputFile)) { + return; + } + + const nsTArray<OwningFileOrDirectory>& filesOrDirs = + GetFilesOrDirectoriesInternal(); + for (uint32_t i = 0; i < filesOrDirs.Length(); i++) { + nsAutoString str; + GetDOMFileOrDirectoryPath(filesOrDirs[i], str, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + aArray.AppendElement(str); + } +} + +void HTMLInputElement::MozSetFileArray( + const Sequence<OwningNonNull<File>>& aFiles) { + if (NS_WARN_IF(mType != FormControlType::InputFile)) { + return; + } + + nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject(); + MOZ_ASSERT(global); + if (!global) { + return; + } + + nsTArray<OwningFileOrDirectory> files; + for (uint32_t i = 0; i < aFiles.Length(); ++i) { + RefPtr<File> file = File::Create(global, aFiles[i].get()->Impl()); + if (NS_WARN_IF(!file)) { + return; + } + + OwningFileOrDirectory* element = files.AppendElement(); + element->SetAsFile() = file; + } + + SetFilesOrDirectories(files, true); +} + +void HTMLInputElement::MozSetFileNameArray(const Sequence<nsString>& aFileNames, + ErrorResult& aRv) { + if (NS_WARN_IF(mType != FormControlType::InputFile)) { + return; + } + + if (XRE_IsContentProcess()) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return; + } + + nsTArray<OwningFileOrDirectory> files; + for (uint32_t i = 0; i < aFileNames.Length(); ++i) { + nsCOMPtr<nsIFile> file; + + if (StringBeginsWith(aFileNames[i], u"file:"_ns, + nsASCIICaseInsensitiveStringComparator)) { + // Converts the URL string into the corresponding nsIFile if possible + // A local file will be created if the URL string begins with file:// + NS_GetFileFromURLSpec(NS_ConvertUTF16toUTF8(aFileNames[i]), + getter_AddRefs(file)); + } + + if (!file) { + // this is no "file://", try as local file + NS_NewLocalFile(aFileNames[i], false, getter_AddRefs(file)); + } + + if (!file) { + continue; // Not much we can do if the file doesn't exist + } + + nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject(); + if (!global) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<File> domFile = File::CreateFromFile(global, file); + if (NS_WARN_IF(!domFile)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + OwningFileOrDirectory* element = files.AppendElement(); + element->SetAsFile() = domFile; + } + + SetFilesOrDirectories(files, true); +} + +void HTMLInputElement::MozSetDirectory(const nsAString& aDirectoryPath, + ErrorResult& aRv) { + if (NS_WARN_IF(mType != FormControlType::InputFile)) { + return; + } + + nsCOMPtr<nsIFile> file; + aRv = NS_NewLocalFile(aDirectoryPath, true, getter_AddRefs(file)); + if (NS_WARN_IF(aRv.Failed())) { + return; + } + + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (NS_WARN_IF(!window)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<Directory> directory = Directory::Create(window->AsGlobal(), file); + MOZ_ASSERT(directory); + + nsTArray<OwningFileOrDirectory> array; + OwningFileOrDirectory* element = array.AppendElement(); + element->SetAsDirectory() = directory; + + SetFilesOrDirectories(array, true); +} + +void HTMLInputElement::GetDateTimeInputBoxValue(DateTimeValue& aValue) { + if (NS_WARN_IF(!IsDateTimeInputType(mType)) || !mDateTimeInputBoxValue) { + return; + } + + aValue = *mDateTimeInputBoxValue; +} + +Element* HTMLInputElement::GetDateTimeBoxElement() { + if (!GetShadowRoot()) { + return nullptr; + } + + // The datetimebox <div> is the only child of the UA Widget Shadow Root + // if it is present. + MOZ_ASSERT(GetShadowRoot()->IsUAWidget()); + MOZ_ASSERT(1 >= GetShadowRoot()->GetChildCount()); + if (nsIContent* inputAreaContent = GetShadowRoot()->GetFirstChild()) { + return inputAreaContent->AsElement(); + } + + return nullptr; +} + +void HTMLInputElement::OpenDateTimePicker(const DateTimeValue& aInitialValue) { + if (NS_WARN_IF(!IsDateTimeInputType(mType))) { + return; + } + + mDateTimeInputBoxValue = MakeUnique<DateTimeValue>(aInitialValue); + nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this), + u"MozOpenDateTimePicker"_ns, + CanBubble::eYes, Cancelable::eYes); +} + +void HTMLInputElement::UpdateDateTimePicker(const DateTimeValue& aValue) { + if (NS_WARN_IF(!IsDateTimeInputType(mType))) { + return; + } + + mDateTimeInputBoxValue = MakeUnique<DateTimeValue>(aValue); + nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this), + u"MozUpdateDateTimePicker"_ns, + CanBubble::eYes, Cancelable::eYes); +} + +void HTMLInputElement::CloseDateTimePicker() { + if (NS_WARN_IF(!IsDateTimeInputType(mType))) { + return; + } + + nsContentUtils::DispatchChromeEvent(OwnerDoc(), static_cast<Element*>(this), + u"MozCloseDateTimePicker"_ns, + CanBubble::eYes, Cancelable::eYes); +} + +void HTMLInputElement::SetFocusState(bool aIsFocused) { + if (NS_WARN_IF(!IsDateTimeInputType(mType))) { + return; + } + SetStates(ElementState::FOCUS | ElementState::FOCUSRING, aIsFocused); +} + +void HTMLInputElement::UpdateValidityState() { + if (NS_WARN_IF(!IsDateTimeInputType(mType))) { + return; + } + + // For now, datetime input box call this function only when the value may + // become valid/invalid. For other validity states, they will be updated when + // .value is actually changed. + UpdateBadInputValidityState(); + UpdateValidityElementStates(true); +} + +bool HTMLInputElement::MozIsTextField(bool aExcludePassword) { + // TODO: temporary until bug 888320 is fixed. + // + // FIXME: Historically we never returned true for `number`, we should consider + // changing that now that it is similar to other inputs. + if (IsDateTimeInputType(mType) || mType == FormControlType::InputNumber) { + return false; + } + + return IsSingleLineTextControl(aExcludePassword); +} + +void HTMLInputElement::SetUserInput(const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal) { + AutoHandlingUserInputStatePusher inputStatePusher(true); + + if (mType == FormControlType::InputFile && + !aSubjectPrincipal.IsSystemPrincipal()) { + return; + } + + if (mType == FormControlType::InputFile) { + Sequence<nsString> list; + if (!list.AppendElement(aValue, fallible)) { + return; + } + + MozSetFileNameArray(list, IgnoreErrors()); + return; + } + + bool isInputEventDispatchedByTextControlState = + GetValueMode() == VALUE_MODE_VALUE && IsSingleLineTextControl(false); + + nsresult rv = SetValueInternal( + aValue, + {ValueSetterOption::BySetUserInputAPI, ValueSetterOption::SetValueChanged, + ValueSetterOption::MoveCursorToEndIfValueChanged}); + NS_ENSURE_SUCCESS_VOID(rv); + + if (!isInputEventDispatchedByTextControlState) { + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + } + + // If this element is not currently focused, it won't receive a change event + // for this update through the normal channels. So fire a change event + // immediately, instead. + if (CreatesDateTimeWidget() || !ShouldBlur(this)) { + FireChangeEventIfNeeded(); + } +} + +nsIEditor* HTMLInputElement::GetEditorForBindings() { + if (!GetPrimaryFrame()) { + // Ensure we construct frames (and thus an editor) if needed. + GetPrimaryFrame(FlushType::Frames); + } + return GetTextEditorFromState(); +} + +bool HTMLInputElement::HasEditor() const { + return !!GetTextEditorWithoutCreation(); +} + +TextEditor* HTMLInputElement::GetTextEditorFromState() { + TextControlState* state = GetEditorState(); + if (state) { + return state->GetTextEditor(); + } + return nullptr; +} + +TextEditor* HTMLInputElement::GetTextEditor() { + return GetTextEditorFromState(); +} + +TextEditor* HTMLInputElement::GetTextEditorWithoutCreation() const { + TextControlState* state = GetEditorState(); + if (!state) { + return nullptr; + } + return state->GetTextEditorWithoutCreation(); +} + +nsISelectionController* HTMLInputElement::GetSelectionController() { + TextControlState* state = GetEditorState(); + if (state) { + return state->GetSelectionController(); + } + return nullptr; +} + +nsFrameSelection* HTMLInputElement::GetConstFrameSelection() { + TextControlState* state = GetEditorState(); + if (state) { + return state->GetConstFrameSelection(); + } + return nullptr; +} + +nsresult HTMLInputElement::BindToFrame(nsTextControlFrame* aFrame) { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript()); + TextControlState* state = GetEditorState(); + if (state) { + return state->BindToFrame(aFrame); + } + return NS_ERROR_FAILURE; +} + +void HTMLInputElement::UnbindFromFrame(nsTextControlFrame* aFrame) { + TextControlState* state = GetEditorState(); + if (state && aFrame) { + state->UnbindFromFrame(aFrame); + } +} + +nsresult HTMLInputElement::CreateEditor() { + TextControlState* state = GetEditorState(); + if (state) { + return state->PrepareEditor(); + } + return NS_ERROR_FAILURE; +} + +void HTMLInputElement::SetPreviewValue(const nsAString& aValue) { + TextControlState* state = GetEditorState(); + if (state) { + state->SetPreviewText(aValue, true); + } +} + +void HTMLInputElement::GetPreviewValue(nsAString& aValue) { + TextControlState* state = GetEditorState(); + if (state) { + state->GetPreviewText(aValue); + } +} + +void HTMLInputElement::EnablePreview() { + if (mIsPreviewEnabled) { + return; + } + + mIsPreviewEnabled = true; + // Reconstruct the frame to append an anonymous preview node + nsLayoutUtils::PostRestyleEvent(this, RestyleHint{0}, + nsChangeHint_ReconstructFrame); +} + +bool HTMLInputElement::IsPreviewEnabled() { return mIsPreviewEnabled; } + +void HTMLInputElement::GetDisplayFileName(nsAString& aValue) const { + MOZ_ASSERT(mFileData); + + if (OwnerDoc()->IsStaticDocument()) { + aValue = mFileData->mStaticDocFileList; + return; + } + + if (mFileData->mFilesOrDirectories.Length() == 1) { + GetDOMFileOrDirectoryName(mFileData->mFilesOrDirectories[0], aValue); + return; + } + + nsAutoString value; + + if (mFileData->mFilesOrDirectories.IsEmpty()) { + if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() && + HasAttr(nsGkAtoms::webkitdirectory)) { + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "NoDirSelected", OwnerDoc(), + value); + } else if (HasAttr(nsGkAtoms::multiple)) { + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "NoFilesSelected", OwnerDoc(), + value); + } else { + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "NoFileSelected", OwnerDoc(), + value); + } + } else { + nsString count; + count.AppendInt(int(mFileData->mFilesOrDirectories.Length())); + + nsContentUtils::FormatMaybeLocalizedString( + value, nsContentUtils::eFORMS_PROPERTIES, "XFilesSelected", OwnerDoc(), + count); + } + + aValue = value; +} + +const nsTArray<OwningFileOrDirectory>& +HTMLInputElement::GetFilesOrDirectoriesInternal() const { + return mFileData->mFilesOrDirectories; +} + +void HTMLInputElement::SetFilesOrDirectories( + const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories, + bool aSetValueChanged) { + if (NS_WARN_IF(mType != FormControlType::InputFile)) { + return; + } + + MOZ_ASSERT(mFileData); + + mFileData->ClearGetFilesHelpers(); + + if (StaticPrefs::dom_webkitBlink_filesystem_enabled()) { + HTMLInputElement_Binding::ClearCachedWebkitEntriesValue(this); + mFileData->mEntries.Clear(); + } + + mFileData->mFilesOrDirectories.Clear(); + mFileData->mFilesOrDirectories.AppendElements(aFilesOrDirectories); + + AfterSetFilesOrDirectories(aSetValueChanged); +} + +void HTMLInputElement::SetFiles(FileList* aFiles, bool aSetValueChanged) { + MOZ_ASSERT(mFileData); + + mFileData->mFilesOrDirectories.Clear(); + mFileData->ClearGetFilesHelpers(); + + if (StaticPrefs::dom_webkitBlink_filesystem_enabled()) { + HTMLInputElement_Binding::ClearCachedWebkitEntriesValue(this); + mFileData->mEntries.Clear(); + } + + if (aFiles) { + uint32_t listLength = aFiles->Length(); + for (uint32_t i = 0; i < listLength; i++) { + OwningFileOrDirectory* element = + mFileData->mFilesOrDirectories.AppendElement(); + element->SetAsFile() = aFiles->Item(i); + } + } + + AfterSetFilesOrDirectories(aSetValueChanged); +} + +// This method is used for testing only. +void HTMLInputElement::MozSetDndFilesAndDirectories( + const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories) { + if (NS_WARN_IF(mType != FormControlType::InputFile)) { + return; + } + + SetFilesOrDirectories(aFilesOrDirectories, true); + + if (StaticPrefs::dom_webkitBlink_filesystem_enabled()) { + UpdateEntries(aFilesOrDirectories); + } + + RefPtr<DispatchChangeEventCallback> dispatchChangeEventCallback = + new DispatchChangeEventCallback(this); + + if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() && + HasAttr(nsGkAtoms::webkitdirectory)) { + ErrorResult rv; + GetFilesHelper* helper = + GetOrCreateGetFilesHelper(true /* recursionFlag */, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + return; + } + + helper->AddCallback(dispatchChangeEventCallback); + } else { + dispatchChangeEventCallback->DispatchEvents(); + } +} + +void HTMLInputElement::AfterSetFilesOrDirectories(bool aSetValueChanged) { + // No need to flush here, if there's no frame at this point we + // don't need to force creation of one just to tell it about this + // new value. We just want the display to update as needed. + nsIFormControlFrame* formControlFrame = GetFormControlFrame(false); + if (formControlFrame) { + nsAutoString readableValue; + GetDisplayFileName(readableValue); + formControlFrame->SetFormProperty(nsGkAtoms::value, readableValue); + } + + // Grab the full path here for any chrome callers who access our .value via a + // CPOW. This path won't be called from a CPOW meaning the potential sync IPC + // call under GetMozFullPath won't be rejected for not being urgent. + if (mFileData->mFilesOrDirectories.IsEmpty()) { + mFileData->mFirstFilePath.Truncate(); + } else { + ErrorResult rv; + GetDOMFileOrDirectoryPath(mFileData->mFilesOrDirectories[0], + mFileData->mFirstFilePath, rv); + if (NS_WARN_IF(rv.Failed())) { + rv.SuppressException(); + } + } + + // Null out |mFileData->mFileList| to return a new file list when asked for. + // Don't clear it since the file list might come from the user via SetFiles. + if (mFileData->mFileList) { + mFileData->mFileList = nullptr; + } + + if (aSetValueChanged) { + SetValueChanged(true); + } + + UpdateAllValidityStates(true); +} + +void HTMLInputElement::FireChangeEventIfNeeded() { + if (!MayFireChangeOnBlur()) { + return; + } + + // We're not exposing the GetValue return value anywhere here, so it's safe to + // claim to be a system caller. + nsAutoString value; + GetValue(value, CallerType::System); + + // NOTE(emilio): Per spec we should not set this if we don't fire the change + // event, but that seems like a bug. Using mValueChanged seems reasonable to + // keep the expected behavior while + // https://github.com/whatwg/html/issues/10013 is resolved. + if (mValueChanged) { + SetUserInteracted(true); + } + if (mFocusedValue.Equals(value)) { + return; + } + // Dispatch the change event. + mFocusedValue = value; + nsContentUtils::DispatchTrustedEvent( + OwnerDoc(), static_cast<nsIContent*>(this), u"change"_ns, CanBubble::eYes, + Cancelable::eNo); +} + +FileList* HTMLInputElement::GetFiles() { + if (mType != FormControlType::InputFile) { + return nullptr; + } + + if (!mFileData->mFileList) { + mFileData->mFileList = new FileList(static_cast<nsIContent*>(this)); + for (const OwningFileOrDirectory& item : GetFilesOrDirectoriesInternal()) { + if (item.IsFile()) { + mFileData->mFileList->Append(item.GetAsFile()); + } + } + } + + return mFileData->mFileList; +} + +void HTMLInputElement::SetFiles(FileList* aFiles) { + if (mType != FormControlType::InputFile || !aFiles) { + return; + } + + // Update |mFileData->mFilesOrDirectories| + SetFiles(aFiles, true); + + MOZ_ASSERT(!mFileData->mFileList, "Should've cleared the existing file list"); + + // Update |mFileData->mFileList| without copy + mFileData->mFileList = aFiles; +} + +/* static */ +void HTMLInputElement::HandleNumberControlSpin(void* aData) { + RefPtr<HTMLInputElement> input = static_cast<HTMLInputElement*>(aData); + + NS_ASSERTION(input->mNumberControlSpinnerIsSpinning, + "Should have called nsRepeatService::Stop()"); + + nsNumberControlFrame* numberControlFrame = + do_QueryFrame(input->GetPrimaryFrame()); + if (input->mType != FormControlType::InputNumber || !numberControlFrame) { + // Type has changed (and possibly our frame type hasn't been updated yet) + // or else we've lost our frame. Either way, stop the timer and don't do + // anything else. + input->StopNumberControlSpinnerSpin(); + } else { + input->StepNumberControlForUserEvent( + input->mNumberControlSpinnerSpinsUp ? 1 : -1); + } +} + +nsresult HTMLInputElement::SetValueInternal( + const nsAString& aValue, const nsAString* aOldValue, + const ValueSetterOptions& aOptions) { + MOZ_ASSERT(GetValueMode() != VALUE_MODE_FILENAME, + "Don't call SetValueInternal for file inputs"); + + // We want to remember if the SetValueInternal() call is being made for a XUL + // element. We do that by looking at the parent node here, and if that node + // is a XUL node, we consider our control a XUL control. XUL controls preserve + // edit history across value setters. + // + // TODO(emilio): Rather than doing this maybe add an attribute instead and + // read it only on chrome docs or something? That'd allow front-end code to + // move away from xul without weird side-effects. + const bool forcePreserveUndoHistory = mParent && mParent->IsXULElement(); + + switch (GetValueMode()) { + case VALUE_MODE_VALUE: { + // At the moment, only single line text control have to sanitize their + // value Because we have to create a new string for that, we should + // prevent doing it if it's useless. + nsAutoString value(aValue); + + if (mDoneCreating && + !(mType == FormControlType::InputNumber && + aOptions.contains(ValueSetterOption::BySetUserInputAPI))) { + // When the value of a number input is set by a script, we need to make + // sure the value is a valid floating-point number. + // https://html.spec.whatwg.org/#valid-floating-point-number + // When it's set by a user, however, we need to be more permissive, so + // we don't sanitize its value here. See bug 1839572. + SanitizeValue(value, SanitizationKind::ForValueSetter); + } + // else DoneCreatingElement calls us again once mDoneCreating is true + + const bool setValueChanged = + aOptions.contains(ValueSetterOption::SetValueChanged); + if (setValueChanged) { + SetValueChanged(true); + } + + if (IsSingleLineTextControl(false)) { + // Note that if aOptions includes + // ValueSetterOption::BySetUserInputAPI, "input" event is automatically + // dispatched by TextControlState::SetValue(). If you'd change condition + // of calling this method, you need to maintain SetUserInput() too. FYI: + // After calling SetValue(), the input type might have been + // modified so that mInputData may not store TextControlState. + EnsureEditorState(); + if (!mInputData.mState->SetValue( + value, aOldValue, + forcePreserveUndoHistory + ? aOptions + ValueSetterOption::PreserveUndoHistory + : aOptions)) { + return NS_ERROR_OUT_OF_MEMORY; + } + // If the caller won't dispatch "input" event via + // nsContentUtils::DispatchInputEvent(), we need to modify + // validationMessage value here. + // + // FIXME(emilio): ValueSetterOption::ByInternalAPI is not supposed to + // change state, but maybe we could run this too? + if (aOptions.contains(ValueSetterOption::ByContentAPI)) { + MaybeUpdateAllValidityStates(!mDoneCreating); + } + } else { + free(mInputData.mValue); + mInputData.mValue = ToNewUnicode(value); + if (setValueChanged) { + SetValueChanged(true); + } + if (mType == FormControlType::InputRange) { + nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame()); + if (frame) { + frame->UpdateForValueChange(); + } + } else if (CreatesDateTimeWidget() && + !aOptions.contains(ValueSetterOption::BySetUserInputAPI)) { + if (Element* dateTimeBoxElement = GetDateTimeBoxElement()) { + AsyncEventDispatcher::RunDOMEventWhenSafe( + *dateTimeBoxElement, u"MozDateTimeValueChanged"_ns, + CanBubble::eNo, ChromeOnlyDispatch::eNo); + } + } + if (mDoneCreating) { + OnValueChanged(ValueChangeKind::Internal, value.IsEmpty(), &value); + } + // else DoneCreatingElement calls us again once mDoneCreating is true + } + + if (mType == FormControlType::InputColor) { + // Update color frame, to reflect color changes + nsColorControlFrame* colorControlFrame = + do_QueryFrame(GetPrimaryFrame()); + if (colorControlFrame) { + colorControlFrame->UpdateColor(); + } + } + return NS_OK; + } + + case VALUE_MODE_DEFAULT: + case VALUE_MODE_DEFAULT_ON: + // If the value of a hidden input was changed, we mark it changed so that + // we will know we need to save / restore the value. Yes, we are + // overloading the meaning of ValueChanged just a teensy bit to save a + // measly byte of storage space in HTMLInputElement. Yes, you are free to + // make a new flag, NEED_TO_SAVE_VALUE, at such time as mBitField becomes + // a 16-bit value. + if (mType == FormControlType::InputHidden) { + SetValueChanged(true); + } + + // Make sure to keep track of the last value change not being interactive, + // just in case this used to be another kind of editable input before. + // Note that a checked change _could_ really be interactive, but we don't + // keep track of that elsewhere so seems fine to just do this. + SetLastValueChangeWasInteractive(false); + + // Treat value == defaultValue for other input elements. + return nsGenericHTMLFormControlElementWithState::SetAttr( + kNameSpaceID_None, nsGkAtoms::value, aValue, true); + + case VALUE_MODE_FILENAME: + return NS_ERROR_UNEXPECTED; + } + + // This return statement is required for some compilers. + return NS_OK; +} + +void HTMLInputElement::SetValueChanged(bool aValueChanged) { + if (mValueChanged == aValueChanged) { + return; + } + mValueChanged = aValueChanged; + UpdateTooLongValidityState(); + UpdateTooShortValidityState(); + UpdateValidityElementStates(true); +} + +void HTMLInputElement::SetLastValueChangeWasInteractive(bool aWasInteractive) { + if (aWasInteractive == mLastValueChangeWasInteractive) { + return; + } + mLastValueChangeWasInteractive = aWasInteractive; + const bool wasValid = IsValid(); + UpdateTooLongValidityState(); + UpdateTooShortValidityState(); + if (wasValid != IsValid()) { + UpdateValidityElementStates(true); + } +} + +void HTMLInputElement::SetCheckedChanged(bool aCheckedChanged) { + DoSetCheckedChanged(aCheckedChanged, true); +} + +void HTMLInputElement::DoSetCheckedChanged(bool aCheckedChanged, bool aNotify) { + if (mType == FormControlType::InputRadio) { + if (mCheckedChanged != aCheckedChanged) { + nsCOMPtr<nsIRadioVisitor> visitor = + new nsRadioSetCheckedChangedVisitor(aCheckedChanged); + VisitGroup(visitor); + } + } else { + SetCheckedChangedInternal(aCheckedChanged); + } +} + +void HTMLInputElement::SetCheckedChangedInternal(bool aCheckedChanged) { + if (mCheckedChanged == aCheckedChanged) { + return; + } + mCheckedChanged = aCheckedChanged; + UpdateValidityElementStates(true); +} + +void HTMLInputElement::SetChecked(bool aChecked) { + DoSetChecked(aChecked, true, true); +} + +void HTMLInputElement::DoSetChecked(bool aChecked, bool aNotify, + bool aSetValueChanged) { + // If the user or JS attempts to set checked, whether it actually changes the + // value or not, we say the value was changed so that defaultValue don't + // affect it no more. + if (aSetValueChanged) { + DoSetCheckedChanged(true, aNotify); + } + + // Don't do anything if we're not changing whether it's checked (it would + // screw up state actually, especially when you are setting radio button to + // false) + if (mChecked == aChecked) { + return; + } + + // Set checked + if (mType != FormControlType::InputRadio) { + SetCheckedInternal(aChecked, aNotify); + return; + } + + // For radio button, we need to do some extra fun stuff + if (aChecked) { + RadioSetChecked(aNotify); + return; + } + + if (auto* container = GetCurrentRadioGroupContainer()) { + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + container->SetCurrentRadioButton(name, nullptr); + } + // SetCheckedInternal is going to ask all radios to update their + // validity state. We have to be sure the radio group container knows + // the currently selected radio. + SetCheckedInternal(false, aNotify); +} + +void HTMLInputElement::RadioSetChecked(bool aNotify) { + // Find the selected radio button so we can deselect it + HTMLInputElement* currentlySelected = GetSelectedRadioButton(); + + // Deselect the currently selected radio button + if (currentlySelected) { + // Pass true for the aNotify parameter since the currently selected + // button is already in the document. + currentlySelected->SetCheckedInternal(false, true); + } + + // Let the group know that we are now the One True Radio Button + if (auto* container = GetCurrentRadioGroupContainer()) { + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + container->SetCurrentRadioButton(name, this); + } + + // SetCheckedInternal is going to ask all radios to update their + // validity state. + SetCheckedInternal(true, aNotify); +} + +RadioGroupContainer* HTMLInputElement::GetCurrentRadioGroupContainer() const { + NS_ASSERTION( + mType == FormControlType::InputRadio, + "GetRadioGroupContainer should only be called when type='radio'"); + return mRadioGroupContainer; +} + +RadioGroupContainer* HTMLInputElement::FindTreeRadioGroupContainer() const { + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + + if (name.IsEmpty()) { + return nullptr; + } + if (mForm) { + return &mForm->OwnedRadioGroupContainer(); + } + if (IsInNativeAnonymousSubtree()) { + return nullptr; + } + if (Document* doc = GetUncomposedDoc()) { + return &doc->OwnedRadioGroupContainer(); + } + return &static_cast<FragmentOrElement*>(SubtreeRoot()) + ->OwnedRadioGroupContainer(); +} + +void HTMLInputElement::DisconnectRadioGroupContainer() { + mRadioGroupContainer = nullptr; +} + +HTMLInputElement* HTMLInputElement::GetSelectedRadioButton() const { + auto* container = GetCurrentRadioGroupContainer(); + if (!container) { + return nullptr; + } + + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + + return container->GetCurrentRadioButton(name); +} + +void HTMLInputElement::MaybeSubmitForm(nsPresContext* aPresContext) { + if (!mForm) { + // Nothing to do here. + return; + } + + RefPtr<PresShell> presShell = aPresContext->GetPresShell(); + if (!presShell) { + return; + } + + // Get the default submit element + if (RefPtr<nsGenericHTMLFormElement> submitContent = + mForm->GetDefaultSubmitElement()) { + WidgetMouseEvent event(true, eMouseClick, nullptr, WidgetMouseEvent::eReal); + nsEventStatus status = nsEventStatus_eIgnore; + presShell->HandleDOMEventWithTarget(submitContent, &event, &status); + } else if (!mForm->ImplicitSubmissionIsDisabled()) { + // If there's only one text control, just submit the form + // Hold strong ref across the event + RefPtr<dom::HTMLFormElement> form(mForm); + form->MaybeSubmit(nullptr); + } +} + +void HTMLInputElement::UpdateCheckedState(bool aNotify) { + SetStates(ElementState::CHECKED, IsRadioOrCheckbox() && mChecked, aNotify); +} + +void HTMLInputElement::UpdateIndeterminateState(bool aNotify) { + bool indeterminate = [&] { + if (mType == FormControlType::InputCheckbox) { + return mIndeterminate; + } + if (mType == FormControlType::InputRadio) { + return !mChecked && !GetSelectedRadioButton(); + } + return false; + }(); + SetStates(ElementState::INDETERMINATE, indeterminate, aNotify); +} + +void HTMLInputElement::SetCheckedInternal(bool aChecked, bool aNotify) { + // Set the value + mChecked = aChecked; + + if (IsRadioOrCheckbox()) { + SetStates(ElementState::CHECKED, aChecked, aNotify); + } + + // No need to update element state, since we're about to call + // UpdateState anyway. + UpdateAllValidityStatesButNotElementState(); + UpdateIndeterminateState(aNotify); + UpdateValidityElementStates(aNotify); + + // Notify all radios in the group that value has changed, this is to let + // radios to have the chance to update its states, e.g., :indeterminate. + if (mType == FormControlType::InputRadio) { + nsCOMPtr<nsIRadioVisitor> visitor = new nsRadioUpdateStateVisitor(this); + VisitGroup(visitor); + } +} + +#if !defined(ANDROID) && !defined(XP_MACOSX) +bool HTMLInputElement::IsNodeApzAwareInternal() const { + // Tell APZC we may handle mouse wheel event and do preventDefault when input + // type is number. + return mType == FormControlType::InputNumber || + mType == FormControlType::InputRange || + nsINode::IsNodeApzAwareInternal(); +} +#endif + +bool HTMLInputElement::IsInteractiveHTMLContent() const { + return mType != FormControlType::InputHidden || + nsGenericHTMLFormControlElementWithState::IsInteractiveHTMLContent(); +} + +void HTMLInputElement::AsyncEventRunning(AsyncEventDispatcher* aEvent) { + nsImageLoadingContent::AsyncEventRunning(aEvent); +} + +void HTMLInputElement::Select() { + if (!IsSingleLineTextControl(false)) { + return; + } + + TextControlState* state = GetEditorState(); + MOZ_ASSERT(state, "Single line text controls are expected to have a state"); + + if (FocusState() != FocusTristate::eUnfocusable) { + RefPtr<nsFrameSelection> fs = state->GetConstFrameSelection(); + if (fs && fs->MouseDownRecorded()) { + // This means that we're being called while the frame selection has a + // mouse down event recorded to adjust the caret during the mouse up + // event. We are probably called from the focus event handler. We should + // override the delayed caret data in this case to ensure that this + // select() call takes effect. + fs->SetDelayedCaretData(nullptr); + } + + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + fm->SetFocus(this, nsIFocusManager::FLAG_NOSCROLL); + + // A focus event handler may change the type attribute, which will destroy + // the previous state object. + state = GetEditorState(); + if (!state) { + return; + } + } + } + + // Directly call TextControlState::SetSelectionRange because + // HTMLInputElement::SetSelectionRange only applies to fewer types + state->SetSelectionRange(0, UINT32_MAX, Optional<nsAString>(), IgnoreErrors(), + TextControlState::ScrollAfterSelection::No); +} + +void HTMLInputElement::SelectAll(nsPresContext* aPresContext) { + nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); + + if (formControlFrame) { + formControlFrame->SetFormProperty(nsGkAtoms::select, u""_ns); + } +} + +bool HTMLInputElement::NeedToInitializeEditorForEvent( + EventChainPreVisitor& aVisitor) const { + // We only need to initialize the editor for single line input controls + // because they are lazily initialized. We don't need to initialize the + // control for certain types of events, because we know that those events are + // safe to be handled without the editor being initialized. These events + // include: mousein/move/out, overflow/underflow, DOM mutation, and void + // events. Void events are dispatched frequently by async keyboard scrolling + // to focused elements, so it's important to handle them to prevent excessive + // DOM mutations. + if (!IsSingleLineTextControl(false) || + aVisitor.mEvent->mClass == eMutationEventClass) { + return false; + } + + switch (aVisitor.mEvent->mMessage) { + case eVoidEvent: + case eMouseMove: + case eMouseEnterIntoWidget: + case eMouseExitFromWidget: + case eMouseOver: + case eMouseOut: + case eScrollPortUnderflow: + case eScrollPortOverflow: + return false; + default: + return true; + } +} + +bool HTMLInputElement::IsDisabledForEvents(WidgetEvent* aEvent) { + return IsElementDisabledForEvents(aEvent, GetPrimaryFrame()); +} + +bool HTMLInputElement::CheckActivationBehaviorPreconditions( + EventChainVisitor& aVisitor) const { + switch (mType) { + case FormControlType::InputColor: + case FormControlType::InputCheckbox: + case FormControlType::InputRadio: + case FormControlType::InputFile: + case FormControlType::InputSubmit: + case FormControlType::InputImage: + case FormControlType::InputReset: + case FormControlType::InputButton: { + // Track whether we're in the outermost Dispatch invocation that will + // cause activation of the input. That is, if we're a click event, or a + // DOMActivate that was dispatched directly, this will be set, but if + // we're a DOMActivate dispatched from click handling, it will not be set. + WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); + bool outerActivateEvent = + (mouseEvent && mouseEvent->IsLeftClickEvent()) || + (aVisitor.mEvent->mMessage == eLegacyDOMActivate && + !mInInternalActivate); + if (outerActivateEvent) { + aVisitor.mItemFlags |= NS_OUTER_ACTIVATE_EVENT; + } + return outerActivateEvent; + } + default: + return false; + } +} + +void HTMLInputElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + // Do not process any DOM events if the element is disabled + aVisitor.mCanHandle = false; + if (IsDisabledForEvents(aVisitor.mEvent)) { + return; + } + + // Initialize the editor if needed. + if (NeedToInitializeEditorForEvent(aVisitor)) { + nsITextControlFrame* textControlFrame = do_QueryFrame(GetPrimaryFrame()); + if (textControlFrame) textControlFrame->EnsureEditorInitialized(); + } + + if (CheckActivationBehaviorPreconditions(aVisitor)) { + aVisitor.mWantsActivationBehavior = true; + } + + // We must cache type because mType may change during JS event (bug 2369) + aVisitor.mItemFlags |= uint8_t(mType); + + if (aVisitor.mEvent->mMessage == eFocus && aVisitor.mEvent->IsTrusted() && + MayFireChangeOnBlur() && + // StartRangeThumbDrag already set mFocusedValue on 'mousedown' before + // we get the 'focus' event. + !mIsDraggingRange) { + GetValue(mFocusedValue, CallerType::System); + } + + // Fire onchange (if necessary), before we do the blur, bug 357684. + if (aVisitor.mEvent->mMessage == eBlur) { + // We set NS_PRE_HANDLE_BLUR_EVENT here and handle it in PreHandleEvent to + // prevent breaking event target chain creation. + aVisitor.mWantsPreHandleEvent = true; + aVisitor.mItemFlags |= NS_PRE_HANDLE_BLUR_EVENT; + } + + if (mType == FormControlType::InputRange && + (aVisitor.mEvent->mMessage == eFocus || + aVisitor.mEvent->mMessage == eBlur)) { + // Just as nsGenericHTMLFormControlElementWithState::GetEventTargetParent + // calls nsIFormControlFrame::SetFocus, we handle focus here. + nsIFrame* frame = GetPrimaryFrame(); + if (frame) { + frame->InvalidateFrameSubtree(); + } + } + + if (mType == FormControlType::InputNumber && aVisitor.mEvent->IsTrusted()) { + if (mNumberControlSpinnerIsSpinning) { + // If the timer is running the user has depressed the mouse on one of the + // spin buttons. If the mouse exits the button we either want to reverse + // the direction of spin if it has moved over the other button, or else + // we want to end the spin. We do this here (rather than in + // PostHandleEvent) because we don't want to let content preventDefault() + // the end of the spin. + if (aVisitor.mEvent->mMessage == eMouseMove) { + // Be aggressive about stopping the spin: + bool stopSpin = true; + nsNumberControlFrame* numberControlFrame = + do_QueryFrame(GetPrimaryFrame()); + if (numberControlFrame) { + bool oldNumberControlSpinTimerSpinsUpValue = + mNumberControlSpinnerSpinsUp; + switch (numberControlFrame->GetSpinButtonForPointerEvent( + aVisitor.mEvent->AsMouseEvent())) { + case nsNumberControlFrame::eSpinButtonUp: + mNumberControlSpinnerSpinsUp = true; + stopSpin = false; + break; + case nsNumberControlFrame::eSpinButtonDown: + mNumberControlSpinnerSpinsUp = false; + stopSpin = false; + break; + } + if (mNumberControlSpinnerSpinsUp != + oldNumberControlSpinTimerSpinsUpValue) { + nsNumberControlFrame* numberControlFrame = + do_QueryFrame(GetPrimaryFrame()); + if (numberControlFrame) { + numberControlFrame->SpinnerStateChanged(); + } + } + } + if (stopSpin) { + StopNumberControlSpinnerSpin(); + } + } else if (aVisitor.mEvent->mMessage == eMouseUp) { + StopNumberControlSpinnerSpin(); + } + } + } + + nsGenericHTMLFormControlElementWithState::GetEventTargetParent(aVisitor); + + // Stop the event if the related target's first non-native ancestor is the + // same as the original target's first non-native ancestor (we are moving + // inside of the same element). + // + // FIXME(emilio): Is this still needed now that we use Shadow DOM for this? + if (CreatesDateTimeWidget() && aVisitor.mEvent->IsTrusted() && + (aVisitor.mEvent->mMessage == eFocus || + aVisitor.mEvent->mMessage == eFocusIn || + aVisitor.mEvent->mMessage == eFocusOut || + aVisitor.mEvent->mMessage == eBlur)) { + nsIContent* originalTarget = nsIContent::FromEventTargetOrNull( + aVisitor.mEvent->AsFocusEvent()->mOriginalTarget); + nsIContent* relatedTarget = nsIContent::FromEventTargetOrNull( + aVisitor.mEvent->AsFocusEvent()->mRelatedTarget); + + if (originalTarget && relatedTarget && + originalTarget->FindFirstNonChromeOnlyAccessContent() == + relatedTarget->FindFirstNonChromeOnlyAccessContent()) { + aVisitor.mCanHandle = false; + } + } +} + +void HTMLInputElement::LegacyPreActivationBehavior( + EventChainVisitor& aVisitor) { + // + // Web pages expect the value of a radio button or checkbox to be set + // *before* onclick and DOMActivate fire, and they expect that if they set + // the value explicitly during onclick or DOMActivate it will not be toggled + // or any such nonsense. + // In order to support that (bug 57137 and 58460 are examples) we toggle + // the checked attribute *first*, and then fire onclick. If the user + // returns false, we reset the control to the old checked value. Otherwise, + // we dispatch DOMActivate. If DOMActivate is cancelled, we also reset + // the control to the old checked value. We need to keep track of whether + // we've already toggled the state from onclick since the user could + // explicitly dispatch DOMActivate on the element. + // + // These are compatibility hacks and are defined as legacy-pre-activation + // and legacy-canceled-activation behavior in HTML. + // + + // Assert mType didn't change after GetEventTargetParent + MOZ_ASSERT(NS_CONTROL_TYPE(aVisitor.mItemFlags) == uint8_t(mType)); + + bool originalCheckedValue = false; + mCheckedIsToggled = false; + + if (mType == FormControlType::InputCheckbox) { + if (mIndeterminate) { + // indeterminate is always set to FALSE when the checkbox is toggled + SetIndeterminateInternal(false, false); + aVisitor.mItemFlags |= NS_ORIGINAL_INDETERMINATE_VALUE; + } + + originalCheckedValue = Checked(); + DoSetChecked(!originalCheckedValue, true, true); + mCheckedIsToggled = true; + + if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) { + aVisitor.mEventStatus = nsEventStatus_eConsumeDoDefault; + } + } else if (mType == FormControlType::InputRadio) { + HTMLInputElement* selectedRadioButton = GetSelectedRadioButton(); + aVisitor.mItemData = static_cast<Element*>(selectedRadioButton); + + originalCheckedValue = Checked(); + if (!originalCheckedValue) { + DoSetChecked(true, true, true); + mCheckedIsToggled = true; + } + + if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) { + aVisitor.mEventStatus = nsEventStatus_eConsumeDoDefault; + } + } + + if (originalCheckedValue) { + aVisitor.mItemFlags |= NS_ORIGINAL_CHECKED_VALUE; + } + + // out-of-spec legacy pre-activation behavior needed because of bug 1803805 + if ((mType == FormControlType::InputSubmit || + mType == FormControlType::InputImage) && + mForm) { + aVisitor.mItemFlags |= NS_IN_SUBMIT_CLICK; + aVisitor.mItemData = static_cast<Element*>(mForm); + // tell the form that we are about to enter a click handler. + // that means that if there are scripted submissions, the + // latest one will be deferred until after the exit point of the + // handler. + mForm->OnSubmitClickBegin(this); + } +} + +nsresult HTMLInputElement::PreHandleEvent(EventChainVisitor& aVisitor) { + if (aVisitor.mItemFlags & NS_PRE_HANDLE_BLUR_EVENT) { + MOZ_ASSERT(aVisitor.mEvent->mMessage == eBlur); + FireChangeEventIfNeeded(); + } + return nsGenericHTMLFormControlElementWithState::PreHandleEvent(aVisitor); +} + +void HTMLInputElement::StartRangeThumbDrag(WidgetGUIEvent* aEvent) { + nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame()); + if (!rangeFrame) { + return; + } + + mIsDraggingRange = true; + mRangeThumbDragStartValue = GetValueAsDecimal(); + // Don't use CaptureFlags::RetargetToElement, as that breaks pseudo-class + // styling of the thumb. + PresShell::SetCapturingContent(this, CaptureFlags::IgnoreAllowedState); + + // Before we change the value, record the current value so that we'll + // correctly send a 'change' event if appropriate. We need to do this here + // because the 'focus' event is handled after the 'mousedown' event that + // we're being called for (i.e. too late to update mFocusedValue, since we'll + // have changed it by then). + GetValue(mFocusedValue, CallerType::System); + + SetValueOfRangeForUserEvent(rangeFrame->GetValueAtEventPoint(aEvent), + SnapToTickMarks::Yes); +} + +void HTMLInputElement::FinishRangeThumbDrag(WidgetGUIEvent* aEvent) { + MOZ_ASSERT(mIsDraggingRange); + + if (PresShell::GetCapturingContent() == this) { + PresShell::ReleaseCapturingContent(); + } + if (aEvent) { + nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame()); + SetValueOfRangeForUserEvent(rangeFrame->GetValueAtEventPoint(aEvent), + SnapToTickMarks::Yes); + } + mIsDraggingRange = false; + FireChangeEventIfNeeded(); +} + +void HTMLInputElement::CancelRangeThumbDrag(bool aIsForUserEvent) { + MOZ_ASSERT(mIsDraggingRange); + + mIsDraggingRange = false; + if (PresShell::GetCapturingContent() == this) { + PresShell::ReleaseCapturingContent(); + } + if (aIsForUserEvent) { + SetValueOfRangeForUserEvent(mRangeThumbDragStartValue, + SnapToTickMarks::Yes); + } else { + // Don't dispatch an 'input' event - at least not using + // DispatchTrustedEvent. + // TODO: decide what we should do here - bug 851782. + nsAutoString val; + mInputType->ConvertNumberToString(mRangeThumbDragStartValue, val); + // TODO: What should we do if SetValueInternal fails? (The allocation + // is small, so we should be fine here.) + SetValueInternal(val, {ValueSetterOption::BySetUserInputAPI, + ValueSetterOption::SetValueChanged}); + if (nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame())) { + frame->UpdateForValueChange(); + } + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + } +} + +void HTMLInputElement::SetValueOfRangeForUserEvent( + Decimal aValue, SnapToTickMarks aSnapToTickMarks) { + MOZ_ASSERT(aValue.isFinite()); + if (aSnapToTickMarks == SnapToTickMarks::Yes) { + MaybeSnapToTickMark(aValue); + } + + Decimal oldValue = GetValueAsDecimal(); + + nsAutoString val; + mInputType->ConvertNumberToString(aValue, val); + // TODO: What should we do if SetValueInternal fails? (The allocation + // is small, so we should be fine here.) + SetValueInternal(val, {ValueSetterOption::BySetUserInputAPI, + ValueSetterOption::SetValueChanged}); + if (nsRangeFrame* frame = do_QueryFrame(GetPrimaryFrame())) { + frame->UpdateForValueChange(); + } + + if (GetValueAsDecimal() != oldValue) { + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + } +} + +void HTMLInputElement::StartNumberControlSpinnerSpin() { + MOZ_ASSERT(!mNumberControlSpinnerIsSpinning); + + mNumberControlSpinnerIsSpinning = true; + + nsRepeatService::GetInstance()->Start( + HandleNumberControlSpin, this, OwnerDoc(), "HandleNumberControlSpin"_ns); + + // Capture the mouse so that we can tell if the pointer moves from one + // spin button to the other, or to some other element: + PresShell::SetCapturingContent(this, CaptureFlags::IgnoreAllowedState); + + nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); + if (numberControlFrame) { + numberControlFrame->SpinnerStateChanged(); + } +} + +void HTMLInputElement::StopNumberControlSpinnerSpin(SpinnerStopState aState) { + if (mNumberControlSpinnerIsSpinning) { + if (PresShell::GetCapturingContent() == this) { + PresShell::ReleaseCapturingContent(); + } + + nsRepeatService::GetInstance()->Stop(HandleNumberControlSpin, this); + + mNumberControlSpinnerIsSpinning = false; + + if (aState == eAllowDispatchingEvents) { + FireChangeEventIfNeeded(); + } + + nsNumberControlFrame* numberControlFrame = do_QueryFrame(GetPrimaryFrame()); + if (numberControlFrame) { + MOZ_ASSERT(aState == eAllowDispatchingEvents, + "Shouldn't have primary frame for the element when we're not " + "allowed to dispatch events to it anymore."); + numberControlFrame->SpinnerStateChanged(); + } + } +} + +void HTMLInputElement::StepNumberControlForUserEvent(int32_t aDirection) { + // We can't use GetValidityState here because the validity state is not set + // if the user hasn't previously taken an action to set or change the value, + // according to the specs. + if (HasBadInput()) { + // If the user has typed a value into the control and inadvertently made a + // mistake (e.g. put a thousand separator at the wrong point) we do not + // want to wipe out what they typed if they try to increment/decrement the + // value. Better is to highlight the value as being invalid so that they + // can correct what they typed. + // We only do this if there actually is a value typed in by/displayed to + // the user. (IsValid() can return false if the 'required' attribute is + // set and the value is the empty string.) + if (!IsValueEmpty()) { + // We pass 'true' for SetUserInteracted because we need the UI to update + // _now_ or the user will wonder why the step behavior isn't functioning. + SetUserInteracted(true); + return; + } + } + + Decimal newValue = Decimal::nan(); // unchanged if value will not change + + nsresult rv = GetValueIfStepped(aDirection, CALLED_FOR_USER_EVENT, &newValue); + + if (NS_FAILED(rv) || !newValue.isFinite()) { + return; // value should not or will not change + } + + nsAutoString newVal; + mInputType->ConvertNumberToString(newValue, newVal); + // TODO: What should we do if SetValueInternal fails? (The allocation + // is small, so we should be fine here.) + SetValueInternal(newVal, {ValueSetterOption::BySetUserInputAPI, + ValueSetterOption::SetValueChanged}); +} + +static bool SelectTextFieldOnFocus() { + if (!gSelectTextFieldOnFocus) { + int32_t selectTextfieldsOnKeyFocus = -1; + nsresult rv = + LookAndFeel::GetInt(LookAndFeel::IntID::SelectTextfieldsOnKeyFocus, + &selectTextfieldsOnKeyFocus); + if (NS_FAILED(rv)) { + gSelectTextFieldOnFocus = -1; + } else { + gSelectTextFieldOnFocus = selectTextfieldsOnKeyFocus != 0 ? 1 : -1; + } + } + + return gSelectTextFieldOnFocus == 1; +} + +bool HTMLInputElement::ShouldPreventDOMActivateDispatch( + EventTarget* aOriginalTarget) { + /* + * For the moment, there is only one situation where we actually want to + * prevent firing a DOMActivate event: + * - we are a <input type='file'> that just got a click event, + * - the event was targeted to our button which should have sent a + * DOMActivate event. + */ + + if (mType != FormControlType::InputFile) { + return false; + } + + Element* target = Element::FromEventTargetOrNull(aOriginalTarget); + if (!target) { + return false; + } + + return target->GetParent() == this && + target->IsRootOfNativeAnonymousSubtree() && + target->IsHTMLElement(nsGkAtoms::button); +} + +nsresult HTMLInputElement::MaybeInitPickers(EventChainPostVisitor& aVisitor) { + // Open a file picker when we receive a click on a <input type='file'>, or + // open a color picker when we receive a click on a <input type='color'>. + // A click is handled if it's the left mouse button. + // We do not prevent non-trusted click because authors can already use + // .click(). However, the pickers will follow the rules of popup-blocking. + WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); + if (!(mouseEvent && mouseEvent->IsLeftClickEvent())) { + return NS_OK; + } + if (mType == FormControlType::InputFile) { + // If the user clicked on the "Choose folder..." button we open the + // directory picker, else we open the file picker. + FilePickerType type = FILE_PICKER_FILE; + nsIContent* target = + nsIContent::FromEventTargetOrNull(aVisitor.mEvent->mOriginalTarget); + if (target && target->FindFirstNonChromeOnlyAccessContent() == this && + StaticPrefs::dom_webkitBlink_dirPicker_enabled() && + HasAttr(nsGkAtoms::webkitdirectory)) { + type = FILE_PICKER_DIRECTORY; + } + return InitFilePicker(type); + } + if (mType == FormControlType::InputColor) { + return InitColorPicker(); + } + + return NS_OK; +} + +/** + * Return true if the input event should be ignored because of its modifiers. + * Control is treated specially, since sometimes we ignore it, and sometimes + * we don't (for webcompat reasons). + */ +static bool IgnoreInputEventWithModifier(const WidgetInputEvent& aEvent, + bool ignoreControl) { + return (ignoreControl && aEvent.IsControl()) || + aEvent.IsAltGraph() +#if defined(XP_WIN) || defined(MOZ_WIDGET_GTK) + // Meta key is the Windows Logo key on Windows and Linux which may + // assign some special meaning for the events while it's pressed. + // On the other hand, it's a normal modifier in macOS and Android. + // Therefore, We should ignore it only in Win/Linux. + || aEvent.IsMeta() +#endif + || aEvent.IsFn(); +} + +bool HTMLInputElement::StepsInputValue( + const WidgetKeyboardEvent& aEvent) const { + if (mType != FormControlType::InputNumber) { + return false; + } + if (aEvent.mMessage != eKeyPress) { + return false; + } + if (!aEvent.IsTrusted()) { + return false; + } + if (aEvent.mKeyCode != NS_VK_UP && aEvent.mKeyCode != NS_VK_DOWN) { + return false; + } + if (IgnoreInputEventWithModifier(aEvent, false)) { + return false; + } + if (aEvent.DefaultPrevented()) { + return false; + } + if (!IsMutable()) { + return false; + } + return true; +} + +static bool ActivatesWithKeyboard(FormControlType aType, uint32_t aKeyCode) { + switch (aType) { + case FormControlType::InputCheckbox: + case FormControlType::InputRadio: + // Checkbox and Radio try to submit on Enter press + return aKeyCode != NS_VK_RETURN; + case FormControlType::InputButton: + case FormControlType::InputReset: + case FormControlType::InputSubmit: + case FormControlType::InputFile: + case FormControlType::InputImage: // Bug 34418 + case FormControlType::InputColor: + return true; + default: + return false; + } +} + +nsresult HTMLInputElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + if (aVisitor.mEvent->mMessage == eBlur) { + if (mIsDraggingRange) { + FinishRangeThumbDrag(); + } else if (mNumberControlSpinnerIsSpinning) { + StopNumberControlSpinnerSpin(); + } + } + + nsresult rv = NS_OK; + auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags)); + + // Ideally we would make the default action for click and space just dispatch + // DOMActivate, and the default action for DOMActivate flip the checkbox/ + // radio state and fire onchange. However, for backwards compatibility, we + // need to flip the state before firing click, and we need to fire click + // when space is pressed. So, we just nest the firing of DOMActivate inside + // the click event handling, and allow cancellation of DOMActivate to cancel + // the click. + if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault && + !IsSingleLineTextControl(true) && mType != FormControlType::InputNumber) { + WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); + if (mouseEvent && mouseEvent->IsLeftClickEvent() && + OwnerDoc()->MayHaveDOMActivateListeners() && + !ShouldPreventDOMActivateDispatch(aVisitor.mEvent->mOriginalTarget)) { + // DOMActive event should be trusted since the activation is actually + // occurred even if the cause is an untrusted click event. + InternalUIEvent actEvent(true, eLegacyDOMActivate, mouseEvent); + actEvent.mDetail = 1; + + if (RefPtr<PresShell> presShell = + aVisitor.mPresContext ? aVisitor.mPresContext->GetPresShell() + : nullptr) { + nsEventStatus status = nsEventStatus_eIgnore; + mInInternalActivate = true; + rv = presShell->HandleDOMEventWithTarget(this, &actEvent, &status); + mInInternalActivate = false; + + // If activate is cancelled, we must do the same as when click is + // cancelled (revert the checkbox to its original value). + if (status == nsEventStatus_eConsumeNoDefault) { + aVisitor.mEventStatus = status; + } + } + } + } + + bool preventDefault = + aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault; + if (IsDisabled() && oldType != FormControlType::InputCheckbox && + oldType != FormControlType::InputRadio) { + // Behave as if defaultPrevented when the element becomes disabled by event + // listeners. Checkboxes and radio buttons should still process clicks for + // web compat. See: + // https://html.spec.whatwg.org/multipage/input.html#the-input-element:activation-behaviour + preventDefault = true; + } + + if (NS_SUCCEEDED(rv)) { + WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent(); + if (keyEvent && StepsInputValue(*keyEvent)) { + StepNumberControlForUserEvent(keyEvent->mKeyCode == NS_VK_UP ? 1 : -1); + FireChangeEventIfNeeded(); + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } else if (!preventDefault) { + if (keyEvent && ActivatesWithKeyboard(mType, keyEvent->mKeyCode) && + keyEvent->IsTrusted()) { + // We maybe dispatch a synthesized click for keyboard activation. + HandleKeyboardActivation(aVisitor); + } + + switch (aVisitor.mEvent->mMessage) { + case eFocus: { + // see if we should select the contents of the textbox. This happens + // for text and password fields when the field was focused by the + // keyboard or a navigation, the platform allows it, and it wasn't + // just because we raised a window. + // + // While it'd usually make sense, we don't do this for JS callers + // because it causes some compat issues, see bug 1712724 for example. + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (fm && IsSingleLineTextControl(false) && + !aVisitor.mEvent->AsFocusEvent()->mFromRaise && + SelectTextFieldOnFocus()) { + if (Document* document = GetComposedDoc()) { + uint32_t lastFocusMethod = + fm->GetLastFocusMethod(document->GetWindow()); + const bool shouldSelectAllOnFocus = [&] { + if (lastFocusMethod & nsIFocusManager::FLAG_BYMOVEFOCUS) { + return true; + } + if (lastFocusMethod & nsIFocusManager::FLAG_BYJS) { + return false; + } + return bool(lastFocusMethod & nsIFocusManager::FLAG_BYKEY); + }(); + if (shouldSelectAllOnFocus) { + RefPtr<nsPresContext> presContext = + GetPresContext(eForComposedDoc); + SelectAll(presContext); + } + } + } + break; + } + + case eKeyDown: { + // For compatibility with the other browsers, we should active this + // element at least when a checkbox or a radio button. + // TODO: Investigate which elements are activated by space key in the + // other browsers. + if (aVisitor.mPresContext && keyEvent->IsTrusted() && !IsDisabled() && + keyEvent->ShouldWorkAsSpaceKey() && + (mType == FormControlType::InputCheckbox || + mType == FormControlType::InputRadio)) { + EventStateManager::SetActiveManager( + aVisitor.mPresContext->EventStateManager(), this); + } + break; + } + + case eKeyPress: { + if (mType == FormControlType::InputRadio && keyEvent->IsTrusted() && + !keyEvent->IsAlt() && !keyEvent->IsControl() && + !keyEvent->IsMeta()) { + rv = MaybeHandleRadioButtonNavigation(aVisitor, keyEvent->mKeyCode); + } + + /* + * For some input types, if the user hits enter, the form is + * submitted. + * + * Bug 99920, bug 109463 and bug 147850: + * (a) if there is a submit control in the form, click the first + * submit control in the form. + * (b) if there is just one text control in the form, submit by + * sending a submit event directly to the form + * (c) if there is more than one text input and no submit buttons, do + * not submit, period. + */ + + if (keyEvent->mKeyCode == NS_VK_RETURN && keyEvent->IsTrusted() && + (IsSingleLineTextControl(false, mType) || + IsDateTimeInputType(mType) || + mType == FormControlType::InputCheckbox || + mType == FormControlType::InputRadio)) { + if (IsSingleLineTextControl(false, mType) || + IsDateTimeInputType(mType)) { + FireChangeEventIfNeeded(); + } + + if (aVisitor.mPresContext) { + MaybeSubmitForm(aVisitor.mPresContext); + } + } + + if (mType == FormControlType::InputRange && keyEvent->IsTrusted() && + !keyEvent->IsAlt() && !keyEvent->IsControl() && + !keyEvent->IsMeta() && + (keyEvent->mKeyCode == NS_VK_LEFT || + keyEvent->mKeyCode == NS_VK_RIGHT || + keyEvent->mKeyCode == NS_VK_UP || + keyEvent->mKeyCode == NS_VK_DOWN || + keyEvent->mKeyCode == NS_VK_PAGE_UP || + keyEvent->mKeyCode == NS_VK_PAGE_DOWN || + keyEvent->mKeyCode == NS_VK_HOME || + keyEvent->mKeyCode == NS_VK_END)) { + Decimal minimum = GetMinimum(); + Decimal maximum = GetMaximum(); + MOZ_ASSERT(minimum.isFinite() && maximum.isFinite()); + if (minimum < maximum) { // else the value is locked to the minimum + Decimal value = GetValueAsDecimal(); + Decimal step = GetStep(); + if (step == kStepAny) { + step = GetDefaultStep(); + } + MOZ_ASSERT(value.isFinite() && step.isFinite()); + Decimal newValue; + switch (keyEvent->mKeyCode) { + case NS_VK_LEFT: + newValue = value + + (GetComputedDirectionality() == Directionality::Rtl + ? step + : -step); + break; + case NS_VK_RIGHT: + newValue = value + + (GetComputedDirectionality() == Directionality::Rtl + ? -step + : step); + break; + case NS_VK_UP: + // Even for horizontal range, "up" means "increase" + newValue = value + step; + break; + case NS_VK_DOWN: + // Even for horizontal range, "down" means "decrease" + newValue = value - step; + break; + case NS_VK_HOME: + newValue = minimum; + break; + case NS_VK_END: + newValue = maximum; + break; + case NS_VK_PAGE_UP: + // For PgUp/PgDn we jump 10% of the total range, unless step + // requires us to jump more. + newValue = + value + std::max(step, (maximum - minimum) / Decimal(10)); + break; + case NS_VK_PAGE_DOWN: + newValue = + value - std::max(step, (maximum - minimum) / Decimal(10)); + break; + } + SetValueOfRangeForUserEvent(newValue); + FireChangeEventIfNeeded(); + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + } + + } break; // eKeyPress + + case eMouseDown: + case eMouseUp: + case eMouseDoubleClick: { + // cancel all of these events for buttons + // XXXsmaug Why? + WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); + if (mouseEvent->mButton == MouseButton::eMiddle || + mouseEvent->mButton == MouseButton::eSecondary) { + if (mType == FormControlType::InputButton || + mType == FormControlType::InputReset || + mType == FormControlType::InputSubmit) { + if (aVisitor.mDOMEvent) { + aVisitor.mDOMEvent->StopPropagation(); + } else { + rv = NS_ERROR_FAILURE; + } + } + } + if (mType == FormControlType::InputNumber && + aVisitor.mEvent->IsTrusted()) { + if (mouseEvent->mButton == MouseButton::ePrimary && + !IgnoreInputEventWithModifier(*mouseEvent, false)) { + nsNumberControlFrame* numberControlFrame = + do_QueryFrame(GetPrimaryFrame()); + if (numberControlFrame) { + if (aVisitor.mEvent->mMessage == eMouseDown && IsMutable()) { + switch (numberControlFrame->GetSpinButtonForPointerEvent( + aVisitor.mEvent->AsMouseEvent())) { + case nsNumberControlFrame::eSpinButtonUp: + StepNumberControlForUserEvent(1); + mNumberControlSpinnerSpinsUp = true; + StartNumberControlSpinnerSpin(); + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + break; + case nsNumberControlFrame::eSpinButtonDown: + StepNumberControlForUserEvent(-1); + mNumberControlSpinnerSpinsUp = false; + StartNumberControlSpinnerSpin(); + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + break; + } + } + } + } + if (aVisitor.mEventStatus != nsEventStatus_eConsumeNoDefault) { + // We didn't handle this to step up/down. Whatever this was, be + // aggressive about stopping the spin. (And don't set + // nsEventStatus_eConsumeNoDefault after doing so, since that + // might prevent, say, the context menu from opening.) + StopNumberControlSpinnerSpin(); + } + } + break; + } +#if !defined(ANDROID) && !defined(XP_MACOSX) + case eWheel: { + // Handle wheel events as increasing / decreasing the input element's + // value when it's focused and it's type is number or range. + WidgetWheelEvent* wheelEvent = aVisitor.mEvent->AsWheelEvent(); + if (!aVisitor.mEvent->DefaultPrevented() && + aVisitor.mEvent->IsTrusted() && IsMutable() && wheelEvent && + wheelEvent->mDeltaY != 0 && + wheelEvent->mDeltaMode != WheelEvent_Binding::DOM_DELTA_PIXEL) { + if (mType == FormControlType::InputNumber) { + if (nsContentUtils::IsFocusedContent(this)) { + StepNumberControlForUserEvent(wheelEvent->mDeltaY > 0 ? -1 : 1); + FireChangeEventIfNeeded(); + aVisitor.mEvent->PreventDefault(); + } + } else if (mType == FormControlType::InputRange && + nsContentUtils::IsFocusedContent(this) && + GetMinimum() < GetMaximum()) { + Decimal value = GetValueAsDecimal(); + Decimal step = GetStep(); + if (step == kStepAny) { + step = GetDefaultStep(); + } + MOZ_ASSERT(value.isFinite() && step.isFinite()); + SetValueOfRangeForUserEvent( + wheelEvent->mDeltaY < 0 ? value + step : value - step); + FireChangeEventIfNeeded(); + aVisitor.mEvent->PreventDefault(); + } + } + break; + } +#endif + case eMouseClick: { + if (!aVisitor.mEvent->DefaultPrevented() && + aVisitor.mEvent->IsTrusted() && + aVisitor.mEvent->AsMouseEvent()->mButton == + MouseButton::ePrimary) { + // TODO(emilio): Handling this should ideally not move focus. + if (mType == FormControlType::InputSearch) { + if (nsSearchControlFrame* searchControlFrame = + do_QueryFrame(GetPrimaryFrame())) { + Element* clearButton = searchControlFrame->GetAnonClearButton(); + if (clearButton && + aVisitor.mEvent->mOriginalTarget == clearButton) { + SetUserInput(EmptyString(), + *nsContentUtils::GetSystemPrincipal()); + // TODO(emilio): This should focus the input, but calling + // SetFocus(this, FLAG_NOSCROLL) for some reason gets us into + // an inconsistent state where we're focused but don't match + // :focus-visible / :focus. + } + } + } else if (mType == FormControlType::InputPassword) { + if (nsTextControlFrame* textControlFrame = + do_QueryFrame(GetPrimaryFrame())) { + auto* reveal = textControlFrame->GetRevealButton(); + if (reveal && aVisitor.mEvent->mOriginalTarget == reveal) { + SetRevealPassword(!RevealPassword()); + // TODO(emilio): This should focus the input, but calling + // SetFocus(this, FLAG_NOSCROLL) for some reason gets us into + // an inconsistent state where we're focused but don't match + // :focus-visible / :focus. + } + } + } + } + break; + } + default: + break; + } + + // Bug 1459231: Temporarily needed till links respect activation target, + // then also remove NS_OUTER_ACTIVATE_EVENT. The appropriate + // behavior/model for links is still under discussion (see + // https://github.com/whatwg/html/issues/1576). For now, we aim for + // consistency with other browsers. + if (aVisitor.mItemFlags & NS_OUTER_ACTIVATE_EVENT) { + switch (mType) { + case FormControlType::InputReset: + case FormControlType::InputSubmit: + case FormControlType::InputImage: + if (mForm) { + aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; + } + break; + case FormControlType::InputCheckbox: + case FormControlType::InputRadio: + aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; + break; + default: + break; + } + } + } + } // if + + if (NS_SUCCEEDED(rv) && mType == FormControlType::InputRange) { + PostHandleEventForRangeThumb(aVisitor); + } + + if (!preventDefault) { + MOZ_TRY(MaybeInitPickers(aVisitor)); + } + return NS_OK; +} + +void EndSubmitClick(EventChainPostVisitor& aVisitor) { + auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags)); + if ((aVisitor.mItemFlags & NS_IN_SUBMIT_CLICK) && + (oldType == FormControlType::InputSubmit || + oldType == FormControlType::InputImage)) { + nsCOMPtr<nsIContent> content(do_QueryInterface(aVisitor.mItemData)); + RefPtr<HTMLFormElement> form = HTMLFormElement::FromNodeOrNull(content); + // Tell the form that we are about to exit a click handler, + // so the form knows not to defer subsequent submissions. + // The pending ones that were created during the handler + // will be flushed or forgotten. + form->OnSubmitClickEnd(); + // tell the form to flush a possible pending submission. + // the reason is that the script returned false (the event was + // not ignored) so if there is a stored submission, it needs to + // be submitted immediately. + form->FlushPendingSubmission(); + } +} + +void HTMLInputElement::ActivationBehavior(EventChainPostVisitor& aVisitor) { + auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags)); + + if (IsDisabled() && oldType != FormControlType::InputCheckbox && + oldType != FormControlType::InputRadio) { + // Behave as if defaultPrevented when the element becomes disabled by event + // listeners. Checkboxes and radio buttons should still process clicks for + // web compat. See: + // https://html.spec.whatwg.org/multipage/input.html#the-input-element:activation-behaviour + EndSubmitClick(aVisitor); + return; + } + + if (mCheckedIsToggled) { + SetUserInteracted(true); + + // Fire input event and then change event. + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + + // FIXME: Why is this different than every other change event? + nsContentUtils::DispatchTrustedEvent<WidgetEvent>( + OwnerDoc(), static_cast<Element*>(this), eFormChange, CanBubble::eYes, + Cancelable::eNo); +#ifdef ACCESSIBILITY + // Fire an event to notify accessibility + if (mType == FormControlType::InputCheckbox) { + if (nsContentUtils::MayHaveFormCheckboxStateChangeListeners()) { + FireEventForAccessibility(this, eFormCheckboxStateChange); + } + } else if (nsContentUtils::MayHaveFormRadioStateChangeListeners()) { + FireEventForAccessibility(this, eFormRadioStateChange); + // Fire event for the previous selected radio. + nsCOMPtr<nsIContent> content = do_QueryInterface(aVisitor.mItemData); + if (auto* previous = HTMLInputElement::FromNodeOrNull(content)) { + FireEventForAccessibility(previous, eFormRadioStateChange); + } + } +#endif + } + + switch (mType) { + case FormControlType::InputReset: + case FormControlType::InputSubmit: + case FormControlType::InputImage: + if (mForm) { + // Hold a strong ref while dispatching + RefPtr<HTMLFormElement> form(mForm); + if (mType == FormControlType::InputReset) { + form->MaybeReset(this); + } else { + form->MaybeSubmit(this); + } + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + break; + + default: + break; + } // switch + if (IsButtonControl()) { + if (!GetInvokeTargetElement()) { + HandlePopoverTargetAction(); + } else { + HandleInvokeTargetAction(); + } + } + + EndSubmitClick(aVisitor); +} + +void HTMLInputElement::LegacyCanceledActivationBehavior( + EventChainPostVisitor& aVisitor) { + bool originalCheckedValue = + !!(aVisitor.mItemFlags & NS_ORIGINAL_CHECKED_VALUE); + auto oldType = FormControlType(NS_CONTROL_TYPE(aVisitor.mItemFlags)); + + if (mCheckedIsToggled) { + // if it was canceled and a radio button, then set the old + // selected btn to TRUE. if it is a checkbox then set it to its + // original value (legacy-canceled-activation) + if (oldType == FormControlType::InputRadio) { + nsCOMPtr<nsIContent> content = do_QueryInterface(aVisitor.mItemData); + HTMLInputElement* selectedRadioButton = + HTMLInputElement::FromNodeOrNull(content); + if (selectedRadioButton) { + selectedRadioButton->SetChecked(true); + } + // If there was no checked radio button or this one is no longer a + // radio button we must reset it back to false to cancel the action. + // See how the web of hack grows? + if (!selectedRadioButton || mType != FormControlType::InputRadio) { + DoSetChecked(false, true, true); + } + } else if (oldType == FormControlType::InputCheckbox) { + bool originalIndeterminateValue = + !!(aVisitor.mItemFlags & NS_ORIGINAL_INDETERMINATE_VALUE); + SetIndeterminateInternal(originalIndeterminateValue, false); + DoSetChecked(originalCheckedValue, true, true); + } + } + + // Relevant for bug 242494: submit button with "submit(); return false;" + EndSubmitClick(aVisitor); +} + +enum class RadioButtonMove { Back, Forward, None }; +nsresult HTMLInputElement::MaybeHandleRadioButtonNavigation( + EventChainPostVisitor& aVisitor, uint32_t aKeyCode) { + auto move = [&] { + switch (aKeyCode) { + case NS_VK_UP: + return RadioButtonMove::Back; + case NS_VK_DOWN: + return RadioButtonMove::Forward; + case NS_VK_LEFT: + case NS_VK_RIGHT: { + const bool isRtl = GetComputedDirectionality() == Directionality::Rtl; + return isRtl == (aKeyCode == NS_VK_LEFT) ? RadioButtonMove::Forward + : RadioButtonMove::Back; + } + } + return RadioButtonMove::None; + }(); + if (move == RadioButtonMove::None) { + return NS_OK; + } + // Arrow key pressed, focus+select prev/next radio button + RefPtr<HTMLInputElement> selectedRadioButton; + if (auto* container = GetCurrentRadioGroupContainer()) { + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + container->GetNextRadioButton(name, move == RadioButtonMove::Back, this, + getter_AddRefs(selectedRadioButton)); + } + if (!selectedRadioButton) { + return NS_OK; + } + FocusOptions options; + ErrorResult error; + selectedRadioButton->Focus(options, CallerType::System, error); + if (error.Failed()) { + return error.StealNSResult(); + } + nsresult rv = DispatchSimulatedClick( + selectedRadioButton, aVisitor.mEvent->IsTrusted(), aVisitor.mPresContext); + if (NS_SUCCEEDED(rv)) { + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + return rv; +} + +void HTMLInputElement::PostHandleEventForRangeThumb( + EventChainPostVisitor& aVisitor) { + MOZ_ASSERT(mType == FormControlType::InputRange); + + if (nsEventStatus_eConsumeNoDefault == aVisitor.mEventStatus || + !(aVisitor.mEvent->mClass == eMouseEventClass || + aVisitor.mEvent->mClass == eTouchEventClass || + aVisitor.mEvent->mClass == eKeyboardEventClass)) { + return; + } + + nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame()); + if (!rangeFrame && mIsDraggingRange) { + CancelRangeThumbDrag(); + return; + } + + switch (aVisitor.mEvent->mMessage) { + case eMouseDown: + case eTouchStart: { + if (mIsDraggingRange) { + break; + } + if (PresShell::GetCapturingContent()) { + break; // don't start drag if someone else is already capturing + } + WidgetInputEvent* inputEvent = aVisitor.mEvent->AsInputEvent(); + if (IgnoreInputEventWithModifier(*inputEvent, true)) { + break; // ignore + } + if (aVisitor.mEvent->mMessage == eMouseDown) { + if (aVisitor.mEvent->AsMouseEvent()->mButtons == + MouseButtonsFlag::ePrimaryFlag) { + StartRangeThumbDrag(inputEvent); + } else if (mIsDraggingRange) { + CancelRangeThumbDrag(); + } + } else { + if (aVisitor.mEvent->AsTouchEvent()->mTouches.Length() == 1) { + StartRangeThumbDrag(inputEvent); + } else if (mIsDraggingRange) { + CancelRangeThumbDrag(); + } + } + aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; + } break; + + case eMouseMove: + case eTouchMove: + if (!mIsDraggingRange) { + break; + } + if (PresShell::GetCapturingContent() != this) { + // Someone else grabbed capture. + CancelRangeThumbDrag(); + break; + } + SetValueOfRangeForUserEvent( + rangeFrame->GetValueAtEventPoint(aVisitor.mEvent->AsInputEvent()), + SnapToTickMarks::Yes); + aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; + break; + + case eMouseUp: + case eTouchEnd: + if (!mIsDraggingRange) { + break; + } + // We don't check to see whether we are the capturing content here and + // call CancelRangeThumbDrag() if that is the case. We just finish off + // the drag and set our final value (unless someone has called + // preventDefault() and prevents us getting here). + FinishRangeThumbDrag(aVisitor.mEvent->AsInputEvent()); + aVisitor.mEvent->mFlags.mMultipleActionsPrevented = true; + break; + + case eKeyPress: + if (mIsDraggingRange && + aVisitor.mEvent->AsKeyboardEvent()->mKeyCode == NS_VK_ESCAPE) { + CancelRangeThumbDrag(); + } + break; + + case eTouchCancel: + if (mIsDraggingRange) { + CancelRangeThumbDrag(); + } + break; + + default: + break; + } +} + +void HTMLInputElement::MaybeLoadImage() { + // Our base URI may have changed; claim that our URI changed, and the + // nsImageLoadingContent will decide whether a new image load is warranted. + nsAutoString uri; + if (mType == FormControlType::InputImage && GetAttr(nsGkAtoms::src, uri) && + (NS_FAILED(LoadImage(uri, false, true, eImageLoadType_Normal, + mSrcTriggeringPrincipal)) || + !LoadingEnabled())) { + CancelImageRequests(true); + } +} + +nsresult HTMLInputElement::BindToTree(BindContext& aContext, nsINode& aParent) { + // If we are currently bound to a disconnected subtree root, remove + // ourselves from it first. + if (!mForm && mType == FormControlType::InputRadio) { + RemoveFromRadioGroup(); + } + + nsresult rv = + nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + nsImageLoadingContent::BindToTree(aContext, aParent); + + if (mType == FormControlType::InputImage) { + // Our base URI may have changed; claim that our URI changed, and the + // nsImageLoadingContent will decide whether a new image load is warranted. + if (HasAttr(nsGkAtoms::src)) { + // Mark channel as urgent-start before load image if the image load is + // initaiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + nsContentUtils::AddScriptRunner( + NewRunnableMethod("dom::HTMLInputElement::MaybeLoadImage", this, + &HTMLInputElement::MaybeLoadImage)); + } + } + + // Add radio to document if we don't have a form already (if we do it's + // already been added into that group) + if (!mForm && mType == FormControlType::InputRadio) { + AddToRadioGroup(); + } + + // Set direction based on value if dir=auto + if (HasDirAuto()) { + SetAutoDirectionality(false); + } + + // An element can't suffer from value missing if it is not in a document. + // We have to check if we suffer from that as we are now in a document. + UpdateValueMissingValidityState(); + + // If there is a disabled fieldset in the parent chain, the element is now + // barred from constraint validation and can't suffer from value missing + // (call done before). + UpdateBarredFromConstraintValidation(); + + // And now make sure our state is up to date + UpdateValidityElementStates(true); + + if (CreatesDateTimeWidget() && IsInComposedDoc()) { + // Construct Shadow Root so web content can be hidden in the DOM. + AttachAndSetUAShadowRoot(NotifyUAWidgetSetup::Yes, DelegatesFocus::Yes); + } + + MaybeDispatchLoginManagerEvents(mForm); + + return rv; +} + +void HTMLInputElement::MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm) { + // Don't disptach the event if the <input> is disconnected + // or belongs to a disconnected form + if (!IsInComposedDoc()) { + return; + } + + nsString eventType; + Element* target = nullptr; + + if (mType == FormControlType::InputPassword) { + // Don't fire another event if we have a pending event. + if (aForm && aForm->mHasPendingPasswordEvent) { + return; + } + + // TODO(Bug 1864404): Use one event for formless and form inputs. + eventType = aForm ? u"DOMFormHasPassword"_ns : u"DOMInputPasswordAdded"_ns; + + target = aForm ? static_cast<Element*>(aForm) : this; + + if (aForm) { + aForm->mHasPendingPasswordEvent = true; + } + + } else if (mType == FormControlType::InputEmail || + mType == FormControlType::InputText) { + // Don't fire a username event if: + // - <input> is not part of a form + // - we have a pending event + // - username only forms are not supported + if (!aForm || aForm->mHasPendingPossibleUsernameEvent || + !StaticPrefs::signon_usernameOnlyForm_enabled()) { + return; + } + + eventType = u"DOMFormHasPossibleUsername"_ns; + target = aForm; + + aForm->mHasPendingPossibleUsernameEvent = true; + + } else { + return; + } + + RefPtr<AsyncEventDispatcher> dispatcher = new AsyncEventDispatcher( + target, eventType, CanBubble::eYes, ChromeOnlyDispatch::eYes); + dispatcher->PostDOMEvent(); +} + +void HTMLInputElement::UnbindFromTree(bool aNullParent) { + if (mType == FormControlType::InputPassword) { + MaybeFireInputPasswordRemoved(); + } + + // If we have a form and are unbound from it, + // nsGenericHTMLFormControlElementWithState::UnbindFromTree() will unset the + // form and that takes care of form's WillRemove so we just have to take care + // of the case where we're removing from the document and we don't + // have a form + if (!mForm && mType == FormControlType::InputRadio) { + RemoveFromRadioGroup(); + } + + if (CreatesDateTimeWidget() && IsInComposedDoc()) { + NotifyUAWidgetTeardown(); + } + + nsImageLoadingContent::UnbindFromTree(aNullParent); + nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent); + + // If we are contained within a disconnected subtree, attempt to add + // ourselves to the subtree root's radio group. + if (!mForm && mType == FormControlType::InputRadio) { + AddToRadioGroup(); + } + + // GetCurrentDoc is returning nullptr so we can update the value + // missing validity state to reflect we are no longer into a doc. + UpdateValueMissingValidityState(); + // We might be no longer disabled because of parent chain changed. + UpdateBarredFromConstraintValidation(); + // And now make sure our state is up to date + UpdateValidityElementStates(false); +} + +/** + * @param aType InputElementTypes + * @return true, iff SetRangeText applies to aType as specified at + * https://html.spec.whatwg.org/#concept-input-apply. + */ +static bool SetRangeTextApplies(FormControlType aType) { + return aType == FormControlType::InputText || + aType == FormControlType::InputSearch || + aType == FormControlType::InputUrl || + aType == FormControlType::InputTel || + aType == FormControlType::InputPassword; +} + +void HTMLInputElement::HandleTypeChange(FormControlType aNewType, + bool aNotify) { + FormControlType oldType = mType; + MOZ_ASSERT(oldType != aNewType); + + mHasBeenTypePassword = + mHasBeenTypePassword || aNewType == FormControlType::InputPassword; + + if (nsFocusManager* fm = nsFocusManager::GetFocusManager()) { + // Input element can represent very different kinds of UIs, and we may + // need to flush styling even when focusing the already focused input + // element. + fm->NeedsFlushBeforeEventHandling(this); + } + + if (oldType == FormControlType::InputPassword && + State().HasState(ElementState::REVEALED)) { + // Modify the state directly to avoid dispatching events. + RemoveStates(ElementState::REVEALED, aNotify); + } + + if (aNewType == FormControlType::InputFile || + oldType == FormControlType::InputFile) { + if (aNewType == FormControlType::InputFile) { + mFileData.reset(new FileData()); + } else { + mFileData->Unlink(); + mFileData = nullptr; + } + } + + if (oldType == FormControlType::InputRange && mIsDraggingRange) { + CancelRangeThumbDrag(false); + } + + const ValueModeType oldValueMode = GetValueMode(); + nsAutoString oldValue; + if (oldValueMode == VALUE_MODE_VALUE) { + // Doesn't matter what caller type we pass here, since we know we're not a + // file input anyway. + GetValue(oldValue, CallerType::NonSystem); + } + + TextControlState::SelectionProperties sp; + + if (IsSingleLineTextControl(false) && mInputData.mState) { + mInputData.mState->SyncUpSelectionPropertiesBeforeDestruction(); + sp = mInputData.mState->GetSelectionProperties(); + } + + // We already have a copy of the value, lets free it and changes the type. + FreeData(); + mType = aNewType; + void* memory = mInputTypeMem; + mInputType = InputType::Create(this, mType, memory); + + if (IsSingleLineTextControl()) { + mInputData.mState = TextControlState::Construct(this); + if (!sp.IsDefault()) { + mInputData.mState->SetSelectionProperties(sp); + } + } + + // Whether placeholder applies might have changed. + UpdatePlaceholderShownState(); + // Whether readonly applies might have changed. + UpdateReadOnlyState(aNotify); + UpdateCheckedState(aNotify); + UpdateIndeterminateState(aNotify); + const bool isDefault = IsRadioOrCheckbox() + ? DefaultChecked() + : (mForm && mForm->IsDefaultSubmitElement(this)); + SetStates(ElementState::DEFAULT, isDefault, aNotify); + + // https://html.spec.whatwg.org/#input-type-change + switch (GetValueMode()) { + case VALUE_MODE_DEFAULT: + case VALUE_MODE_DEFAULT_ON: + // 1. If the previous state of the element's type attribute put the value + // IDL attribute in the value mode, and the element's value is not the + // empty string, and the new state of the element's type attribute puts + // the value IDL attribute in either the default mode or the default/on + // mode, then set the element's value content attribute to the + // element's value. + if (oldValueMode == VALUE_MODE_VALUE && !oldValue.IsEmpty()) { + SetAttr(kNameSpaceID_None, nsGkAtoms::value, oldValue, true); + } + break; + case VALUE_MODE_VALUE: { + ValueSetterOptions options{ValueSetterOption::ByInternalAPI}; + if (!SetRangeTextApplies(oldType) && SetRangeTextApplies(mType)) { + options += + ValueSetterOption::MoveCursorToBeginSetSelectionDirectionForward; + } + if (oldValueMode != VALUE_MODE_VALUE) { + // 2. Otherwise, if the previous state of the element's type attribute + // put the value IDL attribute in any mode other than the value + // mode, and the new state of the element's type attribute puts the + // value IDL attribute in the value mode, then set the value of the + // element to the value of the value content attribute, if there is + // one, or the empty string otherwise, and then set the control's + // dirty value flag to false. + nsAutoString value; + GetAttr(nsGkAtoms::value, value); + SetValueInternal(value, options); + SetValueChanged(false); + } else if (mValueChanged) { + // We're both in the "value" mode state, we need to make no change per + // spec, but due to how we store the value internally we need to call + // SetValueInternal, if our value had changed at all. + // TODO: What should we do if SetValueInternal fails? (The allocation + // may potentially be big, but most likely we've failed to allocate + // before the type change.) + SetValueInternal(oldValue, options); + } else { + // The value dirty flag is not set, so our value is based on our default + // value. But our default value might be dependent on the type. Make + // sure to set it so that state is consistent. + SetDefaultValueAsValue(); + } + break; + } + case VALUE_MODE_FILENAME: + default: + // 3. Otherwise, if the previous state of the element's type attribute + // put the value IDL attribute in any mode other than the filename + // mode, and the new state of the element's type attribute puts the + // value IDL attribute in the filename mode, then set the value of the + // element to the empty string. + // + // Setting the attribute to the empty string is basically calling + // ClearFiles, but there can't be any files. + break; + } + + // Updating mFocusedValue in consequence: + // If the new type fires a change event on blur, but the previous type + // doesn't, we should set mFocusedValue to the current value. + // Otherwise, if the new type doesn't fire a change event on blur, but the + // previous type does, we should clear out mFocusedValue. + if (MayFireChangeOnBlur(mType) && !MayFireChangeOnBlur(oldType)) { + GetValue(mFocusedValue, CallerType::System); + } else if (!IsSingleLineTextControl(false, mType) && + IsSingleLineTextControl(false, oldType)) { + mFocusedValue.Truncate(); + } + + // Update or clear our required states since we may have changed from a + // required input type to a non-required input type or viceversa. + if (DoesRequiredApply()) { + const bool isRequired = HasAttr(nsGkAtoms::required); + UpdateRequiredState(isRequired, aNotify); + } else { + RemoveStates(ElementState::REQUIRED_STATES, aNotify); + } + + UpdateHasRange(aNotify); + + // Update validity states, but not element state. We'll update + // element state later, as part of this attribute change. + UpdateAllValidityStatesButNotElementState(); + + UpdateApzAwareFlag(); + + UpdateBarredFromConstraintValidation(); + + // Changing type may affect auto directionality, or non-auto directionality + // because of the special-case for <input type=tel>, as specified in + // https://html.spec.whatwg.org/multipage/dom.html#the-directionality + if (HasDirAuto()) { + const bool autoDirAssociated = IsAutoDirectionalityAssociated(mType); + if (IsAutoDirectionalityAssociated(oldType) != autoDirAssociated) { + SetAutoDirectionality(aNotify); + } + } else if (oldType == FormControlType::InputTel || + mType == FormControlType::InputTel) { + RecomputeDirectionality(this, aNotify); + } + + if (oldType == FormControlType::InputImage || + mType == FormControlType::InputImage) { + if (oldType == FormControlType::InputImage) { + // We're no longer an image input. Cancel our image requests, if we have + // any. + CancelImageRequests(aNotify); + RemoveStates(ElementState::BROKEN, aNotify); + } else { + // We just got switched to be an image input; we should see whether we + // have an image to load; + bool hasSrc = false; + if (aNotify) { + nsAutoString src; + if ((hasSrc = GetAttr(nsGkAtoms::src, src))) { + // Mark channel as urgent-start before load image if the image load is + // initiated by a user interaction. + mUseUrgentStartForChannel = UserActivation::IsHandlingUserInput(); + + LoadImage(src, false, aNotify, eImageLoadType_Normal, + mSrcTriggeringPrincipal); + } + } else { + hasSrc = HasAttr(nsGkAtoms::src); + } + if (!hasSrc) { + AddStates(ElementState::BROKEN, aNotify); + } + } + // We should update our mapped attribute mapping function. + if (mAttrs.HasAttrs() && !mAttrs.IsPendingMappedAttributeEvaluation()) { + mAttrs.InfallibleMarkAsPendingPresAttributeEvaluation(); + if (auto* doc = GetComposedDoc()) { + doc->ScheduleForPresAttrEvaluation(this); + } + } + } + + MaybeDispatchLoginManagerEvents(mForm); + + if (IsInComposedDoc()) { + if (CreatesDateTimeWidget(oldType)) { + if (!CreatesDateTimeWidget()) { + // Switch away from date/time type. + NotifyUAWidgetTeardown(); + } else { + // Switch between date and time. + NotifyUAWidgetSetupOrChange(); + } + } else if (CreatesDateTimeWidget()) { + // Switch to date/time type. + AttachAndSetUAShadowRoot(NotifyUAWidgetSetup::Yes, DelegatesFocus::Yes); + } + // If we're becoming a text control and have focus, make sure to show focus + // rings. + if (State().HasState(ElementState::FOCUS) && IsSingleLineTextControl() && + !IsSingleLineTextControl(/* aExcludePassword = */ false, oldType)) { + AddStates(ElementState::FOCUSRING); + } + } +} + +void HTMLInputElement::MaybeSnapToTickMark(Decimal& aValue) { + nsRangeFrame* rangeFrame = do_QueryFrame(GetPrimaryFrame()); + if (!rangeFrame) { + return; + } + auto tickMark = rangeFrame->NearestTickMark(aValue); + if (tickMark.isNaN()) { + return; + } + auto rangeFrameSize = CSSPixel::FromAppUnits(rangeFrame->GetSize()); + CSSCoord rangeTrackLength; + if (rangeFrame->IsHorizontal()) { + rangeTrackLength = rangeFrameSize.width; + } else { + rangeTrackLength = rangeFrameSize.height; + } + auto stepBase = GetStepBase(); + auto distanceToTickMark = + rangeTrackLength * float(rangeFrame->GetDoubleAsFractionOfRange( + stepBase + (tickMark - aValue).abs())); + const CSSCoord magnetEffectRange( + StaticPrefs::dom_range_element_magnet_effect_threshold()); + if (distanceToTickMark <= magnetEffectRange) { + aValue = tickMark; + } +} + +void HTMLInputElement::SanitizeValue(nsAString& aValue, + SanitizationKind aKind) const { + NS_ASSERTION(mDoneCreating, "The element creation should be finished!"); + + switch (mType) { + case FormControlType::InputText: + case FormControlType::InputSearch: + case FormControlType::InputTel: + case FormControlType::InputPassword: { + aValue.StripCRLF(); + } break; + case FormControlType::InputEmail: { + aValue.StripCRLF(); + aValue = nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>( + aValue); + + if (Multiple() && !aValue.IsEmpty()) { + nsAutoString oldValue(aValue); + HTMLSplitOnSpacesTokenizer tokenizer(oldValue, ','); + aValue.Truncate(0); + aValue.Append(tokenizer.nextToken()); + while (tokenizer.hasMoreTokens() || + tokenizer.separatorAfterCurrentToken()) { + aValue.Append(','); + aValue.Append(tokenizer.nextToken()); + } + } + } break; + case FormControlType::InputUrl: { + aValue.StripCRLF(); + + aValue = nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>( + aValue); + } break; + case FormControlType::InputNumber: { + if (aKind == SanitizationKind::ForValueSetter && !aValue.IsEmpty() && + (aValue.First() == '+' || aValue.Last() == '.')) { + // A value with a leading plus or trailing dot should fail to parse. + // However, the localized parser accepts this, and when we convert it + // back to a Decimal, it disappears. So, we need to check first. + // + // FIXME(emilio): Should we just use the unlocalized parser + // (StringToDecimal) for the value setter? Other browsers don't seem to + // allow setting localized strings there, and that way we don't need + // this special-case. + aValue.Truncate(); + return; + } + + InputType::StringToNumberResult result = + mInputType->ConvertStringToNumber(aValue); + if (!result.mResult.isFinite()) { + aValue.Truncate(); + return; + } + switch (aKind) { + case SanitizationKind::ForValueGetter: { + // If the default non-localized algorithm parses the value, then we're + // done, don't un-localize it, to avoid precision loss, and to + // preserve scientific notation as well for example. + if (!result.mLocalized) { + return; + } + // For the <input type=number> value getter, we return the unlocalized + // value if it doesn't parse as StringToDecimal, for compat with other + // browsers. + char buf[32]; + DebugOnly<bool> ok = result.mResult.toString(buf, ArrayLength(buf)); + aValue.AssignASCII(buf); + MOZ_ASSERT(ok, "buf not big enough"); + break; + } + case SanitizationKind::ForDisplay: + case SanitizationKind::ForValueSetter: { + // We localize as needed, but if both the localized and unlocalized + // version parse with the generic parser, we just use the unlocalized + // one, to preserve the input as much as possible. + // + // FIXME(emilio, bug 1622808): Localization should ideally be more + // input-preserving. + nsString localizedValue; + mInputType->ConvertNumberToString(result.mResult, localizedValue); + if (!StringToDecimal(localizedValue).isFinite()) { + aValue = std::move(localizedValue); + } + break; + } + } + break; + } + case FormControlType::InputRange: { + Decimal minimum = GetMinimum(); + Decimal maximum = GetMaximum(); + MOZ_ASSERT(minimum.isFinite() && maximum.isFinite(), + "type=range should have a default maximum/minimum"); + + // We use this to avoid modifying the string unnecessarily, since that + // may introduce rounding. This is set to true only if the value we + // parse out from aValue needs to be sanitized. + bool needSanitization = false; + + Decimal value = mInputType->ConvertStringToNumber(aValue).mResult; + if (!value.isFinite()) { + needSanitization = true; + // Set value to midway between minimum and maximum. + value = maximum <= minimum ? minimum + : minimum + (maximum - minimum) / Decimal(2); + } else if (value < minimum || maximum < minimum) { + needSanitization = true; + value = minimum; + } else if (value > maximum) { + needSanitization = true; + value = maximum; + } + + Decimal step = GetStep(); + if (step != kStepAny) { + Decimal stepBase = GetStepBase(); + // There could be rounding issues below when dealing with fractional + // numbers, but let's ignore that until ECMAScript supplies us with a + // decimal number type. + Decimal deltaToStep = NS_floorModulo(value - stepBase, step); + if (deltaToStep != Decimal(0)) { + // "suffering from a step mismatch" + // Round the element's value to the nearest number for which the + // element would not suffer from a step mismatch, and which is + // greater than or equal to the minimum, and, if the maximum is not + // less than the minimum, which is less than or equal to the + // maximum, if there is a number that matches these constraints: + MOZ_ASSERT(deltaToStep > Decimal(0), + "stepBelow/stepAbove will be wrong"); + Decimal stepBelow = value - deltaToStep; + Decimal stepAbove = value - deltaToStep + step; + Decimal halfStep = step / Decimal(2); + bool stepAboveIsClosest = (stepAbove - value) <= halfStep; + bool stepAboveInRange = stepAbove >= minimum && stepAbove <= maximum; + bool stepBelowInRange = stepBelow >= minimum && stepBelow <= maximum; + + if ((stepAboveIsClosest || !stepBelowInRange) && stepAboveInRange) { + needSanitization = true; + value = stepAbove; + } else if ((!stepAboveIsClosest || !stepAboveInRange) && + stepBelowInRange) { + needSanitization = true; + value = stepBelow; + } + } + } + + if (needSanitization) { + char buf[32]; + DebugOnly<bool> ok = value.toString(buf, ArrayLength(buf)); + aValue.AssignASCII(buf); + MOZ_ASSERT(ok, "buf not big enough"); + } + } break; + case FormControlType::InputDate: { + if (!aValue.IsEmpty() && !IsValidDate(aValue)) { + aValue.Truncate(); + } + } break; + case FormControlType::InputTime: { + if (!aValue.IsEmpty() && !IsValidTime(aValue)) { + aValue.Truncate(); + } + } break; + case FormControlType::InputMonth: { + if (!aValue.IsEmpty() && !IsValidMonth(aValue)) { + aValue.Truncate(); + } + } break; + case FormControlType::InputWeek: { + if (!aValue.IsEmpty() && !IsValidWeek(aValue)) { + aValue.Truncate(); + } + } break; + case FormControlType::InputDatetimeLocal: { + if (!aValue.IsEmpty() && !IsValidDateTimeLocal(aValue)) { + aValue.Truncate(); + } else { + NormalizeDateTimeLocal(aValue); + } + } break; + case FormControlType::InputColor: { + if (IsValidSimpleColor(aValue)) { + ToLowerCase(aValue); + } else { + // Set default (black) color, if aValue wasn't parsed correctly. + aValue.AssignLiteral("#000000"); + } + } break; + default: + break; + } +} + +Maybe<nscolor> HTMLInputElement::ParseSimpleColor(const nsAString& aColor) { + // Input color string should be 7 length (i.e. a string representing a valid + // simple color) + if (aColor.Length() != 7 || aColor.First() != '#') { + return {}; + } + + const nsAString& withoutHash = StringTail(aColor, 6); + nscolor color; + if (!NS_HexToRGBA(withoutHash, nsHexColorType::NoAlpha, &color)) { + return {}; + } + + return Some(color); +} + +bool HTMLInputElement::IsValidSimpleColor(const nsAString& aValue) const { + if (aValue.Length() != 7 || aValue.First() != '#') { + return false; + } + + for (int i = 1; i < 7; ++i) { + if (!IsAsciiDigit(aValue[i]) && !(aValue[i] >= 'a' && aValue[i] <= 'f') && + !(aValue[i] >= 'A' && aValue[i] <= 'F')) { + return false; + } + } + return true; +} + +bool HTMLInputElement::IsLeapYear(uint32_t aYear) const { + if ((aYear % 4 == 0 && aYear % 100 != 0) || (aYear % 400 == 0)) { + return true; + } + return false; +} + +uint32_t HTMLInputElement::DayOfWeek(uint32_t aYear, uint32_t aMonth, + uint32_t aDay, bool isoWeek) const { + MOZ_ASSERT(1 <= aMonth && aMonth <= 12, "month is in 1..12"); + MOZ_ASSERT(1 <= aDay && aDay <= 31, "day is in 1..31"); + + // Tomohiko Sakamoto algorithm. + int monthTable[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; + aYear -= aMonth < 3; + + uint32_t day = (aYear + aYear / 4 - aYear / 100 + aYear / 400 + + monthTable[aMonth - 1] + aDay) % + 7; + + if (isoWeek) { + return ((day + 6) % 7) + 1; + } + + return day; +} + +uint32_t HTMLInputElement::MaximumWeekInYear(uint32_t aYear) const { + int day = DayOfWeek(aYear, 1, 1, true); // January 1. + // A year starting on Thursday or a leap year starting on Wednesday has 53 + // weeks. All other years have 52 weeks. + return day == 4 || (day == 3 && IsLeapYear(aYear)) ? kMaximumWeekInYear + : kMaximumWeekInYear - 1; +} + +bool HTMLInputElement::IsValidWeek(const nsAString& aValue) const { + uint32_t year, week; + return ParseWeek(aValue, &year, &week); +} + +bool HTMLInputElement::IsValidMonth(const nsAString& aValue) const { + uint32_t year, month; + return ParseMonth(aValue, &year, &month); +} + +bool HTMLInputElement::IsValidDate(const nsAString& aValue) const { + uint32_t year, month, day; + return ParseDate(aValue, &year, &month, &day); +} + +bool HTMLInputElement::IsValidDateTimeLocal(const nsAString& aValue) const { + uint32_t year, month, day, time; + return ParseDateTimeLocal(aValue, &year, &month, &day, &time); +} + +bool HTMLInputElement::ParseYear(const nsAString& aValue, + uint32_t* aYear) const { + if (aValue.Length() < 4) { + return false; + } + + return DigitSubStringToNumber(aValue, 0, aValue.Length(), aYear) && + *aYear > 0; +} + +bool HTMLInputElement::ParseMonth(const nsAString& aValue, uint32_t* aYear, + uint32_t* aMonth) const { + // Parse the year, month values out a string formatted as 'yyyy-mm'. + if (aValue.Length() < 7) { + return false; + } + + uint32_t endOfYearOffset = aValue.Length() - 3; + if (aValue[endOfYearOffset] != '-') { + return false; + } + + const nsAString& yearStr = Substring(aValue, 0, endOfYearOffset); + if (!ParseYear(yearStr, aYear)) { + return false; + } + + return DigitSubStringToNumber(aValue, endOfYearOffset + 1, 2, aMonth) && + *aMonth > 0 && *aMonth <= 12; +} + +bool HTMLInputElement::ParseWeek(const nsAString& aValue, uint32_t* aYear, + uint32_t* aWeek) const { + // Parse the year, month values out a string formatted as 'yyyy-Www'. + if (aValue.Length() < 8) { + return false; + } + + uint32_t endOfYearOffset = aValue.Length() - 4; + if (aValue[endOfYearOffset] != '-') { + return false; + } + + if (aValue[endOfYearOffset + 1] != 'W') { + return false; + } + + const nsAString& yearStr = Substring(aValue, 0, endOfYearOffset); + if (!ParseYear(yearStr, aYear)) { + return false; + } + + return DigitSubStringToNumber(aValue, endOfYearOffset + 2, 2, aWeek) && + *aWeek > 0 && *aWeek <= MaximumWeekInYear(*aYear); +} + +bool HTMLInputElement::ParseDate(const nsAString& aValue, uint32_t* aYear, + uint32_t* aMonth, uint32_t* aDay) const { + /* + * Parse the year, month, day values out a date string formatted as + * yyyy-mm-dd. -The year must be 4 or more digits long, and year > 0 -The + * month must be exactly 2 digits long, and 01 <= month <= 12 -The day must be + * exactly 2 digit long, and 01 <= day <= maxday Where maxday is the number of + * days in the month 'month' and year 'year' + */ + if (aValue.Length() < 10) { + return false; + } + + uint32_t endOfMonthOffset = aValue.Length() - 3; + if (aValue[endOfMonthOffset] != '-') { + return false; + } + + const nsAString& yearMonthStr = Substring(aValue, 0, endOfMonthOffset); + if (!ParseMonth(yearMonthStr, aYear, aMonth)) { + return false; + } + + return DigitSubStringToNumber(aValue, endOfMonthOffset + 1, 2, aDay) && + *aDay > 0 && *aDay <= NumberOfDaysInMonth(*aMonth, *aYear); +} + +bool HTMLInputElement::ParseDateTimeLocal(const nsAString& aValue, + uint32_t* aYear, uint32_t* aMonth, + uint32_t* aDay, + uint32_t* aTime) const { + // Parse the year, month, day and time values out a string formatted as + // 'yyyy-mm-ddThh:mm[:ss.s] or 'yyyy-mm-dd hh:mm[:ss.s]', where fractions of + // seconds can be 1 to 3 digits. + // The minimum length allowed is 16, which is of the form 'yyyy-mm-ddThh:mm' + // or 'yyyy-mm-dd hh:mm'. + if (aValue.Length() < 16) { + return false; + } + + int32_t sepIndex = aValue.FindChar('T'); + if (sepIndex == -1) { + sepIndex = aValue.FindChar(' '); + + if (sepIndex == -1) { + return false; + } + } + + const nsAString& dateStr = Substring(aValue, 0, sepIndex); + if (!ParseDate(dateStr, aYear, aMonth, aDay)) { + return false; + } + + const nsAString& timeStr = + Substring(aValue, sepIndex + 1, aValue.Length() - sepIndex + 1); + if (!ParseTime(timeStr, aTime)) { + return false; + } + + return true; +} + +void HTMLInputElement::NormalizeDateTimeLocal(nsAString& aValue) const { + if (aValue.IsEmpty()) { + return; + } + + // Use 'T' as the separator between date string and time string. + int32_t sepIndex = aValue.FindChar(' '); + if (sepIndex != -1) { + aValue.ReplaceLiteral(sepIndex, 1, u"T"); + } else { + sepIndex = aValue.FindChar('T'); + } + + // Time expressed as the shortest possible string, which is hh:mm. + if ((aValue.Length() - sepIndex) == 6) { + return; + } + + // Fractions of seconds part is optional, ommit it if it's 0. + if ((aValue.Length() - sepIndex) > 9) { + const uint32_t millisecSepIndex = sepIndex + 9; + uint32_t milliseconds; + if (!DigitSubStringToNumber(aValue, millisecSepIndex + 1, + aValue.Length() - (millisecSepIndex + 1), + &milliseconds)) { + return; + } + + if (milliseconds != 0) { + return; + } + + aValue.Cut(millisecSepIndex, aValue.Length() - millisecSepIndex); + } + + // Seconds part is optional, ommit it if it's 0. + const uint32_t secondSepIndex = sepIndex + 6; + uint32_t seconds; + if (!DigitSubStringToNumber(aValue, secondSepIndex + 1, + aValue.Length() - (secondSepIndex + 1), + &seconds)) { + return; + } + + if (seconds != 0) { + return; + } + + aValue.Cut(secondSepIndex, aValue.Length() - secondSepIndex); +} + +double HTMLInputElement::DaysSinceEpochFromWeek(uint32_t aYear, + uint32_t aWeek) const { + double days = JS::DayFromYear(aYear) + (aWeek - 1) * 7; + uint32_t dayOneIsoWeekday = DayOfWeek(aYear, 1, 1, true); + + // If day one of that year is on/before Thursday, we should subtract the + // days that belong to last year in our first week, otherwise, our first + // days belong to last year's last week, and we should add those days + // back. + if (dayOneIsoWeekday <= 4) { + days -= (dayOneIsoWeekday - 1); + } else { + days += (7 - dayOneIsoWeekday + 1); + } + + return days; +} + +uint32_t HTMLInputElement::NumberOfDaysInMonth(uint32_t aMonth, + uint32_t aYear) const { + /* + * Returns the number of days in a month. + * Months that are |longMonths| always have 31 days. + * Months that are not |longMonths| have 30 days except February (month 2). + * February has 29 days during leap years which are years that are divisible + * by 400. or divisible by 100 and 4. February has 28 days otherwise. + */ + + static const bool longMonths[] = {true, false, true, false, true, false, + true, true, false, true, false, true}; + MOZ_ASSERT(aMonth <= 12 && aMonth > 0); + + if (longMonths[aMonth - 1]) { + return 31; + } + + if (aMonth != 2) { + return 30; + } + + return IsLeapYear(aYear) ? 29 : 28; +} + +/* static */ +bool HTMLInputElement::DigitSubStringToNumber(const nsAString& aStr, + uint32_t aStart, uint32_t aLen, + uint32_t* aRetVal) { + MOZ_ASSERT(aStr.Length() > (aStart + aLen - 1)); + + for (uint32_t offset = 0; offset < aLen; ++offset) { + if (!IsAsciiDigit(aStr[aStart + offset])) { + return false; + } + } + + nsresult ec; + *aRetVal = static_cast<uint32_t>( + PromiseFlatString(Substring(aStr, aStart, aLen)).ToInteger(&ec)); + + return NS_SUCCEEDED(ec); +} + +bool HTMLInputElement::IsValidTime(const nsAString& aValue) const { + return ParseTime(aValue, nullptr); +} + +/* static */ +bool HTMLInputElement::ParseTime(const nsAString& aValue, uint32_t* aResult) { + /* The string must have the following parts: + * - HOURS: two digits, value being in [0, 23]; + * - Colon (:); + * - MINUTES: two digits, value being in [0, 59]; + * - Optional: + * - Colon (:); + * - SECONDS: two digits, value being in [0, 59]; + * - Optional: + * - DOT (.); + * - FRACTIONAL SECONDS: one to three digits, no value range. + */ + + // The following format is the shorter one allowed: "HH:MM". + if (aValue.Length() < 5) { + return false; + } + + uint32_t hours; + if (!DigitSubStringToNumber(aValue, 0, 2, &hours) || hours > 23) { + return false; + } + + // Hours/minutes separator. + if (aValue[2] != ':') { + return false; + } + + uint32_t minutes; + if (!DigitSubStringToNumber(aValue, 3, 2, &minutes) || minutes > 59) { + return false; + } + + if (aValue.Length() == 5) { + if (aResult) { + *aResult = ((hours * 60) + minutes) * 60000; + } + return true; + } + + // The following format is the next shorter one: "HH:MM:SS". + if (aValue.Length() < 8 || aValue[5] != ':') { + return false; + } + + uint32_t seconds; + if (!DigitSubStringToNumber(aValue, 6, 2, &seconds) || seconds > 59) { + return false; + } + + if (aValue.Length() == 8) { + if (aResult) { + *aResult = (((hours * 60) + minutes) * 60 + seconds) * 1000; + } + return true; + } + + // The string must follow this format now: "HH:MM:SS.{s,ss,sss}". + // There can be 1 to 3 digits for the fractions of seconds. + if (aValue.Length() == 9 || aValue.Length() > 12 || aValue[8] != '.') { + return false; + } + + uint32_t fractionsSeconds; + if (!DigitSubStringToNumber(aValue, 9, aValue.Length() - 9, + &fractionsSeconds)) { + return false; + } + + if (aResult) { + *aResult = (((hours * 60) + minutes) * 60 + seconds) * 1000 + + // NOTE: there is 10.0 instead of 10 and static_cast<int> because + // some old [and stupid] compilers can't just do the right thing. + fractionsSeconds * + pow(10.0, static_cast<int>(3 - (aValue.Length() - 9))); + } + + return true; +} + +/* static */ +bool HTMLInputElement::IsDateTimeTypeSupported( + FormControlType aDateTimeInputType) { + switch (aDateTimeInputType) { + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputDatetimeLocal: + return true; + case FormControlType::InputMonth: + case FormControlType::InputWeek: + return StaticPrefs::dom_forms_datetime_others(); + default: + return false; + } +} + +void HTMLInputElement::GetLastInteractiveValue(nsAString& aValue) { + if (mLastValueChangeWasInteractive) { + return GetValue(aValue, CallerType::System); + } + if (TextControlState* state = GetEditorState()) { + return aValue.Assign( + state->LastInteractiveValueIfLastChangeWasNonInteractive()); + } + aValue.Truncate(); +} + +bool HTMLInputElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + // We can't make these static_asserts because kInputDefaultType and + // kInputTypeTable aren't constexpr. + MOZ_ASSERT( + FormControlType(kInputDefaultType->value) == FormControlType::InputText, + "Someone forgot to update kInputDefaultType when adding a new " + "input type."); + MOZ_ASSERT(kInputTypeTable[ArrayLength(kInputTypeTable) - 1].tag == nullptr, + "Last entry in the table must be the nullptr guard"); + MOZ_ASSERT(FormControlType( + kInputTypeTable[ArrayLength(kInputTypeTable) - 2].value) == + FormControlType::InputText, + "Next to last entry in the table must be the \"text\" entry"); + + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::type) { + aResult.ParseEnumValue(aValue, kInputTypeTable, false, kInputDefaultType); + auto newType = FormControlType(aResult.GetEnumValue()); + if (IsDateTimeInputType(newType) && !IsDateTimeTypeSupported(newType)) { + // There's no public way to set an nsAttrValue to an enum value, but we + // can just re-parse with a table that doesn't have any types other than + // "text" in it. + aResult.ParseEnumValue(aValue, kInputDefaultType, false, + kInputDefaultType); + } + + return true; + } + if (aAttribute == nsGkAtoms::width) { + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::height) { + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::maxlength) { + return aResult.ParseNonNegativeIntValue(aValue); + } + if (aAttribute == nsGkAtoms::minlength) { + return aResult.ParseNonNegativeIntValue(aValue); + } + if (aAttribute == nsGkAtoms::size) { + return aResult.ParsePositiveIntValue(aValue); + } + if (aAttribute == nsGkAtoms::align) { + return ParseAlignValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::formmethod) { + return aResult.ParseEnumValue(aValue, kFormMethodTable, false); + } + if (aAttribute == nsGkAtoms::formenctype) { + return aResult.ParseEnumValue(aValue, kFormEnctypeTable, false); + } + if (aAttribute == nsGkAtoms::autocomplete) { + aResult.ParseAtomArray(aValue); + return true; + } + if (aAttribute == nsGkAtoms::capture) { + return aResult.ParseEnumValue(aValue, kCaptureTable, false, + kCaptureDefault); + } + if (ParseImageAttribute(aAttribute, aValue, aResult)) { + // We have to call |ParseImageAttribute| unconditionally since we + // don't know if we're going to have a type="image" attribute yet, + // (or could have it set dynamically in the future). See bug + // 214077. + return true; + } + } + + return TextControlElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLInputElement::ImageInputMapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + nsGenericHTMLFormControlElementWithState::MapImageBorderAttributeInto( + aBuilder); + nsGenericHTMLFormControlElementWithState::MapImageMarginAttributeInto( + aBuilder); + nsGenericHTMLFormControlElementWithState::MapImageSizeAttributesInto( + aBuilder, MapAspectRatio::Yes); + // Images treat align as "float" + nsGenericHTMLFormControlElementWithState::MapImageAlignAttributeInto( + aBuilder); + nsGenericHTMLFormControlElementWithState::MapCommonAttributesInto(aBuilder); +} + +nsChangeHint HTMLInputElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + nsChangeHint retval = + nsGenericHTMLFormControlElementWithState::GetAttributeChangeHint( + aAttribute, aModType); + + const bool isAdditionOrRemoval = + aModType == MutationEvent_Binding::ADDITION || + aModType == MutationEvent_Binding::REMOVAL; + + const bool reconstruct = [&] { + if (aAttribute == nsGkAtoms::type) { + return true; + } + + if (PlaceholderApplies() && aAttribute == nsGkAtoms::placeholder && + isAdditionOrRemoval) { + // We need to re-create our placeholder text. + return true; + } + + if (mType == FormControlType::InputFile && + aAttribute == nsGkAtoms::webkitdirectory) { + // The presence or absence of the 'directory' attribute determines what + // value we show in the file label when empty, via GetDisplayFileName. + return true; + } + + if (mType == FormControlType::InputImage && isAdditionOrRemoval && + (aAttribute == nsGkAtoms::alt || aAttribute == nsGkAtoms::value)) { + // We might need to rebuild our alt text. Just go ahead and + // reconstruct our frame. This should be quite rare.. + return true; + } + return false; + }(); + + if (reconstruct) { + retval |= nsChangeHint_ReconstructFrame; + } else if (aAttribute == nsGkAtoms::value) { + retval |= NS_STYLE_HINT_REFLOW; + } else if (aAttribute == nsGkAtoms::size && IsSingleLineTextControl(false)) { + retval |= NS_STYLE_HINT_REFLOW; + } + + return retval; +} + +NS_IMETHODIMP_(bool) +HTMLInputElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::align}, + {nullptr}, + }; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + sImageMarginSizeAttributeMap, + sImageBorderAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLInputElement::GetAttributeMappingFunction() + const { + // GetAttributeChangeHint guarantees that changes to mType will trigger a + // reframe, and we update the mapping function in our mapped attrs when our + // type changes, so it's safe to condition our attribute mapping function on + // mType. + if (mType == FormControlType::InputImage) { + return &ImageInputMapAttributesIntoRule; + } + + return &MapCommonAttributesInto; +} + +// Directory picking methods: + +already_AddRefed<Promise> HTMLInputElement::GetFilesAndDirectories( + ErrorResult& aRv) { + if (mType != FormControlType::InputFile) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return nullptr; + } + + nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject(); + MOZ_ASSERT(global); + if (!global) { + return nullptr; + } + + RefPtr<Promise> p = Promise::Create(global, aRv); + if (aRv.Failed()) { + return nullptr; + } + + const nsTArray<OwningFileOrDirectory>& filesAndDirs = + GetFilesOrDirectoriesInternal(); + + Sequence<OwningFileOrDirectory> filesAndDirsSeq; + + if (!filesAndDirsSeq.SetLength(filesAndDirs.Length(), fallible)) { + p->MaybeReject(NS_ERROR_OUT_OF_MEMORY); + return p.forget(); + } + + for (uint32_t i = 0; i < filesAndDirs.Length(); ++i) { + if (filesAndDirs[i].IsDirectory()) { + RefPtr<Directory> directory = filesAndDirs[i].GetAsDirectory(); + + // In future we could refactor SetFilePickerFiltersFromAccept to return a + // semicolon separated list of file extensions and include that in the + // filter string passed here. + directory->SetContentFilters(u"filter-out-sensitive"_ns); + filesAndDirsSeq[i].SetAsDirectory() = directory; + } else { + MOZ_ASSERT(filesAndDirs[i].IsFile()); + + // This file was directly selected by the user, so don't filter it. + filesAndDirsSeq[i].SetAsFile() = filesAndDirs[i].GetAsFile(); + } + } + + p->MaybeResolve(filesAndDirsSeq); + return p.forget(); +} + +// Controllers Methods + +nsIControllers* HTMLInputElement::GetControllers(ErrorResult& aRv) { + // XXX: what about type "file"? + if (IsSingleLineTextControl(false)) { + if (!mControllers) { + mControllers = new nsXULControllers(); + if (!mControllers) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<nsBaseCommandController> commandController = + nsBaseCommandController::CreateEditorController(); + if (!commandController) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + mControllers->AppendController(commandController); + + commandController = nsBaseCommandController::CreateEditingController(); + if (!commandController) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + mControllers->AppendController(commandController); + } + } + + return mControllers; +} + +nsresult HTMLInputElement::GetControllers(nsIControllers** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + ErrorResult rv; + RefPtr<nsIControllers> controller = GetControllers(rv); + controller.forget(aResult); + return rv.StealNSResult(); +} + +int32_t HTMLInputElement::InputTextLength(CallerType aCallerType) { + nsAutoString val; + GetValue(val, aCallerType); + return val.Length(); +} + +void HTMLInputElement::SetSelectionRange(uint32_t aSelectionStart, + uint32_t aSelectionEnd, + const Optional<nsAString>& aDirection, + ErrorResult& aRv) { + if (!SupportsTextSelection()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + TextControlState* state = GetEditorState(); + MOZ_ASSERT(state, "SupportsTextSelection() returned true!"); + state->SetSelectionRange(aSelectionStart, aSelectionEnd, aDirection, aRv); +} + +void HTMLInputElement::SetRangeText(const nsAString& aReplacement, + ErrorResult& aRv) { + if (!SupportsTextSelection()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + TextControlState* state = GetEditorState(); + MOZ_ASSERT(state, "SupportsTextSelection() returned true!"); + state->SetRangeText(aReplacement, aRv); +} + +void HTMLInputElement::SetRangeText(const nsAString& aReplacement, + uint32_t aStart, uint32_t aEnd, + SelectionMode aSelectMode, + ErrorResult& aRv) { + if (!SupportsTextSelection()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + TextControlState* state = GetEditorState(); + MOZ_ASSERT(state, "SupportsTextSelection() returned true!"); + state->SetRangeText(aReplacement, aStart, aEnd, aSelectMode, aRv); +} + +void HTMLInputElement::GetValueFromSetRangeText(nsAString& aValue) { + GetNonFileValueInternal(aValue); +} + +nsresult HTMLInputElement::SetValueFromSetRangeText(const nsAString& aValue) { + return SetValueInternal(aValue, {ValueSetterOption::ByContentAPI, + ValueSetterOption::BySetRangeTextAPI, + ValueSetterOption::SetValueChanged}); +} + +Nullable<uint32_t> HTMLInputElement::GetSelectionStart(ErrorResult& aRv) { + if (!SupportsTextSelection()) { + return Nullable<uint32_t>(); + } + + uint32_t selStart = GetSelectionStartIgnoringType(aRv); + if (aRv.Failed()) { + return Nullable<uint32_t>(); + } + + return Nullable<uint32_t>(selStart); +} + +uint32_t HTMLInputElement::GetSelectionStartIgnoringType(ErrorResult& aRv) { + uint32_t selEnd, selStart; + GetSelectionRange(&selStart, &selEnd, aRv); + return selStart; +} + +void HTMLInputElement::SetSelectionStart( + const Nullable<uint32_t>& aSelectionStart, ErrorResult& aRv) { + if (!SupportsTextSelection()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + TextControlState* state = GetEditorState(); + MOZ_ASSERT(state, "SupportsTextSelection() returned true!"); + state->SetSelectionStart(aSelectionStart, aRv); +} + +Nullable<uint32_t> HTMLInputElement::GetSelectionEnd(ErrorResult& aRv) { + if (!SupportsTextSelection()) { + return Nullable<uint32_t>(); + } + + uint32_t selEnd = GetSelectionEndIgnoringType(aRv); + if (aRv.Failed()) { + return Nullable<uint32_t>(); + } + + return Nullable<uint32_t>(selEnd); +} + +uint32_t HTMLInputElement::GetSelectionEndIgnoringType(ErrorResult& aRv) { + uint32_t selEnd, selStart; + GetSelectionRange(&selStart, &selEnd, aRv); + return selEnd; +} + +void HTMLInputElement::SetSelectionEnd(const Nullable<uint32_t>& aSelectionEnd, + ErrorResult& aRv) { + if (!SupportsTextSelection()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + TextControlState* state = GetEditorState(); + MOZ_ASSERT(state, "SupportsTextSelection() returned true!"); + state->SetSelectionEnd(aSelectionEnd, aRv); +} + +void HTMLInputElement::GetSelectionRange(uint32_t* aSelectionStart, + uint32_t* aSelectionEnd, + ErrorResult& aRv) { + TextControlState* state = GetEditorState(); + if (!state) { + // Not a text control. + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + state->GetSelectionRange(aSelectionStart, aSelectionEnd, aRv); +} + +void HTMLInputElement::GetSelectionDirection(nsAString& aDirection, + ErrorResult& aRv) { + if (!SupportsTextSelection()) { + aDirection.SetIsVoid(true); + return; + } + + TextControlState* state = GetEditorState(); + MOZ_ASSERT(state, "SupportsTextSelection came back true!"); + state->GetSelectionDirectionString(aDirection, aRv); +} + +void HTMLInputElement::SetSelectionDirection(const nsAString& aDirection, + ErrorResult& aRv) { + if (!SupportsTextSelection()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + TextControlState* state = GetEditorState(); + MOZ_ASSERT(state, "SupportsTextSelection came back true!"); + state->SetSelectionDirection(aDirection, aRv); +} + +// https://html.spec.whatwg.org/multipage/input.html#dom-input-showpicker +void HTMLInputElement::ShowPicker(ErrorResult& aRv) { + // Step 1. If this is not mutable, then throw an "InvalidStateError" + // DOMException. + if (!IsMutable()) { + return aRv.ThrowInvalidStateError( + "This input is either disabled or readonly."); + } + + // Step 2. If this's relevant settings object's origin is not same origin with + // this's relevant settings object's top-level origin, and this's type + // attribute is not in the File Upload state or Color state, then throw a + // "SecurityError" DOMException. + if (mType != FormControlType::InputFile && + mType != FormControlType::InputColor) { + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + WindowGlobalChild* windowGlobalChild = + window ? window->GetWindowGlobalChild() : nullptr; + if (!windowGlobalChild || !windowGlobalChild->SameOriginWithTop()) { + return aRv.ThrowSecurityError( + "Call was blocked because the current origin isn't same-origin with " + "top."); + } + } + + // Step 3. If this's relevant global object does not have transient + // activation, then throw a "NotAllowedError" DOMException. + if (!OwnerDoc()->HasValidTransientUserGestureActivation()) { + return aRv.ThrowNotAllowedError( + "Call was blocked due to lack of user activation."); + } + + // Step 4. Show the picker, if applicable, for this. + // + // https://html.spec.whatwg.org/multipage/input.html#show-the-picker,-if-applicable + // To show the picker, if applicable for an input element element: + + // Step 1. Assert: element's relevant global object has transient activation. + // Step 2. If element is not mutable, then return. + // (See above.) + + // Step 3. If element's type attribute is in the File Upload state, then run + // these steps in parallel: + if (mType == FormControlType::InputFile) { + FilePickerType type = FILE_PICKER_FILE; + if (StaticPrefs::dom_webkitBlink_dirPicker_enabled() && + HasAttr(nsGkAtoms::webkitdirectory)) { + type = FILE_PICKER_DIRECTORY; + } + InitFilePicker(type); + return; + } + + // Step 4. Otherwise, the user agent should show any relevant user interface + // for selecting a value for element, in the way it normally would when the + // user interacts with the control + if (mType == FormControlType::InputColor) { + InitColorPicker(); + return; + } + + if (!IsInComposedDoc()) { + return; + } + + if (IsDateTimeTypeSupported(mType)) { + if (CreatesDateTimeWidget()) { + if (RefPtr<Element> dateTimeBoxElement = GetDateTimeBoxElement()) { + // Event is dispatched to closed-shadow tree and doesn't bubble. + RefPtr<Document> doc = dateTimeBoxElement->OwnerDoc(); + nsContentUtils::DispatchTrustedEvent(doc, dateTimeBoxElement, + u"MozDateTimeShowPickerForJS"_ns, + CanBubble::eNo, Cancelable::eNo); + } + } else { + DateTimeValue value; + GetDateTimeInputBoxValue(value); + OpenDateTimePicker(value); + } + } +} + +#ifdef ACCESSIBILITY +/*static*/ nsresult FireEventForAccessibility(HTMLInputElement* aTarget, + EventMessage aEventMessage) { + Element* element = static_cast<Element*>(aTarget); + return nsContentUtils::DispatchTrustedEvent<WidgetEvent>( + element->OwnerDoc(), element, aEventMessage, CanBubble::eYes, + Cancelable::eYes); +} +#endif + +void HTMLInputElement::UpdateApzAwareFlag() { +#if !defined(ANDROID) && !defined(XP_MACOSX) + if (mType == FormControlType::InputNumber || + mType == FormControlType::InputRange) { + SetMayBeApzAware(); + } +#endif +} + +nsresult HTMLInputElement::SetDefaultValueAsValue() { + NS_ASSERTION(GetValueMode() == VALUE_MODE_VALUE, + "GetValueMode() should return VALUE_MODE_VALUE!"); + + // The element has a content attribute value different from it's value when + // it's in the value mode value. + nsAutoString resetVal; + GetDefaultValue(resetVal); + + // SetValueInternal is going to sanitize the value. + // TODO(mbrodesser): sanitizing will only happen if `mDoneCreating` is true. + return SetValueInternal(resetVal, ValueSetterOption::ByInternalAPI); +} + +// https://html.spec.whatwg.org/#auto-directionality +void HTMLInputElement::SetAutoDirectionality(bool aNotify, + const nsAString* aKnownValue) { + if (!IsAutoDirectionalityAssociated()) { + return SetDirectionality(GetParentDirectionality(this), aNotify); + } + nsAutoString value; + if (!aKnownValue) { + // It's unclear if per spec we should use the sanitized or unsanitized + // value to set the directionality, but aKnownValue is unsanitized, so be + // consistent. Using what the user is seeing to determine directionality + // instead of the sanitized (empty if invalid) value probably makes more + // sense. + GetValueInternal(value, CallerType::System); + aKnownValue = &value; + } + SetDirectionalityFromValue(this, *aKnownValue, aNotify); +} + +NS_IMETHODIMP +HTMLInputElement::Reset() { + // We should be able to reset all dirty flags regardless of the type. + SetCheckedChanged(false); + SetValueChanged(false); + SetLastValueChangeWasInteractive(false); + SetUserInteracted(false); + + switch (GetValueMode()) { + case VALUE_MODE_VALUE: { + nsresult result = SetDefaultValueAsValue(); + if (CreatesDateTimeWidget()) { + // mFocusedValue has to be set here, so that `FireChangeEventIfNeeded` + // can fire a change event if necessary. + GetValue(mFocusedValue, CallerType::System); + } + return result; + } + case VALUE_MODE_DEFAULT_ON: + DoSetChecked(DefaultChecked(), true, false); + return NS_OK; + case VALUE_MODE_FILENAME: + ClearFiles(false); + return NS_OK; + case VALUE_MODE_DEFAULT: + default: + return NS_OK; + } +} + +NS_IMETHODIMP +HTMLInputElement::SubmitNamesValues(FormData* aFormData) { + // For type=reset, and type=button, we just never submit, period. + // For type=image and type=button, we only submit if we were the button + // pressed + // For type=radio and type=checkbox, we only submit if checked=true + if (mType == FormControlType::InputReset || + mType == FormControlType::InputButton || + ((mType == FormControlType::InputSubmit || + mType == FormControlType::InputImage) && + aFormData->GetSubmitterElement() != this) || + ((mType == FormControlType::InputRadio || + mType == FormControlType::InputCheckbox) && + !mChecked)) { + return NS_OK; + } + + // Get the name + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + + // Submit .x, .y for input type=image + if (mType == FormControlType::InputImage) { + // Get a property set by the frame to find out where it was clicked. + const auto* lastClickedPoint = + static_cast<CSSIntPoint*>(GetProperty(nsGkAtoms::imageClickedPoint)); + int32_t x, y; + if (lastClickedPoint) { + // Convert the values to strings for submission + x = lastClickedPoint->x; + y = lastClickedPoint->y; + } else { + x = y = 0; + } + + nsAutoString xVal, yVal; + xVal.AppendInt(x); + yVal.AppendInt(y); + + if (!name.IsEmpty()) { + aFormData->AddNameValuePair(name + u".x"_ns, xVal); + aFormData->AddNameValuePair(name + u".y"_ns, yVal); + } else { + // If the Image Element has no name, simply return x and y + // to Nav and IE compatibility. + aFormData->AddNameValuePair(u"x"_ns, xVal); + aFormData->AddNameValuePair(u"y"_ns, yVal); + } + + return NS_OK; + } + + // If name not there, don't submit + if (name.IsEmpty()) { + return NS_OK; + } + + // + // Submit file if its input type=file and this encoding method accepts files + // + if (mType == FormControlType::InputFile) { + // Submit files + + const nsTArray<OwningFileOrDirectory>& files = + GetFilesOrDirectoriesInternal(); + + if (files.IsEmpty()) { + NS_ENSURE_STATE(GetOwnerGlobal()); + ErrorResult rv; + RefPtr<Blob> blob = Blob::CreateStringBlob( + GetOwnerGlobal(), ""_ns, u"application/octet-stream"_ns); + RefPtr<File> file = blob->ToFile(u""_ns, rv); + + if (!rv.Failed()) { + aFormData->AddNameBlobPair(name, file); + } + + return rv.StealNSResult(); + } + + for (uint32_t i = 0; i < files.Length(); ++i) { + if (files[i].IsFile()) { + aFormData->AddNameBlobPair(name, files[i].GetAsFile()); + } else { + MOZ_ASSERT(files[i].IsDirectory()); + aFormData->AddNameDirectoryPair(name, files[i].GetAsDirectory()); + } + } + + return NS_OK; + } + + if (mType == FormControlType::InputHidden && + name.LowerCaseEqualsLiteral("_charset_")) { + nsCString charset; + aFormData->GetCharset(charset); + return aFormData->AddNameValuePair(name, NS_ConvertASCIItoUTF16(charset)); + } + + // + // Submit name=value + // + + // Get the value + nsAutoString value; + GetValue(value, CallerType::System); + + if (mType == FormControlType::InputSubmit && value.IsEmpty() && + !HasAttr(nsGkAtoms::value)) { + // Get our default value, which is the same as our default label + nsAutoString defaultValue; + nsContentUtils::GetMaybeLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "Submit", OwnerDoc(), defaultValue); + value = defaultValue; + } + + const nsresult rv = aFormData->AddNameValuePair(name, value); + if (NS_FAILED(rv)) { + return rv; + } + + // Submit dirname=dir + if (IsAutoDirectionalityAssociated()) { + return SubmitDirnameDir(aFormData); + } + + return NS_OK; +} + +static nsTArray<FileContentData> SaveFileContentData( + const nsTArray<OwningFileOrDirectory>& aArray) { + nsTArray<FileContentData> res(aArray.Length()); + for (const auto& it : aArray) { + if (it.IsFile()) { + RefPtr<BlobImpl> impl = it.GetAsFile()->Impl(); + res.AppendElement(std::move(impl)); + } else { + MOZ_ASSERT(it.IsDirectory()); + nsString fullPath; + nsresult rv = it.GetAsDirectory()->GetFullRealPath(fullPath); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + res.AppendElement(std::move(fullPath)); + } + } + return res; +} + +void HTMLInputElement::SaveState() { + PresState* state = nullptr; + switch (GetValueMode()) { + case VALUE_MODE_DEFAULT_ON: + if (mCheckedChanged) { + state = GetPrimaryPresState(); + if (!state) { + return; + } + + state->contentData() = CheckedContentData(mChecked); + } + break; + case VALUE_MODE_FILENAME: + if (!mFileData->mFilesOrDirectories.IsEmpty()) { + state = GetPrimaryPresState(); + if (!state) { + return; + } + + state->contentData() = + SaveFileContentData(mFileData->mFilesOrDirectories); + } + break; + case VALUE_MODE_VALUE: + case VALUE_MODE_DEFAULT: + // VALUE_MODE_DEFAULT shouldn't have their value saved except 'hidden', + // mType should have never been FormControlType::InputPassword and value + // should have changed. + if ((GetValueMode() == VALUE_MODE_DEFAULT && + mType != FormControlType::InputHidden) || + mHasBeenTypePassword || !mValueChanged) { + break; + } + + state = GetPrimaryPresState(); + if (!state) { + return; + } + + nsAutoString value; + GetValue(value, CallerType::System); + + if (!IsSingleLineTextControl(false) && + NS_FAILED(nsLinebreakConverter::ConvertStringLineBreaks( + value, nsLinebreakConverter::eLinebreakPlatform, + nsLinebreakConverter::eLinebreakContent))) { + NS_ERROR("Converting linebreaks failed!"); + return; + } + + state->contentData() = + TextContentData(value, mLastValueChangeWasInteractive); + break; + } + + if (mDisabledChanged) { + if (!state) { + state = GetPrimaryPresState(); + } + if (state) { + // We do not want to save the real disabled state but the disabled + // attribute. + state->disabled() = HasAttr(nsGkAtoms::disabled); + state->disabledSet() = true; + } + } +} + +void HTMLInputElement::DoneCreatingElement() { + mDoneCreating = true; + + // + // Restore state as needed. Note that disabled state applies to all control + // types. + // + bool restoredCheckedState = false; + if (!mInhibitRestoration) { + GenerateStateKey(); + restoredCheckedState = RestoreFormControlState(); + } + + // + // If restore does not occur, we initialize .checked using the CHECKED + // property. + // + if (!restoredCheckedState && mShouldInitChecked) { + DoSetChecked(DefaultChecked(), false, false); + } + + // Sanitize the value and potentially set mFocusedValue. + if (GetValueMode() == VALUE_MODE_VALUE) { + nsAutoString value; + GetValue(value, CallerType::System); + // TODO: What should we do if SetValueInternal fails? (The allocation + // may potentially be big, but most likely we've failed to allocate + // before the type change.) + SetValueInternal(value, ValueSetterOption::ByInternalAPI); + + if (CreatesDateTimeWidget()) { + // mFocusedValue has to be set here, so that `FireChangeEventIfNeeded` can + // fire a change event if necessary. + mFocusedValue = value; + } + } + + mShouldInitChecked = false; +} + +void HTMLInputElement::DestroyContent() { + nsImageLoadingContent::Destroy(); + TextControlElement::DestroyContent(); +} + +void HTMLInputElement::UpdateValidityElementStates(bool aNotify) { + AutoStateChangeNotifier notifier(*this, aNotify); + RemoveStatesSilently(ElementState::VALIDITY_STATES); + if (!IsCandidateForConstraintValidation()) { + return; + } + ElementState state; + if (IsValid()) { + state |= ElementState::VALID; + if (mUserInteracted) { + state |= ElementState::USER_VALID; + } + } else { + state |= ElementState::INVALID; + if (mUserInteracted) { + state |= ElementState::USER_INVALID; + } + } + AddStatesSilently(state); +} + +static nsTArray<OwningFileOrDirectory> RestoreFileContentData( + nsPIDOMWindowInner* aWindow, const nsTArray<FileContentData>& aData) { + nsTArray<OwningFileOrDirectory> res(aData.Length()); + for (const auto& it : aData) { + if (it.type() == FileContentData::TBlobImpl) { + if (!it.get_BlobImpl()) { + // Serialization failed, skip this file. + continue; + } + + RefPtr<File> file = File::Create(aWindow->AsGlobal(), it.get_BlobImpl()); + if (NS_WARN_IF(!file)) { + continue; + } + + OwningFileOrDirectory* element = res.AppendElement(); + element->SetAsFile() = file; + } else { + MOZ_ASSERT(it.type() == FileContentData::TnsString); + nsCOMPtr<nsIFile> file; + nsresult rv = + NS_NewLocalFile(it.get_nsString(), true, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + RefPtr<Directory> directory = + Directory::Create(aWindow->AsGlobal(), file); + MOZ_ASSERT(directory); + + OwningFileOrDirectory* element = res.AppendElement(); + element->SetAsDirectory() = directory; + } + } + return res; +} + +bool HTMLInputElement::RestoreState(PresState* aState) { + bool restoredCheckedState = false; + + const PresContentData& inputState = aState->contentData(); + + switch (GetValueMode()) { + case VALUE_MODE_DEFAULT_ON: + if (inputState.type() == PresContentData::TCheckedContentData) { + restoredCheckedState = true; + bool checked = inputState.get_CheckedContentData().checked(); + DoSetChecked(checked, true, true); + } + break; + case VALUE_MODE_FILENAME: + if (inputState.type() == PresContentData::TArrayOfFileContentData) { + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (window) { + nsTArray<OwningFileOrDirectory> array = + RestoreFileContentData(window, inputState); + SetFilesOrDirectories(array, true); + } + } + break; + case VALUE_MODE_VALUE: + case VALUE_MODE_DEFAULT: + if (GetValueMode() == VALUE_MODE_DEFAULT && + mType != FormControlType::InputHidden) { + break; + } + + if (inputState.type() == PresContentData::TTextContentData) { + // TODO: What should we do if SetValueInternal fails? (The allocation + // may potentially be big, but most likely we've failed to allocate + // before the type change.) + SetValueInternal(inputState.get_TextContentData().value(), + ValueSetterOption::SetValueChanged); + if (inputState.get_TextContentData().lastValueChangeWasInteractive()) { + SetLastValueChangeWasInteractive(true); + } + } + break; + } + + if (aState->disabledSet() && !aState->disabled()) { + SetDisabled(false, IgnoreErrors()); + } + + return restoredCheckedState; +} + +/* + * Radio group stuff + */ + +void HTMLInputElement::AddToRadioGroup() { + MOZ_ASSERT(!mRadioGroupContainer, + "Radio button must be removed from previous radio group container " + "before being added to another!"); + + // If the element has no radio group container we can stop here. + auto* container = FindTreeRadioGroupContainer(); + if (!container) { + return; + } + + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + // If we are part of a radio group, the element must have a name. + MOZ_ASSERT(!name.IsEmpty()); + + // + // Add the radio to the radio group container. + // + container->AddToRadioGroup(name, this, mForm); + mRadioGroupContainer = container; + + // + // If the input element is checked, and we add it to the group, it will + // deselect whatever is currently selected in that group + // + if (mChecked) { + // + // If it is checked, call "RadioSetChecked" to perform the selection/ + // deselection ritual. This has the side effect of repainting the + // radio button, but as adding a checked radio button into the group + // should not be that common an occurrence, I think we can live with + // that. + // Make sure not to notify if we're still being created. + // + RadioSetChecked(mDoneCreating); + } else { + bool indeterminate = !container->GetCurrentRadioButton(name); + SetStates(ElementState::INDETERMINATE, indeterminate, mDoneCreating); + } + + // + // For integrity purposes, we have to ensure that "checkedChanged" is + // the same for this new element as for all the others in the group + // + bool checkedChanged = mCheckedChanged; + + nsCOMPtr<nsIRadioVisitor> visitor = + new nsRadioGetCheckedChangedVisitor(&checkedChanged, this); + VisitGroup(visitor); + + SetCheckedChangedInternal(checkedChanged); + + // We initialize the validity of the element to the validity of the group + // because we assume UpdateValueMissingState() will be called after. + SetValidityState(VALIDITY_STATE_VALUE_MISSING, + container->GetValueMissingState(name)); +} + +void HTMLInputElement::RemoveFromRadioGroup() { + auto* container = GetCurrentRadioGroupContainer(); + if (!container) { + return; + } + + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + + // If this button was checked, we need to notify the group that there is no + // longer a selected radio button + if (mChecked) { + container->SetCurrentRadioButton(name, nullptr); + nsCOMPtr<nsIRadioVisitor> visitor = new nsRadioUpdateStateVisitor(this); + VisitGroup(visitor); + } else { + AddStates(ElementState::INDETERMINATE); + } + + // Remove this radio from its group in the container. + // We need to call UpdateValueMissingValidityStateForRadio before to make sure + // the group validity is updated (with this element being ignored). + UpdateValueMissingValidityStateForRadio(true); + container->RemoveFromRadioGroup(name, this); + mRadioGroupContainer = nullptr; +} + +bool HTMLInputElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable( + aWithMouse, aIsFocusable, aTabIndex)) { + return true; + } + + if (IsDisabled()) { + *aIsFocusable = false; + return true; + } + + if (IsSingleLineTextControl(false) || mType == FormControlType::InputRange) { + *aIsFocusable = true; + return false; + } + + const bool defaultFocusable = IsFormControlDefaultFocusable(aWithMouse); + if (CreatesDateTimeWidget()) { + if (aTabIndex) { + // We only want our native anonymous child to be tabable to, not ourself. + *aTabIndex = -1; + } + *aIsFocusable = true; + return true; + } + + if (mType == FormControlType::InputHidden) { + if (aTabIndex) { + *aTabIndex = -1; + } + *aIsFocusable = false; + return false; + } + + if (!aTabIndex) { + // The other controls are all focusable + *aIsFocusable = defaultFocusable; + return false; + } + + if (mType != FormControlType::InputRadio) { + *aIsFocusable = defaultFocusable; + return false; + } + + if (mChecked) { + // Selected radio buttons are tabbable + *aIsFocusable = defaultFocusable; + return false; + } + + // Current radio button is not selected. + // But make it tabbable if nothing in group is selected. + auto* container = GetCurrentRadioGroupContainer(); + if (!container) { + *aIsFocusable = defaultFocusable; + return false; + } + + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + + if (container->GetCurrentRadioButton(name)) { + *aTabIndex = -1; + } + *aIsFocusable = defaultFocusable; + return false; +} + +nsresult HTMLInputElement::VisitGroup(nsIRadioVisitor* aVisitor) { + if (auto* container = GetCurrentRadioGroupContainer()) { + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + return container->WalkRadioGroup(name, aVisitor); + } + + aVisitor->Visit(this); + return NS_OK; +} + +HTMLInputElement::ValueModeType HTMLInputElement::GetValueMode() const { + switch (mType) { + case FormControlType::InputHidden: + case FormControlType::InputSubmit: + case FormControlType::InputButton: + case FormControlType::InputReset: + case FormControlType::InputImage: + return VALUE_MODE_DEFAULT; + case FormControlType::InputCheckbox: + case FormControlType::InputRadio: + return VALUE_MODE_DEFAULT_ON; + case FormControlType::InputFile: + return VALUE_MODE_FILENAME; +#ifdef DEBUG + case FormControlType::InputText: + case FormControlType::InputPassword: + case FormControlType::InputSearch: + case FormControlType::InputTel: + case FormControlType::InputEmail: + case FormControlType::InputUrl: + case FormControlType::InputNumber: + case FormControlType::InputRange: + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputColor: + case FormControlType::InputMonth: + case FormControlType::InputWeek: + case FormControlType::InputDatetimeLocal: + return VALUE_MODE_VALUE; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected input type in GetValueMode()"); + return VALUE_MODE_VALUE; +#else // DEBUG + default: + return VALUE_MODE_VALUE; +#endif // DEBUG + } +} + +bool HTMLInputElement::IsMutable() const { + return !IsDisabled() && + !(DoesReadOnlyApply() && State().HasState(ElementState::READONLY)); +} + +bool HTMLInputElement::DoesRequiredApply() const { + switch (mType) { + case FormControlType::InputHidden: + case FormControlType::InputButton: + case FormControlType::InputImage: + case FormControlType::InputReset: + case FormControlType::InputSubmit: + case FormControlType::InputRange: + case FormControlType::InputColor: + return false; +#ifdef DEBUG + case FormControlType::InputRadio: + case FormControlType::InputCheckbox: + case FormControlType::InputFile: + case FormControlType::InputText: + case FormControlType::InputPassword: + case FormControlType::InputSearch: + case FormControlType::InputTel: + case FormControlType::InputEmail: + case FormControlType::InputUrl: + case FormControlType::InputNumber: + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputMonth: + case FormControlType::InputWeek: + case FormControlType::InputDatetimeLocal: + return true; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected input type in DoesRequiredApply()"); + return true; +#else // DEBUG + default: + return true; +#endif // DEBUG + } +} + +bool HTMLInputElement::PlaceholderApplies() const { + if (IsDateTimeInputType(mType)) { + return false; + } + return IsSingleLineTextControl(false); +} + +bool HTMLInputElement::DoesMinMaxApply() const { + switch (mType) { + case FormControlType::InputNumber: + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputRange: + case FormControlType::InputMonth: + case FormControlType::InputWeek: + case FormControlType::InputDatetimeLocal: + return true; +#ifdef DEBUG + case FormControlType::InputReset: + case FormControlType::InputSubmit: + case FormControlType::InputImage: + case FormControlType::InputButton: + case FormControlType::InputHidden: + case FormControlType::InputRadio: + case FormControlType::InputCheckbox: + case FormControlType::InputFile: + case FormControlType::InputText: + case FormControlType::InputPassword: + case FormControlType::InputSearch: + case FormControlType::InputTel: + case FormControlType::InputEmail: + case FormControlType::InputUrl: + case FormControlType::InputColor: + return false; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected input type in DoesRequiredApply()"); + return false; +#else // DEBUG + default: + return false; +#endif // DEBUG + } +} + +bool HTMLInputElement::DoesAutocompleteApply() const { + switch (mType) { + case FormControlType::InputHidden: + case FormControlType::InputText: + case FormControlType::InputSearch: + case FormControlType::InputUrl: + case FormControlType::InputTel: + case FormControlType::InputEmail: + case FormControlType::InputPassword: + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputNumber: + case FormControlType::InputRange: + case FormControlType::InputColor: + case FormControlType::InputMonth: + case FormControlType::InputWeek: + case FormControlType::InputDatetimeLocal: + return true; +#ifdef DEBUG + case FormControlType::InputReset: + case FormControlType::InputSubmit: + case FormControlType::InputImage: + case FormControlType::InputButton: + case FormControlType::InputRadio: + case FormControlType::InputCheckbox: + case FormControlType::InputFile: + return false; + default: + MOZ_ASSERT_UNREACHABLE( + "Unexpected input type in DoesAutocompleteApply()"); + return false; +#else // DEBUG + default: + return false; +#endif // DEBUG + } +} + +Decimal HTMLInputElement::GetStep() const { + MOZ_ASSERT(DoesStepApply(), "GetStep() can only be called if @step applies"); + + if (!HasAttr(nsGkAtoms::step)) { + return GetDefaultStep() * GetStepScaleFactor(); + } + + nsAutoString stepStr; + GetAttr(nsGkAtoms::step, stepStr); + + if (stepStr.LowerCaseEqualsLiteral("any")) { + // The element can't suffer from step mismatch if there is no step. + return kStepAny; + } + + Decimal step = StringToDecimal(stepStr); + if (!step.isFinite() || step <= Decimal(0)) { + step = GetDefaultStep(); + } + + // For input type=date, we round the step value to have a rounded day. + if (mType == FormControlType::InputDate || + mType == FormControlType::InputMonth || + mType == FormControlType::InputWeek) { + step = std::max(step.round(), Decimal(1)); + } + + return step * GetStepScaleFactor(); +} + +// ConstraintValidation + +void HTMLInputElement::SetCustomValidity(const nsAString& aError) { + ConstraintValidation::SetCustomValidity(aError); + UpdateValidityElementStates(true); +} + +bool HTMLInputElement::IsTooLong() { + if (!mValueChanged || !mLastValueChangeWasInteractive) { + return false; + } + + return mInputType->IsTooLong(); +} + +bool HTMLInputElement::IsTooShort() { + if (!mValueChanged || !mLastValueChangeWasInteractive) { + return false; + } + + return mInputType->IsTooShort(); +} + +bool HTMLInputElement::IsValueMissing() const { + // Should use UpdateValueMissingValidityStateForRadio() for type radio. + MOZ_ASSERT(mType != FormControlType::InputRadio); + + return mInputType->IsValueMissing(); +} + +bool HTMLInputElement::HasTypeMismatch() const { + return mInputType->HasTypeMismatch(); +} + +Maybe<bool> HTMLInputElement::HasPatternMismatch() const { + return mInputType->HasPatternMismatch(); +} + +bool HTMLInputElement::IsRangeOverflow() const { + return mInputType->IsRangeOverflow(); +} + +bool HTMLInputElement::IsRangeUnderflow() const { + return mInputType->IsRangeUnderflow(); +} + +bool HTMLInputElement::ValueIsStepMismatch(const Decimal& aValue) const { + if (aValue.isNaN()) { + // The element can't suffer from step mismatch if its value isn't a + // number. + return false; + } + + Decimal step = GetStep(); + if (step == kStepAny) { + return false; + } + + // Value has to be an integral multiple of step. + return NS_floorModulo(aValue - GetStepBase(), step) != Decimal(0); +} + +bool HTMLInputElement::HasStepMismatch() const { + return mInputType->HasStepMismatch(); +} + +bool HTMLInputElement::HasBadInput() const { return mInputType->HasBadInput(); } + +void HTMLInputElement::UpdateTooLongValidityState() { + SetValidityState(VALIDITY_STATE_TOO_LONG, IsTooLong()); +} + +void HTMLInputElement::UpdateTooShortValidityState() { + SetValidityState(VALIDITY_STATE_TOO_SHORT, IsTooShort()); +} + +void HTMLInputElement::UpdateValueMissingValidityStateForRadio( + bool aIgnoreSelf) { + MOZ_ASSERT(mType == FormControlType::InputRadio, + "This should be called only for radio input types"); + + HTMLInputElement* selection = GetSelectedRadioButton(); + + // If there is no selection, that might mean the radio is not in a group. + // In that case, we can look for the checked state of the radio. + bool selected = selection || (!aIgnoreSelf && mChecked); + bool required = !aIgnoreSelf && IsRequired(); + + auto* container = GetCurrentRadioGroupContainer(); + if (!container) { + SetValidityState(VALIDITY_STATE_VALUE_MISSING, false); + return; + } + + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + + // If the current radio is required and not ignored, we can assume the entire + // group is required. + if (!required) { + required = (aIgnoreSelf && IsRequired()) + ? container->GetRequiredRadioCount(name) - 1 + : container->GetRequiredRadioCount(name); + } + + bool valueMissing = required && !selected; + if (container->GetValueMissingState(name) != valueMissing) { + container->SetValueMissingState(name, valueMissing); + + SetValidityState(VALIDITY_STATE_VALUE_MISSING, valueMissing); + + // nsRadioSetValueMissingState will call ElementStateChanged while visiting. + nsAutoScriptBlocker scriptBlocker; + nsCOMPtr<nsIRadioVisitor> visitor = + new nsRadioSetValueMissingState(this, valueMissing); + VisitGroup(visitor); + } +} + +void HTMLInputElement::UpdateValueMissingValidityState() { + if (mType == FormControlType::InputRadio) { + UpdateValueMissingValidityStateForRadio(false); + return; + } + + SetValidityState(VALIDITY_STATE_VALUE_MISSING, IsValueMissing()); +} + +void HTMLInputElement::UpdateTypeMismatchValidityState() { + SetValidityState(VALIDITY_STATE_TYPE_MISMATCH, HasTypeMismatch()); +} + +void HTMLInputElement::UpdatePatternMismatchValidityState() { + Maybe<bool> hasMismatch = HasPatternMismatch(); + // Don't update if the JS engine failed to evaluate it. + if (hasMismatch.isSome()) { + SetValidityState(VALIDITY_STATE_PATTERN_MISMATCH, hasMismatch.value()); + } +} + +void HTMLInputElement::UpdateRangeOverflowValidityState() { + SetValidityState(VALIDITY_STATE_RANGE_OVERFLOW, IsRangeOverflow()); + UpdateInRange(true); +} + +void HTMLInputElement::UpdateRangeUnderflowValidityState() { + SetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW, IsRangeUnderflow()); + UpdateInRange(true); +} + +void HTMLInputElement::UpdateStepMismatchValidityState() { + SetValidityState(VALIDITY_STATE_STEP_MISMATCH, HasStepMismatch()); +} + +void HTMLInputElement::UpdateBadInputValidityState() { + SetValidityState(VALIDITY_STATE_BAD_INPUT, HasBadInput()); +} + +void HTMLInputElement::UpdateAllValidityStates(bool aNotify) { + bool validBefore = IsValid(); + UpdateAllValidityStatesButNotElementState(); + if (validBefore != IsValid()) { + UpdateValidityElementStates(aNotify); + } +} + +void HTMLInputElement::UpdateAllValidityStatesButNotElementState() { + UpdateTooLongValidityState(); + UpdateTooShortValidityState(); + UpdateValueMissingValidityState(); + UpdateTypeMismatchValidityState(); + UpdatePatternMismatchValidityState(); + UpdateRangeOverflowValidityState(); + UpdateRangeUnderflowValidityState(); + UpdateStepMismatchValidityState(); + UpdateBadInputValidityState(); +} + +void HTMLInputElement::UpdateBarredFromConstraintValidation() { + // NOTE: readonly attribute causes an element to be barred from constraint + // validation even if it doesn't apply to that input type. That's rather + // weird, but pre-existing behavior. + bool wasCandidate = IsCandidateForConstraintValidation(); + SetBarredFromConstraintValidation( + mType == FormControlType::InputHidden || + mType == FormControlType::InputButton || + mType == FormControlType::InputReset || IsDisabled() || + HasAttr(nsGkAtoms::readonly) || + HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR)); + if (IsCandidateForConstraintValidation() != wasCandidate) { + UpdateInRange(true); + } +} + +nsresult HTMLInputElement::GetValidationMessage(nsAString& aValidationMessage, + ValidityStateType aType) { + return mInputType->GetValidationMessage(aValidationMessage, aType); +} + +bool HTMLInputElement::IsSingleLineTextControl() const { + return IsSingleLineTextControl(false); +} + +bool HTMLInputElement::IsTextArea() const { return false; } + +bool HTMLInputElement::IsPasswordTextControl() const { + return mType == FormControlType::InputPassword; +} + +int32_t HTMLInputElement::GetCols() { + // Else we know (assume) it is an input with size attr + const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::size); + if (attr && attr->Type() == nsAttrValue::eInteger) { + int32_t cols = attr->GetIntegerValue(); + if (cols > 0) { + return cols; + } + } + + return DEFAULT_COLS; +} + +int32_t HTMLInputElement::GetWrapCols() { + return 0; // only textarea's can have wrap cols +} + +int32_t HTMLInputElement::GetRows() { return DEFAULT_ROWS; } + +void HTMLInputElement::GetDefaultValueFromContent(nsAString& aValue, + bool aForDisplay) { + if (!GetEditorState()) { + return; + } + GetDefaultValue(aValue); + // This is called by the frame to show the value. + // We have to sanitize it when needed. + // FIXME: Do we want to sanitize even when aForDisplay is false? + if (mDoneCreating) { + SanitizeValue(aValue, aForDisplay ? SanitizationKind::ForDisplay + : SanitizationKind::ForValueGetter); + } +} + +bool HTMLInputElement::ValueChanged() const { return mValueChanged; } + +void HTMLInputElement::GetTextEditorValue(nsAString& aValue) const { + if (TextControlState* state = GetEditorState()) { + state->GetValue(aValue, /* aIgnoreWrap = */ true, /* aForDisplay = */ true); + } +} + +void HTMLInputElement::InitializeKeyboardEventListeners() { + TextControlState* state = GetEditorState(); + if (state) { + state->InitializeKeyboardEventListeners(); + } +} + +void HTMLInputElement::UpdatePlaceholderShownState() { + SetStates(ElementState::PLACEHOLDER_SHOWN, + IsValueEmpty() && PlaceholderApplies() && + HasAttr(nsGkAtoms::placeholder)); +} + +void HTMLInputElement::OnValueChanged(ValueChangeKind aKind, + bool aNewValueEmpty, + const nsAString* aKnownNewValue) { + MOZ_ASSERT_IF(aKnownNewValue, aKnownNewValue->IsEmpty() == aNewValueEmpty); + if (aKind != ValueChangeKind::Internal) { + mLastValueChangeWasInteractive = aKind == ValueChangeKind::UserInteraction; + } + + if (aNewValueEmpty != IsValueEmpty()) { + SetStates(ElementState::VALUE_EMPTY, aNewValueEmpty); + UpdatePlaceholderShownState(); + } + + UpdateAllValidityStates(true); + + if (HasDirAuto()) { + SetAutoDirectionality(true, aKnownNewValue); + } +} + +bool HTMLInputElement::HasCachedSelection() { + TextControlState* state = GetEditorState(); + if (!state) { + return false; + } + return state->IsSelectionCached() && state->HasNeverInitializedBefore() && + state->GetSelectionProperties().GetStart() != + state->GetSelectionProperties().GetEnd(); +} + +void HTMLInputElement::SetRevealPassword(bool aValue) { + if (NS_WARN_IF(mType != FormControlType::InputPassword)) { + return; + } + if (aValue == State().HasState(ElementState::REVEALED)) { + return; + } + RefPtr doc = OwnerDoc(); + // We allow chrome code to prevent this. This is important for about:logins, + // which may need to run some OS-dependent authentication code before + // revealing the saved passwords. + bool defaultAction = true; + nsContentUtils::DispatchEventOnlyToChrome( + doc, this, u"MozWillToggleReveal"_ns, CanBubble::eYes, Cancelable::eYes, + &defaultAction); + if (NS_WARN_IF(!defaultAction)) { + return; + } + SetStates(ElementState::REVEALED, aValue); +} + +bool HTMLInputElement::RevealPassword() const { + if (NS_WARN_IF(mType != FormControlType::InputPassword)) { + return false; + } + return State().HasState(ElementState::REVEALED); +} + +void HTMLInputElement::FieldSetDisabledChanged(bool aNotify) { + // This *has* to be called *before* UpdateBarredFromConstraintValidation and + // UpdateValueMissingValidityState because these two functions depend on our + // disabled state. + nsGenericHTMLFormControlElementWithState::FieldSetDisabledChanged(aNotify); + + UpdateValueMissingValidityState(); + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(aNotify); +} + +void HTMLInputElement::SetFilePickerFiltersFromAccept( + nsIFilePicker* filePicker) { + // We always add |filterAll| + filePicker->AppendFilters(nsIFilePicker::filterAll); + + NS_ASSERTION(HasAttr(nsGkAtoms::accept), + "You should not call SetFilePickerFiltersFromAccept if the" + " element has no accept attribute!"); + + // Services to retrieve image/*, audio/*, video/* filters + nsCOMPtr<nsIStringBundleService> stringService = + components::StringBundle::Service(); + if (!stringService) { + return; + } + nsCOMPtr<nsIStringBundle> filterBundle; + if (NS_FAILED(stringService->CreateBundle( + "chrome://global/content/filepicker.properties", + getter_AddRefs(filterBundle)))) { + return; + } + + // Service to retrieve mime type information for mime types filters + nsCOMPtr<nsIMIMEService> mimeService = do_GetService("@mozilla.org/mime;1"); + if (!mimeService) { + return; + } + + nsAutoString accept; + GetAttr(nsGkAtoms::accept, accept); + + HTMLSplitOnSpacesTokenizer tokenizer(accept, ','); + + nsTArray<nsFilePickerFilter> filters; + nsString allExtensionsList; + + // Retrieve all filters + while (tokenizer.hasMoreTokens()) { + const nsDependentSubstring& token = tokenizer.nextToken(); + + if (token.IsEmpty()) { + continue; + } + + int32_t filterMask = 0; + nsString filterName; + nsString extensionListStr; + + // First, check for image/audio/video filters... + if (token.EqualsLiteral("image/*")) { + filterMask = nsIFilePicker::filterImages; + filterBundle->GetStringFromName("imageFilter", extensionListStr); + } else if (token.EqualsLiteral("audio/*")) { + filterMask = nsIFilePicker::filterAudio; + filterBundle->GetStringFromName("audioFilter", extensionListStr); + } else if (token.EqualsLiteral("video/*")) { + filterMask = nsIFilePicker::filterVideo; + filterBundle->GetStringFromName("videoFilter", extensionListStr); + } else if (token.First() == '.') { + if (token.Contains(';') || token.Contains('*')) { + // Ignore this filter as it contains reserved characters + continue; + } + extensionListStr = u"*"_ns + token; + filterName = extensionListStr; + } else { + //... if no image/audio/video filter is found, check mime types filters + nsCOMPtr<nsIMIMEInfo> mimeInfo; + if (NS_FAILED( + mimeService->GetFromTypeAndExtension(NS_ConvertUTF16toUTF8(token), + ""_ns, // No extension + getter_AddRefs(mimeInfo))) || + !mimeInfo) { + continue; + } + + // Get a name for the filter: first try the description, then the mime + // type name if there is no description + mimeInfo->GetDescription(filterName); + if (filterName.IsEmpty()) { + nsCString mimeTypeName; + mimeInfo->GetType(mimeTypeName); + CopyUTF8toUTF16(mimeTypeName, filterName); + } + + // Get extension list + nsCOMPtr<nsIUTF8StringEnumerator> extensions; + mimeInfo->GetFileExtensions(getter_AddRefs(extensions)); + + bool hasMore; + while (NS_SUCCEEDED(extensions->HasMore(&hasMore)) && hasMore) { + nsCString extension; + if (NS_FAILED(extensions->GetNext(extension))) { + continue; + } + if (!extensionListStr.IsEmpty()) { + extensionListStr.AppendLiteral("; "); + } + extensionListStr += u"*."_ns + NS_ConvertUTF8toUTF16(extension); + } + } + + if (!filterMask && (extensionListStr.IsEmpty() || filterName.IsEmpty())) { + // No valid filter found + continue; + } + + // At this point we're sure the token represents a valid filter, so pass + // it directly as a raw filter. + filePicker->AppendRawFilter(token); + + // If we arrived here, that means we have a valid filter: let's create it + // and add it to our list, if no similar filter is already present + nsFilePickerFilter filter; + if (filterMask) { + filter = nsFilePickerFilter(filterMask); + } else { + filter = nsFilePickerFilter(filterName, extensionListStr); + } + + if (!filters.Contains(filter)) { + if (!allExtensionsList.IsEmpty()) { + allExtensionsList.AppendLiteral("; "); + } + allExtensionsList += extensionListStr; + filters.AppendElement(filter); + } + } + + // Remove similar filters + // Iterate over a copy, as we might modify the original filters list + const nsTArray<nsFilePickerFilter> filtersCopy = filters.Clone(); + for (uint32_t i = 0; i < filtersCopy.Length(); ++i) { + const nsFilePickerFilter& filterToCheck = filtersCopy[i]; + if (filterToCheck.mFilterMask) { + continue; + } + for (uint32_t j = 0; j < filtersCopy.Length(); ++j) { + if (i == j) { + continue; + } + // Check if this filter's extension list is a substring of the other one. + // e.g. if filters are "*.jpeg" and "*.jpeg; *.jpg" the first one should + // be removed. + // Add an extra "; " to be sure the check will work and avoid cases like + // "*.xls" being a subtring of "*.xslx" while those are two differents + // filters and none should be removed. + if (FindInReadable(filterToCheck.mFilter + u";"_ns, + filtersCopy[j].mFilter + u";"_ns)) { + // We already have a similar, less restrictive filter (i.e. + // filterToCheck extensionList is just a subset of another filter + // extension list): remove this one + filters.RemoveElement(filterToCheck); + } + } + } + + // Add "All Supported Types" filter + if (filters.Length() > 1) { + nsAutoString title; + nsContentUtils::GetLocalizedString(nsContentUtils::eFORMS_PROPERTIES, + "AllSupportedTypes", title); + filePicker->AppendFilter(title, allExtensionsList); + } + + // Add each filter + for (uint32_t i = 0; i < filters.Length(); ++i) { + const nsFilePickerFilter& filter = filters[i]; + if (filter.mFilterMask) { + filePicker->AppendFilters(filter.mFilterMask); + } else { + filePicker->AppendFilter(filter.mTitle, filter.mFilter); + } + } + + if (filters.Length() >= 1) { + // |filterAll| will always use index=0 so we need to set index=1 as the + // current filter. This will be "All Supported Types" for multiple filters. + filePicker->SetFilterIndex(1); + } +} + +Decimal HTMLInputElement::GetStepScaleFactor() const { + MOZ_ASSERT(DoesStepApply()); + + switch (mType) { + case FormControlType::InputDate: + return kStepScaleFactorDate; + case FormControlType::InputNumber: + case FormControlType::InputRange: + return kStepScaleFactorNumberRange; + case FormControlType::InputTime: + case FormControlType::InputDatetimeLocal: + return kStepScaleFactorTime; + case FormControlType::InputMonth: + return kStepScaleFactorMonth; + case FormControlType::InputWeek: + return kStepScaleFactorWeek; + default: + MOZ_ASSERT(false, "Unrecognized input type"); + return Decimal::nan(); + } +} + +Decimal HTMLInputElement::GetDefaultStep() const { + MOZ_ASSERT(DoesStepApply()); + + switch (mType) { + case FormControlType::InputDate: + case FormControlType::InputMonth: + case FormControlType::InputWeek: + case FormControlType::InputNumber: + case FormControlType::InputRange: + return kDefaultStep; + case FormControlType::InputTime: + case FormControlType::InputDatetimeLocal: + return kDefaultStepTime; + default: + MOZ_ASSERT(false, "Unrecognized input type"); + return Decimal::nan(); + } +} + +void HTMLInputElement::SetUserInteracted(bool aInteracted) { + if (mUserInteracted == aInteracted) { + return; + } + mUserInteracted = aInteracted; + UpdateValidityElementStates(true); +} + +void HTMLInputElement::UpdateInRange(bool aNotify) { + AutoStateChangeNotifier notifier(*this, aNotify); + RemoveStatesSilently(ElementState::INRANGE | ElementState::OUTOFRANGE); + if (!mHasRange || !IsCandidateForConstraintValidation()) { + return; + } + bool outOfRange = GetValidityState(VALIDITY_STATE_RANGE_OVERFLOW) || + GetValidityState(VALIDITY_STATE_RANGE_UNDERFLOW); + AddStatesSilently(outOfRange ? ElementState::OUTOFRANGE + : ElementState::INRANGE); +} + +void HTMLInputElement::UpdateHasRange(bool aNotify) { + // There is a range if min/max applies for the type and if the element + // currently have a valid min or max. + const bool newHasRange = [&] { + if (!DoesMinMaxApply()) { + return false; + } + return !GetMinimum().isNaN() || !GetMaximum().isNaN(); + }(); + + if (newHasRange == mHasRange) { + return; + } + + mHasRange = newHasRange; + UpdateInRange(aNotify); +} + +void HTMLInputElement::PickerClosed() { mPickerRunning = false; } + +JSObject* HTMLInputElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLInputElement_Binding::Wrap(aCx, this, aGivenProto); +} + +GetFilesHelper* HTMLInputElement::GetOrCreateGetFilesHelper(bool aRecursiveFlag, + ErrorResult& aRv) { + MOZ_ASSERT(mFileData); + + if (aRecursiveFlag) { + if (!mFileData->mGetFilesRecursiveHelper) { + mFileData->mGetFilesRecursiveHelper = GetFilesHelper::Create( + GetFilesOrDirectoriesInternal(), aRecursiveFlag, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + return mFileData->mGetFilesRecursiveHelper; + } + + if (!mFileData->mGetFilesNonRecursiveHelper) { + mFileData->mGetFilesNonRecursiveHelper = GetFilesHelper::Create( + GetFilesOrDirectoriesInternal(), aRecursiveFlag, aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + } + + return mFileData->mGetFilesNonRecursiveHelper; +} + +void HTMLInputElement::UpdateEntries( + const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories) { + MOZ_ASSERT(mFileData && mFileData->mEntries.IsEmpty()); + + nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject(); + MOZ_ASSERT(global); + + RefPtr<FileSystem> fs = FileSystem::Create(global); + if (NS_WARN_IF(!fs)) { + return; + } + + Sequence<RefPtr<FileSystemEntry>> entries; + for (uint32_t i = 0; i < aFilesOrDirectories.Length(); ++i) { + RefPtr<FileSystemEntry> entry = + FileSystemEntry::Create(global, aFilesOrDirectories[i], fs); + MOZ_ASSERT(entry); + + if (!entries.AppendElement(entry, fallible)) { + return; + } + } + + // The root fileSystem is a DirectoryEntry object that contains only the + // dropped fileEntry and directoryEntry objects. + fs->CreateRoot(entries); + + mFileData->mEntries = std::move(entries); +} + +void HTMLInputElement::GetWebkitEntries( + nsTArray<RefPtr<FileSystemEntry>>& aSequence) { + if (NS_WARN_IF(mType != FormControlType::InputFile)) { + return; + } + + Telemetry::Accumulate(Telemetry::BLINK_FILESYSTEM_USED, true); + aSequence.AppendElements(mFileData->mEntries); +} + +already_AddRefed<nsINodeList> HTMLInputElement::GetLabels() { + if (!IsLabelable()) { + return nullptr; + } + + return nsGenericHTMLElement::Labels(); +} + +void HTMLInputElement::MaybeFireInputPasswordRemoved() { + // We want this event to be fired only when the password field is removed + // from the DOM tree, not when it is released (ex, tab is closed). So don't + // fire an event when the password input field doesn't have a docshell. + Document* doc = GetComposedDoc(); + nsIDocShell* container = doc ? doc->GetDocShell() : nullptr; + if (!container) { + return; + } + + // Right now, only the password manager listens to the event and only listen + // to it under certain circumstances. So don't fire this event unless + // necessary. + if (!doc->ShouldNotifyFormOrPasswordRemoved()) { + return; + } + + AsyncEventDispatcher::RunDOMEventWhenSafe( + *this, u"DOMInputPasswordRemoved"_ns, CanBubble::eNo, + ChromeOnlyDispatch::eYes); +} + +} // namespace mozilla::dom + +#undef NS_ORIGINAL_CHECKED_VALUE diff --git a/dom/html/HTMLInputElement.h b/dom/html/HTMLInputElement.h new file mode 100644 index 0000000000..8805ce762b --- /dev/null +++ b/dom/html/HTMLInputElement.h @@ -0,0 +1,1679 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLInputElement_h +#define mozilla_dom_HTMLInputElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/Decimal.h" +#include "mozilla/Maybe.h" +#include "mozilla/TextControlElement.h" +#include "mozilla/TextControlState.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Variant.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/HTMLInputElementBinding.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/SingleLineTextInputTypes.h" +#include "mozilla/dom/NumericInputTypes.h" +#include "mozilla/dom/CheckableInputTypes.h" +#include "mozilla/dom/ButtonInputTypes.h" +#include "mozilla/dom/DateTimeInputTypes.h" +#include "mozilla/dom/ColorInputType.h" +#include "mozilla/dom/ConstraintValidation.h" +#include "mozilla/dom/FileInputType.h" +#include "mozilla/dom/HiddenInputType.h" +#include "mozilla/dom/RadioGroupContainer.h" +#include "nsGenericHTMLElement.h" +#include "nsImageLoadingContent.h" +#include "nsCOMPtr.h" +#include "nsIFilePicker.h" +#include "nsIContentPrefService2.h" +#include "nsContentUtils.h" + +class nsIEditor; +class nsIRadioVisitor; + +namespace mozilla { + +class EventChainPostVisitor; +class EventChainPreVisitor; + +namespace dom { + +class AfterSetFilesOrDirectoriesRunnable; +class Date; +class DispatchChangeEventCallback; +class File; +class FileList; +class FileSystemEntry; +class FormData; +class GetFilesHelper; +class InputType; + +/** + * A class we use to create a singleton object that is used to keep track of + * the last directory from which the user has picked files (via + * <input type=file>) on a per-domain basis. The implementation uses + * nsIContentPrefService2/NS_CONTENT_PREF_SERVICE_CONTRACTID to store the last + * directory per-domain, and to ensure that whether the directories are + * persistently saved (saved across sessions) or not honors whether or not the + * page is being viewed in private browsing. + */ +class UploadLastDir final : public nsIObserver, public nsSupportsWeakReference { + ~UploadLastDir() = default; + + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + /** + * Fetch the last used directory for this location from the content + * pref service, and display the file picker opened in that directory. + * + * @param aDoc current document + * @param aFilePicker the file picker to open + * @param aFpCallback the callback object to be run when the file is shown. + */ + nsresult FetchDirectoryAndDisplayPicker( + Document* aDoc, nsIFilePicker* aFilePicker, + nsIFilePickerShownCallback* aFpCallback); + + /** + * Store the last used directory for this location using the + * content pref service, if it is available + * @param aURI URI of the current page + * @param aDir Parent directory of the file(s)/directory chosen by the user + */ + nsresult StoreLastUsedDirectory(Document* aDoc, nsIFile* aDir); + + class ContentPrefCallback final : public nsIContentPrefCallback2 { + virtual ~ContentPrefCallback() = default; + + public: + ContentPrefCallback(nsIFilePicker* aFilePicker, + nsIFilePickerShownCallback* aFpCallback) + : mFilePicker(aFilePicker), mFpCallback(aFpCallback) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTPREFCALLBACK2 + + nsCOMPtr<nsIFilePicker> mFilePicker; + nsCOMPtr<nsIFilePickerShownCallback> mFpCallback; + nsCOMPtr<nsIContentPref> mResult; + }; +}; + +class HTMLInputElement final : public TextControlElement, + public nsImageLoadingContent, + public ConstraintValidation { + friend class AfterSetFilesOrDirectoriesCallback; + friend class DispatchChangeEventCallback; + friend class InputType; + + public: + using ConstraintValidation::GetValidationMessage; + using nsGenericHTMLFormControlElementWithState::GetForm; + using nsGenericHTMLFormControlElementWithState::GetFormAction; + using ValueSetterOption = TextControlState::ValueSetterOption; + using ValueSetterOptions = TextControlState::ValueSetterOptions; + + enum class FromClone { No, Yes }; + + HTMLInputElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser, + FromClone aFromClone = FromClone::No); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLInputElement, input) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + int32_t TabIndexDefault() override; + using nsGenericHTMLElement::Focus; + + // nsINode +#if !defined(ANDROID) && !defined(XP_MACOSX) + bool IsNodeApzAwareInternal() const override; +#endif + + // Element + bool IsInteractiveHTMLContent() const override; + + // nsGenericHTMLElement + bool IsDisabledForEvents(WidgetEvent* aEvent) override; + + // nsGenericHTMLFormElement + void SaveState() override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool RestoreState(PresState* aState) override; + + // EventTarget + void AsyncEventRunning(AsyncEventDispatcher* aEvent) override; + + // Overriden nsIFormControl methods + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD Reset() override; + NS_IMETHOD SubmitNamesValues(FormData* aFormData) override; + + void FieldSetDisabledChanged(bool aNotify) override; + + // nsIContent + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + // Note: if this returns false, then attributes may not yet be sanitized + // (per SetValueInternal's dependence on mDoneCreating). + bool IsDoneCreating() const { return mDoneCreating; } + + bool LastValueChangeWasInteractive() const { + return mLastValueChangeWasInteractive; + } + + void GetLastInteractiveValue(nsAString&); + + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + void LegacyPreActivationBehavior(EventChainVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT + void ActivationBehavior(EventChainPostVisitor& aVisitor) override; + void LegacyCanceledActivationBehavior( + EventChainPostVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult PreHandleEvent(EventChainVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT + nsresult MaybeHandleRadioButtonNavigation(EventChainPostVisitor&, + uint32_t aKeyCode); + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void PostHandleEventForRangeThumb(EventChainPostVisitor& aVisitor); + MOZ_CAN_RUN_SCRIPT + void StartRangeThumbDrag(WidgetGUIEvent* aEvent); + MOZ_CAN_RUN_SCRIPT + void FinishRangeThumbDrag(WidgetGUIEvent* aEvent = nullptr); + MOZ_CAN_RUN_SCRIPT + void CancelRangeThumbDrag(bool aIsForUserEvent = true); + + enum class SnapToTickMarks : bool { No, Yes }; + MOZ_CAN_RUN_SCRIPT + void SetValueOfRangeForUserEvent(Decimal aValue, + SnapToTickMarks = SnapToTickMarks::No); + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void DoneCreatingElement() override; + + void DestroyContent() override; + + void SetLastValueChangeWasInteractive(bool); + + // TextControlElement + bool IsSingleLineTextControlOrTextArea() const override { + return IsSingleLineTextControl(false); + } + void SetValueChanged(bool aValueChanged) override; + bool IsSingleLineTextControl() const override; + bool IsTextArea() const override; + bool IsPasswordTextControl() const override; + int32_t GetCols() override; + int32_t GetWrapCols() override; + int32_t GetRows() override; + void GetDefaultValueFromContent(nsAString& aValue, bool aForDisplay) override; + bool ValueChanged() const override; + void GetTextEditorValue(nsAString& aValue) const override; + MOZ_CAN_RUN_SCRIPT TextEditor* GetTextEditor() override; + TextEditor* GetTextEditorWithoutCreation() const override; + nsISelectionController* GetSelectionController() override; + nsFrameSelection* GetConstFrameSelection() override; + TextControlState* GetTextControlState() const override { + return GetEditorState(); + } + nsresult BindToFrame(nsTextControlFrame* aFrame) override; + MOZ_CAN_RUN_SCRIPT void UnbindFromFrame(nsTextControlFrame* aFrame) override; + MOZ_CAN_RUN_SCRIPT nsresult CreateEditor() override; + void SetPreviewValue(const nsAString& aValue) override; + void GetPreviewValue(nsAString& aValue) override; + void EnablePreview() override; + bool IsPreviewEnabled() override; + void InitializeKeyboardEventListeners() override; + void OnValueChanged(ValueChangeKind, bool aNewValueEmpty, + const nsAString* aKnownNewValue) override; + void GetValueFromSetRangeText(nsAString& aValue) override; + MOZ_CAN_RUN_SCRIPT nsresult + SetValueFromSetRangeText(const nsAString& aValue) override; + bool HasCachedSelection() override; + MOZ_CAN_RUN_SCRIPT void SetRevealPassword(bool aValue); + bool RevealPassword() const; + + // Methods for nsFormFillController so it can do selection operations on input + // types the HTML spec doesn't support them on, like "email". + uint32_t GetSelectionStartIgnoringType(ErrorResult& aRv); + uint32_t GetSelectionEndIgnoringType(ErrorResult& aRv); + + void GetDisplayFileName(nsAString& aFileName) const; + + const nsTArray<OwningFileOrDirectory>& GetFilesOrDirectoriesInternal() const; + + void SetFilesOrDirectories( + const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories, + bool aSetValueChanged); + void SetFiles(FileList* aFiles, bool aSetValueChanged); + + // This method is used for test only. Onces the data is set, a 'change' event + // is dispatched. + void MozSetDndFilesAndDirectories( + const nsTArray<OwningFileOrDirectory>& aSequence); + + // Called when a nsIFilePicker or a nsIColorPicker terminate. + void PickerClosed(); + + void SetCheckedChangedInternal(bool aCheckedChanged); + bool GetCheckedChanged() const { return mCheckedChanged; } + void AddToRadioGroup(); + void RemoveFromRadioGroup(); + void DisconnectRadioGroupContainer(); + + /** + * Helper function returning the currently selected button in the radio group. + * Returning null if the element is not a button or if there is no selectied + * button in the group. + * + * @return the selected button (or null). + */ + HTMLInputElement* GetSelectedRadioButton() const; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLInputElement, TextControlElement) + + static UploadLastDir* gUploadLastDir; + // create and destroy the static UploadLastDir object for remembering + // which directory was last used on a site-by-site basis + static void InitUploadLastDir(); + static void DestroyUploadLastDir(); + + // If the valueAsDate attribute should be enabled in webIDL + static bool ValueAsDateEnabled(JSContext* cx, JSObject* obj); + + void MaybeLoadImage(); + + bool HasPatternAttribute() const { return mHasPatternAttribute; } + + // nsIConstraintValidation + bool IsTooLong(); + bool IsTooShort(); + bool IsValueMissing() const; + bool HasTypeMismatch() const; + Maybe<bool> HasPatternMismatch() const; + bool IsRangeOverflow() const; + bool IsRangeUnderflow() const; + bool ValueIsStepMismatch(const Decimal& aValue) const; + bool HasStepMismatch() const; + bool HasBadInput() const; + void UpdateTooLongValidityState(); + void UpdateTooShortValidityState(); + void UpdateValueMissingValidityState(); + void UpdateTypeMismatchValidityState(); + void UpdatePatternMismatchValidityState(); + void UpdateRangeOverflowValidityState(); + void UpdateRangeUnderflowValidityState(); + void UpdateStepMismatchValidityState(); + void UpdateBadInputValidityState(); + void UpdatePlaceholderShownState(); + void UpdateCheckedState(bool aNotify); + void UpdateIndeterminateState(bool aNotify); + // Update all our validity states and then update our element state + // as needed. aNotify controls whether the element state update + // needs to notify. + void UpdateAllValidityStates(bool aNotify); + void UpdateValidityElementStates(bool aNotify); + MOZ_CAN_RUN_SCRIPT + void MaybeUpdateAllValidityStates(bool aNotify) { + // If you need to add new type which supports validationMessage, you should + // add test cases into test_MozEditableElement_setUserInput.html. + if (mType == FormControlType::InputEmail) { + UpdateAllValidityStates(aNotify); + } + } + + // Update all our validity states without updating element state. + // This should be called instead of UpdateAllValidityStates any time + // we're guaranteed that element state will be updated anyway. + void UpdateAllValidityStatesButNotElementState(); + void UpdateBarredFromConstraintValidation(); + nsresult GetValidationMessage(nsAString& aValidationMessage, + ValidityStateType aType) override; + + // Override SetCustomValidity so we update our state properly when it's called + // via bindings. + void SetCustomValidity(const nsAString& aError); + + /** + * Update the value missing validity state for radio elements when they have + * a group. + * + * @param aIgnoreSelf Whether the required attribute and the checked state + * of the current radio should be ignored. + * @note This method shouldn't be called if the radio element hasn't a group. + */ + void UpdateValueMissingValidityStateForRadio(bool aIgnoreSelf); + + /** + * Set filters to the filePicker according to the accept attribute value. + * + * See: + * http://dev.w3.org/html5/spec/forms.html#attr-input-accept + * + * @note You should not call this function if the element has no @accept. + * @note "All Files" filter is always set, no matter if there is a valid + * filter specified or not. + * @note If more than one valid filter is found, the "All Supported Types" + * filter is added, which is the concatenation of all valid filters. + * @note Duplicate filters and similar filters (i.e. filters whose file + * extensions already exist in another filter) are ignored. + * @note "All Files" filter will be selected by default if unknown mime types + * have been specified and no file extension filter has been specified. + * Otherwise, specified filter or "All Supported Types" filter will be + * selected by default. + * The logic behind is that having unknown mime type means we might restrict + * user's input too much, as some filters will be missing. + * However, if author has also specified some file extension filters, it's + * likely those are fallback for the unusual mime type we haven't been able + * to resolve; so it's better to select author specified filters in that case. + */ + void SetFilePickerFiltersFromAccept(nsIFilePicker* filePicker); + + void SetUserInteracted(bool) final; + + /** + * Fires change event if mFocusedValue and current value held are unequal and + * if a change event may be fired on bluring. + * Sets mFocusedValue to value, if a change event is fired. + */ + void FireChangeEventIfNeeded(); + + /** + * Returns the input element's value as a Decimal. + * Returns NaN if the current element's value is not a floating point number. + * + * @return the input element's value as a Decimal. + */ + Decimal GetValueAsDecimal() const; + + /** + * Returns the input's "minimum" (as defined by the HTML5 spec) as a double. + * Note this takes account of any default minimum that the type may have. + * Returns NaN if the min attribute isn't a valid floating point number and + * the input's type does not have a default minimum. + * + * NOTE: Only call this if you know DoesMinMaxApply() returns true. + */ + Decimal GetMinimum() const; + + /** + * Returns the input's "maximum" (as defined by the HTML5 spec) as a double. + * Note this takes account of any default maximum that the type may have. + * Returns NaN if the max attribute isn't a valid floating point number and + * the input's type does not have a default maximum. + * + * NOTE:Only call this if you know DoesMinMaxApply() returns true. + */ + Decimal GetMaximum() const; + + // WebIDL + + void GetAccept(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::accept, aValue); } + void SetAccept(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::accept, aValue, aRv); + } + + void GetAlt(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::alt, aValue); } + void SetAlt(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::alt, aValue, aRv); + } + + void GetAutocomplete(nsAString& aValue); + void SetAutocomplete(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::autocomplete, aValue, aRv); + } + + void GetAutocompleteInfo(Nullable<AutocompleteInfo>& aInfo); + + void GetCapture(nsAString& aValue); + void SetCapture(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::capture, aValue, aRv); + } + + bool DefaultChecked() const { return HasAttr(nsGkAtoms::checked); } + + void SetDefaultChecked(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::checked, aValue, aRv); + } + + bool Checked() const { return mChecked; } + void SetChecked(bool aChecked); + + bool IsRadioOrCheckbox() const { + return mType == FormControlType::InputCheckbox || + mType == FormControlType::InputRadio; + } + + bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); } + + void SetDisabled(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::disabled, aValue, aRv); + } + + FileList* GetFiles(); + void SetFiles(FileList* aFiles); + + void SetFormAction(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::formaction, aValue, aRv); + } + + void GetFormEnctype(nsAString& aValue); + void SetFormEnctype(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::formenctype, aValue, aRv); + } + + void GetFormMethod(nsAString& aValue); + void SetFormMethod(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::formmethod, aValue, aRv); + } + + bool FormNoValidate() const { return GetBoolAttr(nsGkAtoms::formnovalidate); } + + void SetFormNoValidate(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::formnovalidate, aValue, aRv); + } + + void GetFormTarget(nsAString& aValue) { + GetHTMLAttr(nsGkAtoms::formtarget, aValue); + } + void SetFormTarget(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::formtarget, aValue, aRv); + } + + MOZ_CAN_RUN_SCRIPT uint32_t Height(); + + void SetHeight(uint32_t aValue, ErrorResult& aRv) { + SetUnsignedIntAttr(nsGkAtoms::height, aValue, 0, aRv); + } + + bool Indeterminate() const { return mIndeterminate; } + + bool IsDraggingRange() const { return mIsDraggingRange; } + void SetIndeterminate(bool aValue); + + HTMLDataListElement* GetList() const; + + void GetMax(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::max, aValue); } + void SetMax(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::max, aValue, aRv); + } + + int32_t MaxLength() const { return GetIntAttr(nsGkAtoms::maxlength, -1); } + + int32_t UsedMaxLength() const final { + if (!mInputType->MinAndMaxLengthApply()) { + return -1; + } + return MaxLength(); + } + + void SetMaxLength(int32_t aValue, ErrorResult& aRv) { + int32_t minLength = MinLength(); + if (aValue < 0 || (minLength >= 0 && aValue < minLength)) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + SetHTMLIntAttr(nsGkAtoms::maxlength, aValue, aRv); + } + + int32_t MinLength() const { return GetIntAttr(nsGkAtoms::minlength, -1); } + + void SetMinLength(int32_t aValue, ErrorResult& aRv) { + int32_t maxLength = MaxLength(); + if (aValue < 0 || (maxLength >= 0 && aValue > maxLength)) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + SetHTMLIntAttr(nsGkAtoms::minlength, aValue, aRv); + } + + void GetMin(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::min, aValue); } + void SetMin(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::min, aValue, aRv); + } + + bool Multiple() const { return GetBoolAttr(nsGkAtoms::multiple); } + + void SetMultiple(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::multiple, aValue, aRv); + } + + void GetName(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); } + void SetName(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aValue, aRv); + } + + void GetPattern(nsAString& aValue) { + GetHTMLAttr(nsGkAtoms::pattern, aValue); + } + void SetPattern(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::pattern, aValue, aRv); + } + + void GetPlaceholder(nsAString& aValue) { + GetHTMLAttr(nsGkAtoms::placeholder, aValue); + } + void SetPlaceholder(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::placeholder, aValue, aRv); + } + + bool ReadOnly() const { return GetBoolAttr(nsGkAtoms::readonly); } + + void SetReadOnly(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::readonly, aValue, aRv); + } + + bool Required() const { return GetBoolAttr(nsGkAtoms::required); } + + void SetRequired(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::required, aValue, aRv); + } + + uint32_t Size() const { + return GetUnsignedIntAttr(nsGkAtoms::size, DEFAULT_COLS); + } + + void SetSize(uint32_t aValue, ErrorResult& aRv) { + if (aValue == 0) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + SetUnsignedIntAttr(nsGkAtoms::size, aValue, DEFAULT_COLS, aRv); + } + + void GetSrc(nsAString& aValue) { + GetURIAttr(nsGkAtoms::src, nullptr, aValue); + } + void SetSrc(const nsAString& aValue, nsIPrincipal* aTriggeringPrincipal, + ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::src, aValue, aTriggeringPrincipal, aRv); + } + + void GetStep(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::step, aValue); } + void SetStep(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::step, aValue, aRv); + } + + void GetType(nsAString& aValue) const; + void SetType(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::type, aValue, aRv); + } + + void GetDefaultValue(nsAString& aValue) { + GetHTMLAttr(nsGkAtoms::value, aValue); + } + void SetDefaultValue(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::value, aValue, aRv); + } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void SetValue(const nsAString& aValue, CallerType aCallerType, + ErrorResult& aRv); + void GetValue(nsAString& aValue, CallerType aCallerType); + + void GetValueAsDate(JSContext* aCx, JS::MutableHandle<JSObject*> aObj, + ErrorResult& aRv); + + void SetValueAsDate(JSContext* aCx, JS::Handle<JSObject*> aObj, + ErrorResult& aRv); + + double ValueAsNumber() const { + return DoesValueAsNumberApply() ? GetValueAsDecimal().toDouble() + : UnspecifiedNaN<double>(); + } + + void SetValueAsNumber(double aValue, ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT uint32_t Width(); + + void SetWidth(uint32_t aValue, ErrorResult& aRv) { + SetUnsignedIntAttr(nsGkAtoms::width, aValue, 0, aRv); + } + + void StepUp(int32_t aN, ErrorResult& aRv) { aRv = ApplyStep(aN); } + + void StepDown(int32_t aN, ErrorResult& aRv) { aRv = ApplyStep(-aN); } + + /** + * Returns the current step value. + * Returns kStepAny if the current step is "any" string. + * + * @return the current step value. + */ + Decimal GetStep() const; + + // Returns whether the given keyboard event steps up or down the value of an + // <input> element. + bool StepsInputValue(const WidgetKeyboardEvent&) const; + + already_AddRefed<nsINodeList> GetLabels(); + + MOZ_CAN_RUN_SCRIPT void Select(); + + Nullable<uint32_t> GetSelectionStart(ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void SetSelectionStart(const Nullable<uint32_t>& aValue, + ErrorResult& aRv); + + Nullable<uint32_t> GetSelectionEnd(ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void SetSelectionEnd(const Nullable<uint32_t>& aValue, + ErrorResult& aRv); + + void GetSelectionDirection(nsAString& aValue, ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void SetSelectionDirection(const nsAString& aValue, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void SetSelectionRange( + uint32_t aStart, uint32_t aEnd, const Optional<nsAString>& direction, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement, + uint32_t aStart, uint32_t aEnd, + SelectionMode aSelectMode, + ErrorResult& aRv); + + void ShowPicker(ErrorResult& aRv); + + bool WebkitDirectoryAttr() const { + return HasAttr(nsGkAtoms::webkitdirectory); + } + + void SetWebkitDirectoryAttr(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::webkitdirectory, aValue, aRv); + } + + void GetWebkitEntries(nsTArray<RefPtr<FileSystemEntry>>& aSequence); + + already_AddRefed<Promise> GetFilesAndDirectories(ErrorResult& aRv); + + void GetAlign(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::align, aValue); } + void SetAlign(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::align, aValue, aRv); + } + + void GetUseMap(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::usemap, aValue); } + void SetUseMap(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::usemap, aValue, aRv); + } + + void GetDirName(nsAString& aValue) { + GetHTMLAttr(nsGkAtoms::dirname, aValue); + } + void SetDirName(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::dirname, aValue, aRv); + } + + nsIControllers* GetControllers(ErrorResult& aRv); + // XPCOM adapter function widely used throughout code, leaving it as is. + nsresult GetControllers(nsIControllers** aResult); + + int32_t InputTextLength(CallerType aCallerType); + + void MozGetFileNameArray(nsTArray<nsString>& aFileNames, ErrorResult& aRv); + + void MozSetFileNameArray(const Sequence<nsString>& aFileNames, + ErrorResult& aRv); + void MozSetFileArray(const Sequence<OwningNonNull<File>>& aFiles); + void MozSetDirectory(const nsAString& aDirectoryPath, ErrorResult& aRv); + + /* + * The following functions are called from datetime picker to let input box + * know the current state of the picker or to update the input box on changes. + */ + void GetDateTimeInputBoxValue(DateTimeValue& aValue); + + /* + * This allows chrome JavaScript to dispatch event to the inner datetimebox + * anonymous or UA Widget element. + */ + Element* GetDateTimeBoxElement(); + + /* + * The following functions are called from datetime input box XBL to control + * and update the picker. + */ + void OpenDateTimePicker(const DateTimeValue& aInitialValue); + void UpdateDateTimePicker(const DateTimeValue& aValue); + void CloseDateTimePicker(); + + /* + * Called from datetime input box binding when inner text fields are focused + * or blurred. + */ + void SetFocusState(bool aIsFocused); + + /* + * Called from datetime input box binding when the the user entered value + * becomes valid/invalid. + */ + void UpdateValidityState(); + + /* + * The following are called from datetime input box binding to get the + * corresponding computed values. + */ + double GetStepAsDouble() { return GetStep().toDouble(); } + double GetStepBaseAsDouble() { return GetStepBase().toDouble(); } + double GetMinimumAsDouble() { return GetMinimum().toDouble(); } + double GetMaximumAsDouble() { return GetMaximum().toDouble(); } + + void StartNumberControlSpinnerSpin(); + enum SpinnerStopState { eAllowDispatchingEvents, eDisallowDispatchingEvents }; + void StopNumberControlSpinnerSpin( + SpinnerStopState aState = eAllowDispatchingEvents); + MOZ_CAN_RUN_SCRIPT + void StepNumberControlForUserEvent(int32_t aDirection); + + /** + * The callback function used by the nsRepeatService that we use to spin the + * spinner for <input type=number>. + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY + static void HandleNumberControlSpin(void* aData); + + bool NumberSpinnerUpButtonIsDepressed() const { + return mNumberControlSpinnerIsSpinning && mNumberControlSpinnerSpinsUp; + } + + bool NumberSpinnerDownButtonIsDepressed() const { + return mNumberControlSpinnerIsSpinning && !mNumberControlSpinnerSpinsUp; + } + + bool MozIsTextField(bool aExcludePassword); + + MOZ_CAN_RUN_SCRIPT nsIEditor* GetEditorForBindings(); + // For WebIDL bindings. + bool HasEditor() const; + + bool IsInputEventTarget() const { return IsSingleLineTextControl(false); } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void SetUserInput(const nsAString& aInput, nsIPrincipal& aSubjectPrincipal); + + /** + * If aValue contains a valid floating-point number in the format specified + * by the HTML 5 spec: + * + * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#floating-point-numbers + * + * then this function will return the number parsed as a Decimal, otherwise + * it will return a Decimal for which Decimal::isFinite() will return false. + */ + static Decimal StringToDecimal(const nsAString& aValue); + + void UpdateEntries( + const nsTArray<OwningFileOrDirectory>& aFilesOrDirectories); + + /** + * Returns if the required attribute applies for the current type. + */ + bool DoesRequiredApply() const; + + /** + * Returns the current required state of the element. This function differs + * from Required() in that this function only returns true for input types + * that @required attribute applies and the attribute is set; in contrast, + * Required() returns true whenever @required attribute is set. + */ + bool IsRequired() const { return State().HasState(ElementState::REQUIRED); } + + bool HasBeenTypePassword() const { return mHasBeenTypePassword; } + + /** + * Returns whether the current value is the empty string. This only makes + * sense for some input types; does NOT make sense for file inputs. + * + * @return whether the current value is the empty string. + */ + bool IsValueEmpty() const { + return State().HasState(ElementState::VALUE_EMPTY); + } + + // Parse a simple (hex) color. + static mozilla::Maybe<nscolor> ParseSimpleColor(const nsAString& aColor); + + protected: + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual ~HTMLInputElement(); + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Pull IsSingleLineTextControl into our scope, otherwise it'd be hidden + // by the TextControlElement version. + using nsGenericHTMLFormControlElementWithState::IsSingleLineTextControl; + + /** + * The ValueModeType specifies how the value IDL attribute should behave. + * + * See: http://dev.w3.org/html5/spec/forms.html#dom-input-value + */ + enum ValueModeType { + // On getting, returns the value. + // On setting, sets value. + VALUE_MODE_VALUE, + // On getting, returns the value if present or the empty string. + // On setting, sets the value. + VALUE_MODE_DEFAULT, + // On getting, returns the value if present or "on". + // On setting, sets the value. + VALUE_MODE_DEFAULT_ON, + // On getting, returns "C:\fakepath\" followed by the file name of the + // first file of the selected files if any. + // On setting the empty string, empties the selected files list, otherwise + // throw the INVALID_STATE_ERR exception. + VALUE_MODE_FILENAME + }; + + /** + * This helper method convert a sub-string that contains only digits to a + * number (unsigned int given that it can't contain a minus sign). + * This method will return whether the sub-string is correctly formatted + * (ie. contains only digit) and it can be successfuly parsed to generate a + * number). + * If the method returns true, |aResult| will contained the parsed number. + * + * @param aValue the string on which the sub-string will be extracted and + * parsed. + * @param aStart the beginning of the sub-string in aValue. + * @param aLen the length of the sub-string. + * @param aResult the parsed number. + * @return whether the sub-string has been parsed successfully. + */ + static bool DigitSubStringToNumber(const nsAString& aValue, uint32_t aStart, + uint32_t aLen, uint32_t* aResult); + + // Helper method + + /** + * Setting the value. + * + * @param aValue String to set. + * @param aOldValue Previous value before setting aValue. + If previous value is unknown, aOldValue can be nullptr. + * @param aOptions See TextControlState::ValueSetterOption. + */ + MOZ_CAN_RUN_SCRIPT nsresult + SetValueInternal(const nsAString& aValue, const nsAString* aOldValue, + const ValueSetterOptions& aOptions); + MOZ_CAN_RUN_SCRIPT nsresult SetValueInternal( + const nsAString& aValue, const ValueSetterOptions& aOptions) { + return SetValueInternal(aValue, nullptr, aOptions); + } + + // Generic getter for the value that doesn't do experimental control type + // sanitization. + void GetValueInternal(nsAString& aValue, CallerType aCallerType) const; + + // A getter for callers that know we're not dealing with a file input, so they + // don't have to think about the caller type. + void GetNonFileValueInternal(nsAString& aValue) const; + + void ClearFiles(bool aSetValueChanged); + + void SetIndeterminateInternal(bool aValue, bool aShouldInvalidate); + + /** + * Called when an attribute is about to be changed + */ + void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + /** + * Called when an attribute has just been changed + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + void BeforeSetForm(HTMLFormElement* aForm, bool aBindToTree) override; + + void AfterClearForm(bool aUnbindOrDelete) override; + + void ResultForDialogSubmit(nsAString& aResult) override; + + void SelectAll(nsPresContext* aPresContext); + bool IsImage() const { + return AttrValueIs(kNameSpaceID_None, nsGkAtoms::type, nsGkAtoms::image, + eIgnoreCase); + } + + /** + * Visit the group of radio buttons this radio belongs to + * @param aVisitor the visitor to visit with + */ + nsresult VisitGroup(nsIRadioVisitor* aVisitor); + + /** + * Do all the work that |SetChecked| does (radio button handling, etc.), but + * take an |aNotify| parameter. + */ + void DoSetChecked(bool aValue, bool aNotify, bool aSetValueChanged); + + /** + * Do all the work that |SetCheckedChanged| does (radio button handling, + * etc.), but take an |aNotify| parameter that lets it avoid flushing content + * when it can. + */ + void DoSetCheckedChanged(bool aCheckedChanged, bool aNotify); + + /** + * Actually set checked and notify the frame of the change. + * @param aValue the value of checked to set + */ + void SetCheckedInternal(bool aValue, bool aNotify); + + void RadioSetChecked(bool aNotify); + void SetCheckedChanged(bool aCheckedChanged); + + /** + * MaybeSubmitForm looks for a submit input or a single text control + * and submits the form if either is present. + */ + MOZ_CAN_RUN_SCRIPT void MaybeSubmitForm(nsPresContext* aPresContext); + + /** + * Called after calling one of the SetFilesOrDirectories() functions. + * This method can explore the directory recursively if needed. + */ + void AfterSetFilesOrDirectories(bool aSetValueChanged); + + /** + * Recursively explore the directory and populate mFileOrDirectories correctly + * for webkitdirectory. + */ + void ExploreDirectoryRecursively(bool aSetValuechanged); + + /** + * Determine whether the editor needs to be initialized explicitly for + * a particular event. + */ + bool NeedToInitializeEditorForEvent(EventChainPreVisitor& aVisitor) const; + + /** + * Get the value mode of the element, depending of the type. + */ + ValueModeType GetValueMode() const; + + /** + * Get the mutable state of the element. + * When the element isn't mutable (immutable), the value or checkedness + * should not be changed by the user. + * + * See: https://html.spec.whatwg.org/#concept-fe-mutable + */ + bool IsMutable() const; + + /** + * Returns if the min and max attributes apply for the current type. + */ + bool DoesMinMaxApply() const; + + /** + * Returns if the step attribute apply for the current type. + */ + bool DoesStepApply() const { return DoesMinMaxApply(); } + + /** + * Returns if stepDown and stepUp methods apply for the current type. + */ + bool DoStepDownStepUpApply() const { return DoesStepApply(); } + + /** + * Returns if valueAsNumber attribute applies for the current type. + */ + bool DoesValueAsNumberApply() const { return DoesMinMaxApply(); } + + /** + * Returns if autocomplete attribute applies for the current type. + */ + bool DoesAutocompleteApply() const; + + MOZ_CAN_RUN_SCRIPT void FreeData(); + TextControlState* GetEditorState() const; + void EnsureEditorState(); + + MOZ_CAN_RUN_SCRIPT TextEditor* GetTextEditorFromState(); + + /** + * Manages the internal data storage across type changes. + */ + MOZ_CAN_RUN_SCRIPT + void HandleTypeChange(FormControlType aNewType, bool aNotify); + + /** + * If the input range has a list, this function will snap the given value to + * the nearest tick mark, but only if the given value is close enough to that + * tick mark. + */ + void MaybeSnapToTickMark(Decimal& aValue); + + enum class SanitizationKind { ForValueGetter, ForValueSetter, ForDisplay }; + /** + * Sanitize the value of the element depending of its current type. + * See: + * http://www.whatwg.org/specs/web-apps/current-work/#value-sanitization-algorithm + */ + void SanitizeValue(nsAString& aValue, SanitizationKind) const; + + /** + * Returns whether the placeholder attribute applies for the current type. + */ + bool PlaceholderApplies() const; + + /** + * Set the current default value to the value of the input element. + * @note You should not call this method if GetValueMode() doesn't return + * VALUE_MODE_VALUE. + */ + MOZ_CAN_RUN_SCRIPT + nsresult SetDefaultValueAsValue(); + + /** + * Sets the direction from the input value. if aKnownValue is provided, it + * saves a GetValue call. + */ + void SetAutoDirectionality(bool aNotify, + const nsAString* aKnownValue = nullptr); + + /** + * Returns the radio group container within the DOM tree that the element + * is currently a member of, if one exists. + */ + RadioGroupContainer* GetCurrentRadioGroupContainer() const; + /** + * Returns the radio group container within the DOM tree that the element + * should be added into, if one exists. + */ + RadioGroupContainer* FindTreeRadioGroupContainer() const; + + /** + * Parse a color string of the form #XXXXXX where X should be hexa characters + * @param the string to be parsed. + * @return whether the string is a valid simple color. + * Note : this function does not consider the empty string as valid. + */ + bool IsValidSimpleColor(const nsAString& aValue) const; + + /** + * Parse a week string of the form yyyy-Www + * @param the string to be parsed. + * @return whether the string is a valid week. + * Note : this function does not consider the empty string as valid. + */ + bool IsValidWeek(const nsAString& aValue) const; + + /** + * Parse a month string of the form yyyy-mm + * @param the string to be parsed. + * @return whether the string is a valid month. + * Note : this function does not consider the empty string as valid. + */ + bool IsValidMonth(const nsAString& aValue) const; + + /** + * Parse a date string of the form yyyy-mm-dd + * @param the string to be parsed. + * @return whether the string is a valid date. + * Note : this function does not consider the empty string as valid. + */ + bool IsValidDate(const nsAString& aValue) const; + + /** + * Parse a datetime-local string of the form yyyy-mm-ddThh:mm[:ss.s] or + * yyyy-mm-dd hh:mm[:ss.s], where fractions of seconds can be 1 to 3 digits. + * + * @param the string to be parsed. + * @return whether the string is a valid datetime-local string. + * Note : this function does not consider the empty string as valid. + */ + bool IsValidDateTimeLocal(const nsAString& aValue) const; + + /** + * Parse a year string of the form yyyy + * + * @param the string to be parsed. + * + * @return the year in aYear. + * @return whether the parsing was successful. + */ + bool ParseYear(const nsAString& aValue, uint32_t* aYear) const; + + /** + * Parse a month string of the form yyyy-mm + * + * @param the string to be parsed. + * @return the year and month in aYear and aMonth. + * @return whether the parsing was successful. + */ + bool ParseMonth(const nsAString& aValue, uint32_t* aYear, + uint32_t* aMonth) const; + + /** + * Parse a week string of the form yyyy-Www + * + * @param the string to be parsed. + * @return the year and week in aYear and aWeek. + * @return whether the parsing was successful. + */ + bool ParseWeek(const nsAString& aValue, uint32_t* aYear, + uint32_t* aWeek) const; + /** + * Parse a date string of the form yyyy-mm-dd + * + * @param the string to be parsed. + * @return the date in aYear, aMonth, aDay. + * @return whether the parsing was successful. + */ + bool ParseDate(const nsAString& aValue, uint32_t* aYear, uint32_t* aMonth, + uint32_t* aDay) const; + + /** + * Parse a datetime-local string of the form yyyy-mm-ddThh:mm[:ss.s] or + * yyyy-mm-dd hh:mm[:ss.s], where fractions of seconds can be 1 to 3 digits. + * + * @param the string to be parsed. + * @return the date in aYear, aMonth, aDay and time expressed in milliseconds + * in aTime. + * @return whether the parsing was successful. + */ + bool ParseDateTimeLocal(const nsAString& aValue, uint32_t* aYear, + uint32_t* aMonth, uint32_t* aDay, + uint32_t* aTime) const; + + /** + * Normalize the datetime-local string following the HTML specifications: + * https://html.spec.whatwg.org/multipage/infrastructure.html#valid-normalised-local-date-and-time-string + */ + void NormalizeDateTimeLocal(nsAString& aValue) const; + + /** + * This methods returns the number of days since epoch for a given year and + * week. + */ + double DaysSinceEpochFromWeek(uint32_t aYear, uint32_t aWeek) const; + + /** + * This methods returns the number of days in a given month, for a given year. + */ + uint32_t NumberOfDaysInMonth(uint32_t aMonth, uint32_t aYear) const; + + /** + * This methods returns the number of months between January 1970 and the + * given year and month. + */ + int32_t MonthsSinceJan1970(uint32_t aYear, uint32_t aMonth) const; + + /** + * This methods returns the day of the week given a date. If @isoWeek is true, + * 7=Sunday, otherwise, 0=Sunday. + */ + uint32_t DayOfWeek(uint32_t aYear, uint32_t aMonth, uint32_t aDay, + bool isoWeek) const; + + /** + * This methods returns the maximum number of week in a given year, the + * result is either 52 or 53. + */ + uint32_t MaximumWeekInYear(uint32_t aYear) const; + + /** + * This methods returns true if it's a leap year. + */ + bool IsLeapYear(uint32_t aYear) const; + + /** + * Returns whether aValue is a valid time as described by HTML specifications: + * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-time-string + * + * @param aValue the string to be tested. + * @return Whether the string is a valid time per HTML specifications. + */ + bool IsValidTime(const nsAString& aValue) const; + + /** + * Returns the time expressed in milliseconds of |aValue| being parsed as a + * time following the HTML specifications: + * http://www.whatwg.org/specs/web-apps/current-work/#parse-a-time-string + * + * Note: |aResult| can be null. + * + * @param aValue the string to be parsed. + * @param aResult the time expressed in milliseconds representing the time + * [out] + * @return Whether the parsing was successful. + */ + static bool ParseTime(const nsAString& aValue, uint32_t* aResult); + + /** + * Sets the value of the element to the string representation of the Decimal. + * + * @param aValue The Decimal that will be used to set the value. + */ + void SetValue(Decimal aValue, CallerType aCallerType); + + void UpdateHasRange(bool aNotify); + // Updates the :in-range / :out-of-range states. + void UpdateInRange(bool aNotify); + + /** + * Get the step scale value for the current type. + * See: + * http://www.whatwg.org/specs/web-apps/current-work/multipage/common-input-element-attributes.html#concept-input-step-scale + */ + Decimal GetStepScaleFactor() const; + + /** + * Return the base used to compute if a value matches step. + * Basically, it's the min attribute if present and a default value otherwise. + * + * @return The step base. + */ + Decimal GetStepBase() const; + + /** + * Returns the default step for the current type. + * @return the default step for the current type. + */ + Decimal GetDefaultStep() const; + + enum StepCallerType { CALLED_FOR_USER_EVENT, CALLED_FOR_SCRIPT }; + + /** + * Sets the aValue outparam to the value that this input would take if + * someone tries to step aStep steps and this input's value would change as + * a result. Leaves aValue untouched if this inputs value would not change + * (e.g. already at max, and asking for the next step up). + * + * Negative aStep means step down, positive means step up. + * + * Returns NS_OK or else the error values that should be thrown if this call + * was initiated by a stepUp()/stepDown() call from script under conditions + * that such a call should throw. + */ + nsresult GetValueIfStepped(int32_t aStepCount, StepCallerType aCallerType, + Decimal* aNextStep); + + /** + * Apply a step change from stepUp or stepDown by multiplying aStep by the + * current step value. + * + * @param aStep The value used to be multiplied against the step value. + */ + nsresult ApplyStep(int32_t aStep); + + /** + * Returns if the current type is an experimental mobile type. + */ + static bool IsExperimentalMobileType(FormControlType); + + /* + * Returns if the current type is one of the date/time input types: date, + * time, month, week and datetime-local. + */ + static bool IsDateTimeInputType(FormControlType); + + /** + * Returns whether getting `.value` as a string should sanitize the value. + * + * See SanitizeValue. + */ + bool SanitizesOnValueGetter() const; + + /** + * Returns true if the element should prevent dispatching another DOMActivate. + * This is used in situations where the anonymous subtree should already have + * sent a DOMActivate and prevents firing more than once. + */ + bool ShouldPreventDOMActivateDispatch(EventTarget* aOriginalTarget); + + /** + * Some input type (color and file) let user choose a value using a picker: + * this function checks if it is needed, and if so, open the corresponding + * picker (color picker or file picker). + */ + nsresult MaybeInitPickers(EventChainPostVisitor& aVisitor); + + /** + * Returns all valid colors in the <datalist> for the input with type=color. + */ + nsTArray<nsString> GetColorsFromList(); + + enum FilePickerType { FILE_PICKER_FILE, FILE_PICKER_DIRECTORY }; + nsresult InitFilePicker(FilePickerType aType); + nsresult InitColorPicker(); + + GetFilesHelper* GetOrCreateGetFilesHelper(bool aRecursiveFlag, + ErrorResult& aRv); + + void ClearGetFilesHelpers(); + + /** + * nsINode::SetMayBeApzAware() will be invoked in this function if necessary + * to prevent default action of APZC so that we can increase/decrease the + * value of this InputElement when mouse wheel event comes without scrolling + * the page. + * + * SetMayBeApzAware() will set flag MayBeApzAware which is checked by apzc to + * decide whether to add this element into its dispatch-to-content region. + */ + void UpdateApzAwareFlag(); + + /** + * A helper to get the current selection range. Will throw on the ErrorResult + * if we have no editor state. + */ + void GetSelectionRange(uint32_t* aSelectionStart, uint32_t* aSelectionEnd, + ErrorResult& aRv); + + /** + * Override for nsImageLoadingContent. + */ + nsIContent* AsContent() override { return this; } + + nsCOMPtr<nsIControllers> mControllers; + + /* + * In mInputData, the mState field is used if IsSingleLineTextControl returns + * true and mValue is used otherwise. We have to be careful when handling it + * on a type change. + * + * Accessing the mState member should be done using the GetEditorState + * function, which returns null if the state is not present. + */ + union InputData { + /** + * The current value of the input if it has been changed from the default + */ + char16_t* mValue; + /** + * The state of the text editor associated with the text/password input + */ + TextControlState* mState; + } mInputData; + + struct FileData; + UniquePtr<FileData> mFileData; + + /** + * The value of the input element when first initialized and it is updated + * when the element is either changed through a script, focused or dispatches + * a change event. This is to ensure correct future change event firing. + * NB: This is ONLY applicable where the element is a text control. ie, + * where type= "date", "time", "text", "email", "search", "tel", "url" or + * "password". + */ + nsString mFocusedValue; + + /** + * If mIsDraggingRange is true, this is the value that the input had before + * the drag started. Used to reset the input to its old value if the drag is + * canceled. + */ + Decimal mRangeThumbDragStartValue; + + /** + * Current value in the input box, in DateTimeValue dictionary format, see + * HTMLInputElement.webidl for details. + */ + UniquePtr<DateTimeValue> mDateTimeInputBoxValue; + + /** + * The triggering principal for the src attribute. + */ + nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal; + + /* + * InputType object created based on input type. + */ + UniquePtr<InputType, InputType::DoNotDelete> mInputType; + + static constexpr size_t INPUT_TYPE_SIZE = + sizeof(Variant<TextInputType, SearchInputType, TelInputType, URLInputType, + EmailInputType, PasswordInputType, NumberInputType, + RangeInputType, RadioInputType, CheckboxInputType, + ButtonInputType, ImageInputType, ResetInputType, + SubmitInputType, DateInputType, TimeInputType, + WeekInputType, MonthInputType, DateTimeLocalInputType, + FileInputType, ColorInputType, HiddenInputType>); + + // Memory allocated for mInputType, reused when type changes. + char mInputTypeMem[INPUT_TYPE_SIZE]; + + // Step scale factor values, for input types that have one. + static const Decimal kStepScaleFactorDate; + static const Decimal kStepScaleFactorNumberRange; + static const Decimal kStepScaleFactorTime; + static const Decimal kStepScaleFactorMonth; + static const Decimal kStepScaleFactorWeek; + + // Default step base value when a type do not have specific one. + static const Decimal kDefaultStepBase; + // Default step base value when type=week does not not have a specific one, + // which is −259200000, the start of week 1970-W01. + static const Decimal kDefaultStepBaseWeek; + + // Default step used when there is no specified step. + static const Decimal kDefaultStep; + static const Decimal kDefaultStepTime; + + // Float value returned by GetStep() when the step attribute is set to 'any'. + static const Decimal kStepAny; + + // Minimum year limited by HTML standard, year >= 1. + static const double kMinimumYear; + // Maximum year limited by ECMAScript date object range, year <= 275760. + static const double kMaximumYear; + // Maximum valid week is 275760-W37. + static const double kMaximumWeekInMaximumYear; + // Maximum valid day is 275760-09-13. + static const double kMaximumDayInMaximumYear; + // Maximum valid month is 275760-09. + static const double kMaximumMonthInMaximumYear; + // Long years in a ISO calendar have 53 weeks in them. + static const double kMaximumWeekInYear; + // Milliseconds in a day. + static const double kMsPerDay; + + nsContentUtils::AutocompleteAttrState mAutocompleteAttrState; + nsContentUtils::AutocompleteAttrState mAutocompleteInfoState; + bool mDisabledChanged : 1; + // https://html.spec.whatwg.org/#concept-fe-dirty + // TODO: Maybe rename to match the spec? + bool mValueChanged : 1; + // https://html.spec.whatwg.org/#user-interacted + bool mUserInteracted : 1; + bool mLastValueChangeWasInteractive : 1; + bool mCheckedChanged : 1; + bool mChecked : 1; + bool mHandlingSelectEvent : 1; + bool mShouldInitChecked : 1; + bool mDoneCreating : 1; + bool mInInternalActivate : 1; + bool mCheckedIsToggled : 1; + bool mIndeterminate : 1; + bool mInhibitRestoration : 1; + bool mHasRange : 1; + bool mIsDraggingRange : 1; + bool mNumberControlSpinnerIsSpinning : 1; + bool mNumberControlSpinnerSpinsUp : 1; + bool mPickerRunning : 1; + bool mIsPreviewEnabled : 1; + bool mHasBeenTypePassword : 1; + bool mHasPatternAttribute : 1; + + private: + static void ImageInputMapAttributesIntoRule(MappedDeclarationsBuilder&); + + /** + * Returns true if this input's type will fire a DOM "change" event when it + * loses focus if its value has changed since it gained focus. + */ + bool MayFireChangeOnBlur() const { return MayFireChangeOnBlur(mType); } + + /** + * Returns true if selection methods can be called on element + */ + bool SupportsTextSelection() const { + switch (mType) { + case FormControlType::InputText: + case FormControlType::InputSearch: + case FormControlType::InputUrl: + case FormControlType::InputTel: + case FormControlType::InputPassword: + return true; + default: + return false; + } + } + + /** + * https://html.spec.whatwg.org/#auto-directionality-form-associated-elements + */ + static bool IsAutoDirectionalityAssociated(FormControlType aType) { + switch (aType) { + case FormControlType::InputHidden: + case FormControlType::InputText: + case FormControlType::InputSearch: + case FormControlType::InputTel: + case FormControlType::InputUrl: + case FormControlType::InputEmail: + case FormControlType::InputPassword: + case FormControlType::InputSubmit: + case FormControlType::InputReset: + case FormControlType::InputButton: + return true; + default: + return false; + } + } + + bool IsAutoDirectionalityAssociated() const { + return IsAutoDirectionalityAssociated(mType); + } + + static bool CreatesDateTimeWidget(FormControlType aType) { + return aType == FormControlType::InputDate || + aType == FormControlType::InputTime || + aType == FormControlType::InputDatetimeLocal; + } + + bool CreatesDateTimeWidget() const { return CreatesDateTimeWidget(mType); } + + static bool MayFireChangeOnBlur(FormControlType aType) { + return IsSingleLineTextControl(false, aType) || + CreatesDateTimeWidget(aType) || + aType == FormControlType::InputRange || + aType == FormControlType::InputNumber; + } + + bool CheckActivationBehaviorPreconditions(EventChainVisitor& aVisitor) const; + + /** + * Call MaybeDispatchPasswordEvent or MaybeDispatchUsernameEvent + * in order to dispatch LoginManager events. + */ + void MaybeDispatchLoginManagerEvents(HTMLFormElement* aForm); + + /** + * Fire an event when the password input field is removed from the DOM tree. + * This is now only used by the password manager and formautofill. + */ + void MaybeFireInputPasswordRemoved(); + + /** + * Checks if aDateTimeInputType should be supported. + */ + static bool IsDateTimeTypeSupported(FormControlType); + + /** + * The radio group container containing the group the element is a part of. + * This allows the element to only access a container it has been added to. + */ + RadioGroupContainer* mRadioGroupContainer; + + struct nsFilePickerFilter { + nsFilePickerFilter() : mFilterMask(0) {} + + explicit nsFilePickerFilter(int32_t aFilterMask) + : mFilterMask(aFilterMask) {} + + nsFilePickerFilter(const nsString& aTitle, const nsString& aFilter) + : mFilterMask(0), mTitle(aTitle), mFilter(aFilter) {} + + nsFilePickerFilter(const nsFilePickerFilter& other) { + mFilterMask = other.mFilterMask; + mTitle = other.mTitle; + mFilter = other.mFilter; + } + + bool operator==(const nsFilePickerFilter& other) const { + if ((mFilter == other.mFilter) && (mFilterMask == other.mFilterMask)) { + return true; + } else { + return false; + } + } + + // Filter mask, using values defined in nsIFilePicker + int32_t mFilterMask; + // If mFilterMask is defined, mTitle and mFilter are useless and should be + // ignored + nsString mTitle; + nsString mFilter; + }; + + class nsFilePickerShownCallback : public nsIFilePickerShownCallback { + virtual ~nsFilePickerShownCallback() = default; + + public: + nsFilePickerShownCallback(HTMLInputElement* aInput, + nsIFilePicker* aFilePicker); + NS_DECL_ISUPPORTS + + NS_IMETHOD Done(nsIFilePicker::ResultCode aResult) override; + + private: + nsCOMPtr<nsIFilePicker> mFilePicker; + const RefPtr<HTMLInputElement> mInput; + }; +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/html/HTMLLIElement.cpp b/dom/html/HTMLLIElement.cpp new file mode 100644 index 0000000000..fcaa120b03 --- /dev/null +++ b/dom/html/HTMLLIElement.cpp @@ -0,0 +1,100 @@ +/* -*- 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/HTMLLIElement.h" +#include "mozilla/dom/HTMLLIElementBinding.h" + +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(LI) + +namespace mozilla::dom { + +HTMLLIElement::~HTMLLIElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLLIElement) + +// https://html.spec.whatwg.org/#lists +const nsAttrValue::EnumTable HTMLLIElement::kULTypeTable[] = { + {"none", ListStyle::None}, + {"disc", ListStyle::Disc}, + {"circle", ListStyle::Circle}, + {"square", ListStyle::Square}, + {nullptr, 0}}; + +// https://html.spec.whatwg.org/#lists +const nsAttrValue::EnumTable HTMLLIElement::kOLTypeTable[] = { + {"A", ListStyle::UpperAlpha}, {"a", ListStyle::LowerAlpha}, + {"I", ListStyle::UpperRoman}, {"i", ListStyle::LowerRoman}, + {"1", ListStyle::Decimal}, {nullptr, 0}}; + +bool HTMLLIElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::type) { + return aResult.ParseEnumValue(aValue, kOLTypeTable, true) || + aResult.ParseEnumValue(aValue, kULTypeTable, false); + } + if (aAttribute == nsGkAtoms::value) { + return aResult.ParseIntValue(aValue); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLLIElement::MapAttributesIntoRule(MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_list_style_type)) { + // type: enum + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::type); + if (value && value->Type() == nsAttrValue::eEnum) { + aBuilder.SetKeywordValue(eCSSProperty_list_style_type, + value->GetEnumValue()); + } + } + + // Map <li value=INTEGER> to 'counter-set: list-item INTEGER'. + const nsAttrValue* attrVal = aBuilder.GetAttr(nsGkAtoms::value); + if (attrVal && attrVal->Type() == nsAttrValue::eInteger) { + if (!aBuilder.PropertyIsSet(eCSSProperty_counter_set)) { + aBuilder.SetCounterSetListItem(attrVal->GetIntegerValue()); + } + } + + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLLIElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::type}, + {nsGkAtoms::value}, + {nullptr}, + }; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLLIElement::GetAttributeMappingFunction() const { + return &MapAttributesIntoRule; +} + +JSObject* HTMLLIElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLLIElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLLIElement.h b/dom/html/HTMLLIElement.h new file mode 100644 index 0000000000..e73a49107e --- /dev/null +++ b/dom/html/HTMLLIElement.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLLIElement_h +#define mozilla_dom_HTMLLIElement_h + +#include "mozilla/Attributes.h" + +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLLIElement final : public nsGenericHTMLElement { + public: + explicit HTMLLIElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLLIElement, nsGenericHTMLElement) + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // WebIDL API + void GetType(DOMString& aType) { GetHTMLAttr(nsGkAtoms::type, aType); } + void SetType(const nsAString& aType, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::type, aType, rv); + } + int32_t Value() const { return GetIntAttr(nsGkAtoms::value, 0); } + void SetValue(int32_t aValue, mozilla::ErrorResult& rv) { + SetHTMLIntAttr(nsGkAtoms::value, aValue, rv); + } + + static const nsAttrValue::EnumTable kULTypeTable[]; + static const nsAttrValue::EnumTable kOLTypeTable[]; + + protected: + virtual ~HTMLLIElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLLIElement_h diff --git a/dom/html/HTMLLabelElement.cpp b/dom/html/HTMLLabelElement.cpp new file mode 100644 index 0000000000..32acbd06be --- /dev/null +++ b/dom/html/HTMLLabelElement.cpp @@ -0,0 +1,246 @@ +/* -*- 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/. */ + +/** + * Implementation of HTML <label> elements. + */ +#include "HTMLLabelElement.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/dom/HTMLLabelElementBinding.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "nsFocusManager.h" +#include "nsIFrame.h" +#include "nsContentUtils.h" +#include "nsQueryObject.h" +#include "mozilla/dom/ShadowRoot.h" + +// construction, destruction + +NS_IMPL_NS_NEW_HTML_ELEMENT(Label) + +namespace mozilla::dom { + +HTMLLabelElement::~HTMLLabelElement() = default; + +JSObject* HTMLLabelElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLLabelElement_Binding::Wrap(aCx, this, aGivenProto); +} + +// nsIDOMHTMLLabelElement + +NS_IMPL_ELEMENT_CLONE(HTMLLabelElement) + +HTMLFormElement* HTMLLabelElement::GetForm() const { + nsGenericHTMLElement* control = GetControl(); + if (!control) { + return nullptr; + } + + // Not all labeled things have a form association. Stick to the ones that do. + nsCOMPtr<nsIFormControl> formControl = do_QueryObject(control); + if (!formControl) { + return nullptr; + } + + return formControl->GetForm(); +} + +void HTMLLabelElement::Focus(const FocusOptions& aOptions, + const CallerType aCallerType, + ErrorResult& aError) { + { + nsIFrame* frame = GetPrimaryFrame(FlushType::Frames); + if (frame && frame->IsFocusable()) { + return nsGenericHTMLElement::Focus(aOptions, aCallerType, aError); + } + } + + if (RefPtr<Element> elem = GetLabeledElement()) { + return elem->Focus(aOptions, aCallerType, aError); + } +} + +nsresult HTMLLabelElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + WidgetMouseEvent* mouseEvent = aVisitor.mEvent->AsMouseEvent(); + if (mHandlingEvent || + (!(mouseEvent && mouseEvent->IsLeftClickEvent()) && + aVisitor.mEvent->mMessage != eMouseDown) || + aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault || + !aVisitor.mPresContext || + // Don't handle the event if it's already been handled by another label + aVisitor.mEvent->mFlags.mMultipleActionsPrevented) { + return NS_OK; + } + + nsCOMPtr<Element> target = + do_QueryInterface(aVisitor.mEvent->GetOriginalDOMEventTarget()); + if (nsContentUtils::IsInInteractiveHTMLContent(target, this)) { + return NS_OK; + } + + // Strong ref because event dispatch is going to happen. + RefPtr<Element> content = GetLabeledElement(); + + if (!content || content->IsDisabled()) { + return NS_OK; + } + + mHandlingEvent = true; + switch (aVisitor.mEvent->mMessage) { + case eMouseDown: + if (mouseEvent->mButton == MouseButton::ePrimary) { + // We reset the mouse-down point on every event because there is + // no guarantee we will reach the eMouseClick code below. + LayoutDeviceIntPoint* curPoint = + new LayoutDeviceIntPoint(mouseEvent->mRefPoint); + SetProperty(nsGkAtoms::labelMouseDownPtProperty, + static_cast<void*>(curPoint), + nsINode::DeleteProperty<LayoutDeviceIntPoint>); + } + break; + + case eMouseClick: + if (mouseEvent->IsLeftClickEvent()) { + LayoutDeviceIntPoint* mouseDownPoint = + static_cast<LayoutDeviceIntPoint*>( + GetProperty(nsGkAtoms::labelMouseDownPtProperty)); + + bool dragSelect = false; + if (mouseDownPoint) { + LayoutDeviceIntPoint dragDistance = *mouseDownPoint; + RemoveProperty(nsGkAtoms::labelMouseDownPtProperty); + + dragDistance -= mouseEvent->mRefPoint; + const int CLICK_DISTANCE = 2; + dragSelect = dragDistance.x > CLICK_DISTANCE || + dragDistance.x < -CLICK_DISTANCE || + dragDistance.y > CLICK_DISTANCE || + dragDistance.y < -CLICK_DISTANCE; + } + // Don't click the for-content if we did drag-select text or if we + // have a kbd modifier (which adjusts a selection). + if (dragSelect || mouseEvent->IsShift() || mouseEvent->IsControl() || + mouseEvent->IsAlt() || mouseEvent->IsMeta()) { + break; + } + // Only set focus on the first click of multiple clicks to prevent + // to prevent immediate de-focus. + if (mouseEvent->mClickCount <= 1) { + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + // Use FLAG_BYMOVEFOCUS here so that the label is scrolled to. + // Also, within HTMLInputElement::PostHandleEvent, inputs will + // be selected only when focused via a key or when the navigation + // flag is used and we want to select the text on label clicks as + // well. + // If the label has been clicked by the user, we also want to + // pass FLAG_BYMOUSE so that we get correct focus ring behavior, + // but we don't want to pass FLAG_BYMOUSE if this click event was + // caused by the user pressing an accesskey. + bool byMouse = (mouseEvent->mInputSource != + MouseEvent_Binding::MOZ_SOURCE_KEYBOARD); + bool byTouch = (mouseEvent->mInputSource == + MouseEvent_Binding::MOZ_SOURCE_TOUCH); + fm->SetFocus(content, + nsIFocusManager::FLAG_BYMOVEFOCUS | + (byMouse ? nsIFocusManager::FLAG_BYMOUSE : 0) | + (byTouch ? nsIFocusManager::FLAG_BYTOUCH : 0)); + } + } + // Dispatch a new click event to |content| + // (For compatibility with IE, we do only left click. If + // we wanted to interpret the HTML spec very narrowly, we + // would do nothing. If we wanted to do something + // sensible, we might send more events through like + // this.) See bug 7554, bug 49897, and bug 96813. + nsEventStatus status = aVisitor.mEventStatus; + // Ok to use aVisitor.mEvent as parameter because DispatchClickEvent + // will actually create a new event. + EventFlags eventFlags; + eventFlags.mMultipleActionsPrevented = true; + DispatchClickEvent(aVisitor.mPresContext, mouseEvent, content, false, + &eventFlags, &status); + // Do we care about the status this returned? I don't think we do... + // Don't run another <label> off of this click + mouseEvent->mFlags.mMultipleActionsPrevented = true; + } + break; + + default: + break; + } + mHandlingEvent = false; + return NS_OK; +} + +Result<bool, nsresult> HTMLLabelElement::PerformAccesskey( + bool aKeyCausesActivation, bool aIsTrustedEvent) { + if (!aKeyCausesActivation) { + RefPtr<Element> element = GetLabeledElement(); + if (element) { + return element->PerformAccesskey(aKeyCausesActivation, aIsTrustedEvent); + } + return Err(NS_ERROR_ABORT); + } + + RefPtr<nsPresContext> presContext = GetPresContext(eForUncomposedDoc); + if (!presContext) { + return Err(NS_ERROR_UNEXPECTED); + } + + // Click on it if the users prefs indicate to do so. + AutoHandlingUserInputStatePusher userInputStatePusher(aIsTrustedEvent); + AutoPopupStatePusher popupStatePusher( + aIsTrustedEvent ? PopupBlocker::openAllowed : PopupBlocker::openAbused); + DispatchSimulatedClick(this, aIsTrustedEvent, presContext); + + // XXXedgar, do we need to check whether the focus is really changed? + return true; +} + +nsGenericHTMLElement* HTMLLabelElement::GetLabeledElement() const { + nsAutoString elementId; + + if (!GetAttr(nsGkAtoms::_for, elementId)) { + // No @for, so we are a label for our first form control element. + // Do a depth-first traversal to look for the first form control element. + return GetFirstLabelableDescendant(); + } + + // We have a @for. The id has to be linked to an element in the same tree + // and this element should be a labelable form control. + Element* element = nullptr; + + if (ShadowRoot* shadowRoot = GetContainingShadow()) { + element = shadowRoot->GetElementById(elementId); + } else if (Document* doc = GetUncomposedDoc()) { + element = doc->GetElementById(elementId); + } else { + element = + nsContentUtils::MatchElementId(SubtreeRoot()->AsContent(), elementId); + } + + if (element && element->IsLabelable()) { + return static_cast<nsGenericHTMLElement*>(element); + } + + return nullptr; +} + +nsGenericHTMLElement* HTMLLabelElement::GetFirstLabelableDescendant() const { + for (nsIContent* cur = nsINode::GetFirstChild(); cur; + cur = cur->GetNextNode(this)) { + Element* element = Element::FromNode(cur); + if (element && element->IsLabelable()) { + return static_cast<nsGenericHTMLElement*>(element); + } + } + + return nullptr; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLLabelElement.h b/dom/html/HTMLLabelElement.h new file mode 100644 index 0000000000..f6c574cebe --- /dev/null +++ b/dom/html/HTMLLabelElement.h @@ -0,0 +1,73 @@ +/* -*- 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/. */ + +/** + * Declaration of HTML <label> elements. + */ +#ifndef HTMLLabelElement_h +#define HTMLLabelElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla { +class EventChainPostVisitor; +namespace dom { + +class HTMLLabelElement final : public nsGenericHTMLElement { + public: + explicit HTMLLabelElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), mHandlingEvent(false) {} + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLLabelElement, label) + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLLabelElement, nsGenericHTMLElement) + + // Element + virtual bool IsInteractiveHTMLContent() const override { return true; } + + HTMLFormElement* GetForm() const; + void GetHtmlFor(nsString& aHtmlFor) { + GetHTMLAttr(nsGkAtoms::_for, aHtmlFor); + } + void SetHtmlFor(const nsAString& aHtmlFor) { + SetHTMLAttr(nsGkAtoms::_for, aHtmlFor); + } + nsGenericHTMLElement* GetControl() const { return GetLabeledElement(); } + + using nsGenericHTMLElement::Focus; + virtual void Focus(const FocusOptions& aOptions, + const mozilla::dom::CallerType aCallerType, + ErrorResult& aError) override; + + // nsIContent + MOZ_CAN_RUN_SCRIPT_BOUNDARY + virtual nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + MOZ_CAN_RUN_SCRIPT + virtual Result<bool, nsresult> PerformAccesskey( + bool aKeyCausesActivation, bool aIsTrustedEvent) override; + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + nsGenericHTMLElement* GetLabeledElement() const; + + protected: + virtual ~HTMLLabelElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsGenericHTMLElement* GetFirstLabelableDescendant() const; + + // XXX It would be nice if we could use an event flag instead. + bool mHandlingEvent; +}; + +} // namespace dom +} // namespace mozilla + +#endif /* HTMLLabelElement_h */ diff --git a/dom/html/HTMLLegendElement.cpp b/dom/html/HTMLLegendElement.cpp new file mode 100644 index 0000000000..2f110dd6ce --- /dev/null +++ b/dom/html/HTMLLegendElement.cpp @@ -0,0 +1,140 @@ +/* -*- 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/HTMLLegendElement.h" +#include "mozilla/dom/ElementBinding.h" +#include "mozilla/dom/HTMLLegendElementBinding.h" +#include "nsFocusManager.h" +#include "nsIFrame.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Legend) + +namespace mozilla::dom { + +HTMLLegendElement::~HTMLLegendElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLLegendElement) + +nsIContent* HTMLLegendElement::GetFieldSet() const { + nsIContent* parent = GetParent(); + + if (parent && parent->IsHTMLElement(nsGkAtoms::fieldset)) { + return parent; + } + + return nullptr; +} + +bool HTMLLegendElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + // this contains center, because IE4 does + static const nsAttrValue::EnumTable kAlignTable[] = { + {"left", LegendAlignValue::Left}, + {"right", LegendAlignValue::Right}, + {"center", LegendAlignValue::Center}, + {nullptr, 0}}; + + if (aAttribute == nsGkAtoms::align && aNamespaceID == kNameSpaceID_None) { + return aResult.ParseEnumValue(aValue, kAlignTable, false); + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +nsChangeHint HTMLLegendElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + nsChangeHint retval = + nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType); + if (aAttribute == nsGkAtoms::align) { + retval |= NS_STYLE_HINT_REFLOW; + } + return retval; +} + +nsresult HTMLLegendElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + return nsGenericHTMLElement::BindToTree(aContext, aParent); +} + +void HTMLLegendElement::UnbindFromTree(bool aNullParent) { + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +void HTMLLegendElement::Focus(const FocusOptions& aOptions, + const CallerType aCallerType, + ErrorResult& aError) { + nsIFrame* frame = GetPrimaryFrame(); + if (!frame) { + return; + } + + if (frame->IsFocusable()) { + nsGenericHTMLElement::Focus(aOptions, aCallerType, aError); + return; + } + + // If the legend isn't focusable, focus whatever is focusable following + // the legend instead, bug 81481. + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + if (!fm) { + return; + } + + RefPtr<Element> result; + aError = fm->MoveFocus(nullptr, this, nsIFocusManager::MOVEFOCUS_FORWARD, + nsIFocusManager::FLAG_NOPARENTFRAME | + nsFocusManager::ProgrammaticFocusFlags(aOptions), + getter_AddRefs(result)); +} + +Result<bool, nsresult> HTMLLegendElement::PerformAccesskey( + bool aKeyCausesActivation, bool aIsTrustedEvent) { + FocusOptions options; + ErrorResult rv; + + Focus(options, CallerType::System, rv); + if (rv.Failed()) { + return Err(rv.StealNSResult()); + } + + // XXXedgar, do we need to check whether the focus is really changed? + return true; +} + +HTMLLegendElement::LegendAlignValue HTMLLegendElement::LogicalAlign( + mozilla::WritingMode aCBWM) const { + const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::align); + if (!attr || attr->Type() != nsAttrValue::eEnum) { + return LegendAlignValue::InlineStart; + } + + auto value = static_cast<LegendAlignValue>(attr->GetEnumValue()); + switch (value) { + case LegendAlignValue::Left: + return aCBWM.IsBidiLTR() ? LegendAlignValue::InlineStart + : LegendAlignValue::InlineEnd; + case LegendAlignValue::Right: + return aCBWM.IsBidiLTR() ? LegendAlignValue::InlineEnd + : LegendAlignValue::InlineStart; + default: + return value; + } +} + +HTMLFormElement* HTMLLegendElement::GetForm() const { + nsCOMPtr<nsIFormControl> fieldsetControl = do_QueryInterface(GetFieldSet()); + return fieldsetControl ? fieldsetControl->GetForm() : nullptr; +} + +JSObject* HTMLLegendElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLLegendElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLLegendElement.h b/dom/html/HTMLLegendElement.h new file mode 100644 index 0000000000..cccab9239b --- /dev/null +++ b/dom/html/HTMLLegendElement.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLLegendElement_h +#define mozilla_dom_HTMLLegendElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "mozilla/dom/HTMLFormElement.h" + +namespace mozilla::dom { + +class HTMLLegendElement final : public nsGenericHTMLElement { + public: + explicit HTMLLegendElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLLegendElement, legend) + + using nsGenericHTMLElement::Focus; + virtual void Focus(const FocusOptions& aOptions, + const mozilla::dom::CallerType aCallerType, + ErrorResult& aError) override; + + virtual Result<bool, nsresult> PerformAccesskey( + bool aKeyCausesActivation, bool aIsTrustedEvent) override; + + // nsIContent + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + virtual void UnbindFromTree(bool aNullParent = true) override; + virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + virtual nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + enum class LegendAlignValue : uint8_t { + Left, + Right, + Center, + Bottom, + Top, + InlineStart, + InlineEnd, + }; + + /** + * Return the align value to use for the given fieldset writing-mode. + * (This method resolves Left/Right to the appropriate InlineStart/InlineEnd). + * @param aCBWM the fieldset writing-mode + * @note we only parse left/right/center, so this method returns Center, + * InlineStart or InlineEnd. + */ + LegendAlignValue LogicalAlign(mozilla::WritingMode aCBWM) const; + + /** + * WebIDL Interface + */ + + HTMLFormElement* GetForm() const; + + void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } + + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + + nsINode* GetScopeChainParent() const override { + Element* form = GetForm(); + return form ? form : nsGenericHTMLElement::GetScopeChainParent(); + } + + protected: + virtual ~HTMLLegendElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + /** + * Get the fieldset content element that contains this legend. + * Returns null if there is no fieldset containing this legend. + */ + nsIContent* GetFieldSet() const; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLLegendElement_h */ diff --git a/dom/html/HTMLLinkElement.cpp b/dom/html/HTMLLinkElement.cpp new file mode 100644 index 0000000000..53c2d86d8d --- /dev/null +++ b/dom/html/HTMLLinkElement.cpp @@ -0,0 +1,706 @@ +/* -*- 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/HTMLLinkElement.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "mozilla/Components.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/HTMLLinkElementBinding.h" +#include "mozilla/dom/HTMLDNSPrefetch.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "mozilla/dom/ScriptLoader.h" +#include "nsContentUtils.h" +#include "nsDOMTokenList.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsIContentInlines.h" +#include "mozilla/dom/Document.h" +#include "nsINode.h" +#include "nsIPrefetchService.h" +#include "nsISizeOf.h" +#include "nsPIDOMWindow.h" +#include "nsReadableUtils.h" +#include "nsStyleConsts.h" +#include "nsUnicharUtils.h" +#include "nsWindowSizes.h" +#include "nsIContentPolicy.h" +#include "nsMimeTypes.h" +#include "imgLoader.h" +#include "MediaContainerType.h" +#include "DecoderDoctorDiagnostics.h" +#include "DecoderTraits.h" +#include "MediaList.h" +#include "nsAttrValueInlines.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Link) + +namespace mozilla::dom { + +HTMLLinkElement::HTMLLinkElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLLinkElement::~HTMLLinkElement() { SupportsDNSPrefetch::Destroyed(*this); } + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLLinkElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLLinkElement, + nsGenericHTMLElement) + tmp->LinkStyle::Traverse(cb); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRelList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSizes) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlocking) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLLinkElement, + nsGenericHTMLElement) + tmp->LinkStyle::Unlink(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRelList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSizes) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlocking) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLLinkElement, + nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLLinkElement) + +bool HTMLLinkElement::Disabled() const { + return GetBoolAttr(nsGkAtoms::disabled); +} + +void HTMLLinkElement::SetDisabled(bool aDisabled, ErrorResult& aRv) { + return SetHTMLBoolAttr(nsGkAtoms::disabled, aDisabled, aRv); +} + +nsresult HTMLLinkElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInComposedDoc()) { + TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender(); + } + + LinkStyle::BindToTree(); + + if (IsInUncomposedDoc()) { + if (AttrValueIs(kNameSpaceID_None, nsGkAtoms::rel, nsGkAtoms::localization, + eIgnoreCase)) { + aContext.OwnerDoc().LocalizationLinkAdded(this); + } + + LinkAdded(); + } + + return rv; +} + +void HTMLLinkElement::LinkAdded() { + CreateAndDispatchEvent(u"DOMLinkAdded"_ns); +} + +void HTMLLinkElement::UnbindFromTree(bool aNullParent) { + CancelDNSPrefetch(*this); + CancelPrefetchOrPreload(); + + // If this is reinserted back into the document it will not be + // from the parser. + Document* oldDoc = GetUncomposedDoc(); + ShadowRoot* oldShadowRoot = GetContainingShadow(); + + // We want to update the localization but only if the link is removed from a + // DOM change, and not because the document is going away. + bool ignore; + if (oldDoc) { + if (oldDoc->GetScriptHandlingObject(ignore) && + AttrValueIs(kNameSpaceID_None, nsGkAtoms::rel, nsGkAtoms::localization, + eIgnoreCase)) { + oldDoc->LocalizationLinkRemoved(this); + } + } + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + Unused << UpdateStyleSheetInternal(oldDoc, oldShadowRoot); +} + +bool HTMLLinkElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::crossorigin) { + ParseCORSValue(aValue, aResult); + return true; + } + + if (aAttribute == nsGkAtoms::as) { + net::ParseAsValue(aValue, aResult); + return true; + } + + if (aAttribute == nsGkAtoms::sizes) { + aResult.ParseAtomArray(aValue); + return true; + } + + if (aAttribute == nsGkAtoms::integrity) { + aResult.ParseStringOrAtom(aValue); + return true; + } + + if (aAttribute == nsGkAtoms::fetchpriority) { + ParseFetchPriority(aValue, aResult); + return true; + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLLinkElement::CreateAndDispatchEvent(const nsAString& aEventName) { + MOZ_ASSERT(IsInUncomposedDoc()); + + // In the unlikely case that both rev is specified *and* rel=stylesheet, + // this code will cause the event to fire, on the principle that maybe the + // page really does want to specify that its author is a stylesheet. Since + // this should never actually happen and the performance hit is minimal, + // doing the "right" thing costs virtually nothing here, even if it doesn't + // make much sense. + static AttrArray::AttrValuesArray strings[] = { + nsGkAtoms::_empty, nsGkAtoms::stylesheet, nullptr}; + + if (!nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None, + nsGkAtoms::rev) && + FindAttrValueIn(kNameSpaceID_None, nsGkAtoms::rel, strings, + eIgnoreCase) != AttrArray::ATTR_VALUE_NO_MATCH) { + return; + } + + RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher( + this, aEventName, CanBubble::eYes, ChromeOnlyDispatch::eYes); + // Always run async in order to avoid running script when the content + // sink isn't expecting it. + asyncDispatcher->PostDOMEvent(); +} + +void HTMLLinkElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && + (aName == nsGkAtoms::href || aName == nsGkAtoms::rel)) { + CancelDNSPrefetch(*this); + CancelPrefetchOrPreload(); + } + + return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue, + aNotify); +} + +void HTMLLinkElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::href) { + mCachedURI = nullptr; + if (IsInUncomposedDoc()) { + CreateAndDispatchEvent(u"DOMLinkChanged"_ns); + } + mTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal( + this, aValue ? aValue->GetStringValue() : EmptyString(), + aSubjectPrincipal); + + // If the link has `rel=localization` and its `href` attribute is changed, + // update the list of localization links. + if (AttrValueIs(kNameSpaceID_None, nsGkAtoms::rel, nsGkAtoms::localization, + eIgnoreCase)) { + if (Document* doc = GetUncomposedDoc()) { + if (aOldValue) { + doc->LocalizationLinkRemoved(this); + } + if (aValue) { + doc->LocalizationLinkAdded(this); + } + } + } + } + + // If a link's `rel` attribute was changed from or to `localization`, + // update the list of localization links. + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::rel) { + if (Document* doc = GetUncomposedDoc()) { + if ((aValue && aValue->Equals(nsGkAtoms::localization, eIgnoreCase)) && + (!aOldValue || + !aOldValue->Equals(nsGkAtoms::localization, eIgnoreCase))) { + doc->LocalizationLinkAdded(this); + } else if ((aOldValue && + aOldValue->Equals(nsGkAtoms::localization, eIgnoreCase)) && + (!aValue || + !aValue->Equals(nsGkAtoms::localization, eIgnoreCase))) { + doc->LocalizationLinkRemoved(this); + } + } + } + + if (aValue) { + if (aNameSpaceID == kNameSpaceID_None && + (aName == nsGkAtoms::href || aName == nsGkAtoms::rel || + aName == nsGkAtoms::title || aName == nsGkAtoms::media || + aName == nsGkAtoms::type || aName == nsGkAtoms::as || + aName == nsGkAtoms::crossorigin || aName == nsGkAtoms::disabled)) { + bool dropSheet = false; + if (aName == nsGkAtoms::rel) { + nsAutoString value; + aValue->ToString(value); + uint32_t linkTypes = ParseLinkTypes(value); + if (GetSheet()) { + dropSheet = !(linkTypes & eSTYLESHEET); + } + } + + if ((aName == nsGkAtoms::rel || aName == nsGkAtoms::href) && + IsInComposedDoc()) { + TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender(); + } + + if ((aName == nsGkAtoms::as || aName == nsGkAtoms::type || + aName == nsGkAtoms::crossorigin || aName == nsGkAtoms::media) && + IsInComposedDoc()) { + UpdatePreload(aName, aValue, aOldValue); + } + + const bool forceUpdate = + dropSheet || aName == nsGkAtoms::title || aName == nsGkAtoms::media || + aName == nsGkAtoms::type || aName == nsGkAtoms::disabled; + + Unused << UpdateStyleSheetInternal( + nullptr, nullptr, forceUpdate ? ForceUpdate::Yes : ForceUpdate::No); + } + } else { + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::disabled) { + mExplicitlyEnabled = true; + } + // Since removing href or rel makes us no longer link to a stylesheet, + // force updates for those too. + if (aName == nsGkAtoms::href || aName == nsGkAtoms::rel || + aName == nsGkAtoms::title || aName == nsGkAtoms::media || + aName == nsGkAtoms::type || aName == nsGkAtoms::disabled) { + Unused << UpdateStyleSheetInternal(nullptr, nullptr, ForceUpdate::Yes); + } + if ((aName == nsGkAtoms::as || aName == nsGkAtoms::type || + aName == nsGkAtoms::crossorigin || aName == nsGkAtoms::media) && + IsInComposedDoc()) { + UpdatePreload(aName, aValue, aOldValue); + } + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +// Keep this and the arrays below in sync with ToLinkMask in LinkStyle.cpp. +#define SUPPORTED_REL_VALUES_BASE \ + "preload", "prefetch", "dns-prefetch", "stylesheet", "next", "alternate", \ + "preconnect", "icon", "search", nullptr + +static const DOMTokenListSupportedToken sSupportedRelValueCombinations[][12] = { + {SUPPORTED_REL_VALUES_BASE}, + {"manifest", SUPPORTED_REL_VALUES_BASE}, + {"modulepreload", SUPPORTED_REL_VALUES_BASE}, + {"modulepreload", "manifest", SUPPORTED_REL_VALUES_BASE}}; +#undef SUPPORTED_REL_VALUES_BASE + +nsDOMTokenList* HTMLLinkElement::RelList() { + if (!mRelList) { + int index = (StaticPrefs::dom_manifest_enabled() ? 1 : 0) | + (StaticPrefs::network_modulepreload() ? 2 : 0); + + mRelList = new nsDOMTokenList(this, nsGkAtoms::rel, + sSupportedRelValueCombinations[index]); + } + return mRelList; +} + +Maybe<LinkStyle::SheetInfo> HTMLLinkElement::GetStyleSheetInfo() { + nsAutoString rel; + GetAttr(nsGkAtoms::rel, rel); + uint32_t linkTypes = ParseLinkTypes(rel); + if (!(linkTypes & eSTYLESHEET)) { + return Nothing(); + } + + if (!IsCSSMimeTypeAttributeForLinkElement(*this)) { + return Nothing(); + } + + if (Disabled()) { + return Nothing(); + } + + nsAutoString title; + nsAutoString media; + GetTitleAndMediaForElement(*this, title, media); + + bool alternate = linkTypes & eALTERNATE; + if (alternate && title.IsEmpty()) { + // alternates must have title. + return Nothing(); + } + + if (!HasNonEmptyAttr(nsGkAtoms::href)) { + return Nothing(); + } + + nsAutoString integrity; + GetAttr(nsGkAtoms::integrity, integrity); + + nsCOMPtr<nsIURI> uri = GetURI(); + nsCOMPtr<nsIPrincipal> prin = mTriggeringPrincipal; + + nsAutoString nonce; + nsString* cspNonce = static_cast<nsString*>(GetProperty(nsGkAtoms::nonce)); + if (cspNonce) { + nonce = *cspNonce; + } + + return Some(SheetInfo{ + *OwnerDoc(), + this, + uri.forget(), + prin.forget(), + MakeAndAddRef<ReferrerInfo>(*this), + GetCORSMode(), + title, + media, + integrity, + nonce, + alternate ? HasAlternateRel::Yes : HasAlternateRel::No, + IsInline::No, + mExplicitlyEnabled ? IsExplicitlyEnabled::Yes : IsExplicitlyEnabled::No, + GetFetchPriority(), + }); +} + +void HTMLLinkElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes, + size_t* aNodeSize) const { + nsGenericHTMLElement::AddSizeOfExcludingThis(aSizes, aNodeSize); + if (nsCOMPtr<nsISizeOf> iface = do_QueryInterface(mCachedURI)) { + *aNodeSize += iface->SizeOfExcludingThis(aSizes.mState.mMallocSizeOf); + } +} + +JSObject* HTMLLinkElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLLinkElement_Binding::Wrap(aCx, this, aGivenProto); +} + +void HTMLLinkElement::GetAs(nsAString& aResult) { + GetEnumAttr(nsGkAtoms::as, "", aResult); +} + +void HTMLLinkElement::GetContentPolicyMimeTypeMedia( + nsAttrValue& aAsAttr, nsContentPolicyType& aPolicyType, nsString& aMimeType, + nsAString& aMedia) { + nsAutoString as; + GetAttr(nsGkAtoms::as, as); + net::ParseAsValue(as, aAsAttr); + aPolicyType = net::AsValueToContentPolicy(aAsAttr); + + nsAutoString type; + GetAttr(nsGkAtoms::type, type); + nsAutoString notUsed; + nsContentUtils::SplitMimeType(type, aMimeType, notUsed); + + GetAttr(nsGkAtoms::media, aMedia); +} + +void HTMLLinkElement:: + TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender() { + MOZ_ASSERT(IsInComposedDoc()); + if (!HasAttr(nsGkAtoms::href)) { + return; + } + + nsAutoString rel; + if (!GetAttr(nsGkAtoms::rel, rel)) { + return; + } + + if (!nsContentUtils::PrefetchPreloadEnabled(OwnerDoc()->GetDocShell())) { + return; + } + + uint32_t linkTypes = ParseLinkTypes(rel); + + if ((linkTypes & ePREFETCH) || (linkTypes & eNEXT)) { + nsCOMPtr<nsIPrefetchService> prefetchService( + components::Prefetch::Service()); + if (prefetchService) { + if (nsCOMPtr<nsIURI> uri = GetURI()) { + auto referrerInfo = MakeRefPtr<ReferrerInfo>(*this); + prefetchService->PrefetchURI(uri, referrerInfo, this, + linkTypes & ePREFETCH); + return; + } + } + } + + if (linkTypes & ePRELOAD) { + if (nsCOMPtr<nsIURI> uri = GetURI()) { + nsContentPolicyType policyType; + + nsAttrValue asAttr; + nsAutoString mimeType; + nsAutoString media; + GetContentPolicyMimeTypeMedia(asAttr, policyType, mimeType, media); + + if (policyType == nsIContentPolicy::TYPE_INVALID || + !net::CheckPreloadAttrs(asAttr, mimeType, media, OwnerDoc())) { + // Ignore preload with a wrong or empty as attribute. + net::WarnIgnoredPreload(*OwnerDoc(), *uri); + return; + } + + StartPreload(policyType); + return; + } + } + + if (linkTypes & eMODULE_PRELOAD) { + ScriptLoader* scriptLoader = OwnerDoc()->ScriptLoader(); + ModuleLoader* moduleLoader = scriptLoader->GetModuleLoader(); + + if (!moduleLoader) { + // For the print preview documents, at this moment it doesn't have module + // loader yet, as the (print preview) document is not attached to the + // nsIDocumentViewer yet, so it doesn't have the GlobalObject. + // Also, the script elements won't be processed as they are also cloned + // from the original document. + // So we simply bail out if the module loader is null. + return; + } + + if (!StaticPrefs::network_modulepreload()) { + // Keep behavior from https://phabricator.services.mozilla.com/D149371, + // prior to main implementation of modulepreload + moduleLoader->DisallowImportMaps(); + return; + } + + // https://html.spec.whatwg.org/multipage/semantics.html#processing-the-media-attribute + // TODO: apply this check for all linkTypes + nsAutoString media; + if (GetAttr(nsGkAtoms::media, media)) { + RefPtr<mozilla::dom::MediaList> mediaList = + mozilla::dom::MediaList::Create(NS_ConvertUTF16toUTF8(media)); + if (!mediaList->Matches(*OwnerDoc())) { + return; + } + } + + // TODO: per spec, apply this check for ePREFETCH as well + if (!HasNonEmptyAttr(nsGkAtoms::href)) { + return; + } + + nsAutoString as; + GetAttr(nsGkAtoms::as, as); + + if (!net::IsScriptLikeOrInvalid(as)) { + RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher( + this, u"error"_ns, CanBubble::eNo, ChromeOnlyDispatch::eNo); + asyncDispatcher->PostDOMEvent(); + return; + } + + nsCOMPtr<nsIURI> uri = GetURI(); + if (!uri) { + return; + } + + // https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-modulepreload-module-script-graph + // Step 1. Disallow further import maps given settings object. + moduleLoader->DisallowImportMaps(); + + StartPreload(nsIContentPolicy::TYPE_SCRIPT); + return; + } + + if (linkTypes & ePRECONNECT) { + if (nsCOMPtr<nsIURI> uri = GetURI()) { + OwnerDoc()->MaybePreconnect( + uri, AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin))); + return; + } + } + + if (linkTypes & eDNS_PREFETCH) { + TryDNSPrefetch(*this); + } +} + +void HTMLLinkElement::UpdatePreload(nsAtom* aName, const nsAttrValue* aValue, + const nsAttrValue* aOldValue) { + MOZ_ASSERT(IsInComposedDoc()); + + if (!HasAttr(nsGkAtoms::href)) { + return; + } + + nsAutoString rel; + if (!GetAttr(nsGkAtoms::rel, rel)) { + return; + } + + if (!nsContentUtils::PrefetchPreloadEnabled(OwnerDoc()->GetDocShell())) { + return; + } + + uint32_t linkTypes = ParseLinkTypes(rel); + + if (!(linkTypes & ePRELOAD)) { + return; + } + + nsCOMPtr<nsIURI> uri = GetURI(); + if (!uri) { + return; + } + + nsAttrValue asAttr; + nsContentPolicyType asPolicyType; + nsAutoString mimeType; + nsAutoString media; + GetContentPolicyMimeTypeMedia(asAttr, asPolicyType, mimeType, media); + + if (asPolicyType == nsIContentPolicy::TYPE_INVALID || + !net::CheckPreloadAttrs(asAttr, mimeType, media, OwnerDoc())) { + // Ignore preload with a wrong or empty as attribute, but be sure to cancel + // the old one. + CancelPrefetchOrPreload(); + net::WarnIgnoredPreload(*OwnerDoc(), *uri); + return; + } + + if (aName == nsGkAtoms::crossorigin) { + CORSMode corsMode = AttrValueToCORSMode(aValue); + CORSMode oldCorsMode = AttrValueToCORSMode(aOldValue); + if (corsMode != oldCorsMode) { + CancelPrefetchOrPreload(); + StartPreload(asPolicyType); + } + return; + } + + nsContentPolicyType oldPolicyType; + + if (aName == nsGkAtoms::as) { + if (aOldValue) { + oldPolicyType = net::AsValueToContentPolicy(*aOldValue); + if (!net::CheckPreloadAttrs(*aOldValue, mimeType, media, OwnerDoc())) { + oldPolicyType = nsIContentPolicy::TYPE_INVALID; + } + } else { + oldPolicyType = nsIContentPolicy::TYPE_INVALID; + } + } else if (aName == nsGkAtoms::type) { + nsAutoString oldType; + nsAutoString notUsed; + if (aOldValue) { + aOldValue->ToString(oldType); + } + nsAutoString oldMimeType; + nsContentUtils::SplitMimeType(oldType, oldMimeType, notUsed); + if (net::CheckPreloadAttrs(asAttr, oldMimeType, media, OwnerDoc())) { + oldPolicyType = asPolicyType; + } else { + oldPolicyType = nsIContentPolicy::TYPE_INVALID; + } + } else { + MOZ_ASSERT(aName == nsGkAtoms::media); + nsAutoString oldMedia; + if (aOldValue) { + aOldValue->ToString(oldMedia); + } + if (net::CheckPreloadAttrs(asAttr, mimeType, oldMedia, OwnerDoc())) { + oldPolicyType = asPolicyType; + } else { + oldPolicyType = nsIContentPolicy::TYPE_INVALID; + } + } + + if (asPolicyType != oldPolicyType && + oldPolicyType != nsIContentPolicy::TYPE_INVALID) { + CancelPrefetchOrPreload(); + } + + // Trigger a new preload if the policy type has changed. + if (asPolicyType != oldPolicyType) { + StartPreload(asPolicyType); + } +} + +void HTMLLinkElement::CancelPrefetchOrPreload() { + CancelPreload(); + + nsCOMPtr<nsIPrefetchService> prefetchService(components::Prefetch::Service()); + if (prefetchService) { + if (nsCOMPtr<nsIURI> uri = GetURI()) { + prefetchService->CancelPrefetchPreloadURI(uri, this); + } + } +} + +void HTMLLinkElement::StartPreload(nsContentPolicyType aPolicyType) { + MOZ_ASSERT(!mPreload, "Forgot to cancel the running preload"); + RefPtr<PreloaderBase> preload = + OwnerDoc()->Preloads().PreloadLinkElement(this, aPolicyType); + mPreload = preload.get(); +} + +void HTMLLinkElement::CancelPreload() { + if (mPreload) { + // This will cancel the loading channel if this was the last referred node + // and the preload is not used up until now to satisfy a regular tag load + // request. + mPreload->RemoveLinkPreloadNode(this); + mPreload = nullptr; + } +} + +bool HTMLLinkElement::IsCSSMimeTypeAttributeForLinkElement( + const Element& aSelf) { + // Processing the type attribute per + // https://html.spec.whatwg.org/multipage/semantics.html#processing-the-type-attribute + // for HTML link elements. + nsAutoString type; + nsAutoString mimeType; + nsAutoString notUsed; + aSelf.GetAttr(nsGkAtoms::type, type); + nsContentUtils::SplitMimeType(type, mimeType, notUsed); + return mimeType.IsEmpty() || mimeType.LowerCaseEqualsLiteral("text/css"); +} + +nsDOMTokenList* HTMLLinkElement::Blocking() { + if (!mBlocking) { + mBlocking = + new nsDOMTokenList(this, nsGkAtoms::blocking, sSupportedBlockingValues); + } + return mBlocking; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLLinkElement.h b/dom/html/HTMLLinkElement.h new file mode 100644 index 0000000000..26683aae7b --- /dev/null +++ b/dom/html/HTMLLinkElement.h @@ -0,0 +1,223 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLLinkElement_h +#define mozilla_dom_HTMLLinkElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/HTMLDNSPrefetch.h" +#include "mozilla/dom/LinkStyle.h" +#include "mozilla/dom/Link.h" +#include "mozilla/WeakPtr.h" +#include "nsDOMTokenList.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla { +class EventChainPostVisitor; +class EventChainPreVisitor; +class PreloaderBase; + +namespace dom { + +class HTMLLinkElement final : public nsGenericHTMLElement, + public LinkStyle, + public SupportsDNSPrefetch { + public: + explicit HTMLLinkElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + // CC + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLLinkElement, + nsGenericHTMLElement) + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLLinkElement, link); + NS_DECL_ADDSIZEOFEXCLUDINGTHIS + + void LinkAdded(); + + // nsINode + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // nsIContent + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + // Element + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + void CreateAndDispatchEvent(const nsAString& aEventName); + + // WebIDL + bool Disabled() const; + void SetDisabled(bool aDisabled, ErrorResult& aRv); + + nsIURI* GetURI() { + if (!mCachedURI) { + GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(mCachedURI)); + } + return mCachedURI.get(); + } + + void GetHref(nsAString& aValue) { + GetURIAttr(nsGkAtoms::href, nullptr, aValue); + } + void SetHref(const nsAString& aHref, nsIPrincipal* aTriggeringPrincipal, + ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::href, aHref, aTriggeringPrincipal, aRv); + } + void SetHref(const nsAString& aHref, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::href, aHref, aRv); + } + void GetCrossOrigin(nsAString& aResult) { + // Null for both missing and invalid defaults is ok, since we + // always parse to an enum value, so we don't need an invalid + // default, and we _want_ the missing default to be null. + GetEnumAttr(nsGkAtoms::crossorigin, nullptr, aResult); + } + void SetCrossOrigin(const nsAString& aCrossOrigin, ErrorResult& aError) { + SetOrRemoveNullableStringAttr(nsGkAtoms::crossorigin, aCrossOrigin, aError); + } + // nsAString for WebBrowserPersistLocalDocument + void GetRel(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::rel, aValue); } + void SetRel(const nsAString& aRel, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::rel, aRel, aRv); + } + nsDOMTokenList* RelList(); + void GetMedia(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::media, aValue); } + void SetMedia(const nsAString& aMedia, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::media, aMedia, aRv); + } + void GetHreflang(DOMString& aValue) { + GetHTMLAttr(nsGkAtoms::hreflang, aValue); + } + void SetHreflang(const nsAString& aHreflang, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::hreflang, aHreflang, aRv); + } + void GetAs(nsAString& aResult); + void SetAs(const nsAString& aAs, ErrorResult& aRv) { + SetAttr(nsGkAtoms::as, aAs, aRv); + } + + nsDOMTokenList* Sizes() { + if (!mSizes) { + mSizes = new nsDOMTokenList(this, nsGkAtoms::sizes); + } + return mSizes; + } + void GetType(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::type, aValue); } + void SetType(const nsAString& aType, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::type, aType, aRv); + } + void GetCharset(nsAString& aValue) override { + GetHTMLAttr(nsGkAtoms::charset, aValue); + } + void SetCharset(const nsAString& aCharset, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::charset, aCharset, aRv); + } + void GetRev(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::rev, aValue); } + void SetRev(const nsAString& aRev, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::rev, aRev, aRv); + } + void GetTarget(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::target, aValue); } + void SetTarget(const nsAString& aTarget, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::target, aTarget, aRv); + } + void GetIntegrity(nsAString& aIntegrity) const { + GetHTMLAttr(nsGkAtoms::integrity, aIntegrity); + } + void SetIntegrity(const nsAString& aIntegrity, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::integrity, aIntegrity, aRv); + } + void SetReferrerPolicy(const nsAString& aReferrer, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::referrerpolicy, aReferrer, aError); + } + void GetReferrerPolicy(nsAString& aReferrer) { + GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrer); + } + void GetImageSrcset(nsAString& aImageSrcset) { + GetHTMLAttr(nsGkAtoms::imagesrcset, aImageSrcset); + } + void SetImageSrcset(const nsAString& aImageSrcset, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::imagesrcset, aImageSrcset, aError); + } + void GetImageSizes(nsAString& aImageSizes) { + GetHTMLAttr(nsGkAtoms::imagesizes, aImageSizes); + } + void SetImageSizes(const nsAString& aImageSizes, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::imagesizes, aImageSizes, aError); + } + + CORSMode GetCORSMode() const { + return AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin)); + } + + nsDOMTokenList* Blocking(); + + void NodeInfoChanged(Document* aOldDoc) final { + mCachedURI = nullptr; + nsGenericHTMLElement::NodeInfoChanged(aOldDoc); + } + + protected: + virtual ~HTMLLinkElement(); + + void GetContentPolicyMimeTypeMedia(nsAttrValue& aAsAttr, + nsContentPolicyType& aPolicyType, + nsString& aMimeType, nsAString& aMedia); + void TryDNSPrefetchOrPreconnectOrPrefetchOrPreloadOrPrerender(); + void UpdatePreload(nsAtom* aName, const nsAttrValue* aValue, + const nsAttrValue* aOldValue); + void CancelPrefetchOrPreload(); + + void StartPreload(nsContentPolicyType policyType); + void CancelPreload(); + + // Returns whether the type attribute specifies the text/css mime type for + // link elements. + static bool IsCSSMimeTypeAttributeForLinkElement( + const mozilla::dom::Element&); + + // LinkStyle + nsIContent& AsContent() final { return *this; } + const LinkStyle* AsLinkStyle() const final { return this; } + Maybe<SheetInfo> GetStyleSheetInfo() final; + + RefPtr<nsDOMTokenList> mRelList; + RefPtr<nsDOMTokenList> mSizes; + RefPtr<nsDOMTokenList> mBlocking; + + // A weak reference to our preload is held only to cancel the preload when + // this node updates or unbounds from the tree. We want to prevent cycles, + // the preload is held alive by other means. + WeakPtr<PreloaderBase> mPreload; + + // The cached href attribute value. + nsCOMPtr<nsIURI> mCachedURI; + + // The "explicitly enabled" flag. This flag is set whenever the `disabled` + // attribute is explicitly unset, and makes alternate stylesheets not be + // disabled by default anymore. + // + // See https://github.com/whatwg/html/issues/3840#issuecomment-481034206. + bool mExplicitlyEnabled = false; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_HTMLLinkElement_h diff --git a/dom/html/HTMLMapElement.cpp b/dom/html/HTMLMapElement.cpp new file mode 100644 index 0000000000..e4d3ca4f41 --- /dev/null +++ b/dom/html/HTMLMapElement.cpp @@ -0,0 +1,44 @@ +/* -*- 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/HTMLMapElement.h" +#include "mozilla/dom/HTMLMapElementBinding.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsContentList.h" +#include "nsCOMPtr.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Map) + +namespace mozilla::dom { + +HTMLMapElement::HTMLMapElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLMapElement, nsGenericHTMLElement, mAreas) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLMapElement, + nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLMapElement) + +nsIHTMLCollection* HTMLMapElement::Areas() { + if (!mAreas) { + // Not using NS_GetContentList because this should not be cached + mAreas = new nsContentList(this, kNameSpaceID_XHTML, nsGkAtoms::area, + nsGkAtoms::area, false); + } + + return mAreas; +} + +JSObject* HTMLMapElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLMapElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLMapElement.h b/dom/html/HTMLMapElement.h new file mode 100644 index 0000000000..3a16432a8a --- /dev/null +++ b/dom/html/HTMLMapElement.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLMapElement_h +#define mozilla_dom_HTMLMapElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" + +class nsContentList; + +namespace mozilla::dom { + +class HTMLMapElement final : public nsGenericHTMLElement { + public: + explicit HTMLMapElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLMapElement, nsGenericHTMLElement) + + void GetName(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); } + void SetName(const nsAString& aName, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::name, aName, aError); + } + nsIHTMLCollection* Areas(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + protected: + ~HTMLMapElement() = default; + + RefPtr<nsContentList> mAreas; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLMapElement_h diff --git a/dom/html/HTMLMarqueeElement.cpp b/dom/html/HTMLMarqueeElement.cpp new file mode 100644 index 0000000000..61308bf03e --- /dev/null +++ b/dom/html/HTMLMarqueeElement.cpp @@ -0,0 +1,173 @@ +/* -*- 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/HTMLMarqueeElement.h" +#include "nsGenericHTMLElement.h" +#include "nsStyleConsts.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/HTMLMarqueeElementBinding.h" +#include "mozilla/dom/CustomEvent.h" +// This is to pick up the definition of FunctionStringCallback: +#include "mozilla/dom/DataTransferItemBinding.h" +#include "mozilla/dom/ShadowRoot.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Marquee) + +namespace mozilla::dom { + +HTMLMarqueeElement::~HTMLMarqueeElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLMarqueeElement) + +static const nsAttrValue::EnumTable kBehaviorTable[] = { + {"scroll", 1}, {"slide", 2}, {"alternate", 3}, {nullptr, 0}}; + +// Default behavior value is "scroll". +static const nsAttrValue::EnumTable* kDefaultBehavior = &kBehaviorTable[0]; + +static const nsAttrValue::EnumTable kDirectionTable[] = { + {"left", 1}, {"right", 2}, {"up", 3}, {"down", 4}, {nullptr, 0}}; + +// Default direction value is "left". +static const nsAttrValue::EnumTable* kDefaultDirection = &kDirectionTable[0]; + +bool HTMLMarqueeElement::IsEventAttributeNameInternal(nsAtom* aName) { + return nsContentUtils::IsEventAttributeName( + aName, EventNameType_HTML | EventNameType_HTMLMarqueeOnly); +} + +JSObject* HTMLMarqueeElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return dom::HTMLMarqueeElement_Binding::Wrap(aCx, this, aGivenProto); +} + +nsresult HTMLMarqueeElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInComposedDoc()) { + AttachAndSetUAShadowRoot(); + } + + return rv; +} + +void HTMLMarqueeElement::UnbindFromTree(bool aNullParent) { + if (IsInComposedDoc()) { + // We don't want to unattach the shadow root because it used to + // contain a <slot>. + NotifyUAWidgetTeardown(UnattachShadowRoot::No); + } + + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +void HTMLMarqueeElement::GetBehavior(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::behavior, kDefaultBehavior->tag, aValue); +} + +void HTMLMarqueeElement::GetDirection(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::direction, kDefaultDirection->tag, aValue); +} + +bool HTMLMarqueeElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if ((aAttribute == nsGkAtoms::width) || (aAttribute == nsGkAtoms::height)) { + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::bgcolor) { + return aResult.ParseColor(aValue); + } + if (aAttribute == nsGkAtoms::behavior) { + return aResult.ParseEnumValue(aValue, kBehaviorTable, false, + kDefaultBehavior); + } + if (aAttribute == nsGkAtoms::direction) { + return aResult.ParseEnumValue(aValue, kDirectionTable, false, + kDefaultDirection); + } + if (aAttribute == nsGkAtoms::hspace || aAttribute == nsGkAtoms::vspace) { + return aResult.ParseHTMLDimension(aValue); + } + + if (aAttribute == nsGkAtoms::loop) { + return aResult.ParseIntValue(aValue); + } + + if (aAttribute == nsGkAtoms::scrollamount || + aAttribute == nsGkAtoms::scrolldelay) { + return aResult.ParseNonNegativeIntValue(aValue); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLMarqueeElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (IsInComposedDoc() && aNameSpaceID == kNameSpaceID_None && + aName == nsGkAtoms::direction) { + NotifyUAWidgetSetupOrChange(); + } + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLMarqueeElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + nsGenericHTMLElement::MapImageMarginAttributeInto(aBuilder); + nsGenericHTMLElement::MapImageSizeAttributesInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); + nsGenericHTMLElement::MapBGColorInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLMarqueeElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = { + sImageMarginSizeAttributeMap, sBackgroundColorAttributeMap, + sCommonAttributeMap}; + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLMarqueeElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +void HTMLMarqueeElement::DispatchEventToShadowRoot( + const nsAString& aEventTypeArg) { + // Dispatch the event to the UA Widget Shadow Root, make it inaccessible to + // document. + RefPtr<nsINode> shadow = GetShadowRoot(); + MOZ_ASSERT(shadow); + RefPtr<Event> event = new Event(shadow, nullptr, nullptr); + event->InitEvent(aEventTypeArg, false, false); + event->SetTrusted(true); + shadow->DispatchEvent(*event, IgnoreErrors()); +} + +void HTMLMarqueeElement::Start() { + if (GetShadowRoot()) { + DispatchEventToShadowRoot(u"marquee-start"_ns); + } +} + +void HTMLMarqueeElement::Stop() { + if (GetShadowRoot()) { + DispatchEventToShadowRoot(u"marquee-stop"_ns); + } +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLMarqueeElement.h b/dom/html/HTMLMarqueeElement.h new file mode 100644 index 0000000000..250d7c2cf9 --- /dev/null +++ b/dom/html/HTMLMarqueeElement.h @@ -0,0 +1,130 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef HTMLMarqueeElement_h___ +#define HTMLMarqueeElement_h___ + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsContentUtils.h" + +namespace mozilla::dom { +class HTMLMarqueeElement final : public nsGenericHTMLElement { + public: + explicit HTMLMarqueeElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLMarqueeElement, marquee); + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + + static const int kDefaultLoop = -1; + static const int kDefaultScrollAmount = 6; + static const int kDefaultScrollDelayMS = 85; + + bool IsEventAttributeNameInternal(nsAtom* aName) override; + + void GetBehavior(nsAString& aValue); + void SetBehavior(const nsAString& aValue, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::behavior, aValue, aError); + } + + void GetDirection(nsAString& aValue); + void SetDirection(const nsAString& aValue, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::direction, aValue, aError); + } + + void GetBgColor(DOMString& aBgColor) { + GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor); + } + void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError); + } + void GetHeight(DOMString& aHeight) { + GetHTMLAttr(nsGkAtoms::height, aHeight); + } + void SetHeight(const nsAString& aHeight, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::height, aHeight, aError); + } + uint32_t Hspace() { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::hspace, 0); + } + void SetHspace(uint32_t aValue, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::hspace, aValue, 0, aError); + } + int32_t Loop() { + int loop = GetIntAttr(nsGkAtoms::loop, kDefaultLoop); + if (loop <= 0) { + loop = -1; + } + + return loop; + } + void SetLoop(int32_t aValue, ErrorResult& aError) { + if (aValue == -1 || aValue > 0) { + SetHTMLIntAttr(nsGkAtoms::loop, aValue, aError); + } + } + uint32_t ScrollAmount() { + return GetUnsignedIntAttr(nsGkAtoms::scrollamount, kDefaultScrollAmount); + } + void SetScrollAmount(uint32_t aValue, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::scrollamount, aValue, kDefaultScrollAmount, + aError); + } + uint32_t ScrollDelay() { + return GetUnsignedIntAttr(nsGkAtoms::scrolldelay, kDefaultScrollDelayMS); + } + void SetScrollDelay(uint32_t aValue, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::scrolldelay, aValue, kDefaultScrollDelayMS, + aError); + } + bool TrueSpeed() const { return GetBoolAttr(nsGkAtoms::truespeed); } + void SetTrueSpeed(bool aValue, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::truespeed, aValue, aError); + } + void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); } + void SetWidth(const nsAString& aWidth, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::width, aWidth, aError); + } + uint32_t Vspace() { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::vspace, 0); + } + void SetVspace(uint32_t aValue, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::vspace, aValue, 0, aError); + } + + void Start(); + void Stop(); + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLMarqueeElement(); + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); + + void DispatchEventToShadowRoot(const nsAString& aEventTypeArg); +}; + +} // namespace mozilla::dom + +#endif /* HTMLMarqueeElement_h___ */ diff --git a/dom/html/HTMLMediaElement.cpp b/dom/html/HTMLMediaElement.cpp new file mode 100644 index 0000000000..78e9a7b861 --- /dev/null +++ b/dom/html/HTMLMediaElement.cpp @@ -0,0 +1,7881 @@ +/* -*- 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/. */ + +#ifdef XP_WIN +# include "objbase.h" +#endif + +#include "mozilla/dom/HTMLMediaElement.h" + +#include <unordered_map> + +#include "AudioDeviceInfo.h" +#include "AudioStreamTrack.h" +#include "AutoplayPolicy.h" +#include "ChannelMediaDecoder.h" +#include "CrossGraphPort.h" +#include "DOMMediaStream.h" +#include "DecoderDoctorDiagnostics.h" +#include "DecoderDoctorLogger.h" +#include "DecoderTraits.h" +#include "FrameStatistics.h" +#include "GMPCrashHelper.h" +#include "GVAutoplayPermissionRequest.h" +#ifdef MOZ_ANDROID_HLS_SUPPORT +# include "HLSDecoder.h" +#endif +#include "HTMLMediaElement.h" +#include "ImageContainer.h" +#include "MP4Decoder.h" +#include "MediaContainerType.h" +#include "MediaError.h" +#include "MediaManager.h" +#include "MediaMetadataManager.h" +#include "MediaResource.h" +#include "MediaShutdownManager.h" +#include "MediaSourceDecoder.h" +#include "MediaStreamError.h" +#include "MediaTrackGraphImpl.h" +#include "MediaTrackListener.h" +#include "MediaStreamWindowCapturer.h" +#include "MediaTrack.h" +#include "MediaTrackList.h" +#include "Navigator.h" +#include "TimeRanges.h" +#include "VideoFrameContainer.h" +#include "VideoOutput.h" +#include "VideoStreamTrack.h" +#include "base/basictypes.h" +#include "jsapi.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty +#include "mozilla/ArrayUtils.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/EMEUtils.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/FloatingPoint.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/NotNull.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Sprintf.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/SVGObserverUtils.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/AudioTrack.h" +#include "mozilla/dom/AudioTrackList.h" +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/dom/ContentMediaController.h" +#include "mozilla/dom/ElementInlines.h" +#include "mozilla/dom/FeaturePolicyUtils.h" +#include "mozilla/dom/HTMLAudioElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLMediaElementBinding.h" +#include "mozilla/dom/HTMLSourceElement.h" +#include "mozilla/dom/HTMLVideoElement.h" +#include "mozilla/dom/MediaControlUtils.h" +#include "mozilla/dom/MediaDevices.h" +#include "mozilla/dom/MediaEncryptedEvent.h" +#include "mozilla/dom/MediaErrorBinding.h" +#include "mozilla/dom/MediaSource.h" +#include "mozilla/dom/PlayPromise.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/TextTrack.h" +#include "mozilla/dom/UserActivation.h" +#include "mozilla/dom/VideoPlaybackQuality.h" +#include "mozilla/dom/VideoTrack.h" +#include "mozilla/dom/VideoTrackList.h" +#include "mozilla/dom/WakeLock.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/dom/power/PowerManagerService.h" +#include "mozilla/net/UrlClassifierFeatureFactory.h" +#include "nsAttrValueInlines.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsDisplayList.h" +#include "nsDocShell.h" +#include "nsError.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsICachingChannel.h" +#include "nsIClassOfService.h" +#include "nsIContentPolicy.h" +#include "nsIDocShell.h" +#include "mozilla/dom/Document.h" +#include "nsIFrame.h" +#include "nsIHttpChannel.h" +#include "nsIObserverService.h" +#include "nsIRequest.h" +#include "nsIScriptError.h" +#include "nsISupportsPrimitives.h" +#include "nsIThreadRetargetableStreamListener.h" +#include "nsITimer.h" +#include "nsJSUtils.h" +#include "nsLayoutUtils.h" +#include "nsMediaFragmentURIParser.h" +#include "nsMimeTypes.h" +#include "nsNetUtil.h" +#include "nsNodeInfoManager.h" +#include "nsPresContext.h" +#include "nsQueryObject.h" +#include "nsRange.h" +#include "nsSize.h" +#include "nsThreadUtils.h" +#include "nsURIHashKey.h" +#include "nsURLHelper.h" +#include "nsVideoFrame.h" +#include "ReferrerInfo.h" +#include "TimeUnits.h" +#include "xpcpublic.h" +#include <algorithm> +#include <cmath> +#include <limits> +#include <type_traits> + +mozilla::LazyLogModule gMediaElementLog("HTMLMediaElement"); +mozilla::LazyLogModule gMediaElementEventsLog("HTMLMediaElementEvents"); + +extern mozilla::LazyLogModule gAutoplayPermissionLog; +#define AUTOPLAY_LOG(msg, ...) \ + MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +// avoid redefined macro in unified build +#undef MEDIACONTROL_LOG +#define MEDIACONTROL_LOG(msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("HTMLMediaElement=%p, " msg, this, ##__VA_ARGS__)) + +#undef CONTROLLER_TIMER_LOG +#define CONTROLLER_TIMER_LOG(element, msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("HTMLMediaElement=%p, " msg, element, ##__VA_ARGS__)) + +#define LOG(type, msg) MOZ_LOG(gMediaElementLog, type, msg) +#define LOG_EVENT(type, msg) MOZ_LOG(gMediaElementEventsLog, type, msg) + +using namespace mozilla::layers; +using mozilla::net::nsMediaFragmentURIParser; +using namespace mozilla::dom::HTMLMediaElement_Binding; + +namespace mozilla::dom { + +using AudibleState = AudioChannelService::AudibleState; +using SinkInfoPromise = MediaDevices::SinkInfoPromise; + +// Number of milliseconds between progress events as defined by spec +static const uint32_t PROGRESS_MS = 350; + +// Number of milliseconds of no data before a stall event is fired as defined by +// spec +static const uint32_t STALL_MS = 3000; + +// Used by AudioChannel for suppresssing the volume to this ratio. +#define FADED_VOLUME_RATIO 0.25 + +// These constants are arbitrary +// Minimum playbackRate for a media +static const double MIN_PLAYBACKRATE = 1.0 / 16; +// Maximum playbackRate for a media +static const double MAX_PLAYBACKRATE = 16.0; + +static double ClampPlaybackRate(double aPlaybackRate) { + MOZ_ASSERT(aPlaybackRate >= 0.0); + + if (aPlaybackRate == 0.0) { + return aPlaybackRate; + } + if (aPlaybackRate < MIN_PLAYBACKRATE) { + return MIN_PLAYBACKRATE; + } + if (aPlaybackRate > MAX_PLAYBACKRATE) { + return MAX_PLAYBACKRATE; + } + return aPlaybackRate; +} + +// Media error values. These need to match the ones in MediaError.webidl. +static const unsigned short MEDIA_ERR_ABORTED = 1; +static const unsigned short MEDIA_ERR_NETWORK = 2; +static const unsigned short MEDIA_ERR_DECODE = 3; +static const unsigned short MEDIA_ERR_SRC_NOT_SUPPORTED = 4; + +/** + * EventBlocker helps media element to postpone the event delivery by storing + * the event runner, and execute them once media element decides not to postpone + * the event delivery. If media element never resumes the event delivery, then + * those runner would be cancelled. + * For example, we postpone the event delivery when media element entering to + * the bf-cache. + */ +class HTMLMediaElement::EventBlocker final : public nsISupports { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS_FINAL + NS_DECL_CYCLE_COLLECTION_CLASS(EventBlocker) + + explicit EventBlocker(HTMLMediaElement* aElement) : mElement(aElement) {} + + void SetBlockEventDelivery(bool aShouldBlock) { + MOZ_ASSERT(NS_IsMainThread()); + if (mShouldBlockEventDelivery == aShouldBlock) { + return; + } + LOG_EVENT(LogLevel::Debug, + ("%p %s event delivery", mElement.get(), + mShouldBlockEventDelivery ? "block" : "unblock")); + mShouldBlockEventDelivery = aShouldBlock; + if (!mShouldBlockEventDelivery) { + DispatchPendingMediaEvents(); + } + } + + void PostponeEvent(nsMediaEventRunner* aRunner) { + MOZ_ASSERT(NS_IsMainThread()); + // Element has been CCed, which would break the weak pointer. + if (!mElement) { + return; + } + MOZ_ASSERT(mShouldBlockEventDelivery); + MOZ_ASSERT(mElement); + LOG_EVENT(LogLevel::Debug, + ("%p postpone runner %s for %s", mElement.get(), + NS_ConvertUTF16toUTF8(aRunner->Name()).get(), + NS_ConvertUTF16toUTF8(aRunner->EventName()).get())); + mPendingEventRunners.AppendElement(aRunner); + } + + void Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + for (auto& runner : mPendingEventRunners) { + runner->Cancel(); + } + mPendingEventRunners.Clear(); + } + + bool ShouldBlockEventDelivery() const { + MOZ_ASSERT(NS_IsMainThread()); + return mShouldBlockEventDelivery; + } + + size_t SizeOfExcludingThis(MallocSizeOf& aMallocSizeOf) const { + MOZ_ASSERT(NS_IsMainThread()); + size_t total = 0; + for (const auto& runner : mPendingEventRunners) { + total += aMallocSizeOf(runner); + } + return total; + } + + private: + ~EventBlocker() = default; + + void DispatchPendingMediaEvents() { + MOZ_ASSERT(mElement); + for (auto& runner : mPendingEventRunners) { + LOG_EVENT(LogLevel::Debug, + ("%p execute runner %s for %s", mElement.get(), + NS_ConvertUTF16toUTF8(runner->Name()).get(), + NS_ConvertUTF16toUTF8(runner->EventName()).get())); + GetMainThreadSerialEventTarget()->Dispatch(runner.forget()); + } + mPendingEventRunners.Clear(); + } + + WeakPtr<HTMLMediaElement> mElement; + bool mShouldBlockEventDelivery = false; + // Contains event runners which should not be run for now because we want + // to block all events delivery. They would be dispatched once media element + // decides unblocking them. + nsTArray<RefPtr<nsMediaEventRunner>> mPendingEventRunners; +}; + +NS_IMPL_CYCLE_COLLECTION(HTMLMediaElement::EventBlocker, mPendingEventRunners) +NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLMediaElement::EventBlocker) +NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLMediaElement::EventBlocker) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLMediaElement::EventBlocker) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +/** + * We use MediaControlKeyListener to listen to media control key in order to + * play and pause media element when user press media control keys and update + * media's playback and audible state to the media controller. + * + * Use `Start()` to start listening event and use `Stop()` to stop listening + * event. In addition, notifying any change to media controller MUST be done + * after successfully calling `Start()`. + */ +class HTMLMediaElement::MediaControlKeyListener final + : public ContentMediaControlKeyReceiver { + public: + NS_INLINE_DECL_REFCOUNTING(MediaControlKeyListener, override) + + MOZ_INIT_OUTSIDE_CTOR explicit MediaControlKeyListener( + HTMLMediaElement* aElement) + : mElement(aElement) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aElement); + } + + /** + * Start listening to the media control keys which would make media being able + * to be controlled via pressing media control keys. + */ + void Start() { + MOZ_ASSERT(NS_IsMainThread()); + if (IsStarted()) { + // We have already been started, do not notify start twice. + return; + } + + // Fail to init media agent, we are not able to notify the media controller + // any update and also are not able to receive media control key events. + if (!InitMediaAgent()) { + MEDIACONTROL_LOG("Failed to start due to not able to init media agent!"); + return; + } + + NotifyPlaybackStateChanged(MediaPlaybackState::eStarted); + // If owner has started playing before the listener starts, we should update + // the playing state as well. Eg. media starts inaudily and becomes audible + // later. + if (!Owner()->Paused()) { + NotifyMediaStartedPlaying(); + } + if (StaticPrefs::media_mediacontrol_testingevents_enabled()) { + auto dispatcher = MakeRefPtr<AsyncEventDispatcher>( + Owner(), u"MozStartMediaControl"_ns, CanBubble::eYes, + ChromeOnlyDispatch::eYes); + dispatcher->PostDOMEvent(); + } + } + + /** + * Stop listening to the media control keys which would make media not be able + * to be controlled via pressing media control keys. If we haven't started + * listening to the media control keys, then nothing would happen. + */ + void StopIfNeeded() { + MOZ_ASSERT(NS_IsMainThread()); + if (!IsStarted()) { + // We have already been stopped, do not notify stop twice. + return; + } + NotifyMediaStoppedPlaying(); + NotifyPlaybackStateChanged(MediaPlaybackState::eStopped); + + // Remove ourselves from media agent, which would stop receiving event. + mControlAgent->RemoveReceiver(this); + mControlAgent = nullptr; + } + + bool IsStarted() const { return mState != MediaPlaybackState::eStopped; } + + bool IsPlaying() const override { + return Owner() ? !Owner()->Paused() : false; + } + + /** + * Following methods should only be used after starting listener. + */ + void NotifyMediaStartedPlaying() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsStarted()); + if (mState == MediaPlaybackState::eStarted || + mState == MediaPlaybackState::ePaused) { + NotifyPlaybackStateChanged(MediaPlaybackState::ePlayed); + // If media is `inaudible` in the beginning, then we don't need to notify + // the state, because notifying `inaudible` should always come after + // notifying `audible`. + if (mIsOwnerAudible) { + NotifyAudibleStateChanged(MediaAudibleState::eAudible); + } + } + } + + void NotifyMediaStoppedPlaying() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsStarted()); + if (mState == MediaPlaybackState::ePlayed) { + NotifyPlaybackStateChanged(MediaPlaybackState::ePaused); + // As media are going to be paused, so no sound is possible to be heard. + if (mIsOwnerAudible) { + NotifyAudibleStateChanged(MediaAudibleState::eInaudible); + } + } + } + + // This method can be called before the listener starts, which would cache + // the audible state and update after the listener starts. + void UpdateMediaAudibleState(bool aIsOwnerAudible) { + MOZ_ASSERT(NS_IsMainThread()); + if (mIsOwnerAudible == aIsOwnerAudible) { + return; + } + mIsOwnerAudible = aIsOwnerAudible; + MEDIACONTROL_LOG("Media becomes %s", + mIsOwnerAudible ? "audible" : "inaudible"); + // If media hasn't started playing, it doesn't make sense to update media + // audible state. Therefore, in that case we would noitfy the audible state + // when media starts playing. + if (mState == MediaPlaybackState::ePlayed) { + NotifyAudibleStateChanged(mIsOwnerAudible + ? MediaAudibleState::eAudible + : MediaAudibleState::eInaudible); + } + } + + void SetPictureInPictureModeEnabled(bool aIsEnabled) { + MOZ_ASSERT(NS_IsMainThread()); + if (mIsPictureInPictureEnabled == aIsEnabled) { + return; + } + // PIP state changes might happen before the listener starts or stops where + // we haven't call `InitMediaAgent()` yet. Eg. Reset the PIP video's src, + // then cancel the PIP. In addition, not like playback and audible state + // which should be restricted to update via the same agent in order to keep + // those states correct in each `ContextMediaInfo`, PIP state can be updated + // through any browsing context, so we would use `ContentMediaAgent::Get()` + // directly to update PIP state. + mIsPictureInPictureEnabled = aIsEnabled; + if (RefPtr<IMediaInfoUpdater> updater = + ContentMediaAgent::Get(GetCurrentBrowsingContext())) { + updater->SetIsInPictureInPictureMode(mOwnerBrowsingContextId, + mIsPictureInPictureEnabled); + } + } + + void HandleMediaKey(MediaControlKey aKey) override { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsStarted()); + MEDIACONTROL_LOG("HandleEvent '%s'", ToMediaControlKeyStr(aKey)); + if (aKey == MediaControlKey::Play) { + Owner()->Play(); + } else if (aKey == MediaControlKey::Pause) { + Owner()->Pause(); + } else { + MOZ_ASSERT(aKey == MediaControlKey::Stop, + "Not supported key for media element!"); + Owner()->Pause(); + StopIfNeeded(); + } + } + + void UpdateOwnerBrowsingContextIfNeeded() { + // Has not notified any information about the owner context yet. + if (!IsStarted()) { + return; + } + + BrowsingContext* currentBC = GetCurrentBrowsingContext(); + MOZ_ASSERT(currentBC); + // Still in the same browsing context, no need to update. + if (currentBC->Id() == mOwnerBrowsingContextId) { + return; + } + MEDIACONTROL_LOG("Change browsing context from %" PRIu64 " to %" PRIu64, + mOwnerBrowsingContextId, currentBC->Id()); + // This situation would happen when we start a media in an original browsing + // context, then we move it to another browsing context, such as an iframe, + // so its owner browsing context would be changed. Therefore, we should + // reset the media status for the previous browsing context by calling + // `Stop()`, in which the listener would notify `ePaused` (if it's playing) + // and `eStop`. Then calls `Start()`, in which the listener would notify + // `eStart` to the new browsing context. If the media was playing before, + // we would also notify `ePlayed`. + bool wasInPlayingState = mState == MediaPlaybackState::ePlayed; + StopIfNeeded(); + Start(); + if (wasInPlayingState) { + NotifyMediaStartedPlaying(); + } + } + + private: + ~MediaControlKeyListener() = default; + + // The media can be moved around different browsing contexts, so this context + // might be different from the one that we used to initialize + // `ContentMediaAgent`. + BrowsingContext* GetCurrentBrowsingContext() const { + // Owner has been CCed, which would break the link of the weaker pointer. + if (!Owner()) { + return nullptr; + } + nsPIDOMWindowInner* window = Owner()->OwnerDoc()->GetInnerWindow(); + return window ? window->GetBrowsingContext() : nullptr; + } + + bool InitMediaAgent() { + MOZ_ASSERT(NS_IsMainThread()); + BrowsingContext* currentBC = GetCurrentBrowsingContext(); + mControlAgent = ContentMediaAgent::Get(currentBC); + if (!mControlAgent) { + return false; + } + MOZ_ASSERT(currentBC); + mOwnerBrowsingContextId = currentBC->Id(); + MEDIACONTROL_LOG("Init agent in browsing context %" PRIu64, + mOwnerBrowsingContextId); + mControlAgent->AddReceiver(this); + return true; + } + + HTMLMediaElement* Owner() const { + // `mElement` would be clear during CC unlinked, but it would only happen + // after stopping the listener. + MOZ_ASSERT(mElement || !IsStarted()); + return mElement.get(); + } + + void NotifyPlaybackStateChanged(MediaPlaybackState aState) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mControlAgent); + MEDIACONTROL_LOG("NotifyMediaState from state='%s' to state='%s'", + ToMediaPlaybackStateStr(mState), + ToMediaPlaybackStateStr(aState)); + MOZ_ASSERT(mState != aState, "Should not notify same state again!"); + mState = aState; + mControlAgent->NotifyMediaPlaybackChanged(mOwnerBrowsingContextId, mState); + } + + void NotifyAudibleStateChanged(MediaAudibleState aState) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(IsStarted()); + mControlAgent->NotifyMediaAudibleChanged(mOwnerBrowsingContextId, aState); + } + + MediaPlaybackState mState = MediaPlaybackState::eStopped; + WeakPtr<HTMLMediaElement> mElement; + RefPtr<ContentMediaAgent> mControlAgent; + bool mIsPictureInPictureEnabled = false; + bool mIsOwnerAudible = false; + MOZ_INIT_OUTSIDE_CTOR uint64_t mOwnerBrowsingContextId; +}; + +class HTMLMediaElement::MediaStreamTrackListener + : public DOMMediaStream::TrackListener { + public: + explicit MediaStreamTrackListener(HTMLMediaElement* aElement) + : mElement(aElement) {} + + void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override { + if (!mElement) { + return; + } + mElement->NotifyMediaStreamTrackAdded(aTrack); + } + + void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override { + if (!mElement) { + return; + } + mElement->NotifyMediaStreamTrackRemoved(aTrack); + } + + void OnActive() { + MOZ_ASSERT(mElement); + + // mediacapture-main says: + // Note that once ended equals true the HTMLVideoElement will not play media + // even if new MediaStreamTracks are added to the MediaStream (causing it to + // return to the active state) unless autoplay is true or the web + // application restarts the element, e.g., by calling play(). + // + // This is vague on exactly how to go from becoming active to playing, when + // autoplaying. However, per the media element spec, to play an autoplaying + // media element, we must load the source and reach readyState + // HAVE_ENOUGH_DATA [1]. Hence, a MediaStream being assigned to a media + // element and becoming active runs the load algorithm, so that it can + // eventually be played. + // + // [1] + // https://html.spec.whatwg.org/multipage/media.html#ready-states:event-media-play + + LOG(LogLevel::Debug, ("%p, mSrcStream %p became active, checking if we " + "need to run the load algorithm", + mElement.get(), mElement->mSrcStream.get())); + if (!mElement->IsPlaybackEnded()) { + return; + } + if (!mElement->Autoplay()) { + return; + } + LOG(LogLevel::Info, ("%p, mSrcStream %p became active on autoplaying, " + "ended element. Reloading.", + mElement.get(), mElement->mSrcStream.get())); + mElement->DoLoad(); + } + + void NotifyActive() override { + if (!mElement) { + return; + } + + if (!mElement->IsVideo()) { + // Audio elements use NotifyAudible(). + return; + } + + OnActive(); + } + + void NotifyAudible() override { + if (!mElement) { + return; + } + + if (mElement->IsVideo()) { + // Video elements use NotifyActive(). + return; + } + + OnActive(); + } + + void OnInactive() { + MOZ_ASSERT(mElement); + + if (mElement->IsPlaybackEnded()) { + return; + } + LOG(LogLevel::Debug, ("%p, mSrcStream %p became inactive", mElement.get(), + mElement->mSrcStream.get())); + + mElement->PlaybackEnded(); + } + + void NotifyInactive() override { + if (!mElement) { + return; + } + + if (!mElement->IsVideo()) { + // Audio elements use NotifyInaudible(). + return; + } + + OnInactive(); + } + + void NotifyInaudible() override { + if (!mElement) { + return; + } + + if (mElement->IsVideo()) { + // Video elements use NotifyInactive(). + return; + } + + OnInactive(); + } + + protected: + const WeakPtr<HTMLMediaElement> mElement; +}; + +/** + * Helper class that manages audio and video outputs for all enabled tracks in a + * media element. It also manages calculating the current time when playing a + * MediaStream. + */ +class HTMLMediaElement::MediaStreamRenderer + : public DOMMediaStream::TrackListener { + public: + NS_INLINE_DECL_REFCOUNTING(MediaStreamRenderer) + + MediaStreamRenderer(AbstractThread* aMainThread, + VideoFrameContainer* aVideoContainer, + FirstFrameVideoOutput* aFirstFrameVideoOutput, + void* aAudioOutputKey) + : mVideoContainer(aVideoContainer), + mAudioOutputKey(aAudioOutputKey), + mWatchManager(this, aMainThread), + mFirstFrameVideoOutput(aFirstFrameVideoOutput) { + if (mFirstFrameVideoOutput) { + mWatchManager.Watch(mFirstFrameVideoOutput->mFirstFrameRendered, + &MediaStreamRenderer::SetFirstFrameRendered); + } + } + + void Shutdown() { + for (const auto& t : mAudioTracks.Clone()) { + if (t) { + RemoveTrack(t->AsAudioStreamTrack()); + } + } + if (mVideoTrack) { + RemoveTrack(mVideoTrack->AsVideoStreamTrack()); + } + mWatchManager.Shutdown(); + mFirstFrameVideoOutput = nullptr; + } + + void UpdateGraphTime() { + mGraphTime = + mGraphTimeDummy->mTrack->Graph()->CurrentTime() - *mGraphTimeOffset; + } + + void SetFirstFrameRendered() { + if (!mFirstFrameVideoOutput) { + return; + } + if (mVideoTrack) { + mVideoTrack->AsVideoStreamTrack()->RemoveVideoOutput( + mFirstFrameVideoOutput); + } + mWatchManager.Unwatch(mFirstFrameVideoOutput->mFirstFrameRendered, + &MediaStreamRenderer::SetFirstFrameRendered); + mFirstFrameVideoOutput = nullptr; + } + + void SetProgressingCurrentTime(bool aProgress) { + if (aProgress == mProgressingCurrentTime) { + return; + } + + MOZ_DIAGNOSTIC_ASSERT(mGraphTimeDummy); + mProgressingCurrentTime = aProgress; + MediaTrackGraph* graph = mGraphTimeDummy->mTrack->Graph(); + if (mProgressingCurrentTime) { + mGraphTimeOffset = Some(graph->CurrentTime().Ref() - mGraphTime); + mWatchManager.Watch(graph->CurrentTime(), + &MediaStreamRenderer::UpdateGraphTime); + } else { + mWatchManager.Unwatch(graph->CurrentTime(), + &MediaStreamRenderer::UpdateGraphTime); + } + } + + void Start() { + if (mRendering) { + return; + } + + LOG(LogLevel::Info, ("MediaStreamRenderer=%p Start", this)); + mRendering = true; + + if (!mGraphTimeDummy) { + return; + } + + for (const auto& t : mAudioTracks) { + if (t) { + t->AsAudioStreamTrack()->AddAudioOutput(mAudioOutputKey, + mAudioOutputSink); + t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey, + mAudioOutputVolume); + } + } + + if (mVideoTrack) { + mVideoTrack->AsVideoStreamTrack()->AddVideoOutput(mVideoContainer); + } + } + + void Stop() { + if (!mRendering) { + return; + } + + LOG(LogLevel::Info, ("MediaStreamRenderer=%p Stop", this)); + mRendering = false; + + if (!mGraphTimeDummy) { + return; + } + + for (const auto& t : mAudioTracks) { + if (t) { + t->AsAudioStreamTrack()->RemoveAudioOutput(mAudioOutputKey); + } + } + // There is no longer an audio output that needs the device so the + // device may not start. Ensure the promise is resolved. + ResolveAudioDevicePromiseIfExists(__func__); + + if (mVideoTrack) { + mVideoTrack->AsVideoStreamTrack()->RemoveVideoOutput(mVideoContainer); + } + } + + void SetAudioOutputVolume(float aVolume) { + if (mAudioOutputVolume == aVolume) { + return; + } + mAudioOutputVolume = aVolume; + if (!mRendering) { + return; + } + for (const auto& t : mAudioTracks) { + if (t) { + t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey, + mAudioOutputVolume); + } + } + } + + RefPtr<GenericPromise> SetAudioOutputDevice(AudioDeviceInfo* aSink) { + MOZ_ASSERT(aSink); + MOZ_ASSERT(mAudioOutputSink != aSink); + LOG(LogLevel::Info, + ("MediaStreamRenderer=%p SetAudioOutputDevice name=%s\n", this, + NS_ConvertUTF16toUTF8(aSink->Name()).get())); + + mAudioOutputSink = aSink; + + if (!mRendering) { + MOZ_ASSERT(mSetAudioDevicePromise.IsEmpty()); + return GenericPromise::CreateAndResolve(true, __func__); + } + + nsTArray<RefPtr<GenericPromise>> promises; + for (const auto& t : mAudioTracks) { + t->AsAudioStreamTrack()->RemoveAudioOutput(mAudioOutputKey); + promises.AppendElement(t->AsAudioStreamTrack()->AddAudioOutput( + mAudioOutputKey, mAudioOutputSink)); + t->AsAudioStreamTrack()->SetAudioOutputVolume(mAudioOutputKey, + mAudioOutputVolume); + } + if (!promises.Length()) { + // Not active track, save it for later + MOZ_ASSERT(mSetAudioDevicePromise.IsEmpty()); + return GenericPromise::CreateAndResolve(true, __func__); + } + + // Resolve any existing promise for a previous device so that promises + // resolve in order of setSinkId() invocation. + ResolveAudioDevicePromiseIfExists(__func__); + + RefPtr promise = mSetAudioDevicePromise.Ensure(__func__); + GenericPromise::AllSettled(GetCurrentSerialEventTarget(), promises) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [self = RefPtr{this}, + this](const GenericPromise::AllSettledPromiseType:: + ResolveOrRejectValue& aValue) { + // This handler should have been disconnected if + // mSetAudioDevicePromise has been settled. + MOZ_ASSERT(!mSetAudioDevicePromise.IsEmpty()); + mDeviceStartedRequest.Complete(); + // The AudioStreamTrack::AddAudioOutput() promise is rejected + // either when the graph no longer needs the device, in which + // case this handler would have already been disconnected, or + // the graph is force shutdown. + // mSetAudioDevicePromise is resolved regardless of whether + // the AddAudioOutput() promises resolve or reject because + // the underlying device has been changed. + LOG(LogLevel::Info, + ("MediaStreamRenderer=%p SetAudioOutputDevice settled", + this)); + mSetAudioDevicePromise.Resolve(true, __func__); + }) + ->Track(mDeviceStartedRequest); + + return promise; + } + + void AddTrack(AudioStreamTrack* aTrack) { + MOZ_DIAGNOSTIC_ASSERT(!mAudioTracks.Contains(aTrack)); + mAudioTracks.AppendElement(aTrack); + EnsureGraphTimeDummy(); + if (mRendering) { + aTrack->AddAudioOutput(mAudioOutputKey, mAudioOutputSink); + aTrack->SetAudioOutputVolume(mAudioOutputKey, mAudioOutputVolume); + } + } + void AddTrack(VideoStreamTrack* aTrack) { + MOZ_DIAGNOSTIC_ASSERT(!mVideoTrack); + if (!mVideoContainer) { + return; + } + mVideoTrack = aTrack; + EnsureGraphTimeDummy(); + if (mFirstFrameVideoOutput) { + // Add the first frame output even if we are rendering. It will only + // accept one frame. If we are rendering, then the main output will + // overwrite that with the same frame (and possibly more frames). + aTrack->AddVideoOutput(mFirstFrameVideoOutput); + } + if (mRendering) { + aTrack->AddVideoOutput(mVideoContainer); + } + } + + void RemoveTrack(AudioStreamTrack* aTrack) { + MOZ_DIAGNOSTIC_ASSERT(mAudioTracks.Contains(aTrack)); + if (mRendering) { + aTrack->RemoveAudioOutput(mAudioOutputKey); + } + mAudioTracks.RemoveElement(aTrack); + + if (mAudioTracks.IsEmpty()) { + // There is no longer an audio output that needs the device so the + // device may not start. Ensure the promise is resolved. + ResolveAudioDevicePromiseIfExists(__func__); + } + } + void RemoveTrack(VideoStreamTrack* aTrack) { + MOZ_DIAGNOSTIC_ASSERT(mVideoTrack == aTrack); + if (!mVideoContainer) { + return; + } + if (mFirstFrameVideoOutput) { + aTrack->RemoveVideoOutput(mFirstFrameVideoOutput); + } + if (mRendering) { + aTrack->RemoveVideoOutput(mVideoContainer); + } + mVideoTrack = nullptr; + } + + double CurrentTime() const { + if (!mGraphTimeDummy) { + return 0.0; + } + + return mGraphTimeDummy->mTrack->GraphImpl()->MediaTimeToSeconds(mGraphTime); + } + + Watchable<GraphTime>& CurrentGraphTime() { return mGraphTime; } + + // Set if we're rendering video. + const RefPtr<VideoFrameContainer> mVideoContainer; + + // Set if we're rendering audio, nullptr otherwise. + void* const mAudioOutputKey; + + private: + ~MediaStreamRenderer() { Shutdown(); } + + void EnsureGraphTimeDummy() { + if (mGraphTimeDummy) { + return; + } + + MediaTrackGraph* graph = nullptr; + for (const auto& t : mAudioTracks) { + if (t && !t->Ended()) { + graph = t->Graph(); + break; + } + } + + if (!graph && mVideoTrack && !mVideoTrack->Ended()) { + graph = mVideoTrack->Graph(); + } + + if (!graph) { + return; + } + + // This dummy keeps `graph` alive and ensures access to it. + mGraphTimeDummy = MakeRefPtr<SharedDummyTrack>( + graph->CreateSourceTrack(MediaSegment::AUDIO)); + } + + void ResolveAudioDevicePromiseIfExists(const char* aMethodName) { + if (mSetAudioDevicePromise.IsEmpty()) { + return; + } + LOG(LogLevel::Info, + ("MediaStreamRenderer=%p resolve audio device promise", this)); + mSetAudioDevicePromise.Resolve(true, aMethodName); + mDeviceStartedRequest.Disconnect(); + } + + // True when all tracks are being rendered, i.e., when the media element is + // playing. + bool mRendering = false; + + // True while we're progressing mGraphTime. False otherwise. + bool mProgressingCurrentTime = false; + + // The audio output volume for all audio tracks. + float mAudioOutputVolume = 1.0f; + + // The sink device for all audio tracks. + RefPtr<AudioDeviceInfo> mAudioOutputSink; + // The promise returned from SetAudioOutputDevice() when an output is + // active. + MozPromiseHolder<GenericPromise> mSetAudioDevicePromise; + // Request tracking the promise to indicate when the device passed to + // SetAudioOutputDevice() is running. + MozPromiseRequestHolder<GenericPromise::AllSettledPromiseType> + mDeviceStartedRequest; + + // WatchManager for mGraphTime. + WatchManager<MediaStreamRenderer> mWatchManager; + + // A dummy MediaTrack to guarantee a MediaTrackGraph is kept alive while + // we're actively rendering, so we can track the graph's current time. Set + // when the first track is added, never unset. + RefPtr<SharedDummyTrack> mGraphTimeDummy; + + // Watchable that relays the graph's currentTime updates to the media element + // only while we're rendering. This is the current time of the rendering in + // GraphTime units. + Watchable<GraphTime> mGraphTime = {0, "MediaStreamRenderer::mGraphTime"}; + + // Nothing until a track has been added. Then, the current GraphTime at the + // time when we were last Start()ed. + Maybe<GraphTime> mGraphTimeOffset; + + // Currently enabled (and rendered) audio tracks. + nsTArray<WeakPtr<MediaStreamTrack>> mAudioTracks; + + // Currently selected (and rendered) video track. + WeakPtr<MediaStreamTrack> mVideoTrack; + + // Holds a reference to the first-frame-getting video output attached to + // mVideoTrack. Set by the constructor, unset when the media element tells us + // it has rendered the first frame. + RefPtr<FirstFrameVideoOutput> mFirstFrameVideoOutput; +}; + +static uint32_t sDecoderCaptureSourceId = 0; +static uint32_t sStreamCaptureSourceId = 0; +class HTMLMediaElement::MediaElementTrackSource + : public MediaStreamTrackSource, + public MediaStreamTrackSource::Sink, + public MediaStreamTrackConsumer { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaElementTrackSource, + MediaStreamTrackSource) + + /* MediaDecoder track source */ + MediaElementTrackSource(ProcessedMediaTrack* aTrack, nsIPrincipal* aPrincipal, + OutputMuteState aMuteState, bool aHasAlpha) + : MediaStreamTrackSource( + aPrincipal, nsString(), + TrackingId(TrackingId::Source::MediaElementDecoder, + sDecoderCaptureSourceId++, + TrackingId::TrackAcrossProcesses::Yes)), + mTrack(aTrack), + mIntendedElementMuteState(aMuteState), + mElementMuteState(aMuteState), + mMediaDecoderHasAlpha(Some(aHasAlpha)) { + MOZ_ASSERT(mTrack); + } + + /* MediaStream track source */ + MediaElementTrackSource(MediaStreamTrack* aCapturedTrack, + MediaStreamTrackSource* aCapturedTrackSource, + ProcessedMediaTrack* aTrack, MediaInputPort* aPort, + OutputMuteState aMuteState) + : MediaStreamTrackSource( + aCapturedTrackSource->GetPrincipal(), nsString(), + TrackingId(TrackingId::Source::MediaElementStream, + sStreamCaptureSourceId++, + TrackingId::TrackAcrossProcesses::Yes)), + mCapturedTrack(aCapturedTrack), + mCapturedTrackSource(aCapturedTrackSource), + mTrack(aTrack), + mPort(aPort), + mIntendedElementMuteState(aMuteState), + mElementMuteState(aMuteState) { + MOZ_ASSERT(mTrack); + MOZ_ASSERT(mCapturedTrack); + MOZ_ASSERT(mCapturedTrackSource); + MOZ_ASSERT(mPort); + + mCapturedTrack->AddConsumer(this); + mCapturedTrackSource->RegisterSink(this); + } + + void SetEnabled(bool aEnabled) { + if (!mTrack) { + return; + } + mTrack->SetDisabledTrackMode(aEnabled ? DisabledTrackMode::ENABLED + : DisabledTrackMode::SILENCE_FREEZE); + } + + void SetPrincipal(RefPtr<nsIPrincipal> aPrincipal) { + mPrincipal = std::move(aPrincipal); + MediaStreamTrackSource::PrincipalChanged(); + } + + void SetMutedByElement(OutputMuteState aMuteState) { + if (mIntendedElementMuteState == aMuteState) { + return; + } + mIntendedElementMuteState = aMuteState; + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "MediaElementTrackSource::SetMutedByElement", + [self = RefPtr<MediaElementTrackSource>(this), this, aMuteState] { + mElementMuteState = aMuteState; + MediaStreamTrackSource::MutedChanged(Muted()); + })); + } + + void Destroy() override { + if (mCapturedTrack) { + mCapturedTrack->RemoveConsumer(this); + mCapturedTrack = nullptr; + } + if (mCapturedTrackSource) { + mCapturedTrackSource->UnregisterSink(this); + mCapturedTrackSource = nullptr; + } + if (mTrack && !mTrack->IsDestroyed()) { + mTrack->Destroy(); + } + if (mPort) { + mPort->Destroy(); + mPort = nullptr; + } + } + + MediaSourceEnum GetMediaSource() const override { + return MediaSourceEnum::Other; + } + + void Stop() override { + // Do nothing. There may appear new output streams + // that need tracks sourced from this source, so we + // cannot destroy things yet. + } + + /** + * Do not keep the track source alive. The source lifetime is controlled by + * its associated tracks. + */ + bool KeepsSourceAlive() const override { return false; } + + /** + * Do not keep the track source on. It is controlled by its associated tracks. + */ + bool Enabled() const override { return false; } + + void Disable() override {} + + void Enable() override {} + + void PrincipalChanged() override { + if (!mCapturedTrackSource) { + // This could happen during shutdown. + return; + } + + SetPrincipal(mCapturedTrackSource->GetPrincipal()); + } + + void MutedChanged(bool aNewState) override { + MediaStreamTrackSource::MutedChanged(Muted()); + } + + void OverrideEnded() override { + Destroy(); + MediaStreamTrackSource::OverrideEnded(); + } + + void NotifyEnabledChanged(MediaStreamTrack* aTrack, bool aEnabled) override { + MediaStreamTrackSource::MutedChanged(Muted()); + } + + bool Muted() const { + return mElementMuteState == OutputMuteState::Muted || + (mCapturedTrack && + (mCapturedTrack->Muted() || !mCapturedTrack->Enabled())); + } + + bool HasAlpha() const override { + if (mCapturedTrack) { + return mCapturedTrack->AsVideoStreamTrack() + ? mCapturedTrack->AsVideoStreamTrack()->HasAlpha() + : false; + } + return mMediaDecoderHasAlpha.valueOr(false); + } + + ProcessedMediaTrack* Track() const { return mTrack; } + + private: + virtual ~MediaElementTrackSource() { Destroy(); }; + + RefPtr<MediaStreamTrack> mCapturedTrack; + RefPtr<MediaStreamTrackSource> mCapturedTrackSource; + const RefPtr<ProcessedMediaTrack> mTrack; + RefPtr<MediaInputPort> mPort; + // The mute state as intended by the media element. + OutputMuteState mIntendedElementMuteState; + // The mute state as applied to this track source. It is applied async, so + // needs to be tracked separately from the intended state. + OutputMuteState mElementMuteState; + // Some<bool> if this is a MediaDecoder track source. + const Maybe<bool> mMediaDecoderHasAlpha; +}; + +HTMLMediaElement::OutputMediaStream::OutputMediaStream( + RefPtr<DOMMediaStream> aStream, bool aCapturingAudioOnly, + bool aFinishWhenEnded) + : mStream(std::move(aStream)), + mCapturingAudioOnly(aCapturingAudioOnly), + mFinishWhenEnded(aFinishWhenEnded) {} +HTMLMediaElement::OutputMediaStream::~OutputMediaStream() = default; + +void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + HTMLMediaElement::OutputMediaStream& aField, + const char* aName, uint32_t aFlags) { + ImplCycleCollectionTraverse(aCallback, aField.mStream, "mStream", aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mLiveTracks, "mLiveTracks", + aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mFinishWhenEndedLoadingSrc, + "mFinishWhenEndedLoadingSrc", aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mFinishWhenEndedAttrStream, + "mFinishWhenEndedAttrStream", aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mFinishWhenEndedMediaSource, + "mFinishWhenEndedMediaSource", aFlags); +} + +void ImplCycleCollectionUnlink(HTMLMediaElement::OutputMediaStream& aField) { + ImplCycleCollectionUnlink(aField.mStream); + ImplCycleCollectionUnlink(aField.mLiveTracks); + ImplCycleCollectionUnlink(aField.mFinishWhenEndedLoadingSrc); + ImplCycleCollectionUnlink(aField.mFinishWhenEndedAttrStream); + ImplCycleCollectionUnlink(aField.mFinishWhenEndedMediaSource); +} + +NS_IMPL_ADDREF_INHERITED(HTMLMediaElement::MediaElementTrackSource, + MediaStreamTrackSource) +NS_IMPL_RELEASE_INHERITED(HTMLMediaElement::MediaElementTrackSource, + MediaStreamTrackSource) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + HTMLMediaElement::MediaElementTrackSource) +NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource) +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement::MediaElementTrackSource) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED( + HTMLMediaElement::MediaElementTrackSource, MediaStreamTrackSource) + tmp->Destroy(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCapturedTrack) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCapturedTrackSource) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + HTMLMediaElement::MediaElementTrackSource, MediaStreamTrackSource) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCapturedTrack) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCapturedTrackSource) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +/** + * There is a reference cycle involving this class: MediaLoadListener + * holds a reference to the HTMLMediaElement, which holds a reference + * to an nsIChannel, which holds a reference to this listener. + * We break the reference cycle in OnStartRequest by clearing mElement. + */ +class HTMLMediaElement::MediaLoadListener final + : public nsIChannelEventSink, + public nsIInterfaceRequestor, + public nsIObserver, + public nsIThreadRetargetableStreamListener { + ~MediaLoadListener() = default; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICHANNELEVENTSINK + NS_DECL_NSIOBSERVER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + public: + explicit MediaLoadListener(HTMLMediaElement* aElement) + : mElement(aElement), mLoadID(aElement->GetCurrentLoadID()) { + MOZ_ASSERT(mElement, "Must pass an element to call back"); + } + + private: + RefPtr<HTMLMediaElement> mElement; + nsCOMPtr<nsIStreamListener> mNextListener; + const uint32_t mLoadID; +}; + +NS_IMPL_ISUPPORTS(HTMLMediaElement::MediaLoadListener, nsIRequestObserver, + nsIStreamListener, nsIChannelEventSink, nsIInterfaceRequestor, + nsIObserver, nsIThreadRetargetableStreamListener) + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + nsContentUtils::UnregisterShutdownObserver(this); + + // Clear mElement to break cycle so we don't leak on shutdown + mElement = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::OnStartRequest(nsIRequest* aRequest) { + nsContentUtils::UnregisterShutdownObserver(this); + + if (!mElement) { + // We've been notified by the shutdown observer, and are shutting down. + return NS_BINDING_ABORTED; + } + + // The element is only needed until we've had a chance to call + // InitializeDecoderForChannel. So make sure mElement is cleared here. + RefPtr<HTMLMediaElement> element; + element.swap(mElement); + + if (mLoadID != element->GetCurrentLoadID()) { + // The channel has been cancelled before we had a chance to create + // a decoder. Abort, don't dispatch an "error" event, as the new load + // may not be in an error state. + return NS_BINDING_ABORTED; + } + + // Don't continue to load if the request failed or has been canceled. + nsresult status; + nsresult rv = aRequest->GetStatus(&status); + NS_ENSURE_SUCCESS(rv, rv); + if (NS_FAILED(status)) { + if (element) { + // Handle media not loading error because source was a tracking URL (or + // fingerprinting, cryptomining, etc). + // We make a note of this media node by including it in a dedicated + // array of blocked tracking nodes under its parent document. + if (net::UrlClassifierFeatureFactory::IsClassifierBlockingErrorCode( + status)) { + element->OwnerDoc()->AddBlockedNodeByClassifier(element); + } + element->NotifyLoadError( + nsPrintfCString("%u: %s", uint32_t(status), "Request failed")); + } + return status; + } + + nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(aRequest); + bool succeeded; + if (hc && NS_SUCCEEDED(hc->GetRequestSucceeded(&succeeded)) && !succeeded) { + uint32_t responseStatus = 0; + Unused << hc->GetResponseStatus(&responseStatus); + nsAutoCString statusText; + Unused << hc->GetResponseStatusText(statusText); + // we need status text for resist fingerprinting mode's message allowlist + if (statusText.IsEmpty()) { + net_GetDefaultStatusTextForCode(responseStatus, statusText); + } + element->NotifyLoadError( + nsPrintfCString("%u: %s", responseStatus, statusText.get())); + + nsAutoString code; + code.AppendInt(responseStatus); + nsAutoString src; + element->GetCurrentSrc(src); + AutoTArray<nsString, 2> params = {code, src}; + element->ReportLoadError("MediaLoadHttpError", params); + return NS_BINDING_ABORTED; + } + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel && + NS_SUCCEEDED(rv = element->InitializeDecoderForChannel( + channel, getter_AddRefs(mNextListener))) && + mNextListener) { + rv = mNextListener->OnStartRequest(aRequest); + } else { + // If InitializeDecoderForChannel() returned an error, fire a network error. + if (NS_FAILED(rv) && !mNextListener) { + // Load failed, attempt to load the next candidate resource. If there + // are none, this will trigger a MEDIA_ERR_SRC_NOT_SUPPORTED error. + element->NotifyLoadError("Failed to init decoder"_ns); + } + // If InitializeDecoderForChannel did not return a listener (but may + // have otherwise succeeded), we abort the connection since we aren't + // interested in keeping the channel alive ourselves. + rv = NS_BINDING_ABORTED; + } + + return rv; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::OnStopRequest(nsIRequest* aRequest, + nsresult aStatus) { + if (mNextListener) { + return mNextListener->OnStopRequest(aRequest, aStatus); + } + return NS_OK; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aStream, + uint64_t aOffset, + uint32_t aCount) { + if (!mNextListener) { + NS_ERROR( + "Must have a chained listener; OnStartRequest should have " + "canceled this request"); + return NS_BINDING_ABORTED; + } + return mNextListener->OnDataAvailable(aRequest, aStream, aOffset, aCount); +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::OnDataFinished(nsresult aStatus) { + if (!mNextListener) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable = + do_QueryInterface(mNextListener); + if (retargetable) { + return retargetable->OnDataFinished(aStatus); + } + + return NS_OK; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t aFlags, + nsIAsyncVerifyRedirectCallback* cb) { + // TODO is this really correct?? See bug #579329. + if (mElement) { + mElement->OnChannelRedirect(aOldChannel, aNewChannel, aFlags); + } + nsCOMPtr<nsIChannelEventSink> sink = do_QueryInterface(mNextListener); + if (sink) { + return sink->AsyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, cb); + } + cb->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::CheckListenerChain() { + MOZ_ASSERT(mNextListener); + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable = + do_QueryInterface(mNextListener); + if (retargetable) { + return retargetable->CheckListenerChain(); + } + return NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +HTMLMediaElement::MediaLoadListener::GetInterface(const nsIID& aIID, + void** aResult) { + return QueryInterface(aIID, aResult); +} + +void HTMLMediaElement::ReportLoadError(const char* aMsg, + const nsTArray<nsString>& aParams) { + ReportToConsole(nsIScriptError::warningFlag, aMsg, aParams); +} + +void HTMLMediaElement::ReportToConsole( + uint32_t aErrorFlags, const char* aMsg, + const nsTArray<nsString>& aParams) const { + nsContentUtils::ReportToConsole(aErrorFlags, "Media"_ns, OwnerDoc(), + nsContentUtils::eDOM_PROPERTIES, aMsg, + aParams); +} + +class HTMLMediaElement::AudioChannelAgentCallback final + : public nsIAudioChannelAgentCallback { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(AudioChannelAgentCallback) + + explicit AudioChannelAgentCallback(HTMLMediaElement* aOwner) + : mOwner(aOwner), + mAudioChannelVolume(1.0), + mPlayingThroughTheAudioChannel(false), + mIsOwnerAudible(IsOwnerAudible()), + mIsShutDown(false) { + MOZ_ASSERT(mOwner); + MaybeCreateAudioChannelAgent(); + } + + void UpdateAudioChannelPlayingState() { + MOZ_ASSERT(!mIsShutDown); + bool playingThroughTheAudioChannel = IsPlayingThroughTheAudioChannel(); + + if (playingThroughTheAudioChannel != mPlayingThroughTheAudioChannel) { + if (!MaybeCreateAudioChannelAgent()) { + return; + } + + mPlayingThroughTheAudioChannel = playingThroughTheAudioChannel; + if (mPlayingThroughTheAudioChannel) { + StartAudioChannelAgent(); + } else { + StopAudioChanelAgent(); + } + } + } + + void NotifyPlayStateChanged() { + MOZ_ASSERT(!mIsShutDown); + UpdateAudioChannelPlayingState(); + } + + NS_IMETHODIMP WindowVolumeChanged(float aVolume, bool aMuted) override { + MOZ_ASSERT(mAudioChannelAgent); + + MOZ_LOG( + AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("HTMLMediaElement::AudioChannelAgentCallback, WindowVolumeChanged, " + "this = %p, aVolume = %f, aMuted = %s\n", + this, aVolume, aMuted ? "true" : "false")); + + if (mAudioChannelVolume != aVolume) { + mAudioChannelVolume = aVolume; + mOwner->SetVolumeInternal(); + } + + const uint32_t muted = mOwner->mMuted; + if (aMuted && !mOwner->ComputedMuted()) { + mOwner->SetMutedInternal(muted | MUTED_BY_AUDIO_CHANNEL); + } else if (!aMuted && mOwner->ComputedMuted()) { + mOwner->SetMutedInternal(muted & ~MUTED_BY_AUDIO_CHANNEL); + } + + return NS_OK; + } + + NS_IMETHODIMP WindowSuspendChanged(SuspendTypes aSuspend) override { + // Currently this method is only be used for delaying autoplay, and we've + // separated related codes to `MediaPlaybackDelayPolicy`. + return NS_OK; + } + + NS_IMETHODIMP WindowAudioCaptureChanged(bool aCapture) override { + MOZ_ASSERT(mAudioChannelAgent); + AudioCaptureTrackChangeIfNeeded(); + return NS_OK; + } + + void AudioCaptureTrackChangeIfNeeded() { + MOZ_ASSERT(!mIsShutDown); + if (!IsPlayingStarted()) { + return; + } + + MOZ_ASSERT(mAudioChannelAgent); + bool isCapturing = mAudioChannelAgent->IsWindowAudioCapturingEnabled(); + mOwner->AudioCaptureTrackChange(isCapturing); + } + + void NotifyAudioPlaybackChanged(AudibleChangedReasons aReason) { + MOZ_ASSERT(!mIsShutDown); + AudibleState newAudibleState = IsOwnerAudible(); + MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("HTMLMediaElement::AudioChannelAgentCallback, " + "NotifyAudioPlaybackChanged, this=%p, current=%s, new=%s", + this, AudibleStateToStr(mIsOwnerAudible), + AudibleStateToStr(newAudibleState))); + if (mIsOwnerAudible == newAudibleState) { + return; + } + + mIsOwnerAudible = newAudibleState; + if (IsPlayingStarted()) { + mAudioChannelAgent->NotifyStartedAudible(mIsOwnerAudible, aReason); + } + } + + void Shutdown() { + MOZ_ASSERT(!mIsShutDown); + if (mAudioChannelAgent && mAudioChannelAgent->IsPlayingStarted()) { + StopAudioChanelAgent(); + } + mAudioChannelAgent = nullptr; + mIsShutDown = true; + } + + float GetEffectiveVolume() const { + MOZ_ASSERT(!mIsShutDown); + return static_cast<float>(mOwner->Volume()) * mAudioChannelVolume; + } + + private: + ~AudioChannelAgentCallback() { MOZ_ASSERT(mIsShutDown); }; + + bool MaybeCreateAudioChannelAgent() { + if (mAudioChannelAgent) { + return true; + } + + mAudioChannelAgent = new AudioChannelAgent(); + nsresult rv = + mAudioChannelAgent->Init(mOwner->OwnerDoc()->GetInnerWindow(), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + mAudioChannelAgent = nullptr; + MOZ_LOG( + AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, + ("HTMLMediaElement::AudioChannelAgentCallback, Fail to initialize " + "the audio channel agent, this = %p\n", + this)); + return false; + } + + return true; + } + + void StartAudioChannelAgent() { + MOZ_ASSERT(mAudioChannelAgent); + MOZ_ASSERT(!mAudioChannelAgent->IsPlayingStarted()); + if (NS_WARN_IF(NS_FAILED( + mAudioChannelAgent->NotifyStartedPlaying(IsOwnerAudible())))) { + return; + } + mAudioChannelAgent->PullInitialUpdate(); + } + + void StopAudioChanelAgent() { + MOZ_ASSERT(mAudioChannelAgent); + MOZ_ASSERT(mAudioChannelAgent->IsPlayingStarted()); + mAudioChannelAgent->NotifyStoppedPlaying(); + // If we have started audio capturing before, we have to tell media element + // to clear the output capturing track. + mOwner->AudioCaptureTrackChange(false); + } + + bool IsPlayingStarted() { + if (MaybeCreateAudioChannelAgent()) { + return mAudioChannelAgent->IsPlayingStarted(); + } + return false; + } + + AudibleState IsOwnerAudible() const { + // paused media doesn't produce any sound. + if (mOwner->mPaused) { + return AudibleState::eNotAudible; + } + return mOwner->IsAudible() ? AudibleState::eAudible + : AudibleState::eNotAudible; + } + + bool IsPlayingThroughTheAudioChannel() const { + // If we have an error, we are not playing. + if (mOwner->GetError()) { + return false; + } + + // We should consider any bfcached page or inactive document as non-playing. + if (!mOwner->OwnerDoc()->IsActive()) { + return false; + } + + // Media is suspended by the docshell. + if (mOwner->ShouldBeSuspendedByInactiveDocShell()) { + return false; + } + + // Are we paused + if (mOwner->mPaused) { + return false; + } + + // No audio track + if (!mOwner->HasAudio()) { + return false; + } + + // A loop always is playing + if (mOwner->HasAttr(nsGkAtoms::loop)) { + return true; + } + + // If we are actually playing... + if (mOwner->IsCurrentlyPlaying()) { + return true; + } + + // If we are playing an external stream. + if (mOwner->mSrcAttrStream) { + return true; + } + + return false; + } + + RefPtr<AudioChannelAgent> mAudioChannelAgent; + HTMLMediaElement* mOwner; + + // The audio channel volume + float mAudioChannelVolume; + // Is this media element playing? + bool mPlayingThroughTheAudioChannel; + // Indicate whether media element is audible for users. + AudibleState mIsOwnerAudible; + bool mIsShutDown; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement::AudioChannelAgentCallback) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN( + HTMLMediaElement::AudioChannelAgentCallback) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelAgent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN( + HTMLMediaElement::AudioChannelAgentCallback) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelAgent) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION( + HTMLMediaElement::AudioChannelAgentCallback) + NS_INTERFACE_MAP_ENTRY(nsIAudioChannelAgentCallback) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLMediaElement::AudioChannelAgentCallback) +NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLMediaElement::AudioChannelAgentCallback) + +class HTMLMediaElement::ChannelLoader final { + public: + NS_INLINE_DECL_REFCOUNTING(ChannelLoader); + + void LoadInternal(HTMLMediaElement* aElement) { + if (mCancelled) { + return; + } + + // determine what security checks need to be performed in AsyncOpen(). + nsSecurityFlags securityFlags = + aElement->ShouldCheckAllowOrigin() + ? nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT + : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT; + + if (aElement->GetCORSMode() == CORS_USE_CREDENTIALS) { + securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } + + securityFlags |= nsILoadInfo::SEC_ALLOW_CHROME; + + MOZ_ASSERT( + aElement->IsAnyOfHTMLElements(nsGkAtoms::audio, nsGkAtoms::video)); + nsContentPolicyType contentPolicyType = + aElement->IsHTMLElement(nsGkAtoms::audio) + ? nsIContentPolicy::TYPE_INTERNAL_AUDIO + : nsIContentPolicy::TYPE_INTERNAL_VIDEO; + + // If aElement has 'triggeringprincipal' attribute, we will use the value as + // triggeringPrincipal for the channel, otherwise it will default to use + // aElement->NodePrincipal(). + // This function returns true when aElement has 'triggeringprincipal', so if + // setAttrs is true we will override the origin attributes on the channel + // later. + nsCOMPtr<nsIPrincipal> triggeringPrincipal; + bool setAttrs = nsContentUtils::QueryTriggeringPrincipal( + aElement, aElement->mLoadingSrcTriggeringPrincipal, + getter_AddRefs(triggeringPrincipal)); + + nsCOMPtr<nsILoadGroup> loadGroup = aElement->GetDocumentLoadGroup(); + nsCOMPtr<nsIChannel> channel; + nsresult rv = NS_NewChannelWithTriggeringPrincipal( + getter_AddRefs(channel), aElement->mLoadingSrc, + static_cast<Element*>(aElement), triggeringPrincipal, securityFlags, + contentPolicyType, + nullptr, // aPerformanceStorage + loadGroup, + nullptr, // aCallbacks + nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY | + nsIChannel::LOAD_MEDIA_SNIFFER_OVERRIDES_CONTENT_TYPE | + nsIChannel::LOAD_CALL_CONTENT_SNIFFERS); + + if (NS_FAILED(rv)) { + // Notify load error so the element will try next resource candidate. + aElement->NotifyLoadError("Fail to create channel"_ns); + return; + } + + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + if (setAttrs) { + // The function simply returns NS_OK, so we ignore the return value. + Unused << loadInfo->SetOriginAttributes( + triggeringPrincipal->OriginAttributesRef()); + } + loadInfo->SetIsMediaRequest(true); + loadInfo->SetIsMediaInitialRequest(true); + + nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(channel)); + if (cos) { + if (aElement->mUseUrgentStartForChannel) { + cos->AddClassFlags(nsIClassOfService::UrgentStart); + + // Reset the flag to avoid loading again without initiated by user + // interaction. + aElement->mUseUrgentStartForChannel = false; + } + + // Unconditionally disable throttling since we want the media to fluently + // play even when we switch the tab to background. + cos->AddClassFlags(nsIClassOfService::DontThrottle); + } + + // The listener holds a strong reference to us. This creates a + // reference cycle, once we've set mChannel, which is manually broken + // in the listener's OnStartRequest method after it is finished with + // the element. The cycle will also be broken if we get a shutdown + // notification before OnStartRequest fires. Necko guarantees that + // OnStartRequest will eventually fire if we don't shut down first. + RefPtr<MediaLoadListener> loadListener = new MediaLoadListener(aElement); + + channel->SetNotificationCallbacks(loadListener); + + nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(channel); + if (hc) { + // Use a byte range request from the start of the resource. + // This enables us to detect if the stream supports byte range + // requests, and therefore seeking, early. + rv = hc->SetRequestHeader("Range"_ns, "bytes=0-"_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + aElement->SetRequestHeaders(hc); + } + + rv = channel->AsyncOpen(loadListener); + if (NS_FAILED(rv)) { + // Notify load error so the element will try next resource candidate. + aElement->NotifyLoadError("Failed to open channel"_ns); + return; + } + + // Else the channel must be open and starting to download. If it encounters + // a non-catastrophic failure, it will set a new task to continue loading + // another candidate. It's safe to set it as mChannel now. + mChannel = channel; + + // loadListener will be unregistered either on shutdown or when + // OnStartRequest for the channel we just opened fires. + nsContentUtils::RegisterShutdownObserver(loadListener); + } + + nsresult Load(HTMLMediaElement* aElement) { + MOZ_ASSERT(aElement); + // Per bug 1235183 comment 8, we can't spin the event loop from stable + // state. Defer NS_NewChannel() to a new regular runnable. + return aElement->OwnerDoc()->Dispatch(NewRunnableMethod<HTMLMediaElement*>( + "ChannelLoader::LoadInternal", this, &ChannelLoader::LoadInternal, + aElement)); + } + + void Cancel() { + mCancelled = true; + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "HTMLMediaElement::ChannelLoader::Cancel"_ns); + mChannel = nullptr; + } + } + + void Done() { + MOZ_ASSERT(mChannel); + // Decoder successfully created, the decoder now owns the MediaResource + // which owns the channel. + mChannel = nullptr; + } + + nsresult Redirect(nsIChannel* aChannel, nsIChannel* aNewChannel, + uint32_t aFlags) { + NS_ASSERTION(aChannel == mChannel, "Channels should match!"); + mChannel = aNewChannel; + + // Handle forwarding of Range header so that the intial detection + // of seeking support (via result code 206) works across redirects. + nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aChannel); + NS_ENSURE_STATE(http); + + constexpr auto rangeHdr = "Range"_ns; + + nsAutoCString rangeVal; + if (NS_SUCCEEDED(http->GetRequestHeader(rangeHdr, rangeVal))) { + NS_ENSURE_STATE(!rangeVal.IsEmpty()); + + http = do_QueryInterface(aNewChannel); + NS_ENSURE_STATE(http); + + nsresult rv = http->SetRequestHeader(rangeHdr, rangeVal, false); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + private: + ~ChannelLoader() { MOZ_ASSERT(!mChannel); } + // Holds a reference to the first channel we open to the media resource. + // Once the decoder is created, control over the channel passes to the + // decoder, and we null out this reference. We must store this in case + // we need to cancel the channel before control of it passes to the decoder. + nsCOMPtr<nsIChannel> mChannel; + + bool mCancelled = false; +}; + +class HTMLMediaElement::ErrorSink { + public: + explicit ErrorSink(HTMLMediaElement* aOwner) : mOwner(aOwner) { + MOZ_ASSERT(mOwner); + } + + void SetError(uint16_t aErrorCode, const nsACString& aErrorDetails) { + // Since we have multiple paths calling into DecodeError, e.g. + // MediaKeys::Terminated and EMEH264Decoder::Error. We should take the 1st + // one only in order not to fire multiple 'error' events. + if (mError) { + return; + } + + if (!IsValidErrorCode(aErrorCode)) { + NS_ASSERTION(false, "Undefined MediaError codes!"); + return; + } + + mError = new MediaError(mOwner, aErrorCode, aErrorDetails); + mOwner->DispatchAsyncEvent(u"error"_ns); + if (mOwner->ReadyState() == HAVE_NOTHING && + aErrorCode == MEDIA_ERR_ABORTED) { + // https://html.spec.whatwg.org/multipage/embedded-content.html#media-data-processing-steps-list + // "If the media data fetching process is aborted by the user" + mOwner->DispatchAsyncEvent(u"abort"_ns); + mOwner->ChangeNetworkState(NETWORK_EMPTY); + mOwner->DispatchAsyncEvent(u"emptied"_ns); + if (mOwner->mDecoder) { + mOwner->ShutdownDecoder(); + } + } else if (aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED) { + mOwner->ChangeNetworkState(NETWORK_NO_SOURCE); + } else { + mOwner->ChangeNetworkState(NETWORK_IDLE); + } + } + + void ResetError() { mError = nullptr; } + + RefPtr<MediaError> mError; + + private: + bool IsValidErrorCode(const uint16_t& aErrorCode) const { + return (aErrorCode == MEDIA_ERR_DECODE || aErrorCode == MEDIA_ERR_NETWORK || + aErrorCode == MEDIA_ERR_ABORTED || + aErrorCode == MEDIA_ERR_SRC_NOT_SUPPORTED); + } + + // Media elememt's life cycle would be longer than error sink, so we use the + // raw pointer and this class would only be referenced by media element. + HTMLMediaElement* mOwner; +}; + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLMediaElement, + nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaSource) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcMediaSource) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSrcAttrStream) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourcePointer) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLoadBlockedDoc) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSourceLoadCandidate) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelWrapper) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mErrorSink->mError) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputStreams) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputTrackSources); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPlayed); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextTrackManager) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioTrackList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVideoTrackList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaKeys) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIncomingMediaKeys) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedVideoStreamTrack) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingPlayPromises) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSeekDOMPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSetMediaKeysDOMPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEventBlocker) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLMediaElement, + nsGenericHTMLElement) + tmp->RemoveMutationObserver(tmp); + if (tmp->mSrcStream) { + // Need to unhook everything that EndSrcMediaStreamPlayback would normally + // do, without creating any new strong references. + if (tmp->mSelectedVideoStreamTrack) { + tmp->mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(tmp); + } + if (tmp->mMediaStreamRenderer) { + tmp->mMediaStreamRenderer->Shutdown(); + // We null out mMediaStreamRenderer here since Shutdown() will shut down + // its WatchManager, and UpdateSrcStreamPotentiallyPlaying() contains a + // guard for this. + tmp->mMediaStreamRenderer = nullptr; + } + if (tmp->mSecondaryMediaStreamRenderer) { + tmp->mSecondaryMediaStreamRenderer->Shutdown(); + tmp->mSecondaryMediaStreamRenderer = nullptr; + } + if (tmp->mMediaStreamTrackListener) { + tmp->mSrcStream->UnregisterTrackListener( + tmp->mMediaStreamTrackListener.get()); + } + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcAttrStream) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcMediaSource) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourcePointer) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoadBlockedDoc) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceLoadCandidate) + if (tmp->mAudioChannelWrapper) { + tmp->mAudioChannelWrapper->Shutdown(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelWrapper) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mErrorSink->mError) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputStreams) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputTrackSources) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlayed) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioTrackList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mVideoTrackList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaKeys) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mIncomingMediaKeys) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedVideoStreamTrack) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingPlayPromises) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSeekDOMPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSetMediaKeysDOMPromise) + if (tmp->mMediaControlKeyListener) { + tmp->mMediaControlKeyListener->StopIfNeeded(); + } + if (tmp->mEventBlocker) { + tmp->mEventBlocker->Shutdown(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLMediaElement, + nsGenericHTMLElement) + +void HTMLMediaElement::AddSizeOfExcludingThis(nsWindowSizes& aSizes, + size_t* aNodeSize) const { + nsGenericHTMLElement::AddSizeOfExcludingThis(aSizes, aNodeSize); + + // There are many other fields that might be worth reporting, but as seen in + // bug 1595603, the event we postpone to dispatch can grow to be very large + // sometimes, so at least report that. + if (mEventBlocker) { + *aNodeSize += + mEventBlocker->SizeOfExcludingThis(aSizes.mState.mMallocSizeOf); + } +} + +void HTMLMediaElement::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + if (aChild == mSourcePointer) { + mSourcePointer = aPreviousSibling; + } +} + +already_AddRefed<MediaSource> HTMLMediaElement::GetMozMediaSourceObject() + const { + RefPtr<MediaSource> source = mMediaSource; + return source.forget(); +} + +already_AddRefed<Promise> HTMLMediaElement::MozRequestDebugInfo( + ErrorResult& aRv) { + RefPtr<Promise> promise = CreateDOMPromise(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + auto result = MakeUnique<dom::HTMLMediaElementDebugInfo>(); + if (mMediaKeys) { + GetEMEInfo(result->mEMEInfo); + } + if (mVideoFrameContainer) { + result->mCompositorDroppedFrames = + mVideoFrameContainer->GetDroppedImageCount(); + } + if (mDecoder) { + mDecoder->RequestDebugInfo(result->mDecoder) + ->Then( + AbstractMainThread(), __func__, + [promise, ptr = std::move(result)]() { + promise->MaybeResolve(ptr.get()); + }, + []() { + MOZ_ASSERT_UNREACHABLE("Unexpected RequestDebugInfo() rejection"); + }); + } else { + promise->MaybeResolve(result.get()); + } + return promise.forget(); +} + +/* static */ +void HTMLMediaElement::MozEnableDebugLog(const GlobalObject&) { + DecoderDoctorLogger::EnableLogging(); +} + +already_AddRefed<Promise> HTMLMediaElement::MozRequestDebugLog( + ErrorResult& aRv) { + RefPtr<Promise> promise = CreateDOMPromise(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + DecoderDoctorLogger::RetrieveMessages(this)->Then( + AbstractMainThread(), __func__, + [promise](const nsACString& aString) { + promise->MaybeResolve(NS_ConvertUTF8toUTF16(aString)); + }, + [promise](nsresult rv) { promise->MaybeReject(rv); }); + + return promise.forget(); +} + +void HTMLMediaElement::SetVisible(bool aVisible) { + mForcedHidden = !aVisible; + if (mDecoder) { + mDecoder->SetForcedHidden(!aVisible); + } +} + +bool HTMLMediaElement::IsVideoDecodingSuspended() const { + return mDecoder && mDecoder->IsVideoDecodingSuspended(); +} + +double HTMLMediaElement::TotalVideoPlayTime() const { + return mDecoder ? mDecoder->GetTotalVideoPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::TotalVideoHDRPlayTime() const { + return mDecoder ? mDecoder->GetTotalVideoHDRPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::VisiblePlayTime() const { + return mDecoder ? mDecoder->GetVisibleVideoPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::InvisiblePlayTime() const { + return mDecoder ? mDecoder->GetInvisibleVideoPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::TotalAudioPlayTime() const { + return mDecoder ? mDecoder->GetTotalAudioPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::AudiblePlayTime() const { + return mDecoder ? mDecoder->GetAudiblePlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::InaudiblePlayTime() const { + return mDecoder ? mDecoder->GetInaudiblePlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::MutedPlayTime() const { + return mDecoder ? mDecoder->GetMutedPlayTimeInSeconds() : -1.0; +} + +double HTMLMediaElement::VideoDecodeSuspendedTime() const { + return mDecoder ? mDecoder->GetVideoDecodeSuspendedTimeInSeconds() : -1.0; +} + +void HTMLMediaElement::SetFormatDiagnosticsReportForMimeType( + const nsAString& aMimeType, DecoderDoctorReportType aType) { + DecoderDoctorDiagnostics diagnostics; + diagnostics.SetDecoderDoctorReportType(aType); + diagnostics.StoreFormatDiagnostics(OwnerDoc(), aMimeType, false /* can play*/, + __func__); +} + +void HTMLMediaElement::SetDecodeError(const nsAString& aError, + ErrorResult& aRv) { + // The reason we use this map-ish structure is because we can't use + // `CR.NS_ERROR.*` directly in test. In order to use them in test, we have to + // add them into `xpc.msg`. As we won't use `CR.NS_ERROR.*` in the production + // code, adding them to `xpc.msg` seems an overdesign and adding maintenance + // effort (exposing them in CR also needs to add a description, which is + // useless because we won't show them to users) + static struct { + const char* mName; + nsresult mResult; + } kSupportedErrorList[] = { + {"NS_ERROR_DOM_MEDIA_ABORT_ERR", NS_ERROR_DOM_MEDIA_ABORT_ERR}, + {"NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR", + NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR}, + {"NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR", + NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR}, + {"NS_ERROR_DOM_MEDIA_DECODE_ERR", NS_ERROR_DOM_MEDIA_DECODE_ERR}, + {"NS_ERROR_DOM_MEDIA_FATAL_ERR", NS_ERROR_DOM_MEDIA_FATAL_ERR}, + {"NS_ERROR_DOM_MEDIA_METADATA_ERR", NS_ERROR_DOM_MEDIA_METADATA_ERR}, + {"NS_ERROR_DOM_MEDIA_OVERFLOW_ERR", NS_ERROR_DOM_MEDIA_OVERFLOW_ERR}, + {"NS_ERROR_DOM_MEDIA_MEDIASINK_ERR", NS_ERROR_DOM_MEDIA_MEDIASINK_ERR}, + {"NS_ERROR_DOM_MEDIA_DEMUXER_ERR", NS_ERROR_DOM_MEDIA_DEMUXER_ERR}, + {"NS_ERROR_DOM_MEDIA_CDM_ERR", NS_ERROR_DOM_MEDIA_CDM_ERR}, + {"NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR", + NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR}}; + for (auto& error : kSupportedErrorList) { + if (strcmp(error.mName, NS_ConvertUTF16toUTF8(aError).get()) == 0) { + DecoderDoctorDiagnostics diagnostics; + diagnostics.StoreDecodeError(OwnerDoc(), error.mResult, u""_ns, __func__); + return; + } + } + aRv.Throw(NS_ERROR_FAILURE); +} + +void HTMLMediaElement::SetAudioSinkFailedStartup() { + DecoderDoctorDiagnostics diagnostics; + diagnostics.StoreEvent(OwnerDoc(), + {DecoderDoctorEvent::eAudioSinkStartup, + NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR}, + __func__); +} + +already_AddRefed<layers::Image> HTMLMediaElement::GetCurrentImage() { + MarkAsTainted(); + + // TODO: In bug 1345404, handle case when video decoder is already suspended. + ImageContainer* container = GetImageContainer(); + if (!container) { + return nullptr; + } + + AutoLockImage lockImage(container); + RefPtr<layers::Image> image = lockImage.GetImage(TimeStamp::Now()); + return image.forget(); +} + +bool HTMLMediaElement::HasSuspendTaint() const { + MOZ_ASSERT(!mDecoder || (mDecoder->HasSuspendTaint() == mHasSuspendTaint)); + return mHasSuspendTaint; +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::GetSrcObject() const { + return do_AddRef(mSrcAttrStream); +} + +void HTMLMediaElement::SetSrcObject(DOMMediaStream& aValue) { + SetSrcObject(&aValue); +} + +void HTMLMediaElement::SetSrcObject(DOMMediaStream* aValue) { + for (auto& outputStream : mOutputStreams) { + if (aValue == outputStream.mStream) { + ReportToConsole(nsIScriptError::warningFlag, + "MediaElementStreamCaptureCycle"); + return; + } + } + mSrcAttrStream = aValue; + UpdateAudioChannelPlayingState(); + DoLoad(); +} + +bool HTMLMediaElement::Ended() { + return (mDecoder && mDecoder->IsEnded()) || + (mSrcStream && mSrcStreamReportPlaybackEnded); +} + +void HTMLMediaElement::GetCurrentSrc(nsAString& aCurrentSrc) { + nsAutoCString src; + GetCurrentSpec(src); + CopyUTF8toUTF16(src, aCurrentSrc); +} + +nsresult HTMLMediaElement::OnChannelRedirect(nsIChannel* aChannel, + nsIChannel* aNewChannel, + uint32_t aFlags) { + MOZ_ASSERT(mChannelLoader); + return mChannelLoader->Redirect(aChannel, aNewChannel, aFlags); +} + +void HTMLMediaElement::ShutdownDecoder() { + RemoveMediaElementFromURITable(); + NS_ASSERTION(mDecoder, "Must have decoder to shut down"); + + mWaitingForKeyListener.DisconnectIfExists(); + if (mMediaSource) { + mMediaSource->CompletePendingTransactions(); + } + mDecoder->Shutdown(); + DDUNLINKCHILD(mDecoder.get()); + mDecoder = nullptr; +} + +void HTMLMediaElement::AbortExistingLoads() { + // Abort any already-running instance of the resource selection algorithm. + mLoadWaitStatus = NOT_WAITING; + + // Set a new load ID. This will cause events which were enqueued + // with a different load ID to silently be cancelled. + mCurrentLoadID++; + + // Immediately reject or resolve the already-dispatched + // nsResolveOrRejectPendingPlayPromisesRunners. These runners won't be + // executed again later since the mCurrentLoadID had been changed. + for (auto& runner : mPendingPlayPromisesRunners) { + runner->ResolveOrReject(); + } + mPendingPlayPromisesRunners.Clear(); + + if (mChannelLoader) { + mChannelLoader->Cancel(); + mChannelLoader = nullptr; + } + + bool fireTimeUpdate = false; + + if (mDecoder) { + fireTimeUpdate = mDecoder->GetCurrentTime() != 0.0; + ShutdownDecoder(); + } + if (mSrcStream) { + EndSrcMediaStreamPlayback(); + } + + if (mMediaSource) { + OwnerDoc()->RemoveMediaElementWithMSE(); + } + + RemoveMediaElementFromURITable(); + mLoadingSrcTriggeringPrincipal = nullptr; + DDLOG(DDLogCategory::Property, "loading_src", ""); + DDUNLINKCHILD(mMediaSource.get()); + mMediaSource = nullptr; + + if (mNetworkState == NETWORK_LOADING || mNetworkState == NETWORK_IDLE) { + DispatchAsyncEvent(u"abort"_ns); + } + + bool hadVideo = HasVideo(); + mErrorSink->ResetError(); + mCurrentPlayRangeStart = -1.0; + mPlayed = new TimeRanges(ToSupports(OwnerDoc())); + mLoadedDataFired = false; + mCanAutoplayFlag = true; + mIsLoadingFromSourceChildren = false; + mSuspendedAfterFirstFrame = false; + mAllowSuspendAfterFirstFrame = true; + mHaveQueuedSelectResource = false; + mSuspendedForPreloadNone = false; + mDownloadSuspendedByCache = false; + mMediaInfo = MediaInfo(); + mIsEncrypted = false; + mPendingEncryptedInitData.Reset(); + mWaitingForKey = NOT_WAITING_FOR_KEY; + mSourcePointer = nullptr; + mIsBlessed = false; + SetAudibleState(false); + + mTags = nullptr; + + if (mNetworkState != NETWORK_EMPTY) { + NS_ASSERTION(!mDecoder && !mSrcStream, + "How did someone setup a new stream/decoder already?"); + + DispatchAsyncEvent(u"emptied"_ns); + + // ChangeNetworkState() will call UpdateAudioChannelPlayingState() + // indirectly which depends on mPaused. So we need to update mPaused first. + if (!mPaused) { + mPaused = true; + PlayPromise::RejectPromises(TakePendingPlayPromises(), + NS_ERROR_DOM_MEDIA_ABORT_ERR); + } + ChangeNetworkState(NETWORK_EMPTY); + RemoveMediaTracks(); + UpdateOutputTrackSources(); + ChangeReadyState(HAVE_NOTHING); + + // TODO: Apply the rules for text track cue rendering Bug 865407 + if (mTextTrackManager) { + mTextTrackManager->GetTextTracks()->SetCuesInactive(); + } + + if (fireTimeUpdate) { + // Since we destroyed the decoder above, the current playback position + // will now be reported as 0. The playback position was non-zero when + // we destroyed the decoder, so fire a timeupdate event so that the + // change will be reflected in the controls. + FireTimeUpdate(TimeupdateType::eMandatory); + } + UpdateAudioChannelPlayingState(); + } + + if (IsVideo() && hadVideo) { + // Ensure we render transparent black after resetting video resolution. + Maybe<nsIntSize> size = Some(nsIntSize(0, 0)); + Invalidate(ImageSizeChanged::Yes, size, ForceInvalidate::No); + } + + // As aborting current load would stop current playback, so we have no need to + // resume a paused media element. + ClearResumeDelayedMediaPlaybackAgentIfNeeded(); + + mMediaControlKeyListener->StopIfNeeded(); + + // We may have changed mPaused, mCanAutoplayFlag, and other + // things which can affect AddRemoveSelfReference + AddRemoveSelfReference(); + + mIsRunningSelectResource = false; + + AssertReadyStateIsNothing(); +} + +void HTMLMediaElement::NoSupportedMediaSourceError( + const nsACString& aErrorDetails) { + if (mDecoder) { + ShutdownDecoder(); + } + + bool isSameOriginLoad = false; + nsresult rv = NS_ERROR_NOT_AVAILABLE; + if (mSrcAttrTriggeringPrincipal && mLoadingSrc) { + rv = mSrcAttrTriggeringPrincipal->IsSameOrigin(mLoadingSrc, + &isSameOriginLoad); + } + + if (NS_SUCCEEDED(rv) && !isSameOriginLoad) { + // aErrorDetails can include sensitive details like MimeType or HTTP Status + // Code. In case we're loading a 3rd party resource we should not leak this + // and pass a Generic Error Message + mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED, + "Failed to open media"_ns); + } else { + mErrorSink->SetError(MEDIA_ERR_SRC_NOT_SUPPORTED, aErrorDetails); + } + + RemoveMediaTracks(); + ChangeDelayLoadStatus(false); + UpdateAudioChannelPlayingState(); + PlayPromise::RejectPromises(TakePendingPlayPromises(), + NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR); +} + +// Runs a "synchronous section", a function that must run once the event loop +// has reached a "stable state" +// http://www.whatwg.org/specs/web-apps/current-work/multipage/webappapis.html#synchronous-section +void HTMLMediaElement::RunInStableState(nsIRunnable* aRunnable) { + if (mShuttingDown) { + return; + } + + nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction( + "HTMLMediaElement::RunInStableState", + [self = RefPtr<HTMLMediaElement>(this), loadId = GetCurrentLoadID(), + runnable = RefPtr<nsIRunnable>(aRunnable)]() { + if (self->GetCurrentLoadID() != loadId) { + return; + } + runnable->Run(); + }); + nsContentUtils::RunInStableState(task.forget()); +} + +void HTMLMediaElement::QueueLoadFromSourceTask() { + if (!mIsLoadingFromSourceChildren || mShuttingDown) { + return; + } + + if (mDecoder) { + // Reset readyState to HAVE_NOTHING since we're going to load a new decoder. + ShutdownDecoder(); + ChangeReadyState(HAVE_NOTHING); + } + + AssertReadyStateIsNothing(); + + ChangeDelayLoadStatus(true); + ChangeNetworkState(NETWORK_LOADING); + RefPtr<Runnable> r = + NewRunnableMethod("HTMLMediaElement::LoadFromSourceChildren", this, + &HTMLMediaElement::LoadFromSourceChildren); + RunInStableState(r); +} + +void HTMLMediaElement::QueueSelectResourceTask() { + // Don't allow multiple async select resource calls to be queued. + if (mHaveQueuedSelectResource) return; + mHaveQueuedSelectResource = true; + ChangeNetworkState(NETWORK_NO_SOURCE); + RefPtr<Runnable> r = + NewRunnableMethod("HTMLMediaElement::SelectResourceWrapper", this, + &HTMLMediaElement::SelectResourceWrapper); + RunInStableState(r); +} + +static bool HasSourceChildren(nsIContent* aElement) { + for (nsIContent* child = aElement->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (child->IsHTMLElement(nsGkAtoms::source)) { + return true; + } + } + return false; +} + +static nsCString DocumentOrigin(Document* aDoc) { + if (!aDoc) { + return "null"_ns; + } + nsCOMPtr<nsIPrincipal> principal = aDoc->NodePrincipal(); + if (!principal) { + return "null"_ns; + } + nsCString origin; + if (NS_FAILED(principal->GetOrigin(origin))) { + return "null"_ns; + } + return origin; +} + +void HTMLMediaElement::Load() { + LOG(LogLevel::Debug, + ("%p Load() hasSrcAttrStream=%d hasSrcAttr=%d hasSourceChildren=%d " + "handlingInput=%d hasAutoplayAttr=%d AllowedToPlay=%d " + "ownerDoc=%p (%s) ownerDocUserActivated=%d " + "muted=%d volume=%f", + this, !!mSrcAttrStream, HasAttr(nsGkAtoms::src), HasSourceChildren(this), + UserActivation::IsHandlingUserInput(), HasAttr(nsGkAtoms::autoplay), + AllowedToPlay(), OwnerDoc(), DocumentOrigin(OwnerDoc()).get(), + OwnerDoc()->HasBeenUserGestureActivated(), mMuted, mVolume)); + + if (mIsRunningLoadMethod) { + return; + } + + mIsDoingExplicitLoad = true; + DoLoad(); +} + +void HTMLMediaElement::DoLoad() { + // Check if media is allowed for the docshell. + nsCOMPtr<nsIDocShell> docShell = OwnerDoc()->GetDocShell(); + if (docShell && !docShell->GetAllowMedia()) { + LOG(LogLevel::Debug, ("%p Media not allowed", this)); + return; + } + + if (mIsRunningLoadMethod) { + return; + } + + if (UserActivation::IsHandlingUserInput()) { + // Detect if user has interacted with element so that play will not be + // blocked when initiated by a script. This enables sites to capture user + // intent to play by calling load() in the click handler of a "catalog + // view" of a gallery of videos. + mIsBlessed = true; + // Mark the channel as urgent-start when autoplay so that it will play the + // media from src after loading enough resource. + if (HasAttr(nsGkAtoms::autoplay)) { + mUseUrgentStartForChannel = true; + } + } + + SetPlayedOrSeeked(false); + mIsRunningLoadMethod = true; + AbortExistingLoads(); + SetPlaybackRate(mDefaultPlaybackRate, IgnoreErrors()); + QueueSelectResourceTask(); + ResetState(); + mIsRunningLoadMethod = false; +} + +void HTMLMediaElement::ResetState() { + // There might be a pending MediaDecoder::PlaybackPositionChanged() which + // will overwrite |mMediaInfo.mVideo.mDisplay| in UpdateMediaSize() to give + // staled videoWidth and videoHeight. We have to call ForgetElement() here + // such that the staled callbacks won't reach us. + if (mVideoFrameContainer) { + mVideoFrameContainer->ForgetElement(); + mVideoFrameContainer = nullptr; + } + if (mMediaStreamRenderer) { + // mMediaStreamRenderer, has a strong reference to mVideoFrameContainer. + mMediaStreamRenderer->Shutdown(); + mMediaStreamRenderer = nullptr; + } + if (mSecondaryMediaStreamRenderer) { + // mSecondaryMediaStreamRenderer, has a strong reference to + // the secondary VideoFrameContainer. + mSecondaryMediaStreamRenderer->Shutdown(); + mSecondaryMediaStreamRenderer = nullptr; + } +} + +void HTMLMediaElement::SelectResourceWrapper() { + SelectResource(); + MaybeBeginCloningVisually(); + mIsRunningSelectResource = false; + mHaveQueuedSelectResource = false; + mIsDoingExplicitLoad = false; +} + +void HTMLMediaElement::SelectResource() { + if (!mSrcAttrStream && !HasAttr(nsGkAtoms::src) && !HasSourceChildren(this)) { + // The media element has neither a src attribute nor any source + // element children, abort the load. + ChangeNetworkState(NETWORK_EMPTY); + ChangeDelayLoadStatus(false); + return; + } + + ChangeDelayLoadStatus(true); + + ChangeNetworkState(NETWORK_LOADING); + DispatchAsyncEvent(u"loadstart"_ns); + + // Delay setting mIsRunningSeletResource until after UpdatePreloadAction + // so that we don't lose our state change by bailing out of the preload + // state update + UpdatePreloadAction(); + mIsRunningSelectResource = true; + + // If we have a 'src' attribute, use that exclusively. + nsAutoString src; + if (mSrcAttrStream) { + SetupSrcMediaStreamPlayback(mSrcAttrStream); + } else if (GetAttr(nsGkAtoms::src, src)) { + nsCOMPtr<nsIURI> uri; + MediaResult rv = NewURIFromString(src, getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv)) { + LOG(LogLevel::Debug, ("%p Trying load from src=%s", this, + NS_ConvertUTF16toUTF8(src).get())); + NS_ASSERTION( + !mIsLoadingFromSourceChildren, + "Should think we're not loading from source children by default"); + + RemoveMediaElementFromURITable(); + if (!mSrcMediaSource) { + mLoadingSrc = uri; + } else { + mLoadingSrc = nullptr; + } + mLoadingSrcTriggeringPrincipal = mSrcAttrTriggeringPrincipal; + DDLOG(DDLogCategory::Property, "loading_src", + nsCString(NS_ConvertUTF16toUTF8(src))); + bool hadMediaSource = !!mMediaSource; + mMediaSource = mSrcMediaSource; + if (mMediaSource && !hadMediaSource) { + OwnerDoc()->AddMediaElementWithMSE(); + } + DDLINKCHILD("mediasource", mMediaSource.get()); + UpdatePreloadAction(); + if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE && !mMediaSource) { + // preload:none media, suspend the load here before we make any + // network requests. + SuspendLoad(); + return; + } + + rv = LoadResource(); + if (NS_SUCCEEDED(rv)) { + return; + } + } else { + AutoTArray<nsString, 1> params = {src}; + ReportLoadError("MediaLoadInvalidURI", params); + rv = MediaResult(rv.Code(), "MediaLoadInvalidURI"); + } + // The media element has neither a src attribute nor a source element child: + // set the networkState to NETWORK_EMPTY, and abort these steps; the + // synchronous section ends. + GetMainThreadSerialEventTarget()->Dispatch(NewRunnableMethod<nsCString>( + "HTMLMediaElement::NoSupportedMediaSourceError", this, + &HTMLMediaElement::NoSupportedMediaSourceError, rv.Description())); + } else { + // Otherwise, the source elements will be used. + mIsLoadingFromSourceChildren = true; + LoadFromSourceChildren(); + } +} + +void HTMLMediaElement::NotifyLoadError(const nsACString& aErrorDetails) { + if (!mIsLoadingFromSourceChildren) { + LOG(LogLevel::Debug, ("NotifyLoadError(), no supported media error")); + NoSupportedMediaSourceError(aErrorDetails); + } else if (mSourceLoadCandidate) { + DispatchAsyncSourceError(mSourceLoadCandidate); + QueueLoadFromSourceTask(); + } else { + NS_WARNING("Should know the source we were loading from!"); + } +} + +void HTMLMediaElement::NotifyMediaTrackAdded(dom::MediaTrack* aTrack) { + // The set of tracks changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); +} + +void HTMLMediaElement::NotifyMediaTrackRemoved(dom::MediaTrack* aTrack) { + // The set of tracks changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); +} + +void HTMLMediaElement::NotifyMediaTrackEnabled(dom::MediaTrack* aTrack) { + MOZ_ASSERT(aTrack); + if (!aTrack) { + return; + } +#ifdef DEBUG + nsString id; + aTrack->GetId(id); + + LOG(LogLevel::Debug, ("MediaElement %p %sTrack with id %s enabled", this, + aTrack->AsAudioTrack() ? "Audio" : "Video", + NS_ConvertUTF16toUTF8(id).get())); +#endif + + MOZ_ASSERT((aTrack->AsAudioTrack() && aTrack->AsAudioTrack()->Enabled()) || + (aTrack->AsVideoTrack() && aTrack->AsVideoTrack()->Selected())); + + if (aTrack->AsAudioTrack()) { + SetMutedInternal(mMuted & ~MUTED_BY_AUDIO_TRACK); + } else if (aTrack->AsVideoTrack()) { + if (!IsVideo()) { + MOZ_ASSERT(false); + return; + } + mDisableVideo = false; + } else { + MOZ_ASSERT(false, "Unknown track type"); + } + + if (mSrcStream) { + if (AudioTrack* t = aTrack->AsAudioTrack()) { + if (mMediaStreamRenderer) { + mMediaStreamRenderer->AddTrack(t->GetAudioStreamTrack()); + } + } else if (VideoTrack* t = aTrack->AsVideoTrack()) { + MOZ_ASSERT(!mSelectedVideoStreamTrack); + + mSelectedVideoStreamTrack = t->GetVideoStreamTrack(); + mSelectedVideoStreamTrack->AddPrincipalChangeObserver(this); + if (mMediaStreamRenderer) { + mMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack); + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack); + } + if (mMediaInfo.HasVideo()) { + mMediaInfo.mVideo.SetAlpha(mSelectedVideoStreamTrack->HasAlpha()); + } + nsContentUtils::CombineResourcePrincipals( + &mSrcStreamVideoPrincipal, mSelectedVideoStreamTrack->GetPrincipal()); + } + } + + // The set of enabled/selected tracks changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); +} + +void HTMLMediaElement::NotifyMediaTrackDisabled(dom::MediaTrack* aTrack) { + MOZ_ASSERT(aTrack); + if (!aTrack) { + return; + } + + nsString id; + aTrack->GetId(id); + + LOG(LogLevel::Debug, ("MediaElement %p %sTrack with id %s disabled", this, + aTrack->AsAudioTrack() ? "Audio" : "Video", + NS_ConvertUTF16toUTF8(id).get())); + + MOZ_ASSERT((!aTrack->AsAudioTrack() || !aTrack->AsAudioTrack()->Enabled()) && + (!aTrack->AsVideoTrack() || !aTrack->AsVideoTrack()->Selected())); + + if (AudioTrack* t = aTrack->AsAudioTrack()) { + if (mSrcStream) { + if (mMediaStreamRenderer) { + mMediaStreamRenderer->RemoveTrack(t->GetAudioStreamTrack()); + } + } + // If we don't have any live tracks, we don't need to mute MediaElement. + MOZ_DIAGNOSTIC_ASSERT(AudioTracks(), "Element can't have been unlinked"); + if (AudioTracks()->Length() > 0) { + bool shouldMute = true; + for (uint32_t i = 0; i < AudioTracks()->Length(); ++i) { + if ((*AudioTracks())[i]->Enabled()) { + shouldMute = false; + break; + } + } + + if (shouldMute) { + SetMutedInternal(mMuted | MUTED_BY_AUDIO_TRACK); + } + } + } else if (aTrack->AsVideoTrack()) { + if (mSrcStream) { + MOZ_DIAGNOSTIC_ASSERT(mSelectedVideoStreamTrack == + aTrack->AsVideoTrack()->GetVideoStreamTrack()); + if (mMediaStreamRenderer) { + mMediaStreamRenderer->RemoveTrack(mSelectedVideoStreamTrack); + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->RemoveTrack(mSelectedVideoStreamTrack); + } + mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(this); + mSelectedVideoStreamTrack = nullptr; + } + } + + // The set of enabled/selected tracks changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); +} + +void HTMLMediaElement::DealWithFailedElement(nsIContent* aSourceElement) { + if (mShuttingDown) { + return; + } + + DispatchAsyncSourceError(aSourceElement); + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod("HTMLMediaElement::QueueLoadFromSourceTask", this, + &HTMLMediaElement::QueueLoadFromSourceTask)); +} + +void HTMLMediaElement::LoadFromSourceChildren() { + NS_ASSERTION(mDelayingLoadEvent, + "Should delay load event (if in document) during load"); + NS_ASSERTION(mIsLoadingFromSourceChildren, + "Must remember we're loading from source children"); + + AddMutationObserverUnlessExists(this); + + RemoveMediaTracks(); + + while (true) { + HTMLSourceElement* child = GetNextSource(); + if (!child) { + // Exhausted candidates, wait for more candidates to be appended to + // the media element. + mLoadWaitStatus = WAITING_FOR_SOURCE; + ChangeNetworkState(NETWORK_NO_SOURCE); + ChangeDelayLoadStatus(false); + ReportLoadError("MediaLoadExhaustedCandidates"); + return; + } + + // Must have src attribute. + nsAutoString src; + if (!child->GetAttr(nsGkAtoms::src, src)) { + ReportLoadError("MediaLoadSourceMissingSrc"); + DealWithFailedElement(child); + return; + } + + // If we have a type attribute, it must be a supported type. + nsAutoString type; + if (child->GetAttr(nsGkAtoms::type, type) && !type.IsEmpty()) { + DecoderDoctorDiagnostics diagnostics; + CanPlayStatus canPlay = GetCanPlay(type, &diagnostics); + diagnostics.StoreFormatDiagnostics(OwnerDoc(), type, + canPlay != CANPLAY_NO, __func__); + if (canPlay == CANPLAY_NO) { + // Check that at least one other source child exists and report that + // we will try to load that one next. + nsIContent* nextChild = mSourcePointer->GetNextSibling(); + AutoTArray<nsString, 2> params = {type, src}; + + while (nextChild) { + if (nextChild && nextChild->IsHTMLElement(nsGkAtoms::source)) { + ReportLoadError("MediaLoadUnsupportedTypeAttributeLoadingNextChild", + params); + break; + } + + nextChild = nextChild->GetNextSibling(); + }; + + if (!nextChild) { + ReportLoadError("MediaLoadUnsupportedTypeAttribute", params); + } + + DealWithFailedElement(child); + return; + } + } + nsAutoString media; + child->GetAttr(nsGkAtoms::media, media); + HTMLSourceElement* childSrc = HTMLSourceElement::FromNode(child); + MOZ_ASSERT(childSrc, "Expect child to be HTMLSourceElement"); + if (childSrc && !childSrc->MatchesCurrentMedia()) { + AutoTArray<nsString, 2> params = {media, src}; + ReportLoadError("MediaLoadSourceMediaNotMatched", params); + DealWithFailedElement(child); + LOG(LogLevel::Debug, + ("%p Media did not match from <source>=%s type=%s media=%s", this, + NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get(), + NS_ConvertUTF16toUTF8(media).get())); + return; + } + LOG(LogLevel::Debug, + ("%p Trying load from <source>=%s type=%s media=%s", this, + NS_ConvertUTF16toUTF8(src).get(), NS_ConvertUTF16toUTF8(type).get(), + NS_ConvertUTF16toUTF8(media).get())); + + nsCOMPtr<nsIURI> uri; + NewURIFromString(src, getter_AddRefs(uri)); + if (!uri) { + AutoTArray<nsString, 1> params = {src}; + ReportLoadError("MediaLoadInvalidURI", params); + DealWithFailedElement(child); + return; + } + + RemoveMediaElementFromURITable(); + mLoadingSrc = uri; + mLoadingSrcTriggeringPrincipal = child->GetSrcTriggeringPrincipal(); + DDLOG(DDLogCategory::Property, "loading_src", + nsCString(NS_ConvertUTF16toUTF8(src))); + bool hadMediaSource = !!mMediaSource; + mMediaSource = child->GetSrcMediaSource(); + if (mMediaSource && !hadMediaSource) { + OwnerDoc()->AddMediaElementWithMSE(); + } + DDLINKCHILD("mediasource", mMediaSource.get()); + NS_ASSERTION(mNetworkState == NETWORK_LOADING, + "Network state should be loading"); + + if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE && !mMediaSource) { + // preload:none media, suspend the load here before we make any + // network requests. + SuspendLoad(); + return; + } + + if (NS_SUCCEEDED(LoadResource())) { + return; + } + + // If we fail to load, loop back and try loading the next resource. + DispatchAsyncSourceError(child); + } + MOZ_ASSERT_UNREACHABLE("Execution should not reach here!"); +} + +void HTMLMediaElement::SuspendLoad() { + mSuspendedForPreloadNone = true; + ChangeNetworkState(NETWORK_IDLE); + ChangeDelayLoadStatus(false); +} + +void HTMLMediaElement::ResumeLoad(PreloadAction aAction) { + NS_ASSERTION(mSuspendedForPreloadNone, + "Must be halted for preload:none to resume from preload:none " + "suspended load."); + mSuspendedForPreloadNone = false; + mPreloadAction = aAction; + ChangeDelayLoadStatus(true); + ChangeNetworkState(NETWORK_LOADING); + if (!mIsLoadingFromSourceChildren) { + // We were loading from the element's src attribute. + MediaResult rv = LoadResource(); + if (NS_FAILED(rv)) { + NoSupportedMediaSourceError(rv.Description()); + } + } else { + // We were loading from a child <source> element. Try to resume the + // load of that child, and if that fails, try the next child. + if (NS_FAILED(LoadResource())) { + LoadFromSourceChildren(); + } + } +} + +bool HTMLMediaElement::AllowedToPlay() const { + return media::AutoplayPolicy::IsAllowedToPlay(*this); +} + +uint32_t HTMLMediaElement::GetPreloadDefault() const { + if (mMediaSource) { + return HTMLMediaElement::PRELOAD_ATTR_METADATA; + } + if (OnCellularConnection()) { + return Preferences::GetInt("media.preload.default.cellular", + HTMLMediaElement::PRELOAD_ATTR_NONE); + } + return Preferences::GetInt("media.preload.default", + HTMLMediaElement::PRELOAD_ATTR_METADATA); +} + +uint32_t HTMLMediaElement::GetPreloadDefaultAuto() const { + if (OnCellularConnection()) { + return Preferences::GetInt("media.preload.auto.cellular", + HTMLMediaElement::PRELOAD_ATTR_METADATA); + } + return Preferences::GetInt("media.preload.auto", + HTMLMediaElement::PRELOAD_ENOUGH); +} + +void HTMLMediaElement::UpdatePreloadAction() { + PreloadAction nextAction = PRELOAD_UNDEFINED; + // If autoplay is set, or we're playing, we should always preload data, + // as we'll need it to play. + if ((AllowedToPlay() && HasAttr(nsGkAtoms::autoplay)) || !mPaused) { + nextAction = HTMLMediaElement::PRELOAD_ENOUGH; + } else { + // Find the appropriate preload action by looking at the attribute. + const nsAttrValue* val = + mAttrs.GetAttr(nsGkAtoms::preload, kNameSpaceID_None); + // MSE doesn't work if preload is none, so it ignores the pref when src is + // from MSE. + uint32_t preloadDefault = GetPreloadDefault(); + uint32_t preloadAuto = GetPreloadDefaultAuto(); + if (!val) { + // Attribute is not set. Use the preload action specified by the + // media.preload.default pref, or just preload metadata if not present. + nextAction = static_cast<PreloadAction>(preloadDefault); + } else if (val->Type() == nsAttrValue::eEnum) { + PreloadAttrValue attr = + static_cast<PreloadAttrValue>(val->GetEnumValue()); + if (attr == HTMLMediaElement::PRELOAD_ATTR_EMPTY || + attr == HTMLMediaElement::PRELOAD_ATTR_AUTO) { + nextAction = static_cast<PreloadAction>(preloadAuto); + } else if (attr == HTMLMediaElement::PRELOAD_ATTR_METADATA) { + nextAction = HTMLMediaElement::PRELOAD_METADATA; + } else if (attr == HTMLMediaElement::PRELOAD_ATTR_NONE) { + nextAction = HTMLMediaElement::PRELOAD_NONE; + } + } else { + // Use the suggested "missing value default" of "metadata", or the value + // specified by the media.preload.default, if present. + nextAction = static_cast<PreloadAction>(preloadDefault); + } + } + + if (nextAction == HTMLMediaElement::PRELOAD_NONE && mIsDoingExplicitLoad) { + nextAction = HTMLMediaElement::PRELOAD_METADATA; + } + + mPreloadAction = nextAction; + + if (nextAction == HTMLMediaElement::PRELOAD_ENOUGH) { + if (mSuspendedForPreloadNone) { + // Our load was previouly suspended due to the media having preload + // value "none". The preload value has changed to preload:auto, so + // resume the load. + ResumeLoad(PRELOAD_ENOUGH); + } else { + // Preload as much of the video as we can, i.e. don't suspend after + // the first frame. + StopSuspendingAfterFirstFrame(); + } + + } else if (nextAction == HTMLMediaElement::PRELOAD_METADATA) { + // Ensure that the video can be suspended after first frame. + mAllowSuspendAfterFirstFrame = true; + if (mSuspendedForPreloadNone) { + // Our load was previouly suspended due to the media having preload + // value "none". The preload value has changed to preload:metadata, so + // resume the load. We'll pause the load again after we've read the + // metadata. + ResumeLoad(PRELOAD_METADATA); + } + } +} + +MediaResult HTMLMediaElement::LoadResource() { + NS_ASSERTION(mDelayingLoadEvent, + "Should delay load event (if in document) during load"); + + if (mChannelLoader) { + mChannelLoader->Cancel(); + mChannelLoader = nullptr; + } + + // Set the media element's CORS mode only when loading a resource + mCORSMode = AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin)); + + HTMLMediaElement* other = LookupMediaElementURITable(mLoadingSrc); + if (other && other->mDecoder) { + // Clone it. + // TODO: remove the cast by storing ChannelMediaDecoder in the URI table. + nsresult rv = InitializeDecoderAsClone( + static_cast<ChannelMediaDecoder*>(other->mDecoder.get())); + if (NS_SUCCEEDED(rv)) return rv; + } + + if (mMediaSource) { + MediaDecoderInit decoderInit( + this, this, mMuted ? 0.0 : mVolume, mPreservesPitch, + ClampPlaybackRate(mPlaybackRate), + mPreloadAction == HTMLMediaElement::PRELOAD_METADATA, mHasSuspendTaint, + HasAttr(nsGkAtoms::loop), + MediaContainerType(MEDIAMIMETYPE("application/x.mediasource"))); + + RefPtr<MediaSourceDecoder> decoder = new MediaSourceDecoder(decoderInit); + if (!mMediaSource->Attach(decoder)) { + // TODO: Handle failure: run "If the media data cannot be fetched at + // all, due to network errors, causing the user agent to give up + // trying to fetch the resource" section of resource fetch algorithm. + decoder->Shutdown(); + return MediaResult(NS_ERROR_FAILURE, "Failed to attach MediaSource"); + } + ChangeDelayLoadStatus(false); + nsresult rv = decoder->Load(mMediaSource->GetPrincipal()); + if (NS_FAILED(rv)) { + decoder->Shutdown(); + LOG(LogLevel::Debug, + ("%p Failed to load for decoder %p", this, decoder.get())); + return MediaResult(rv, "Fail to load decoder"); + } + rv = FinishDecoderSetup(decoder); + return MediaResult(rv, "Failed to set up decoder"); + } + + AssertReadyStateIsNothing(); + + RefPtr<ChannelLoader> loader = new ChannelLoader; + nsresult rv = loader->Load(this); + if (NS_SUCCEEDED(rv)) { + mChannelLoader = std::move(loader); + } + return MediaResult(rv, "Failed to load channel"); +} + +nsresult HTMLMediaElement::LoadWithChannel(nsIChannel* aChannel, + nsIStreamListener** aListener) { + NS_ENSURE_ARG_POINTER(aChannel); + NS_ENSURE_ARG_POINTER(aListener); + + *aListener = nullptr; + + // Make sure we don't reenter during synchronous abort events. + if (mIsRunningLoadMethod) return NS_OK; + mIsRunningLoadMethod = true; + AbortExistingLoads(); + mIsRunningLoadMethod = false; + + mLoadingSrcTriggeringPrincipal = nullptr; + nsresult rv = aChannel->GetOriginalURI(getter_AddRefs(mLoadingSrc)); + NS_ENSURE_SUCCESS(rv, rv); + + ChangeDelayLoadStatus(true); + rv = InitializeDecoderForChannel(aChannel, aListener); + if (NS_FAILED(rv)) { + ChangeDelayLoadStatus(false); + return rv; + } + + SetPlaybackRate(mDefaultPlaybackRate, IgnoreErrors()); + DispatchAsyncEvent(u"loadstart"_ns); + + return NS_OK; +} + +bool HTMLMediaElement::Seeking() const { + return mDecoder && mDecoder->IsSeeking(); +} + +double HTMLMediaElement::CurrentTime() const { + if (mMediaStreamRenderer) { + return ToMicrosecondResolution(mMediaStreamRenderer->CurrentTime()); + } + + if (mDefaultPlaybackStartPosition == 0.0 && mDecoder) { + return std::clamp(mDecoder->GetCurrentTime(), 0.0, mDecoder->GetDuration()); + } + + return mDefaultPlaybackStartPosition; +} + +void HTMLMediaElement::FastSeek(double aTime, ErrorResult& aRv) { + LOG(LogLevel::Debug, ("%p FastSeek(%f) called by JS", this, aTime)); + Seek(aTime, SeekTarget::PrevSyncPoint, IgnoreErrors()); +} + +already_AddRefed<Promise> HTMLMediaElement::SeekToNextFrame(ErrorResult& aRv) { + /* This will cause JIT code to be kept around longer, to help performance + * when using SeekToNextFrame to iterate through every frame of a video. + */ + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + + if (win) { + if (JSObject* obj = win->AsGlobal()->GetGlobalJSObject()) { + js::NotifyAnimationActivity(obj); + } + } + + Seek(CurrentTime(), SeekTarget::NextFrame, aRv); + if (aRv.Failed()) { + return nullptr; + } + + mSeekDOMPromise = CreateDOMPromise(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + return do_AddRef(mSeekDOMPromise); +} + +void HTMLMediaElement::SetCurrentTime(double aCurrentTime, ErrorResult& aRv) { + LOG(LogLevel::Debug, + ("%p SetCurrentTime(%lf) called by JS", this, aCurrentTime)); + Seek(aCurrentTime, SeekTarget::Accurate, IgnoreErrors()); +} + +/** + * Check if aValue is inside a range of aRanges, and if so returns true + * and puts the range index in aIntervalIndex. If aValue is not + * inside a range, returns false, and aIntervalIndex + * is set to the index of the range which starts immediately after aValue + * (and can be aRanges.Length() if aValue is after the last range). + */ +static bool IsInRanges(TimeRanges& aRanges, double aValue, + uint32_t& aIntervalIndex) { + uint32_t length = aRanges.Length(); + + for (uint32_t i = 0; i < length; i++) { + double start = aRanges.Start(i); + if (start > aValue) { + aIntervalIndex = i; + return false; + } + double end = aRanges.End(i); + if (aValue <= end) { + aIntervalIndex = i; + return true; + } + } + aIntervalIndex = length; + return false; +} + +void HTMLMediaElement::Seek(double aTime, SeekTarget::Type aSeekType, + ErrorResult& aRv) { + // Note: Seek is called both by synchronous code that expects errors thrown in + // aRv, as well as asynchronous code that expects a promise. Make sure all + // synchronous errors are returned using aRv, not promise rejections. + + // aTime should be non-NaN. + MOZ_ASSERT(!std::isnan(aTime)); + + // Seeking step1, Set the media element's show poster flag to false. + // https://html.spec.whatwg.org/multipage/media.html#dom-media-seek + mShowPoster = false; + + // Detect if user has interacted with element by seeking so that + // play will not be blocked when initiated by a script. + if (UserActivation::IsHandlingUserInput()) { + mIsBlessed = true; + } + + StopSuspendingAfterFirstFrame(); + + if (mSrcAttrStream) { + // do nothing since media streams have an empty Seekable range. + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (mPlayed && mCurrentPlayRangeStart != -1.0) { + double rangeEndTime = CurrentTime(); + LOG(LogLevel::Debug, ("%p Adding \'played\' a range : [%f, %f]", this, + mCurrentPlayRangeStart, rangeEndTime)); + // Multiple seek without playing, or seek while playing. + if (mCurrentPlayRangeStart != rangeEndTime) { + // Don't round the left of the interval: it comes from script and needs + // to be exact. + mPlayed->Add(mCurrentPlayRangeStart, rangeEndTime); + } + // Reset the current played range start time. We'll re-set it once + // the seek completes. + mCurrentPlayRangeStart = -1.0; + } + + if (mReadyState == HAVE_NOTHING) { + mDefaultPlaybackStartPosition = aTime; + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (!mDecoder) { + // mDecoder must always be set in order to reach this point. + NS_ASSERTION(mDecoder, "SetCurrentTime failed: no decoder"); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // Clamp the seek target to inside the seekable ranges. + media::TimeRanges seekableRanges = mDecoder->GetSeekableTimeRanges(); + if (seekableRanges.IsInvalid()) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + RefPtr<TimeRanges> seekable = + new TimeRanges(ToSupports(OwnerDoc()), seekableRanges); + uint32_t length = seekable->Length(); + if (length == 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // If the position we want to seek to is not in a seekable range, we seek + // to the closest position in the seekable ranges instead. If two positions + // are equally close, we seek to the closest position from the currentTime. + // See seeking spec, point 7 : + // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html#seeking + uint32_t range = 0; + bool isInRange = IsInRanges(*seekable, aTime, range); + if (!isInRange) { + if (range == 0) { + // aTime is before the first range in |seekable|, the closest point we can + // seek to is the start of the first range. + aTime = seekable->Start(0); + } else if (range == length) { + // Seek target is after the end last range in seekable data. + // Clamp the seek target to the end of the last seekable range. + aTime = seekable->End(length - 1); + } else { + double leftBound = seekable->End(range - 1); + double rightBound = seekable->Start(range); + double distanceLeft = Abs(leftBound - aTime); + double distanceRight = Abs(rightBound - aTime); + if (distanceLeft == distanceRight) { + double currentTime = CurrentTime(); + distanceLeft = Abs(leftBound - currentTime); + distanceRight = Abs(rightBound - currentTime); + } + aTime = (distanceLeft < distanceRight) ? leftBound : rightBound; + } + } + + // TODO: The spec requires us to update the current time to reflect the + // actual seek target before beginning the synchronous section, but + // that requires changing all MediaDecoderReaders to support telling + // us the fastSeek target, and it's currently not possible to get + // this information as we don't yet control the demuxer for all + // MediaDecoderReaders. + + mPlayingBeforeSeek = IsPotentiallyPlaying(); + + // The media backend is responsible for dispatching the timeupdate + // event if it changes the playback position as a result of the seek. + LOG(LogLevel::Debug, ("%p SetCurrentTime(%f) starting seek", this, aTime)); + mDecoder->Seek(aTime, aSeekType); + + // We changed whether we're seeking so we need to AddRemoveSelfReference. + AddRemoveSelfReference(); +} + +double HTMLMediaElement::Duration() const { + if (mSrcStream) { + if (mSrcStreamPlaybackEnded) { + return CurrentTime(); + } + return std::numeric_limits<double>::infinity(); + } + + if (mDecoder) { + return mDecoder->GetDuration(); + } + + return std::numeric_limits<double>::quiet_NaN(); +} + +already_AddRefed<TimeRanges> HTMLMediaElement::Seekable() const { + media::TimeRanges seekable = + mDecoder ? mDecoder->GetSeekableTimeRanges() : media::TimeRanges(); + RefPtr<TimeRanges> ranges = new TimeRanges( + ToSupports(OwnerDoc()), seekable.ToMicrosecondResolution()); + return ranges.forget(); +} + +already_AddRefed<TimeRanges> HTMLMediaElement::Played() { + RefPtr<TimeRanges> ranges = new TimeRanges(ToSupports(OwnerDoc())); + + uint32_t timeRangeCount = 0; + if (mPlayed) { + timeRangeCount = mPlayed->Length(); + } + for (uint32_t i = 0; i < timeRangeCount; i++) { + double begin = mPlayed->Start(i); + double end = mPlayed->End(i); + ranges->Add(begin, end); + } + + if (mCurrentPlayRangeStart != -1.0) { + double now = CurrentTime(); + if (mCurrentPlayRangeStart != now) { + // Don't round the left of the interval: it comes from script and needs + // to be exact. + ranges->Add(mCurrentPlayRangeStart, now); + } + } + + ranges->Normalize(); + return ranges.forget(); +} + +void HTMLMediaElement::Pause(ErrorResult& aRv) { + LOG(LogLevel::Debug, ("%p Pause() called by JS", this)); + if (mNetworkState == NETWORK_EMPTY) { + LOG(LogLevel::Debug, ("Loading due to Pause()")); + DoLoad(); + } + PauseInternal(); +} + +void HTMLMediaElement::PauseInternal() { + if (mDecoder && mNetworkState != NETWORK_EMPTY) { + mDecoder->Pause(); + } + bool oldPaused = mPaused; + mPaused = true; + // Step 1, + // https://html.spec.whatwg.org/multipage/media.html#internal-pause-steps + mCanAutoplayFlag = false; + // We changed mPaused and mCanAutoplayFlag which can affect + // AddRemoveSelfReference + AddRemoveSelfReference(); + UpdateSrcMediaStreamPlaying(); + if (mAudioChannelWrapper) { + mAudioChannelWrapper->NotifyPlayStateChanged(); + } + + // We don't need to resume media which is paused explicitly by user. + ClearResumeDelayedMediaPlaybackAgentIfNeeded(); + + if (!oldPaused) { + FireTimeUpdate(TimeupdateType::eMandatory); + DispatchAsyncEvent(u"pause"_ns); + AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_ABORT_ERR); + } +} + +void HTMLMediaElement::SetVolume(double aVolume, ErrorResult& aRv) { + LOG(LogLevel::Debug, ("%p SetVolume(%f) called by JS", this, aVolume)); + + if (aVolume < 0.0 || aVolume > 1.0) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + if (aVolume == mVolume) return; + + mVolume = aVolume; + + // Here we want just to update the volume. + SetVolumeInternal(); + + DispatchAsyncEvent(u"volumechange"_ns); + + // We allow inaudible autoplay. But changing our volume may make this + // media audible. So pause if we are no longer supposed to be autoplaying. + PauseIfShouldNotBePlaying(); +} + +void HTMLMediaElement::MozGetMetadata(JSContext* aCx, + JS::MutableHandle<JSObject*> aResult, + ErrorResult& aRv) { + if (mReadyState < HAVE_METADATA) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + JS::Rooted<JSObject*> tags(aCx, JS_NewPlainObject(aCx)); + if (!tags) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + if (mTags) { + for (const auto& entry : *mTags) { + nsString wideValue; + CopyUTF8toUTF16(entry.GetData(), wideValue); + JS::Rooted<JSString*> string(aCx, + JS_NewUCStringCopyZ(aCx, wideValue.Data())); + if (!string || !JS_DefineProperty(aCx, tags, entry.GetKey().Data(), + string, JSPROP_ENUMERATE)) { + NS_WARNING("couldn't create metadata object!"); + aRv.Throw(NS_ERROR_FAILURE); + return; + } + } + } + + aResult.set(tags); +} + +void HTMLMediaElement::SetMutedInternal(uint32_t aMuted) { + uint32_t oldMuted = mMuted; + mMuted = aMuted; + + if (!!aMuted == !!oldMuted) { + return; + } + + SetVolumeInternal(); +} + +void HTMLMediaElement::PauseIfShouldNotBePlaying() { + if (GetPaused()) { + return; + } + if (!AllowedToPlay()) { + AUTOPLAY_LOG("pause because not allowed to play, element=%p", this); + ErrorResult rv; + Pause(rv); + } +} + +void HTMLMediaElement::SetVolumeInternal() { + float effectiveVolume = ComputedVolume(); + + if (mDecoder) { + mDecoder->SetVolume(effectiveVolume); + } else if (mMediaStreamRenderer) { + mMediaStreamRenderer->SetAudioOutputVolume(effectiveVolume); + } + + NotifyAudioPlaybackChanged( + AudioChannelService::AudibleChangedReasons::eVolumeChanged); +} + +void HTMLMediaElement::SetMuted(bool aMuted) { + LOG(LogLevel::Debug, ("%p SetMuted(%d) called by JS", this, aMuted)); + if (aMuted == Muted()) { + return; + } + + if (aMuted) { + SetMutedInternal(mMuted | MUTED_BY_CONTENT); + } else { + SetMutedInternal(mMuted & ~MUTED_BY_CONTENT); + } + + DispatchAsyncEvent(u"volumechange"_ns); + + // We allow inaudible autoplay. But changing our mute status may make this + // media audible. So pause if we are no longer supposed to be autoplaying. + PauseIfShouldNotBePlaying(); +} + +void HTMLMediaElement::GetAllEnabledMediaTracks( + nsTArray<RefPtr<MediaTrack>>& aTracks) { + if (AudioTrackList* tracks = AudioTracks()) { + for (size_t i = 0; i < tracks->Length(); ++i) { + AudioTrack* track = (*tracks)[i]; + if (track->Enabled()) { + aTracks.AppendElement(track); + } + } + } + if (IsVideo()) { + if (VideoTrackList* tracks = VideoTracks()) { + for (size_t i = 0; i < tracks->Length(); ++i) { + VideoTrack* track = (*tracks)[i]; + if (track->Selected()) { + aTracks.AppendElement(track); + } + } + } + } +} + +void HTMLMediaElement::SetCapturedOutputStreamsEnabled(bool aEnabled) { + for (const auto& entry : mOutputTrackSources.Values()) { + entry->SetEnabled(aEnabled); + } +} + +HTMLMediaElement::OutputMuteState HTMLMediaElement::OutputTracksMuted() { + return mPaused || mReadyState <= HAVE_CURRENT_DATA ? OutputMuteState::Muted + : OutputMuteState::Unmuted; +} + +void HTMLMediaElement::UpdateOutputTracksMuting() { + for (const auto& entry : mOutputTrackSources.Values()) { + entry->SetMutedByElement(OutputTracksMuted()); + } +} + +void HTMLMediaElement::AddOutputTrackSourceToOutputStream( + MediaElementTrackSource* aSource, OutputMediaStream& aOutputStream, + AddTrackMode aMode) { + if (aOutputStream.mStream == mSrcStream) { + // Cycle detected. This can happen since tracks are added async. + // We avoid forwarding it to the output here or we'd get into an infloop. + LOG(LogLevel::Warning, + ("NOT adding output track source %p to output stream " + "%p -- cycle detected", + aSource, aOutputStream.mStream.get())); + return; + } + + LOG(LogLevel::Debug, ("Adding output track source %p to output stream %p", + aSource, aOutputStream.mStream.get())); + + RefPtr<MediaStreamTrack> domTrack; + if (aSource->Track()->mType == MediaSegment::AUDIO) { + domTrack = new AudioStreamTrack( + aOutputStream.mStream->GetOwner(), aSource->Track(), aSource, + MediaStreamTrackState::Live, aSource->Muted()); + } else { + domTrack = new VideoStreamTrack( + aOutputStream.mStream->GetOwner(), aSource->Track(), aSource, + MediaStreamTrackState::Live, aSource->Muted()); + } + + aOutputStream.mLiveTracks.AppendElement(domTrack); + + switch (aMode) { + case AddTrackMode::ASYNC: + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod<StoreRefPtrPassByPtr<MediaStreamTrack>>( + "DOMMediaStream::AddTrackInternal", aOutputStream.mStream, + &DOMMediaStream::AddTrackInternal, domTrack)); + break; + case AddTrackMode::SYNC: + aOutputStream.mStream->AddTrackInternal(domTrack); + break; + default: + MOZ_CRASH("Unexpected mode"); + } + + LOG(LogLevel::Debug, + ("Created capture %s track %p", + domTrack->AsAudioStreamTrack() ? "audio" : "video", domTrack.get())); +} + +void HTMLMediaElement::UpdateOutputTrackSources() { + // This updates the track sources in mOutputTrackSources so they're in sync + // with the tracks being currently played, and state saying whether we should + // be capturing tracks. This method is long so here is a breakdown: + // - Figure out the tracks that should be captured + // - Diff those against currently captured tracks (mOutputTrackSources), into + // tracks-to-add, and tracks-to-remove + // - Remove the tracks in tracks-to-remove and dispatch "removetrack" and + // "ended" events for them + // - If playback has ended, or there is no longer a media provider object, + // remove any OutputMediaStreams that have the finish-when-ended flag set + // - Create track sources for, and add to OutputMediaStreams, the tracks in + // tracks-to-add + + const bool shouldHaveTrackSources = mTracksCaptured.Ref() && + !IsPlaybackEnded() && + mReadyState >= HAVE_METADATA; + + // Add track sources for all enabled/selected MediaTracks. + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (!window) { + return; + } + + if (mDecoder) { + if (!mTracksCaptured.Ref()) { + mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::None); + } else if (!AudioTracks() || !VideoTracks() || !shouldHaveTrackSources) { + // We've been unlinked, or tracks are not yet known. + mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::Halt); + } else { + mDecoder->SetOutputCaptureState(MediaDecoder::OutputCaptureState::Capture, + mTracksCaptured.Ref().get()); + } + } + + // Start with all MediaTracks + AutoTArray<RefPtr<MediaTrack>, 4> mediaTracksToAdd; + if (shouldHaveTrackSources) { + GetAllEnabledMediaTracks(mediaTracksToAdd); + } + + // ...and all MediaElementTrackSources. + auto trackSourcesToRemove = + ToTArray<AutoTArray<nsString, 4>>(mOutputTrackSources.Keys()); + + // Then work out the differences. + mediaTracksToAdd.RemoveLastElements( + mediaTracksToAdd.end() - + std::remove_if(mediaTracksToAdd.begin(), mediaTracksToAdd.end(), + [this, &trackSourcesToRemove](const auto& track) { + const bool remove = + mOutputTrackSources.GetWeak(track->GetId()); + if (remove) { + trackSourcesToRemove.RemoveElement(track->GetId()); + } + return remove; + })); + + // First remove stale track sources. + for (const auto& id : trackSourcesToRemove) { + RefPtr<MediaElementTrackSource> source = mOutputTrackSources.GetWeak(id); + + LOG(LogLevel::Debug, ("Removing output track source %p for track %s", + source.get(), NS_ConvertUTF16toUTF8(id).get())); + + if (mDecoder) { + mDecoder->RemoveOutputTrack(source->Track()); + } + + // The source of this track just ended. Force-notify that it ended. + // If we bounce it to the MediaTrackGraph it might not be picked up, + // for instance if the MediaInputPort was destroyed in the same + // iteration as it was added. + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod("MediaElementTrackSource::OverrideEnded", source, + &MediaElementTrackSource::OverrideEnded)); + + // Remove the track from the MediaStream after it ended. + for (OutputMediaStream& ms : mOutputStreams) { + if (source->Track()->mType == MediaSegment::VIDEO && + ms.mCapturingAudioOnly) { + continue; + } + DebugOnly<size_t> length = ms.mLiveTracks.Length(); + ms.mLiveTracks.RemoveElementsBy( + [&](const RefPtr<MediaStreamTrack>& aTrack) { + if (&aTrack->GetSource() != source) { + return false; + } + GetMainThreadSerialEventTarget()->Dispatch( + NewRunnableMethod<RefPtr<MediaStreamTrack>>( + "DOMMediaStream::RemoveTrackInternal", ms.mStream, + &DOMMediaStream::RemoveTrackInternal, aTrack)); + return true; + }); + MOZ_ASSERT(ms.mLiveTracks.Length() == length - 1); + } + + mOutputTrackSources.Remove(id); + } + + // Then update finish-when-ended output streams as needed. + for (size_t i = mOutputStreams.Length(); i-- > 0;) { + if (!mOutputStreams[i].mFinishWhenEnded) { + continue; + } + + if (!mOutputStreams[i].mFinishWhenEndedLoadingSrc && + !mOutputStreams[i].mFinishWhenEndedAttrStream && + !mOutputStreams[i].mFinishWhenEndedMediaSource) { + // This finish-when-ended stream has not seen any source loaded yet. + // Update the loading src if it's time. + if (!IsPlaybackEnded()) { + if (mLoadingSrc) { + mOutputStreams[i].mFinishWhenEndedLoadingSrc = mLoadingSrc; + } else if (mSrcAttrStream) { + mOutputStreams[i].mFinishWhenEndedAttrStream = mSrcAttrStream; + } else if (mSrcMediaSource) { + mOutputStreams[i].mFinishWhenEndedMediaSource = mSrcMediaSource; + } + } + continue; + } + + // Discard finish-when-ended output streams with a loading src set as + // needed. + if (!IsPlaybackEnded() && + mLoadingSrc == mOutputStreams[i].mFinishWhenEndedLoadingSrc) { + continue; + } + if (!IsPlaybackEnded() && + mSrcAttrStream == mOutputStreams[i].mFinishWhenEndedAttrStream) { + continue; + } + if (!IsPlaybackEnded() && + mSrcMediaSource == mOutputStreams[i].mFinishWhenEndedMediaSource) { + continue; + } + LOG(LogLevel::Debug, + ("Playback ended or source changed. Discarding stream %p", + mOutputStreams[i].mStream.get())); + mOutputStreams.RemoveElementAt(i); + if (mOutputStreams.IsEmpty()) { + mTracksCaptured = nullptr; + // mTracksCaptured is one of the Watchables triggering this method. + // Unsetting it here means we'll run through this method again very soon. + return; + } + } + + // Finally add new MediaTracks. + for (const auto& mediaTrack : mediaTracksToAdd) { + nsAutoString id; + mediaTrack->GetId(id); + + MediaSegment::Type type; + if (mediaTrack->AsAudioTrack()) { + type = MediaSegment::AUDIO; + } else if (mediaTrack->AsVideoTrack()) { + type = MediaSegment::VIDEO; + } else { + MOZ_CRASH("Unknown track type"); + } + + RefPtr<ProcessedMediaTrack> track; + RefPtr<MediaElementTrackSource> source; + if (mDecoder) { + track = mTracksCaptured.Ref()->mTrack->Graph()->CreateForwardedInputTrack( + type); + RefPtr<nsIPrincipal> principal = GetCurrentPrincipal(); + if (!principal || IsCORSSameOrigin()) { + principal = NodePrincipal(); + } + source = MakeAndAddRef<MediaElementTrackSource>( + track, principal, OutputTracksMuted(), + type == MediaSegment::VIDEO + ? HTMLVideoElement::FromNode(this)->HasAlpha() + : false); + mDecoder->AddOutputTrack(track); + } else if (mSrcStream) { + MediaStreamTrack* inputTrack; + if (AudioTrack* t = mediaTrack->AsAudioTrack()) { + inputTrack = t->GetAudioStreamTrack(); + } else if (VideoTrack* t = mediaTrack->AsVideoTrack()) { + inputTrack = t->GetVideoStreamTrack(); + } else { + MOZ_CRASH("Unknown track type"); + } + MOZ_ASSERT(inputTrack); + if (!inputTrack) { + NS_ERROR("Input track not found in source stream"); + return; + } + MOZ_DIAGNOSTIC_ASSERT(!inputTrack->Ended()); + + track = inputTrack->Graph()->CreateForwardedInputTrack(type); + RefPtr<MediaInputPort> port = inputTrack->ForwardTrackContentsTo(track); + source = MakeAndAddRef<MediaElementTrackSource>( + inputTrack, &inputTrack->GetSource(), track, port, + OutputTracksMuted()); + + // Track is muted initially, so we don't leak data if it's added while + // paused and an MTG iteration passes before the mute comes into effect. + source->SetEnabled(mSrcStreamIsPlaying); + } else { + MOZ_CRASH("Unknown source"); + } + + LOG(LogLevel::Debug, ("Adding output track source %p for track %s", + source.get(), NS_ConvertUTF16toUTF8(id).get())); + + track->QueueSetAutoend(false); + MOZ_DIAGNOSTIC_ASSERT(!mOutputTrackSources.Contains(id)); + mOutputTrackSources.InsertOrUpdate(id, RefPtr{source}); + + // Add the new track source to any existing output streams + for (OutputMediaStream& ms : mOutputStreams) { + if (source->Track()->mType == MediaSegment::VIDEO && + ms.mCapturingAudioOnly) { + // If the output stream is for audio only we ignore video sources. + continue; + } + AddOutputTrackSourceToOutputStream(source, ms); + } + } +} + +bool HTMLMediaElement::CanBeCaptured(StreamCaptureType aCaptureType) { + // Don't bother capturing when the document has gone away + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (!window) { + return false; + } + + // Prevent capturing restricted video + if (aCaptureType == StreamCaptureType::CAPTURE_ALL_TRACKS && + ContainsRestrictedContent()) { + return false; + } + return true; +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::CaptureStreamInternal( + StreamCaptureBehavior aFinishBehavior, StreamCaptureType aStreamCaptureType, + MediaTrackGraph* aGraph) { + MOZ_ASSERT(CanBeCaptured(aStreamCaptureType)); + + LogVisibility(CallerAPI::CAPTURE_STREAM); + MarkAsTainted(); + + if (mTracksCaptured.Ref()) { + // Already have an output stream. Check whether the graph rate matches if + // specified. + if (aGraph && aGraph != mTracksCaptured.Ref()->mTrack->Graph()) { + return nullptr; + } + } else { + // This is the first output stream, or there are no tracks. If the former, + // start capturing all tracks. If the latter, they will be added later. + MediaTrackGraph* graph = aGraph; + if (!graph) { + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (!window) { + return nullptr; + } + + MediaTrackGraph::GraphDriverType graphDriverType = + HasAudio() ? MediaTrackGraph::AUDIO_THREAD_DRIVER + : MediaTrackGraph::SYSTEM_THREAD_DRIVER; + graph = MediaTrackGraph::GetInstance( + graphDriverType, window, MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, + MediaTrackGraph::DEFAULT_OUTPUT_DEVICE); + } + mTracksCaptured = MakeRefPtr<SharedDummyTrack>( + graph->CreateSourceTrack(MediaSegment::AUDIO)); + UpdateOutputTrackSources(); + } + + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + OutputMediaStream* out = mOutputStreams.EmplaceBack( + MakeRefPtr<DOMMediaStream>(window), + aStreamCaptureType == StreamCaptureType::CAPTURE_AUDIO, + aFinishBehavior == StreamCaptureBehavior::FINISH_WHEN_ENDED); + + if (aFinishBehavior == StreamCaptureBehavior::FINISH_WHEN_ENDED && + !mOutputTrackSources.IsEmpty()) { + // This output stream won't receive any more tracks when playback of the + // current src of this media element ends, or when the src of this media + // element changes. If we're currently playing something (i.e., if there are + // tracks currently captured), set the current src on the output stream so + // this can be tracked. If we're not playing anything, + // UpdateOutputTrackSources will set the current src when it becomes + // available later. + if (mLoadingSrc) { + out->mFinishWhenEndedLoadingSrc = mLoadingSrc; + } + if (mSrcAttrStream) { + out->mFinishWhenEndedAttrStream = mSrcAttrStream; + } + if (mSrcMediaSource) { + out->mFinishWhenEndedMediaSource = mSrcMediaSource; + } + MOZ_ASSERT(out->mFinishWhenEndedLoadingSrc || + out->mFinishWhenEndedAttrStream || + out->mFinishWhenEndedMediaSource); + } + + if (aStreamCaptureType == StreamCaptureType::CAPTURE_AUDIO) { + if (mSrcStream) { + // We don't support applying volume and mute to the captured stream, when + // capturing a MediaStream. + ReportToConsole(nsIScriptError::errorFlag, + "MediaElementAudioCaptureOfMediaStreamError"); + } + + // mAudioCaptured tells the user that the audio played by this media element + // is being routed to the captureStreams *instead* of being played to + // speakers. + mAudioCaptured = true; + } + + for (const RefPtr<MediaElementTrackSource>& source : + mOutputTrackSources.Values()) { + if (source->Track()->mType == MediaSegment::VIDEO) { + // Only add video tracks if we're a video element and the output stream + // wants video. + if (!IsVideo()) { + continue; + } + if (out->mCapturingAudioOnly) { + continue; + } + } + AddOutputTrackSourceToOutputStream(source, *out, AddTrackMode::SYNC); + } + + return do_AddRef(out->mStream); +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::CaptureAudio( + ErrorResult& aRv, MediaTrackGraph* aGraph) { + MOZ_RELEASE_ASSERT(aGraph); + + if (!CanBeCaptured(StreamCaptureType::CAPTURE_AUDIO)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<DOMMediaStream> stream = + CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED, + StreamCaptureType::CAPTURE_AUDIO, aGraph); + if (!stream) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + return stream.forget(); +} + +RefPtr<GenericNonExclusivePromise> HTMLMediaElement::GetAllowedToPlayPromise() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!mOutputStreams.IsEmpty(), + "This method should only be called during stream capturing!"); + if (AllowedToPlay()) { + AUTOPLAY_LOG("MediaElement %p has allowed to play, resolve promise", this); + return GenericNonExclusivePromise::CreateAndResolve(true, __func__); + } + AUTOPLAY_LOG("create allow-to-play promise for MediaElement %p", this); + return mAllowedToPlayPromise.Ensure(__func__); +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::MozCaptureStream( + ErrorResult& aRv) { + if (!CanBeCaptured(StreamCaptureType::CAPTURE_ALL_TRACKS)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<DOMMediaStream> stream = + CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED, + StreamCaptureType::CAPTURE_ALL_TRACKS, nullptr); + if (!stream) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + return stream.forget(); +} + +already_AddRefed<DOMMediaStream> HTMLMediaElement::MozCaptureStreamUntilEnded( + ErrorResult& aRv) { + if (!CanBeCaptured(StreamCaptureType::CAPTURE_ALL_TRACKS)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<DOMMediaStream> stream = + CaptureStreamInternal(StreamCaptureBehavior::FINISH_WHEN_ENDED, + StreamCaptureType::CAPTURE_ALL_TRACKS, nullptr); + if (!stream) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + return stream.forget(); +} + +class MediaElementSetForURI : public nsURIHashKey { + public: + explicit MediaElementSetForURI(const nsIURI* aKey) : nsURIHashKey(aKey) {} + MediaElementSetForURI(MediaElementSetForURI&& aOther) noexcept + : nsURIHashKey(std::move(aOther)), + mElements(std::move(aOther.mElements)) {} + nsTArray<HTMLMediaElement*> mElements; +}; + +using MediaElementURITable = nsTHashtable<MediaElementSetForURI>; +// Elements in this table must have non-null mDecoder and mLoadingSrc, and those +// can't change while the element is in the table. The table is keyed by +// the element's mLoadingSrc. Each entry has a list of all elements with the +// same mLoadingSrc. +static MediaElementURITable* gElementTable; + +#ifdef DEBUG +static bool URISafeEquals(nsIURI* a1, nsIURI* a2) { + if (!a1 || !a2) { + // Consider two empty URIs *not* equal! + return false; + } + bool equal = false; + nsresult rv = a1->Equals(a2, &equal); + return NS_SUCCEEDED(rv) && equal; +} +// Returns the number of times aElement appears in the media element table +// for aURI. If this returns other than 0 or 1, there's a bug somewhere! +static unsigned MediaElementTableCount(HTMLMediaElement* aElement, + nsIURI* aURI) { + if (!gElementTable || !aElement) { + return 0; + } + uint32_t uriCount = 0; + uint32_t otherCount = 0; + for (const auto& entry : *gElementTable) { + uint32_t count = 0; + for (const auto& elem : entry.mElements) { + if (elem == aElement) { + count++; + } + } + if (URISafeEquals(aURI, entry.GetKey())) { + uriCount = count; + } else { + otherCount += count; + } + } + NS_ASSERTION(otherCount == 0, "Should not have entries for unknown URIs"); + return uriCount; +} +#endif + +void HTMLMediaElement::AddMediaElementToURITable() { + NS_ASSERTION(mDecoder, "Call this only with decoder Load called"); + NS_ASSERTION( + MediaElementTableCount(this, mLoadingSrc) == 0, + "Should not have entry for element in element table before addition"); + if (!gElementTable) { + gElementTable = new MediaElementURITable(); + } + MediaElementSetForURI* entry = gElementTable->PutEntry(mLoadingSrc); + entry->mElements.AppendElement(this); + NS_ASSERTION( + MediaElementTableCount(this, mLoadingSrc) == 1, + "Should have a single entry for element in element table after addition"); +} + +void HTMLMediaElement::RemoveMediaElementFromURITable() { + if (!mDecoder || !mLoadingSrc || !gElementTable) { + return; + } + MediaElementSetForURI* entry = gElementTable->GetEntry(mLoadingSrc); + if (!entry) { + return; + } + entry->mElements.RemoveElement(this); + if (entry->mElements.IsEmpty()) { + gElementTable->RemoveEntry(entry); + if (gElementTable->Count() == 0) { + delete gElementTable; + gElementTable = nullptr; + } + } + NS_ASSERTION(MediaElementTableCount(this, mLoadingSrc) == 0, + "After remove, should no longer have an entry in element table"); +} + +HTMLMediaElement* HTMLMediaElement::LookupMediaElementURITable(nsIURI* aURI) { + if (!gElementTable) { + return nullptr; + } + MediaElementSetForURI* entry = gElementTable->GetEntry(aURI); + if (!entry) { + return nullptr; + } + for (uint32_t i = 0; i < entry->mElements.Length(); ++i) { + HTMLMediaElement* elem = entry->mElements[i]; + bool equal; + // Look for elements that have the same principal and CORS mode. + // Ditto for anything else that could cause us to send different headers. + if (NS_SUCCEEDED(elem->NodePrincipal()->Equals(NodePrincipal(), &equal)) && + equal && elem->mCORSMode == mCORSMode) { + // See SetupDecoder() below. We only add a element to the table when + // mDecoder is a ChannelMediaDecoder. + auto* decoder = static_cast<ChannelMediaDecoder*>(elem->mDecoder.get()); + NS_ASSERTION(decoder, "Decoder gone"); + if (decoder->CanClone()) { + return elem; + } + } + } + return nullptr; +} + +class HTMLMediaElement::ShutdownObserver : public nsIObserver { + enum class Phase : int8_t { Init, Subscribed, Unsubscribed }; + + public: + NS_DECL_ISUPPORTS + + NS_IMETHOD Observe(nsISupports*, const char* aTopic, + const char16_t*) override { + if (mPhase != Phase::Subscribed) { + // Bail out if we are not subscribed for this might be called even after + // |nsContentUtils::UnregisterShutdownObserver(this)|. + return NS_OK; + } + MOZ_DIAGNOSTIC_ASSERT(mWeak); + if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { + mWeak->NotifyShutdownEvent(); + } + return NS_OK; + } + void Subscribe(HTMLMediaElement* aPtr) { + MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Init); + MOZ_DIAGNOSTIC_ASSERT(!mWeak); + mWeak = aPtr; + nsContentUtils::RegisterShutdownObserver(this); + mPhase = Phase::Subscribed; + } + void Unsubscribe() { + MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Subscribed); + MOZ_DIAGNOSTIC_ASSERT(mWeak); + MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, + "ReleaseMediaElement should have been called first"); + mWeak = nullptr; + nsContentUtils::UnregisterShutdownObserver(this); + mPhase = Phase::Unsubscribed; + } + void AddRefMediaElement() { + MOZ_DIAGNOSTIC_ASSERT(mWeak); + MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, "Should only ever AddRef once"); + mWeak->AddRef(); + mAddRefed = true; + } + void ReleaseMediaElement() { + MOZ_DIAGNOSTIC_ASSERT(mWeak); + MOZ_DIAGNOSTIC_ASSERT(mAddRefed, "Should only release after AddRef"); + mWeak->Release(); + mAddRefed = false; + } + + private: + virtual ~ShutdownObserver() { + MOZ_DIAGNOSTIC_ASSERT(mPhase == Phase::Unsubscribed); + MOZ_DIAGNOSTIC_ASSERT(!mWeak); + MOZ_DIAGNOSTIC_ASSERT(!mAddRefed, + "ReleaseMediaElement should have been called first"); + } + // Guaranteed to be valid by HTMLMediaElement. + HTMLMediaElement* mWeak = nullptr; + Phase mPhase = Phase::Init; + bool mAddRefed = false; +}; + +NS_IMPL_ISUPPORTS(HTMLMediaElement::ShutdownObserver, nsIObserver) + +class HTMLMediaElement::TitleChangeObserver final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + + explicit TitleChangeObserver(HTMLMediaElement* aElement) + : mElement(aElement) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aElement); + } + + NS_IMETHOD Observe(nsISupports*, const char* aTopic, + const char16_t*) override { + if (mElement) { + mElement->UpdateStreamName(); + } + + return NS_OK; + } + + void Subscribe() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "document-title-changed", false); + } + } + + void Unsubscribe() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, "document-title-changed"); + } + } + + private: + ~TitleChangeObserver() = default; + + WeakPtr<HTMLMediaElement> mElement; +}; + +NS_IMPL_ISUPPORTS(HTMLMediaElement::TitleChangeObserver, nsIObserver) + +HTMLMediaElement::HTMLMediaElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mWatchManager(this, AbstractThread::MainThread()), + mShutdownObserver(new ShutdownObserver), + mTitleChangeObserver(new TitleChangeObserver(this)), + mEventBlocker(new EventBlocker(this)), + mPlayed(new TimeRanges(ToSupports(OwnerDoc()))), + mTracksCaptured(nullptr, "HTMLMediaElement::mTracksCaptured"), + mErrorSink(new ErrorSink(this)), + mAudioChannelWrapper(new AudioChannelAgentCallback(this)), + mSink(std::pair(nsString(), RefPtr<AudioDeviceInfo>())), + mShowPoster(IsVideo()), + mMediaControlKeyListener(new MediaControlKeyListener(this)) { + MOZ_ASSERT(GetMainThreadSerialEventTarget()); + // Please don't add anything to this constructor or the initialization + // list that can cause AddRef to be called. This prevents subclasses + // from overriding AddRef in a way that works with our refcount + // logging mechanisms. Put these things inside of the ::Init method + // instead. +} + +void HTMLMediaElement::Init() { + MOZ_ASSERT(mRefCnt == 0 && !mRefCnt.IsPurple(), + "HTMLMediaElement::Init called when AddRef has been called " + "at least once already, probably in the constructor. Please " + "see the documentation in the HTMLMediaElement constructor."); + MOZ_ASSERT(!mRefCnt.IsPurple()); + + mAudioTrackList = new AudioTrackList(OwnerDoc()->GetParentObject(), this); + mVideoTrackList = new VideoTrackList(OwnerDoc()->GetParentObject(), this); + + DecoderDoctorLogger::LogConstruction(this); + + mWatchManager.Watch(mPaused, &HTMLMediaElement::UpdateWakeLock); + mWatchManager.Watch(mPaused, &HTMLMediaElement::UpdateOutputTracksMuting); + mWatchManager.Watch( + mPaused, &HTMLMediaElement::NotifyMediaControlPlaybackStateChanged); + mWatchManager.Watch(mReadyState, &HTMLMediaElement::UpdateOutputTracksMuting); + + mWatchManager.Watch(mTracksCaptured, + &HTMLMediaElement::UpdateOutputTrackSources); + mWatchManager.Watch(mReadyState, &HTMLMediaElement::UpdateOutputTrackSources); + + mWatchManager.Watch(mDownloadSuspendedByCache, + &HTMLMediaElement::UpdateReadyStateInternal); + mWatchManager.Watch(mFirstFrameLoaded, + &HTMLMediaElement::UpdateReadyStateInternal); + mWatchManager.Watch(mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateReadyStateInternal); + + ErrorResult rv; + + double defaultVolume = Preferences::GetFloat("media.default_volume", 1.0); + SetVolume(defaultVolume, rv); + + RegisterActivityObserver(); + NotifyOwnerDocumentActivityChanged(); + + // We initialize the MediaShutdownManager as the HTMLMediaElement is always + // constructed on the main thread, and not during stable state. + // (MediaShutdownManager make use of nsIAsyncShutdownClient which is written + // in JS) + MediaShutdownManager::InitStatics(); + +#if defined(MOZ_WIDGET_ANDROID) + GVAutoplayPermissionRequestor::AskForPermissionIfNeeded( + OwnerDoc()->GetInnerWindow()); +#endif + + OwnerDoc()->SetDocTreeHadMedia(); + mShutdownObserver->Subscribe(this); + mInitialized = true; +} + +HTMLMediaElement::~HTMLMediaElement() { + MOZ_ASSERT(mInitialized, + "HTMLMediaElement must be initialized before it is destroyed."); + NS_ASSERTION( + !mHasSelfReference, + "How can we be destroyed if we're still holding a self reference?"); + + mWatchManager.Shutdown(); + + mShutdownObserver->Unsubscribe(); + + mTitleChangeObserver->Unsubscribe(); + + if (mVideoFrameContainer) { + mVideoFrameContainer->ForgetElement(); + } + UnregisterActivityObserver(); + + mSetCDMRequest.DisconnectIfExists(); + mAllowedToPlayPromise.RejectIfExists(NS_ERROR_FAILURE, __func__); + + if (mDecoder) { + ShutdownDecoder(); + } + if (mProgressTimer) { + StopProgress(); + } + if (mSrcStream) { + EndSrcMediaStreamPlayback(); + } + + NS_ASSERTION(MediaElementTableCount(this, mLoadingSrc) == 0, + "Destroyed media element should no longer be in element table"); + + if (mChannelLoader) { + mChannelLoader->Cancel(); + } + + if (mAudioChannelWrapper) { + mAudioChannelWrapper->Shutdown(); + mAudioChannelWrapper = nullptr; + } + + if (mResumeDelayedPlaybackAgent) { + mResumePlaybackRequest.DisconnectIfExists(); + mResumeDelayedPlaybackAgent = nullptr; + } + + mMediaControlKeyListener->StopIfNeeded(); + mMediaControlKeyListener = nullptr; + + WakeLockRelease(); + + DecoderDoctorLogger::LogDestruction(this); +} + +void HTMLMediaElement::StopSuspendingAfterFirstFrame() { + mAllowSuspendAfterFirstFrame = false; + if (!mSuspendedAfterFirstFrame) return; + mSuspendedAfterFirstFrame = false; + if (mDecoder) { + mDecoder->Resume(); + } +} + +void HTMLMediaElement::SetPlayedOrSeeked(bool aValue) { + if (aValue == mHasPlayedOrSeeked) { + return; + } + + mHasPlayedOrSeeked = aValue; + + // Force a reflow so that the poster frame hides or shows immediately. + nsIFrame* frame = GetPrimaryFrame(); + if (!frame) { + return; + } + frame->PresShell()->FrameNeedsReflow(frame, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_IS_DIRTY); +} + +void HTMLMediaElement::NotifyXPCOMShutdown() { ShutdownDecoder(); } + +already_AddRefed<Promise> HTMLMediaElement::Play(ErrorResult& aRv) { + LOG(LogLevel::Debug, + ("%p Play() called by JS readyState=%d", this, mReadyState.Ref())); + + // 4.8.12.8 + // When the play() method on a media element is invoked, the user agent must + // run the following steps. + + RefPtr<PlayPromise> promise = CreatePlayPromise(aRv); + if (NS_WARN_IF(aRv.Failed())) { + return nullptr; + } + + // 4.8.12.8 - Step 1: + // If the media element is not allowed to play, return a promise rejected + // with a "NotAllowedError" DOMException and abort these steps. + // NOTE: we may require requesting permission from the user, so we do the + // "not allowed" check below. + + // 4.8.12.8 - Step 2: + // If the media element's error attribute is not null and its code + // attribute has the value MEDIA_ERR_SRC_NOT_SUPPORTED, return a promise + // rejected with a "NotSupportedError" DOMException and abort these steps. + if (GetError() && GetError()->Code() == MEDIA_ERR_SRC_NOT_SUPPORTED) { + LOG(LogLevel::Debug, + ("%p Play() promise rejected because source not supported.", this)); + promise->MaybeReject(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR); + return promise.forget(); + } + + // 4.8.12.8 - Step 3: + // Let promise be a new promise and append promise to the list of pending + // play promises. + // Note: Promise appended to list of pending promises as needed below. + + if (ShouldBeSuspendedByInactiveDocShell()) { + LOG(LogLevel::Debug, ("%p no allow to play by the docShell for now", this)); + mPendingPlayPromises.AppendElement(promise); + return promise.forget(); + } + + // We may delay starting playback of a media resource for an unvisited tab + // until it's going to foreground or being resumed by the play tab icon. + if (MediaPlaybackDelayPolicy::ShouldDelayPlayback(this)) { + CreateResumeDelayedMediaPlaybackAgentIfNeeded(); + LOG(LogLevel::Debug, ("%p delay Play() call", this)); + MaybeDoLoad(); + // When play is delayed, save a reference to the promise, and return it. + // The promise will be resolved when we resume play by either the tab is + // brought to the foreground, or the audio tab indicator is clicked. + mPendingPlayPromises.AppendElement(promise); + return promise.forget(); + } + + const bool handlingUserInput = UserActivation::IsHandlingUserInput(); + mPendingPlayPromises.AppendElement(promise); + + if (AllowedToPlay()) { + AUTOPLAY_LOG("allow MediaElement %p to play", this); + mAllowedToPlayPromise.ResolveIfExists(true, __func__); + PlayInternal(handlingUserInput); + UpdateCustomPolicyAfterPlayed(); + } else { + AUTOPLAY_LOG("reject MediaElement %p to play", this); + AsyncRejectPendingPlayPromises(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR); + } + return promise.forget(); +} + +void HTMLMediaElement::DispatchEventsWhenPlayWasNotAllowed() { + if (StaticPrefs::media_autoplay_block_event_enabled()) { + DispatchAsyncEvent(u"blocked"_ns); + } + DispatchBlockEventForVideoControl(); + if (!mHasEverBeenBlockedForAutoplay) { + MaybeNotifyAutoplayBlocked(); + ReportToConsole(nsIScriptError::warningFlag, "BlockAutoplayError"); + mHasEverBeenBlockedForAutoplay = true; + } +} + +void HTMLMediaElement::MaybeNotifyAutoplayBlocked() { + // This event is used to notify front-end side that we've blocked autoplay, + // so front-end side should show blocking icon as well. + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(OwnerDoc(), u"GloballyAutoplayBlocked"_ns, + CanBubble::eYes, ChromeOnlyDispatch::eYes); + asyncDispatcher->PostDOMEvent(); +} + +void HTMLMediaElement::DispatchBlockEventForVideoControl() { +#if defined(MOZ_WIDGET_ANDROID) + nsVideoFrame* videoFrame = do_QueryFrame(GetPrimaryFrame()); + if (!videoFrame || !videoFrame->GetVideoControls()) { + return; + } + + RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher( + videoFrame->GetVideoControls(), u"MozNoControlsBlockedVideo"_ns, + CanBubble::eYes); + asyncDispatcher->PostDOMEvent(); +#endif +} + +void HTMLMediaElement::PlayInternal(bool aHandlingUserInput) { + if (mPreloadAction == HTMLMediaElement::PRELOAD_NONE) { + // The media load algorithm will be initiated by a user interaction. + // We want to boost the channel priority for better responsiveness. + // Note this must be done before UpdatePreloadAction() which will + // update |mPreloadAction|. + mUseUrgentStartForChannel = true; + } + + StopSuspendingAfterFirstFrame(); + SetPlayedOrSeeked(true); + + // 4.8.12.8 - Step 4: + // If the media element's networkState attribute has the value NETWORK_EMPTY, + // invoke the media element's resource selection algorithm. + MaybeDoLoad(); + if (mSuspendedForPreloadNone) { + ResumeLoad(PRELOAD_ENOUGH); + } + + // 4.8.12.8 - Step 5: + // If the playback has ended and the direction of playback is forwards, + // seek to the earliest possible position of the media resource. + + // Even if we just did Load() or ResumeLoad(), we could already have a decoder + // here if we managed to clone an existing decoder. + if (mDecoder) { + if (mDecoder->IsEnded()) { + SetCurrentTime(0); + } + if (!mSuspendedByInactiveDocOrDocshell) { + mDecoder->Play(); + } + } + + if (mCurrentPlayRangeStart == -1.0) { + mCurrentPlayRangeStart = CurrentTime(); + } + + const bool oldPaused = mPaused; + mPaused = false; + // Step 5, + // https://html.spec.whatwg.org/multipage/media.html#internal-play-steps + mCanAutoplayFlag = false; + + // We changed mPaused and mCanAutoplayFlag which can affect + // AddRemoveSelfReference and our preload status. + AddRemoveSelfReference(); + UpdatePreloadAction(); + UpdateSrcMediaStreamPlaying(); + StartMediaControlKeyListenerIfNeeded(); + + // Once play() has been called in a user generated event handler, + // it is allowed to autoplay. Note: we can reach here when not in + // a user generated event handler if our readyState has not yet + // reached HAVE_METADATA. + mIsBlessed |= aHandlingUserInput; + + // TODO: If the playback has ended, then the user agent must set + // seek to the effective start. + + // 4.8.12.8 - Step 6: + // If the media element's paused attribute is true, run the following steps: + if (oldPaused) { + // 6.1. Change the value of paused to false. (Already done.) + // This step is uplifted because the "block-media-playback" feature needs + // the mPaused to be false before UpdateAudioChannelPlayingState() being + // called. + + // 6.2. If the show poster flag is true, set the element's show poster flag + // to false and run the time marches on steps. + if (mShowPoster) { + mShowPoster = false; + if (mTextTrackManager) { + mTextTrackManager->TimeMarchesOn(); + } + } + + // 6.3. Queue a task to fire a simple event named play at the element. + DispatchAsyncEvent(u"play"_ns); + + // 6.4. If the media element's readyState attribute has the value + // HAVE_NOTHING, HAVE_METADATA, or HAVE_CURRENT_DATA, queue a task to + // fire a simple event named waiting at the element. + // Otherwise, the media element's readyState attribute has the value + // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA: notify about playing for the + // element. + switch (mReadyState) { + case HAVE_NOTHING: + DispatchAsyncEvent(u"waiting"_ns); + break; + case HAVE_METADATA: + case HAVE_CURRENT_DATA: + DispatchAsyncEvent(u"waiting"_ns); + break; + case HAVE_FUTURE_DATA: + case HAVE_ENOUGH_DATA: + NotifyAboutPlaying(); + break; + } + } else if (mReadyState >= HAVE_FUTURE_DATA) { + // 7. Otherwise, if the media element's readyState attribute has the value + // HAVE_FUTURE_DATA or HAVE_ENOUGH_DATA, take pending play promises and + // queue a task to resolve pending play promises with the result. + AsyncResolvePendingPlayPromises(); + } + + // 8. Set the media element's autoplaying flag to false. (Already done.) + + // 9. Return promise. + // (Done in caller.) +} + +void HTMLMediaElement::MaybeDoLoad() { + if (mNetworkState == NETWORK_EMPTY) { + DoLoad(); + } +} + +void HTMLMediaElement::UpdateWakeLock() { + MOZ_ASSERT(NS_IsMainThread()); + // Ensure we have a wake lock if we're playing audibly. This ensures the + // device doesn't sleep while playing. + bool playing = !mPaused; + bool isAudible = Volume() > 0.0 && !mMuted && mIsAudioTrackAudible; + // WakeLock when playing audible media. + if (playing && isAudible) { + CreateAudioWakeLockIfNeeded(); + } else { + ReleaseAudioWakeLockIfExists(); + } +} + +void HTMLMediaElement::CreateAudioWakeLockIfNeeded() { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return; + } + if (!mWakeLock) { + RefPtr<power::PowerManagerService> pmService = + power::PowerManagerService::GetInstance(); + NS_ENSURE_TRUE_VOID(pmService); + + ErrorResult rv; + mWakeLock = pmService->NewWakeLock(u"audio-playing"_ns, + OwnerDoc()->GetInnerWindow(), rv); + } +} + +void HTMLMediaElement::ReleaseAudioWakeLockIfExists() { + if (mWakeLock) { + ErrorResult rv; + mWakeLock->Unlock(rv); + rv.SuppressException(); + mWakeLock = nullptr; + } +} + +void HTMLMediaElement::WakeLockRelease() { ReleaseAudioWakeLockIfExists(); } + +void HTMLMediaElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + if (!this->Controls() || !aVisitor.mEvent->mFlags.mIsTrusted) { + nsGenericHTMLElement::GetEventTargetParent(aVisitor); + return; + } + + // We will need to trap pointer, touch, and mouse events within the media + // element, allowing media control exclusive consumption on these events, + // and preventing the content from handling them. + switch (aVisitor.mEvent->mMessage) { + case ePointerDown: + case ePointerUp: + case eTouchEnd: + // Always prevent touchmove captured in video element from being handled by + // content, since we always do that for touchstart. + case eTouchMove: + case eTouchStart: + case eMouseClick: + case eMouseDoubleClick: + case eMouseDown: + case eMouseUp: + aVisitor.mCanHandle = false; + return; + + // The *move events however are only comsumed when the range input is being + // dragged. + case ePointerMove: + case eMouseMove: { + nsINode* node = + nsINode::FromEventTargetOrNull(aVisitor.mEvent->mOriginalTarget); + if (MOZ_UNLIKELY(!node)) { + return; + } + HTMLInputElement* el = nullptr; + if (node->ChromeOnlyAccess()) { + if (node->IsHTMLElement(nsGkAtoms::input)) { + // The node is a <input type="range"> + el = static_cast<HTMLInputElement*>(node); + } else if (node->GetParentNode() && + node->GetParentNode()->IsHTMLElement(nsGkAtoms::input)) { + // The node is a child of <input type="range"> + el = static_cast<HTMLInputElement*>(node->GetParentNode()); + } + } + if (el && el->IsDraggingRange()) { + aVisitor.mCanHandle = false; + return; + } + nsGenericHTMLElement::GetEventTargetParent(aVisitor); + return; + } + default: + nsGenericHTMLElement::GetEventTargetParent(aVisitor); + return; + } +} + +bool HTMLMediaElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + // Mappings from 'preload' attribute strings to an enumeration. + static const nsAttrValue::EnumTable kPreloadTable[] = { + {"", HTMLMediaElement::PRELOAD_ATTR_EMPTY}, + {"none", HTMLMediaElement::PRELOAD_ATTR_NONE}, + {"metadata", HTMLMediaElement::PRELOAD_ATTR_METADATA}, + {"auto", HTMLMediaElement::PRELOAD_ATTR_AUTO}, + {nullptr, 0}}; + + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::crossorigin) { + ParseCORSValue(aValue, aResult); + return true; + } + if (aAttribute == nsGkAtoms::preload) { + return aResult.ParseEnumValue(aValue, kPreloadTable, false); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLMediaElement::DoneCreatingElement() { + if (HasAttr(nsGkAtoms::muted)) { + mMuted |= MUTED_BY_CONTENT; + } +} + +bool HTMLMediaElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLElement::IsHTMLFocusable(aWithMouse, aIsFocusable, + aTabIndex)) { + return true; + } + + *aIsFocusable = true; + return false; +} + +int32_t HTMLMediaElement::TabIndexDefault() { return 0; } + +void HTMLMediaElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::src) { + mSrcMediaSource = nullptr; + mSrcAttrTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal( + this, aValue ? aValue->GetStringValue() : EmptyString(), + aMaybeScriptedPrincipal); + if (aValue) { + nsString srcStr = aValue->GetStringValue(); + nsCOMPtr<nsIURI> uri; + NewURIFromString(srcStr, getter_AddRefs(uri)); + if (uri && IsMediaSourceURI(uri)) { + nsresult rv = NS_GetSourceForMediaSourceURI( + uri, getter_AddRefs(mSrcMediaSource)); + if (NS_FAILED(rv)) { + nsAutoString spec; + GetCurrentSrc(spec); + AutoTArray<nsString, 1> params = {spec}; + ReportLoadError("MediaLoadInvalidURI", params); + } + } + } + } else if (aName == nsGkAtoms::autoplay) { + if (aNotify) { + if (aValue) { + StopSuspendingAfterFirstFrame(); + CheckAutoplayDataReady(); + } + // This attribute can affect AddRemoveSelfReference + AddRemoveSelfReference(); + UpdatePreloadAction(); + } + } else if (aName == nsGkAtoms::preload) { + UpdatePreloadAction(); + } else if (aName == nsGkAtoms::loop) { + if (mDecoder) { + mDecoder->SetLooping(!!aValue); + } + } else if (aName == nsGkAtoms::controls && IsInComposedDoc()) { + NotifyUAWidgetSetupOrChange(); + } + } + + // Since AfterMaybeChangeAttr may call DoLoad, make sure that it is called + // *after* any possible changes to mSrcMediaSource. + if (aValue) { + AfterMaybeChangeAttr(aNameSpaceID, aName, aNotify); + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLMediaElement::OnAttrSetButNotChanged(int32_t aNamespaceID, + nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, aNotify); + + return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName, + aValue, aNotify); +} + +void HTMLMediaElement::AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::src) { + DoLoad(); + } + } +} + +nsresult HTMLMediaElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + + if (IsInComposedDoc()) { + // Construct Shadow Root so web content can be hidden in the DOM. + AttachAndSetUAShadowRoot(); + + // The preload action depends on the value of the autoplay attribute. + // It's value may have changed, so update it. + UpdatePreloadAction(); + } + + NotifyDecoderActivityChanges(); + mMediaControlKeyListener->UpdateOwnerBrowsingContextIfNeeded(); + return rv; +} + +void HTMLMediaElement::UnbindFromTree(bool aNullParent) { + mVisibilityState = Visibility::Untracked; + + if (IsInComposedDoc()) { + NotifyUAWidgetTeardown(); + } + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + MOZ_ASSERT(IsActuallyInvisible()); + NotifyDecoderActivityChanges(); + + // https://html.spec.whatwg.org/#playing-the-media-resource:remove-an-element-from-a-document + // + // Dispatch a task to run once we're in a stable state which ensures we're + // paused if we're no longer in a document. Note that we need to dispatch this + // even if there are other tasks in flight for this because these can be + // cancelled if there's a new load. + // + // FIXME(emilio): Per that spec section, we should only do this if we used to + // be connected, though other browsers match our current behavior... + // + // Also, https://github.com/whatwg/html/issues/4928 + nsCOMPtr<nsIRunnable> task = + NS_NewRunnableFunction("dom::HTMLMediaElement::UnbindFromTree", + [self = RefPtr<HTMLMediaElement>(this)]() { + if (!self->IsInComposedDoc()) { + self->PauseInternal(); + self->mMediaControlKeyListener->StopIfNeeded(); + } + }); + RunInStableState(task); +} + +/* static */ +CanPlayStatus HTMLMediaElement::GetCanPlay( + const nsAString& aType, DecoderDoctorDiagnostics* aDiagnostics) { + Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType); + if (!containerType) { + return CANPLAY_NO; + } + CanPlayStatus status = + DecoderTraits::CanHandleContainerType(*containerType, aDiagnostics); + if (status == CANPLAY_YES && + (*containerType).ExtendedType().Codecs().IsEmpty()) { + // Per spec: 'Generally, a user agent should never return "probably" for a + // type that allows the `codecs` parameter if that parameter is not + // present.' As all our currently-supported types allow for `codecs`, we can + // do this check here. + // TODO: Instead, missing `codecs` should be checked in each decoder's + // `IsSupportedType` call from `CanHandleCodecsType()`. + // See bug 1399023. + return CANPLAY_MAYBE; + } + return status; +} + +void HTMLMediaElement::CanPlayType(const nsAString& aType, nsAString& aResult) { + DecoderDoctorDiagnostics diagnostics; + CanPlayStatus canPlay = GetCanPlay(aType, &diagnostics); + diagnostics.StoreFormatDiagnostics(OwnerDoc(), aType, canPlay != CANPLAY_NO, + __func__); + switch (canPlay) { + case CANPLAY_NO: + aResult.Truncate(); + break; + case CANPLAY_YES: + aResult.AssignLiteral("probably"); + break; + case CANPLAY_MAYBE: + aResult.AssignLiteral("maybe"); + break; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected case."); + break; + } + + LOG(LogLevel::Debug, + ("%p CanPlayType(%s) = \"%s\"", this, NS_ConvertUTF16toUTF8(aType).get(), + NS_ConvertUTF16toUTF8(aResult).get())); +} + +void HTMLMediaElement::AssertReadyStateIsNothing() { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + if (mReadyState != HAVE_NOTHING) { + char buf[1024]; + SprintfLiteral(buf, + "readyState=%d networkState=%d mLoadWaitStatus=%d " + "mSourceLoadCandidate=%d " + "mIsLoadingFromSourceChildren=%d mPreloadAction=%d " + "mSuspendedForPreloadNone=%d error=%d", + int(mReadyState), int(mNetworkState), int(mLoadWaitStatus), + !!mSourceLoadCandidate, mIsLoadingFromSourceChildren, + int(mPreloadAction), mSuspendedForPreloadNone, + GetError() ? GetError()->Code() : 0); + MOZ_CRASH_UNSAFE_PRINTF("ReadyState should be HAVE_NOTHING! %s", buf); + } +#endif +} + +nsresult HTMLMediaElement::InitializeDecoderAsClone( + ChannelMediaDecoder* aOriginal) { + NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set"); + NS_ASSERTION(mDecoder == nullptr, "Shouldn't have a decoder"); + AssertReadyStateIsNothing(); + + MediaDecoderInit decoderInit( + this, this, mMuted ? 0.0 : mVolume, mPreservesPitch, + ClampPlaybackRate(mPlaybackRate), + mPreloadAction == HTMLMediaElement::PRELOAD_METADATA, mHasSuspendTaint, + HasAttr(nsGkAtoms::loop), aOriginal->ContainerType()); + + RefPtr<ChannelMediaDecoder> decoder = aOriginal->Clone(decoderInit); + if (!decoder) return NS_ERROR_FAILURE; + + LOG(LogLevel::Debug, + ("%p Cloned decoder %p from %p", this, decoder.get(), aOriginal)); + + return FinishDecoderSetup(decoder); +} + +template <typename DecoderType, typename... LoadArgs> +nsresult HTMLMediaElement::SetupDecoder(DecoderType* aDecoder, + LoadArgs&&... aArgs) { + LOG(LogLevel::Debug, ("%p Created decoder %p for type %s", this, aDecoder, + aDecoder->ContainerType().OriginalString().Data())); + + nsresult rv = aDecoder->Load(std::forward<LoadArgs>(aArgs)...); + if (NS_FAILED(rv)) { + aDecoder->Shutdown(); + LOG(LogLevel::Debug, ("%p Failed to load for decoder %p", this, aDecoder)); + return rv; + } + + rv = FinishDecoderSetup(aDecoder); + // Only ChannelMediaDecoder supports resource cloning. + if (std::is_same_v<DecoderType, ChannelMediaDecoder> && NS_SUCCEEDED(rv)) { + AddMediaElementToURITable(); + NS_ASSERTION( + MediaElementTableCount(this, mLoadingSrc) == 1, + "Media element should have single table entry if decode initialized"); + } + + return rv; +} + +nsresult HTMLMediaElement::InitializeDecoderForChannel( + nsIChannel* aChannel, nsIStreamListener** aListener) { + NS_ASSERTION(mLoadingSrc, "mLoadingSrc must already be set"); + AssertReadyStateIsNothing(); + + DecoderDoctorDiagnostics diagnostics; + + nsAutoCString mimeType; + aChannel->GetContentType(mimeType); + NS_ASSERTION(!mimeType.IsEmpty(), "We should have the Content-Type."); + NS_ConvertUTF8toUTF16 mimeUTF16(mimeType); + + RefPtr<HTMLMediaElement> self = this; + auto reportCanPlay = [&, self](bool aCanPlay) { + diagnostics.StoreFormatDiagnostics(self->OwnerDoc(), mimeUTF16, aCanPlay, + __func__); + if (!aCanPlay) { + nsAutoString src; + self->GetCurrentSrc(src); + AutoTArray<nsString, 2> params = {mimeUTF16, src}; + self->ReportLoadError("MediaLoadUnsupportedMimeType", params); + } + }; + + auto onExit = MakeScopeExit([self] { + if (self->mChannelLoader) { + self->mChannelLoader->Done(); + self->mChannelLoader = nullptr; + } + }); + + Maybe<MediaContainerType> containerType = MakeMediaContainerType(mimeType); + if (!containerType) { + reportCanPlay(false); + return NS_ERROR_FAILURE; + } + + MediaDecoderInit decoderInit( + this, this, mMuted ? 0.0 : mVolume, mPreservesPitch, + ClampPlaybackRate(mPlaybackRate), + mPreloadAction == HTMLMediaElement::PRELOAD_METADATA, mHasSuspendTaint, + HasAttr(nsGkAtoms::loop), *containerType); + +#ifdef MOZ_ANDROID_HLS_SUPPORT + if (HLSDecoder::IsSupportedType(*containerType)) { + RefPtr<HLSDecoder> decoder = HLSDecoder::Create(decoderInit); + if (!decoder) { + reportCanPlay(false); + return NS_ERROR_OUT_OF_MEMORY; + } + reportCanPlay(true); + return SetupDecoder(decoder.get(), aChannel); + } +#endif + + RefPtr<ChannelMediaDecoder> decoder = + ChannelMediaDecoder::Create(decoderInit, &diagnostics); + if (!decoder) { + reportCanPlay(false); + return NS_ERROR_FAILURE; + } + + reportCanPlay(true); + bool isPrivateBrowsing = NodePrincipal()->GetPrivateBrowsingId() > 0; + return SetupDecoder(decoder.get(), aChannel, isPrivateBrowsing, aListener); +} + +nsresult HTMLMediaElement::FinishDecoderSetup(MediaDecoder* aDecoder) { + ChangeNetworkState(NETWORK_LOADING); + + // Set mDecoder now so if methods like GetCurrentSrc get called between + // here and Load(), they work. + SetDecoder(aDecoder); + + // Notify the decoder of the initial activity status. + NotifyDecoderActivityChanges(); + + // Update decoder principal before we start decoding, since it + // can affect how we feed data to MediaStreams + NotifyDecoderPrincipalChanged(); + + // Set sink device if we have one. Otherwise the default is used. + if (mSink.second) { + mDecoder->SetSink(mSink.second); + } + + if (mMediaKeys) { + if (mMediaKeys->GetCDMProxy()) { + mDecoder->SetCDMProxy(mMediaKeys->GetCDMProxy()); + } else { + // CDM must have crashed. + ShutdownDecoder(); + return NS_ERROR_FAILURE; + } + } + + if (mChannelLoader) { + mChannelLoader->Done(); + mChannelLoader = nullptr; + } + + // We may want to suspend the new stream now. + // This will also do an AddRemoveSelfReference. + NotifyOwnerDocumentActivityChanged(); + + if (!mDecoder) { + // NotifyOwnerDocumentActivityChanged may shutdown the decoder if the + // owning document is inactive and we're in the EME case. We could try and + // handle this, but at the time of writing it's a pretty niche case, so just + // bail. + return NS_ERROR_FAILURE; + } + + if (mSuspendedByInactiveDocOrDocshell) { + mDecoder->Suspend(); + } + + if (!mPaused) { + SetPlayedOrSeeked(true); + if (!mSuspendedByInactiveDocOrDocshell) { + mDecoder->Play(); + } + } + + MaybeBeginCloningVisually(); + + return NS_OK; +} + +void HTMLMediaElement::UpdateSrcMediaStreamPlaying(uint32_t aFlags) { + if (!mSrcStream) { + return; + } + + bool shouldPlay = !(aFlags & REMOVING_SRC_STREAM) && !mPaused && + !mSuspendedByInactiveDocOrDocshell; + if (shouldPlay == mSrcStreamIsPlaying) { + return; + } + mSrcStreamIsPlaying = shouldPlay; + + LOG(LogLevel::Debug, + ("MediaElement %p %s playback of DOMMediaStream %p", this, + shouldPlay ? "Setting up" : "Removing", mSrcStream.get())); + + if (shouldPlay) { + mSrcStreamPlaybackEnded = false; + mSrcStreamReportPlaybackEnded = false; + + if (mMediaStreamRenderer) { + mMediaStreamRenderer->Start(); + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->Start(); + } + + SetCapturedOutputStreamsEnabled(true); // Unmute + // If the input is a media stream, we don't check its data and always regard + // it as audible when it's playing. + SetAudibleState(true); + } else { + if (mMediaStreamRenderer) { + mMediaStreamRenderer->Stop(); + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->Stop(); + } + SetCapturedOutputStreamsEnabled(false); // Mute + } +} + +void HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying() { + if (!mMediaStreamRenderer) { + // Notifications are async, the renderer could have been cleared. + return; + } + + mMediaStreamRenderer->SetProgressingCurrentTime(IsPotentiallyPlaying()); +} + +void HTMLMediaElement::UpdateSrcStreamTime() { + MOZ_ASSERT(NS_IsMainThread()); + + if (mSrcStreamPlaybackEnded) { + // We do a separate FireTimeUpdate() when this is set. + return; + } + + FireTimeUpdate(TimeupdateType::ePeriodic); +} + +void HTMLMediaElement::SetupSrcMediaStreamPlayback(DOMMediaStream* aStream) { + NS_ASSERTION(!mSrcStream, "Should have been ended already"); + + mLoadingSrc = nullptr; + mSrcStream = aStream; + + VideoFrameContainer* container = GetVideoFrameContainer(); + RefPtr<FirstFrameVideoOutput> firstFrameOutput = + container ? MakeAndAddRef<FirstFrameVideoOutput>(container, + AbstractMainThread()) + : nullptr; + mMediaStreamRenderer = MakeAndAddRef<MediaStreamRenderer>( + AbstractMainThread(), container, firstFrameOutput, this); + mWatchManager.Watch(mPaused, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Watch(mReadyState, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Watch(mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Watch(mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateSrcStreamReportPlaybackEnded); + mWatchManager.Watch(mMediaStreamRenderer->CurrentGraphTime(), + &HTMLMediaElement::UpdateSrcStreamTime); + SetVolumeInternal(); + if (mSink.second) { + mMediaStreamRenderer->SetAudioOutputDevice(mSink.second); + } + + UpdateSrcMediaStreamPlaying(); + UpdateSrcStreamPotentiallyPlaying(); + mSrcStreamVideoPrincipal = NodePrincipal(); + + // If we pause this media element, track changes in the underlying stream + // will continue to fire events at this element and alter its track list. + // That's simpler than delaying the events, but probably confusing... + nsTArray<RefPtr<MediaStreamTrack>> tracks; + mSrcStream->GetTracks(tracks); + for (const RefPtr<MediaStreamTrack>& track : tracks) { + NotifyMediaStreamTrackAdded(track); + } + + mMediaStreamTrackListener = MakeUnique<MediaStreamTrackListener>(this); + mSrcStream->RegisterTrackListener(mMediaStreamTrackListener.get()); + + ChangeNetworkState(NETWORK_IDLE); + ChangeDelayLoadStatus(false); + + // FirstFrameLoaded() will be called when the stream has tracks. +} + +void HTMLMediaElement::EndSrcMediaStreamPlayback() { + MOZ_ASSERT(mSrcStream); + + UpdateSrcMediaStreamPlaying(REMOVING_SRC_STREAM); + + if (mSelectedVideoStreamTrack) { + mSelectedVideoStreamTrack->RemovePrincipalChangeObserver(this); + } + mSelectedVideoStreamTrack = nullptr; + + MOZ_ASSERT_IF(mSecondaryMediaStreamRenderer, + !mMediaStreamRenderer == !mSecondaryMediaStreamRenderer); + if (mMediaStreamRenderer) { + mWatchManager.Unwatch(mPaused, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Unwatch(mReadyState, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Unwatch(mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateSrcStreamPotentiallyPlaying); + mWatchManager.Unwatch( + mSrcStreamPlaybackEnded, + &HTMLMediaElement::UpdateSrcStreamReportPlaybackEnded); + mWatchManager.Unwatch(mMediaStreamRenderer->CurrentGraphTime(), + &HTMLMediaElement::UpdateSrcStreamTime); + mMediaStreamRenderer->Shutdown(); + mMediaStreamRenderer = nullptr; + } + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->Shutdown(); + mSecondaryMediaStreamRenderer = nullptr; + } + + mSrcStream->UnregisterTrackListener(mMediaStreamTrackListener.get()); + mMediaStreamTrackListener = nullptr; + mSrcStreamPlaybackEnded = false; + mSrcStreamReportPlaybackEnded = false; + mSrcStreamVideoPrincipal = nullptr; + + mSrcStream = nullptr; +} + +static already_AddRefed<AudioTrack> CreateAudioTrack( + AudioStreamTrack* aStreamTrack, nsIGlobalObject* aOwnerGlobal) { + nsAutoString id; + nsAutoString label; + aStreamTrack->GetId(id); + aStreamTrack->GetLabel(label, CallerType::System); + + return MediaTrackList::CreateAudioTrack(aOwnerGlobal, id, u"main"_ns, label, + u""_ns, true, aStreamTrack); +} + +static already_AddRefed<VideoTrack> CreateVideoTrack( + VideoStreamTrack* aStreamTrack, nsIGlobalObject* aOwnerGlobal) { + nsAutoString id; + nsAutoString label; + aStreamTrack->GetId(id); + aStreamTrack->GetLabel(label, CallerType::System); + + return MediaTrackList::CreateVideoTrack(aOwnerGlobal, id, u"main"_ns, label, + u""_ns, aStreamTrack); +} + +void HTMLMediaElement::NotifyMediaStreamTrackAdded( + const RefPtr<MediaStreamTrack>& aTrack) { + MOZ_ASSERT(aTrack); + + if (aTrack->Ended()) { + return; + } + +#ifdef DEBUG + nsAutoString id; + aTrack->GetId(id); + + LOG(LogLevel::Debug, ("%p, Adding %sTrack with id %s", this, + aTrack->AsAudioStreamTrack() ? "Audio" : "Video", + NS_ConvertUTF16toUTF8(id).get())); +#endif + + if (AudioStreamTrack* t = aTrack->AsAudioStreamTrack()) { + MOZ_DIAGNOSTIC_ASSERT(AudioTracks(), "Element can't have been unlinked"); + RefPtr<AudioTrack> audioTrack = + CreateAudioTrack(t, AudioTracks()->GetOwnerGlobal()); + AudioTracks()->AddTrack(audioTrack); + } else if (VideoStreamTrack* t = aTrack->AsVideoStreamTrack()) { + // TODO: Fix this per the spec on bug 1273443. + if (!IsVideo()) { + return; + } + MOZ_DIAGNOSTIC_ASSERT(VideoTracks(), "Element can't have been unlinked"); + RefPtr<VideoTrack> videoTrack = + CreateVideoTrack(t, VideoTracks()->GetOwnerGlobal()); + VideoTracks()->AddTrack(videoTrack); + // New MediaStreamTrack added, set the new added video track as selected + // video track when there is no selected track. + if (VideoTracks()->SelectedIndex() == -1) { + MOZ_ASSERT(!mSelectedVideoStreamTrack); + videoTrack->SetEnabledInternal(true, dom::MediaTrack::FIRE_NO_EVENTS); + } + } + + // The set of enabled AudioTracks and selected video track might have changed. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); + AbstractThread::DispatchDirectTask( + NewRunnableMethod("HTMLMediaElement::FirstFrameLoaded", this, + &HTMLMediaElement::FirstFrameLoaded)); +} + +void HTMLMediaElement::NotifyMediaStreamTrackRemoved( + const RefPtr<MediaStreamTrack>& aTrack) { + MOZ_ASSERT(aTrack); + + nsAutoString id; + aTrack->GetId(id); + + LOG(LogLevel::Debug, ("%p, Removing %sTrack with id %s", this, + aTrack->AsAudioStreamTrack() ? "Audio" : "Video", + NS_ConvertUTF16toUTF8(id).get())); + + MOZ_DIAGNOSTIC_ASSERT(AudioTracks() && VideoTracks(), + "Element can't have been unlinked"); + if (dom::MediaTrack* t = AudioTracks()->GetTrackById(id)) { + AudioTracks()->RemoveTrack(t); + } else if (dom::MediaTrack* t = VideoTracks()->GetTrackById(id)) { + VideoTracks()->RemoveTrack(t); + } else { + NS_ASSERTION(aTrack->AsVideoStreamTrack() && !IsVideo(), + "MediaStreamTrack ended but did not exist in track lists. " + "This is only allowed if a video element ends and we are an " + "audio element."); + return; + } +} + +void HTMLMediaElement::ProcessMediaFragmentURI() { + if (!mLoadingSrc) { + mFragmentStart = mFragmentEnd = -1.0; + return; + } + nsMediaFragmentURIParser parser(mLoadingSrc); + + if (mDecoder && parser.HasEndTime()) { + mFragmentEnd = parser.GetEndTime(); + } + + if (parser.HasStartTime()) { + SetCurrentTime(parser.GetStartTime()); + mFragmentStart = parser.GetStartTime(); + } +} + +void HTMLMediaElement::MetadataLoaded(const MediaInfo* aInfo, + UniquePtr<const MetadataTags> aTags) { + MOZ_ASSERT(NS_IsMainThread()); + + if (mDecoder) { + ConstructMediaTracks(aInfo); + } + + SetMediaInfo(*aInfo); + + mIsEncrypted = + aInfo->IsEncrypted() || mPendingEncryptedInitData.IsEncrypted(); + mTags = std::move(aTags); + mLoadedDataFired = false; + ChangeReadyState(HAVE_METADATA); + + // Add output tracks synchronously now to be sure they're available in + // "loadedmetadata" event handlers. + UpdateOutputTrackSources(); + + DispatchAsyncEvent(u"durationchange"_ns); + if (IsVideo() && HasVideo()) { + DispatchAsyncEvent(u"resize"_ns); + Invalidate(ImageSizeChanged::No, Some(mMediaInfo.mVideo.mDisplay), + ForceInvalidate::No); + } + NS_ASSERTION(!HasVideo() || (mMediaInfo.mVideo.mDisplay.width > 0 && + mMediaInfo.mVideo.mDisplay.height > 0), + "Video resolution must be known on 'loadedmetadata'"); + DispatchAsyncEvent(u"loadedmetadata"_ns); + + if (mDecoder && mDecoder->IsTransportSeekable() && + mDecoder->IsMediaSeekable()) { + ProcessMediaFragmentURI(); + mDecoder->SetFragmentEndTime(mFragmentEnd); + } + if (mIsEncrypted) { + // We only support playback of encrypted content via MSE by default. + if (!mMediaSource && Preferences::GetBool("media.eme.mse-only", true)) { + DecodeError( + MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, + "Encrypted content not supported outside of MSE")); + return; + } + + // Dispatch a distinct 'encrypted' event for each initData we have. + for (const auto& initData : mPendingEncryptedInitData.mInitDatas) { + DispatchEncrypted(initData.mInitData, initData.mType); + } + mPendingEncryptedInitData.Reset(); + } + + if (IsVideo() && aInfo->HasVideo()) { + // We are a video element playing video so update the screen wakelock + NotifyOwnerDocumentActivityChanged(); + } + + if (mDefaultPlaybackStartPosition != 0.0) { + SetCurrentTime(mDefaultPlaybackStartPosition); + mDefaultPlaybackStartPosition = 0.0; + } + + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); +} + +void HTMLMediaElement::FirstFrameLoaded() { + LOG(LogLevel::Debug, + ("%p, FirstFrameLoaded() mFirstFrameLoaded=%d mWaitingForKey=%d", this, + mFirstFrameLoaded.Ref(), mWaitingForKey)); + + NS_ASSERTION(!mSuspendedAfterFirstFrame, "Should not have already suspended"); + + if (!mFirstFrameLoaded) { + mFirstFrameLoaded = true; + } + + ChangeDelayLoadStatus(false); + + if (mDecoder && mAllowSuspendAfterFirstFrame && mPaused && + !HasAttr(nsGkAtoms::autoplay) && + mPreloadAction == HTMLMediaElement::PRELOAD_METADATA) { + mSuspendedAfterFirstFrame = true; + mDecoder->Suspend(); + } +} + +void HTMLMediaElement::NetworkError(const MediaResult& aError) { + if (mReadyState == HAVE_NOTHING) { + NoSupportedMediaSourceError(aError.Description()); + } else { + Error(MEDIA_ERR_NETWORK); + } +} + +void HTMLMediaElement::DecodeError(const MediaResult& aError) { + nsAutoString src; + GetCurrentSrc(src); + AutoTArray<nsString, 1> params = {src}; + ReportLoadError("MediaLoadDecodeError", params); + + DecoderDoctorDiagnostics diagnostics; + diagnostics.StoreDecodeError(OwnerDoc(), aError, src, __func__); + + if (mIsLoadingFromSourceChildren) { + mErrorSink->ResetError(); + if (mSourceLoadCandidate) { + DispatchAsyncSourceError(mSourceLoadCandidate); + QueueLoadFromSourceTask(); + } else { + NS_WARNING("Should know the source we were loading from!"); + } + } else if (mReadyState == HAVE_NOTHING) { + NoSupportedMediaSourceError(aError.Description()); + } else if (IsCORSSameOrigin()) { + Error(MEDIA_ERR_DECODE, aError.Description()); + } else { + Error(MEDIA_ERR_DECODE, "Failed to decode media"_ns); + } +} + +void HTMLMediaElement::DecodeWarning(const MediaResult& aError) { + nsAutoString src; + GetCurrentSrc(src); + DecoderDoctorDiagnostics diagnostics; + diagnostics.StoreDecodeWarning(OwnerDoc(), aError, src, __func__); +} + +bool HTMLMediaElement::HasError() const { return GetError(); } + +void HTMLMediaElement::LoadAborted() { Error(MEDIA_ERR_ABORTED); } + +void HTMLMediaElement::Error(uint16_t aErrorCode, + const nsACString& aErrorDetails) { + mErrorSink->SetError(aErrorCode, aErrorDetails); + ChangeDelayLoadStatus(false); + UpdateAudioChannelPlayingState(); +} + +void HTMLMediaElement::PlaybackEnded() { + // We changed state which can affect AddRemoveSelfReference + AddRemoveSelfReference(); + + NS_ASSERTION(!mDecoder || mDecoder->IsEnded(), + "Decoder fired ended, but not in ended state"); + + // IsPlaybackEnded() became true. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); + + if (mSrcStream) { + LOG(LogLevel::Debug, + ("%p, got duration by reaching the end of the resource", this)); + mSrcStreamPlaybackEnded = true; + DispatchAsyncEvent(u"durationchange"_ns); + } else { + // mediacapture-main: + // Setting the loop attribute has no effect since a MediaStream has no + // defined end and therefore cannot be looped. + if (HasAttr(nsGkAtoms::loop)) { + SetCurrentTime(0); + return; + } + } + + FireTimeUpdate(TimeupdateType::eMandatory); + + if (!mPaused) { + Pause(); + } + + if (mSrcStream) { + // A MediaStream that goes from inactive to active shall be eligible for + // autoplay again according to the mediacapture-main spec. + mCanAutoplayFlag = true; + } + + if (StaticPrefs::media_mediacontrol_stopcontrol_aftermediaends()) { + mMediaControlKeyListener->StopIfNeeded(); + } + DispatchAsyncEvent(u"ended"_ns); +} + +void HTMLMediaElement::UpdateSrcStreamReportPlaybackEnded() { + mSrcStreamReportPlaybackEnded = mSrcStreamPlaybackEnded; +} + +void HTMLMediaElement::SeekStarted() { DispatchAsyncEvent(u"seeking"_ns); } + +void HTMLMediaElement::SeekCompleted() { + mPlayingBeforeSeek = false; + SetPlayedOrSeeked(true); + if (mTextTrackManager) { + mTextTrackManager->DidSeek(); + } + // https://html.spec.whatwg.org/multipage/media.html#seeking:dom-media-seek + // (Step 16) + // TODO (bug 1688131): run these steps in a stable state. + FireTimeUpdate(TimeupdateType::eMandatory); + DispatchAsyncEvent(u"seeked"_ns); + // We changed whether we're seeking so we need to AddRemoveSelfReference + AddRemoveSelfReference(); + if (mCurrentPlayRangeStart == -1.0) { + mCurrentPlayRangeStart = CurrentTime(); + } + + if (mSeekDOMPromise) { + AbstractMainThread()->Dispatch(NS_NewRunnableFunction( + __func__, [promise = std::move(mSeekDOMPromise)] { + promise->MaybeResolveWithUndefined(); + })); + } + MOZ_ASSERT(!mSeekDOMPromise); +} + +void HTMLMediaElement::SeekAborted() { + if (mSeekDOMPromise) { + AbstractMainThread()->Dispatch(NS_NewRunnableFunction( + __func__, [promise = std::move(mSeekDOMPromise)] { + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + })); + } + MOZ_ASSERT(!mSeekDOMPromise); +} + +void HTMLMediaElement::NotifySuspendedByCache(bool aSuspendedByCache) { + LOG(LogLevel::Debug, + ("%p, mDownloadSuspendedByCache=%d", this, aSuspendedByCache)); + mDownloadSuspendedByCache = aSuspendedByCache; +} + +void HTMLMediaElement::DownloadSuspended() { + if (mNetworkState == NETWORK_LOADING) { + DispatchAsyncEvent(u"progress"_ns); + } + ChangeNetworkState(NETWORK_IDLE); +} + +void HTMLMediaElement::DownloadResumed() { + ChangeNetworkState(NETWORK_LOADING); +} + +void HTMLMediaElement::CheckProgress(bool aHaveNewProgress) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mNetworkState == NETWORK_LOADING); + + TimeStamp now = TimeStamp::NowLoRes(); + + if (aHaveNewProgress) { + mDataTime = now; + } + + // If this is the first progress, or PROGRESS_MS has passed since the last + // progress event fired and more data has arrived since then, fire a + // progress event. + NS_ASSERTION( + (mProgressTime.IsNull() && !aHaveNewProgress) || !mDataTime.IsNull(), + "null TimeStamp mDataTime should not be used in comparison"); + if (mProgressTime.IsNull() + ? aHaveNewProgress + : (now - mProgressTime >= + TimeDuration::FromMilliseconds(PROGRESS_MS) && + mDataTime > mProgressTime)) { + DispatchAsyncEvent(u"progress"_ns); + // Resolution() ensures that future data will have now > mProgressTime, + // and so will trigger another event. mDataTime is not reset because it + // is still required to detect stalled; it is similarly offset by + // resolution to indicate the new data has not yet arrived. + mProgressTime = now - TimeDuration::Resolution(); + if (mDataTime > mProgressTime) { + mDataTime = mProgressTime; + } + if (!mProgressTimer) { + NS_ASSERTION(aHaveNewProgress, + "timer dispatched when there was no timer"); + // Were stalled. Restart timer. + StartProgressTimer(); + if (!mLoadedDataFired) { + ChangeDelayLoadStatus(true); + } + } + // Download statistics may have been updated, force a recheck of the + // readyState. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); + } + + if (now - mDataTime >= TimeDuration::FromMilliseconds(STALL_MS)) { + if (!mMediaSource) { + DispatchAsyncEvent(u"stalled"_ns); + } else { + ChangeDelayLoadStatus(false); + } + + NS_ASSERTION(mProgressTimer, "detected stalled without timer"); + // Stop timer events, which prevents repeated stalled events until there + // is more progress. + StopProgress(); + } + + AddRemoveSelfReference(); +} + +/* static */ +void HTMLMediaElement::ProgressTimerCallback(nsITimer* aTimer, void* aClosure) { + auto* decoder = static_cast<HTMLMediaElement*>(aClosure); + decoder->CheckProgress(false); +} + +void HTMLMediaElement::StartProgressTimer() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mNetworkState == NETWORK_LOADING); + NS_ASSERTION(!mProgressTimer, "Already started progress timer."); + + NS_NewTimerWithFuncCallback( + getter_AddRefs(mProgressTimer), ProgressTimerCallback, this, PROGRESS_MS, + nsITimer::TYPE_REPEATING_SLACK, "HTMLMediaElement::ProgressTimerCallback", + GetMainThreadSerialEventTarget()); +} + +void HTMLMediaElement::StartProgress() { + // Record the time now for detecting stalled. + mDataTime = TimeStamp::NowLoRes(); + // Reset mProgressTime so that mDataTime is not indicating bytes received + // after the last progress event. + mProgressTime = TimeStamp(); + StartProgressTimer(); +} + +void HTMLMediaElement::StopProgress() { + MOZ_ASSERT(NS_IsMainThread()); + if (!mProgressTimer) { + return; + } + + mProgressTimer->Cancel(); + mProgressTimer = nullptr; +} + +void HTMLMediaElement::DownloadProgressed() { + if (mNetworkState != NETWORK_LOADING) { + return; + } + CheckProgress(true); +} + +bool HTMLMediaElement::ShouldCheckAllowOrigin() { + return mCORSMode != CORS_NONE; +} + +bool HTMLMediaElement::IsCORSSameOrigin() { + bool subsumes; + RefPtr<nsIPrincipal> principal = GetCurrentPrincipal(); + return (NS_SUCCEEDED(NodePrincipal()->Subsumes(principal, &subsumes)) && + subsumes) || + ShouldCheckAllowOrigin(); +} + +void HTMLMediaElement::UpdateReadyStateInternal() { + if (!mDecoder && !mSrcStream) { + // Not initialized - bail out. + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Not initialized", + this)); + return; + } + + if (mDecoder && mReadyState < HAVE_METADATA) { + // aNextFrame might have a next frame because the decoder can advance + // on its own thread before MetadataLoaded gets a chance to run. + // The arrival of more data can't change us out of this readyState. + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Decoder ready state < HAVE_METADATA", + this)); + return; + } + + if (mDecoder) { + // IsPlaybackEnded() might have become false. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateOutputTrackSources); + } + + if (mSrcStream && mReadyState < HAVE_METADATA) { + bool hasAudioTracks = AudioTracks() && !AudioTracks()->IsEmpty(); + bool hasVideoTracks = VideoTracks() && !VideoTracks()->IsEmpty(); + if (!hasAudioTracks && !hasVideoTracks) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Stream with no tracks", + this)); + // Give it one last chance to remove the self reference if needed. + AddRemoveSelfReference(); + return; + } + + if (IsVideo() && hasVideoTracks && !HasVideo()) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Stream waiting for video", + this)); + return; + } + + LOG(LogLevel::Debug, + ("MediaElement %p UpdateReadyStateInternal() Stream has " + "metadata; audioTracks=%d, videoTracks=%d, " + "hasVideoFrame=%d", + this, AudioTracks()->Length(), VideoTracks()->Length(), HasVideo())); + + // We are playing a stream that has video and a video frame is now set. + // This means we have all metadata needed to change ready state. + MediaInfo mediaInfo = mMediaInfo; + if (hasAudioTracks) { + mediaInfo.EnableAudio(); + } + if (hasVideoTracks) { + mediaInfo.EnableVideo(); + if (mSelectedVideoStreamTrack) { + mediaInfo.mVideo.SetAlpha(mSelectedVideoStreamTrack->HasAlpha()); + } + } + MetadataLoaded(&mediaInfo, nullptr); + } + + if (mMediaSource) { + // readyState has changed, assuming it's following the pending mediasource + // operations. Notify the Mediasource that the operations have completed. + mMediaSource->CompletePendingTransactions(); + } + + enum NextFrameStatus nextFrameStatus = NextFrameStatus(); + if (mWaitingForKey == NOT_WAITING_FOR_KEY) { + if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE && mDecoder && + !mDecoder->IsEnded()) { + nextFrameStatus = mDecoder->NextFrameBufferedStatus(); + } + } else if (mWaitingForKey == WAITING_FOR_KEY) { + if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE || + nextFrameStatus == NEXT_FRAME_UNAVAILABLE_BUFFERING) { + // http://w3c.github.io/encrypted-media/#wait-for-key + // Continuing 7.3.4 Queue a "waitingforkey" Event + // 4. Queue a task to fire a simple event named waitingforkey + // at the media element. + // 5. Set the readyState of media element to HAVE_METADATA. + // NOTE: We'll change to HAVE_CURRENT_DATA or HAVE_METADATA + // depending on whether we've loaded the first frame or not + // below. + // 6. Suspend playback. + // Note: Playback will already be stalled, as the next frame is + // unavailable. + mWaitingForKey = WAITING_FOR_KEY_DISPATCHED; + DispatchAsyncEvent(u"waitingforkey"_ns); + } + } else { + MOZ_ASSERT(mWaitingForKey == WAITING_FOR_KEY_DISPATCHED); + if (nextFrameStatus == NEXT_FRAME_AVAILABLE) { + // We have new frames after dispatching "waitingforkey". + // This means we've got the key and can reset mWaitingForKey now. + mWaitingForKey = NOT_WAITING_FOR_KEY; + } + } + + if (nextFrameStatus == MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING) { + LOG(LogLevel::Debug, + ("MediaElement %p UpdateReadyStateInternal() " + "NEXT_FRAME_UNAVAILABLE_SEEKING; Forcing HAVE_METADATA", + this)); + ChangeReadyState(HAVE_METADATA); + return; + } + + if (IsVideo() && VideoTracks() && !VideoTracks()->IsEmpty() && + !IsPlaybackEnded() && GetImageContainer() && + !GetImageContainer()->HasCurrentImage()) { + // Don't advance if we are playing video, but don't have a video frame. + // Also, if video became available after advancing to HAVE_CURRENT_DATA + // while we are still playing, we need to revert to HAVE_METADATA until + // a video frame is available. + LOG(LogLevel::Debug, + ("MediaElement %p UpdateReadyStateInternal() " + "Playing video but no video frame; Forcing HAVE_METADATA", + this)); + ChangeReadyState(HAVE_METADATA); + return; + } + + if (!mFirstFrameLoaded) { + // We haven't yet loaded the first frame, making us unable to determine + // if we have enough valid data at the present stage. + return; + } + + if (nextFrameStatus == NEXT_FRAME_UNAVAILABLE_BUFFERING) { + // Force HAVE_CURRENT_DATA when buffering. + ChangeReadyState(HAVE_CURRENT_DATA); + return; + } + + // TextTracks must be loaded for the HAVE_ENOUGH_DATA and + // HAVE_FUTURE_DATA. + // So force HAVE_CURRENT_DATA if text tracks not loaded. + if (mTextTrackManager && !mTextTrackManager->IsLoaded()) { + ChangeReadyState(HAVE_CURRENT_DATA); + return; + } + + if (mDownloadSuspendedByCache && mDecoder && !mDecoder->IsEnded()) { + // The decoder has signaled that the download has been suspended by the + // media cache. So move readyState into HAVE_ENOUGH_DATA, in case there's + // script waiting for a "canplaythrough" event; without this forced + // transition, we will never fire the "canplaythrough" event if the + // media cache is too small, and scripts are bound to fail. Don't force + // this transition if the decoder is in ended state; the readyState + // should remain at HAVE_CURRENT_DATA in this case. + // Note that this state transition includes the case where we finished + // downloaded the whole data stream. + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Decoder download suspended by cache", + this)); + ChangeReadyState(HAVE_ENOUGH_DATA); + return; + } + + if (nextFrameStatus != MediaDecoderOwner::NEXT_FRAME_AVAILABLE) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Next frame not available", + this)); + ChangeReadyState(HAVE_CURRENT_DATA); + return; + } + + if (mSrcStream) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Stream HAVE_ENOUGH_DATA", + this)); + ChangeReadyState(HAVE_ENOUGH_DATA); + return; + } + + // Now see if we should set HAVE_ENOUGH_DATA. + // If it's something we don't know the size of, then we can't + // make a real estimate, so we go straight to HAVE_ENOUGH_DATA once + // we've downloaded enough data that our download rate is considered + // reliable. We have to move to HAVE_ENOUGH_DATA at some point or + // autoplay elements for live streams will never play. Otherwise we + // move to HAVE_ENOUGH_DATA if we can play through the entire media + // without stopping to buffer. + if (mDecoder->CanPlayThrough()) { + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Decoder can play through", + this)); + ChangeReadyState(HAVE_ENOUGH_DATA); + return; + } + LOG(LogLevel::Debug, ("MediaElement %p UpdateReadyStateInternal() " + "Default; Decoder has future data", + this)); + ChangeReadyState(HAVE_FUTURE_DATA); +} + +static const char* const gReadyStateToString[] = { + "HAVE_NOTHING", "HAVE_METADATA", "HAVE_CURRENT_DATA", "HAVE_FUTURE_DATA", + "HAVE_ENOUGH_DATA"}; + +void HTMLMediaElement::ChangeReadyState(nsMediaReadyState aState) { + if (mReadyState == aState) { + return; + } + + nsMediaReadyState oldState = mReadyState; + mReadyState = aState; + LOG(LogLevel::Debug, + ("%p Ready state changed to %s", this, gReadyStateToString[aState])); + + DDLOG(DDLogCategory::Property, "ready_state", gReadyStateToString[aState]); + + // https://html.spec.whatwg.org/multipage/media.html#text-track-cue-active-flag + // The user agent must synchronously unset cues' active flag whenever the + // media element's readyState is changed back to HAVE_NOTHING. + if (mReadyState == HAVE_NOTHING && mTextTrackManager) { + mTextTrackManager->NotifyReset(); + } + + if (mNetworkState == NETWORK_EMPTY) { + return; + } + + UpdateAudioChannelPlayingState(); + + // Handle raising of "waiting" event during seek (see 4.8.10.9) + // or + // 4.8.12.7 Ready states: + // "If the previous ready state was HAVE_FUTURE_DATA or more, and the new + // ready state is HAVE_CURRENT_DATA or less + // If the media element was potentially playing before its readyState + // attribute changed to a value lower than HAVE_FUTURE_DATA, and the element + // has not ended playback, and playback has not stopped due to errors, + // paused for user interaction, or paused for in-band content, the user agent + // must queue a task to fire a simple event named timeupdate at the element, + // and queue a task to fire a simple event named waiting at the element." + if (mPlayingBeforeSeek && mReadyState < HAVE_FUTURE_DATA) { + DispatchAsyncEvent(u"waiting"_ns); + } else if (oldState >= HAVE_FUTURE_DATA && mReadyState < HAVE_FUTURE_DATA && + !Paused() && !Ended() && !mErrorSink->mError) { + FireTimeUpdate(TimeupdateType::eMandatory); + DispatchAsyncEvent(u"waiting"_ns); + } + + if (oldState < HAVE_CURRENT_DATA && mReadyState >= HAVE_CURRENT_DATA && + !mLoadedDataFired) { + DispatchAsyncEvent(u"loadeddata"_ns); + mLoadedDataFired = true; + } + + if (oldState < HAVE_FUTURE_DATA && mReadyState >= HAVE_FUTURE_DATA) { + DispatchAsyncEvent(u"canplay"_ns); + if (!mPaused) { + if (mDecoder && !mSuspendedByInactiveDocOrDocshell) { + MOZ_ASSERT(AllowedToPlay()); + mDecoder->Play(); + } + NotifyAboutPlaying(); + } + } + + CheckAutoplayDataReady(); + + if (oldState < HAVE_ENOUGH_DATA && mReadyState >= HAVE_ENOUGH_DATA) { + DispatchAsyncEvent(u"canplaythrough"_ns); + } +} + +static const char* const gNetworkStateToString[] = {"EMPTY", "IDLE", "LOADING", + "NO_SOURCE"}; + +void HTMLMediaElement::ChangeNetworkState(nsMediaNetworkState aState) { + if (mNetworkState == aState) { + return; + } + + nsMediaNetworkState oldState = mNetworkState; + mNetworkState = aState; + LOG(LogLevel::Debug, + ("%p Network state changed to %s", this, gNetworkStateToString[aState])); + DDLOG(DDLogCategory::Property, "network_state", + gNetworkStateToString[aState]); + + if (oldState == NETWORK_LOADING) { + // Stop progress notification when exiting NETWORK_LOADING. + StopProgress(); + } + + if (mNetworkState == NETWORK_LOADING) { + // Start progress notification when entering NETWORK_LOADING. + StartProgress(); + } else if (mNetworkState == NETWORK_IDLE && !mErrorSink->mError) { + // Fire 'suspend' event when entering NETWORK_IDLE and no error presented. + DispatchAsyncEvent(u"suspend"_ns); + } + + // According to the resource selection (step2, step9-18), dedicated media + // source failure step (step4) and aborting existing load (step4), set show + // poster flag to true. https://html.spec.whatwg.org/multipage/media.html + if (mNetworkState == NETWORK_NO_SOURCE || mNetworkState == NETWORK_EMPTY) { + mShowPoster = true; + } + + // Changing mNetworkState affects AddRemoveSelfReference(). + AddRemoveSelfReference(); +} + +bool HTMLMediaElement::IsEligibleForAutoplay() { + // We also activate autoplay when playing a media source since the data + // download is controlled by the script and there is no way to evaluate + // MediaDecoder::CanPlayThrough(). + + if (!HasAttr(nsGkAtoms::autoplay)) { + return false; + } + + if (!mCanAutoplayFlag) { + return false; + } + + if (IsEditable()) { + return false; + } + + if (!mPaused) { + return false; + } + + if (mSuspendedByInactiveDocOrDocshell) { + return false; + } + + // Static document is used for print preview and printing, should not be + // autoplay + if (OwnerDoc()->IsStaticDocument()) { + return false; + } + + if (ShouldBeSuspendedByInactiveDocShell()) { + LOG(LogLevel::Debug, ("%p prohibiting autoplay by the docShell", this)); + return false; + } + + if (MediaPlaybackDelayPolicy::ShouldDelayPlayback(this)) { + CreateResumeDelayedMediaPlaybackAgentIfNeeded(); + LOG(LogLevel::Debug, ("%p delay playing from autoplay", this)); + return false; + } + + return mReadyState >= HAVE_ENOUGH_DATA; +} + +void HTMLMediaElement::CheckAutoplayDataReady() { + if (!IsEligibleForAutoplay()) { + return; + } + if (!AllowedToPlay()) { + DispatchEventsWhenPlayWasNotAllowed(); + return; + } + RunAutoplay(); +} + +void HTMLMediaElement::RunAutoplay() { + mAllowedToPlayPromise.ResolveIfExists(true, __func__); + mPaused = false; + // We changed mPaused which can affect AddRemoveSelfReference + AddRemoveSelfReference(); + UpdateSrcMediaStreamPlaying(); + UpdateAudioChannelPlayingState(); + StartMediaControlKeyListenerIfNeeded(); + + if (mDecoder) { + SetPlayedOrSeeked(true); + if (mCurrentPlayRangeStart == -1.0) { + mCurrentPlayRangeStart = CurrentTime(); + } + MOZ_ASSERT(!mSuspendedByInactiveDocOrDocshell); + mDecoder->Play(); + } else if (mSrcStream) { + SetPlayedOrSeeked(true); + } + + // https://html.spec.whatwg.org/multipage/media.html#ready-states:show-poster-flag + if (mShowPoster) { + mShowPoster = false; + if (mTextTrackManager) { + mTextTrackManager->TimeMarchesOn(); + } + } + + // For blocked media, the event would be pending until it is resumed. + DispatchAsyncEvent(u"play"_ns); + + DispatchAsyncEvent(u"playing"_ns); +} + +bool HTMLMediaElement::IsActuallyInvisible() const { + // That means an element is not connected. It probably hasn't connected to a + // document tree, or connects to a disconnected DOM tree. + if (!IsInComposedDoc()) { + return true; + } + + // An element is not in user's view port, which means it's either existing in + // somewhere in the page where user hasn't seen yet, or is being set + // `display:none`. + if (!IsInViewPort()) { + return true; + } + + // Element being used in picture-in-picture mode would be always visible. + if (IsBeingUsedInPictureInPictureMode()) { + return false; + } + + // That check is the page is in the background. + return OwnerDoc()->Hidden(); +} + +bool HTMLMediaElement::IsInViewPort() const { + return mVisibilityState == Visibility::ApproximatelyVisible; +} + +VideoFrameContainer* HTMLMediaElement::GetVideoFrameContainer() { + if (mShuttingDown) { + return nullptr; + } + + if (mVideoFrameContainer) return mVideoFrameContainer; + + // Only video frames need an image container. + if (!IsVideo()) { + return nullptr; + } + + mVideoFrameContainer = new VideoFrameContainer( + this, MakeAndAddRef<ImageContainer>(ImageContainer::ASYNCHRONOUS)); + + return mVideoFrameContainer; +} + +void HTMLMediaElement::PrincipalChanged(MediaStreamTrack* aTrack) { + if (aTrack != mSelectedVideoStreamTrack) { + return; + } + + nsContentUtils::CombineResourcePrincipals(&mSrcStreamVideoPrincipal, + aTrack->GetPrincipal()); + + LOG(LogLevel::Debug, + ("HTMLMediaElement %p video track principal changed to %p (combined " + "into %p). Waiting for it to reach VideoFrameContainer before setting.", + this, aTrack->GetPrincipal(), mSrcStreamVideoPrincipal.get())); + + if (mVideoFrameContainer) { + UpdateSrcStreamVideoPrincipal( + mVideoFrameContainer->GetLastPrincipalHandle()); + } +} + +void HTMLMediaElement::UpdateSrcStreamVideoPrincipal( + const PrincipalHandle& aPrincipalHandle) { + nsTArray<RefPtr<VideoStreamTrack>> videoTracks; + mSrcStream->GetVideoTracks(videoTracks); + + for (const RefPtr<VideoStreamTrack>& track : videoTracks) { + if (PrincipalHandleMatches(aPrincipalHandle, track->GetPrincipal()) && + !track->Ended()) { + // When the PrincipalHandle for the VideoFrameContainer changes to that of + // a live track in mSrcStream we know that a removed track was displayed + // but is no longer so. + LOG(LogLevel::Debug, ("HTMLMediaElement %p VideoFrameContainer's " + "PrincipalHandle matches track %p. That's all we " + "need.", + this, track.get())); + mSrcStreamVideoPrincipal = track->GetPrincipal(); + break; + } + } +} + +void HTMLMediaElement::PrincipalHandleChangedForVideoFrameContainer( + VideoFrameContainer* aContainer, + const PrincipalHandle& aNewPrincipalHandle) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mSrcStream) { + return; + } + + LOG(LogLevel::Debug, ("HTMLMediaElement %p PrincipalHandle changed in " + "VideoFrameContainer.", + this)); + + UpdateSrcStreamVideoPrincipal(aNewPrincipalHandle); +} + +already_AddRefed<nsMediaEventRunner> HTMLMediaElement::GetEventRunner( + const nsAString& aName, EventFlag aFlag) { + RefPtr<nsMediaEventRunner> runner; + if (aName.EqualsLiteral("playing")) { + runner = new nsNotifyAboutPlayingRunner(this, TakePendingPlayPromises()); + } else if (aName.EqualsLiteral("timeupdate")) { + runner = new nsTimeupdateRunner(this, aFlag == EventFlag::eMandatory); + } else { + runner = new nsAsyncEventRunner(aName, this); + } + return runner.forget(); +} + +nsresult HTMLMediaElement::DispatchEvent(const nsAString& aName) { + LOG_EVENT(LogLevel::Debug, ("%p Dispatching event %s", this, + NS_ConvertUTF16toUTF8(aName).get())); + + if (mEventBlocker->ShouldBlockEventDelivery()) { + RefPtr<nsMediaEventRunner> runner = GetEventRunner(aName); + mEventBlocker->PostponeEvent(runner); + return NS_OK; + } + + return nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, aName, + CanBubble::eNo, Cancelable::eNo); +} + +void HTMLMediaElement::DispatchAsyncEvent(const nsAString& aName) { + RefPtr<nsMediaEventRunner> runner = GetEventRunner(aName); + DispatchAsyncEvent(std::move(runner)); +} + +void HTMLMediaElement::DispatchAsyncEvent(RefPtr<nsMediaEventRunner> aRunner) { + NS_ConvertUTF16toUTF8 eventName(aRunner->EventName()); + LOG_EVENT(LogLevel::Debug, ("%p Queuing event %s", this, eventName.get())); + DDLOG(DDLogCategory::Event, "HTMLMediaElement", nsCString(eventName.get())); + if (mEventBlocker->ShouldBlockEventDelivery()) { + mEventBlocker->PostponeEvent(aRunner); + return; + } + GetMainThreadSerialEventTarget()->Dispatch(aRunner.forget()); +} + +bool HTMLMediaElement::IsPotentiallyPlaying() const { + // TODO: + // playback has not stopped due to errors, + // and the element has not paused for user interaction + return !mPaused && + (mReadyState == HAVE_ENOUGH_DATA || mReadyState == HAVE_FUTURE_DATA) && + !IsPlaybackEnded(); +} + +bool HTMLMediaElement::IsPlaybackEnded() const { + // TODO: + // the current playback position is equal to the effective end of the media + // resource. See bug 449157. + if (mDecoder) { + return mReadyState >= HAVE_METADATA && mDecoder->IsEnded(); + } + if (mSrcStream) { + return mReadyState >= HAVE_METADATA && mSrcStreamPlaybackEnded; + } + return false; +} + +already_AddRefed<nsIPrincipal> HTMLMediaElement::GetCurrentPrincipal() { + if (mDecoder) { + return mDecoder->GetCurrentPrincipal(); + } + if (mSrcStream) { + nsTArray<RefPtr<MediaStreamTrack>> tracks; + mSrcStream->GetTracks(tracks); + nsCOMPtr<nsIPrincipal> principal = mSrcStream->GetPrincipal(); + return principal.forget(); + } + return nullptr; +} + +bool HTMLMediaElement::HadCrossOriginRedirects() { + if (mDecoder) { + return mDecoder->HadCrossOriginRedirects(); + } + return false; +} + +bool HTMLMediaElement::ShouldResistFingerprinting(RFPTarget aTarget) const { + return OwnerDoc()->ShouldResistFingerprinting(aTarget); +} + +already_AddRefed<nsIPrincipal> HTMLMediaElement::GetCurrentVideoPrincipal() { + if (mDecoder) { + return mDecoder->GetCurrentPrincipal(); + } + if (mSrcStream) { + nsCOMPtr<nsIPrincipal> principal = mSrcStreamVideoPrincipal; + return principal.forget(); + } + return nullptr; +} + +void HTMLMediaElement::NotifyDecoderPrincipalChanged() { + RefPtr<nsIPrincipal> principal = GetCurrentPrincipal(); + bool isSameOrigin = !principal || IsCORSSameOrigin(); + mDecoder->UpdateSameOriginStatus(isSameOrigin); + + if (isSameOrigin) { + principal = NodePrincipal(); + } + for (const auto& entry : mOutputTrackSources.Values()) { + entry->SetPrincipal(principal); + } + mDecoder->SetOutputTracksPrincipal(principal); +} + +void HTMLMediaElement::Invalidate(ImageSizeChanged aImageSizeChanged, + const Maybe<nsIntSize>& aNewIntrinsicSize, + ForceInvalidate aForceInvalidate) { + nsIFrame* frame = GetPrimaryFrame(); + if (aNewIntrinsicSize) { + UpdateMediaSize(aNewIntrinsicSize.value()); + if (frame) { + nsPresContext* presContext = frame->PresContext(); + PresShell* presShell = presContext->PresShell(); + presShell->FrameNeedsReflow(frame, + IntrinsicDirty::FrameAncestorsAndDescendants, + NS_FRAME_IS_DIRTY); + } + } + + RefPtr<ImageContainer> imageContainer = GetImageContainer(); + bool asyncInvalidate = imageContainer && imageContainer->IsAsync() && + aForceInvalidate == ForceInvalidate::No; + if (frame) { + if (aImageSizeChanged == ImageSizeChanged::Yes) { + frame->InvalidateFrame(); + } else { + frame->InvalidateLayer(DisplayItemType::TYPE_VIDEO, nullptr, nullptr, + asyncInvalidate ? nsIFrame::UPDATE_IS_ASYNC : 0); + } + } + + SVGObserverUtils::InvalidateDirectRenderingObservers(this); +} + +void HTMLMediaElement::UpdateMediaSize(const nsIntSize& aSize) { + MOZ_ASSERT(NS_IsMainThread()); + + if (IsVideo() && mReadyState != HAVE_NOTHING && + mMediaInfo.mVideo.mDisplay != aSize) { + DispatchAsyncEvent(u"resize"_ns); + } + + mMediaInfo.mVideo.mDisplay = aSize; + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); +} + +void HTMLMediaElement::SuspendOrResumeElement(bool aSuspendElement) { + LOG(LogLevel::Debug, ("%p SuspendOrResumeElement(suspend=%d) docHidden=%d", + this, aSuspendElement, OwnerDoc()->Hidden())); + + if (aSuspendElement == mSuspendedByInactiveDocOrDocshell) { + return; + } + + mSuspendedByInactiveDocOrDocshell = aSuspendElement; + UpdateSrcMediaStreamPlaying(); + UpdateAudioChannelPlayingState(); + + if (aSuspendElement) { + if (mDecoder) { + mDecoder->Pause(); + mDecoder->Suspend(); + mDecoder->SetDelaySeekMode(true); + } + mEventBlocker->SetBlockEventDelivery(true); + // We won't want to resume media element from the bfcache. + ClearResumeDelayedMediaPlaybackAgentIfNeeded(); + mMediaControlKeyListener->StopIfNeeded(); + } else { + if (mDecoder) { + mDecoder->Resume(); + if (!mPaused && !mDecoder->IsEnded()) { + mDecoder->Play(); + } + mDecoder->SetDelaySeekMode(false); + } + mEventBlocker->SetBlockEventDelivery(false); + // If the media element has been blocked and isn't still allowed to play + // when it comes back from the bfcache, we would notify front end to show + // the blocking icon in order to inform user that the site is still being + // blocked. + if (mHasEverBeenBlockedForAutoplay && !AllowedToPlay()) { + MaybeNotifyAutoplayBlocked(); + } + StartMediaControlKeyListenerIfNeeded(); + } + if (StaticPrefs::media_testing_only_events()) { + auto dispatcher = MakeRefPtr<AsyncEventDispatcher>( + this, u"MozMediaSuspendChanged"_ns, CanBubble::eYes, + ChromeOnlyDispatch::eYes); + dispatcher->PostDOMEvent(); + } +} + +bool HTMLMediaElement::IsBeingDestroyed() { + nsIDocShell* docShell = OwnerDoc()->GetDocShell(); + bool isBeingDestroyed = false; + if (docShell) { + docShell->IsBeingDestroyed(&isBeingDestroyed); + } + return isBeingDestroyed; +} + +bool HTMLMediaElement::ShouldBeSuspendedByInactiveDocShell() const { + BrowsingContext* bc = OwnerDoc()->GetBrowsingContext(); + return bc && !bc->IsActive() && bc->Top()->GetSuspendMediaWhenInactive(); +} + +void HTMLMediaElement::NotifyOwnerDocumentActivityChanged() { + if (mDecoder && !IsBeingDestroyed()) { + NotifyDecoderActivityChanges(); + } + + // We would suspend media when the document is inactive, or its docshell has + // been set to hidden and explicitly wants to suspend media. In those cases, + // the media would be not visible and we don't want them to continue playing. + bool shouldSuspend = + !OwnerDoc()->IsActive() || ShouldBeSuspendedByInactiveDocShell(); + SuspendOrResumeElement(shouldSuspend); + + // If the owning document has become inactive we should shutdown the CDM. + if (!OwnerDoc()->IsCurrentActiveDocument() && mMediaKeys) { + // We don't shutdown MediaKeys here because it also listens for document + // activity and will take care of shutting down itself. + DDUNLINKCHILD(mMediaKeys.get()); + mMediaKeys = nullptr; + if (mDecoder) { + ShutdownDecoder(); + } + } + + AddRemoveSelfReference(); +} + +void HTMLMediaElement::NotifyFullScreenChanged() { + const bool isInFullScreen = IsInFullScreen(); + if (isInFullScreen) { + StartMediaControlKeyListenerIfNeeded(); + if (!mMediaControlKeyListener->IsStarted()) { + MEDIACONTROL_LOG("Failed to start the listener when entering fullscreen"); + } + } + // Updating controller fullscreen state no matter the listener starts or not. + BrowsingContext* bc = OwnerDoc()->GetBrowsingContext(); + if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(bc)) { + updater->NotifyMediaFullScreenState(bc->Id(), isInFullScreen); + } +} + +void HTMLMediaElement::AddRemoveSelfReference() { + // XXX we could release earlier here in many situations if we examined + // which event listeners are attached. Right now we assume there is a + // potential listener for every event. We would also have to keep the + // element alive if it was playing and producing audio output --- right now + // that's covered by the !mPaused check. + Document* ownerDoc = OwnerDoc(); + + // See the comment at the top of this file for the explanation of this + // boolean expression. + bool needSelfReference = + !mShuttingDown && ownerDoc->IsActive() && + (mDelayingLoadEvent || (!mPaused && !Ended()) || + (mDecoder && mDecoder->IsSeeking()) || IsEligibleForAutoplay() || + (mMediaSource ? mProgressTimer : mNetworkState == NETWORK_LOADING)); + + if (needSelfReference != mHasSelfReference) { + mHasSelfReference = needSelfReference; + RefPtr<HTMLMediaElement> self = this; + if (needSelfReference) { + // The shutdown observer will hold a strong reference to us. This + // will do to keep us alive. We need to know about shutdown so that + // we can release our self-reference. + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "dom::HTMLMediaElement::AddSelfReference", + [self]() { self->mShutdownObserver->AddRefMediaElement(); })); + } else { + // Dispatch Release asynchronously so that we don't destroy this object + // inside a call stack of method calls on this object + GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction( + "dom::HTMLMediaElement::AddSelfReference", + [self]() { self->mShutdownObserver->ReleaseMediaElement(); })); + } + } +} + +void HTMLMediaElement::NotifyShutdownEvent() { + mShuttingDown = true; + ResetState(); + AddRemoveSelfReference(); +} + +void HTMLMediaElement::DispatchAsyncSourceError(nsIContent* aSourceElement) { + LOG_EVENT(LogLevel::Debug, ("%p Queuing simple source error event", this)); + + nsCOMPtr<nsIRunnable> event = + new nsSourceErrorEventRunner(this, aSourceElement); + GetMainThreadSerialEventTarget()->Dispatch(event.forget()); +} + +void HTMLMediaElement::NotifyAddedSource() { + // If a source element is inserted as a child of a media element + // that has no src attribute and whose networkState has the value + // NETWORK_EMPTY, the user agent must invoke the media element's + // resource selection algorithm. + if (!HasAttr(nsGkAtoms::src) && mNetworkState == NETWORK_EMPTY) { + AssertReadyStateIsNothing(); + QueueSelectResourceTask(); + } + + // A load was paused in the resource selection algorithm, waiting for + // a new source child to be added, resume the resource selection algorithm. + if (mLoadWaitStatus == WAITING_FOR_SOURCE) { + // Rest the flag so we don't queue multiple LoadFromSourceTask() when + // multiple <source> are attached in an event loop. + mLoadWaitStatus = NOT_WAITING; + QueueLoadFromSourceTask(); + } +} + +HTMLSourceElement* HTMLMediaElement::GetNextSource() { + mSourceLoadCandidate = nullptr; + + while (true) { + if (mSourcePointer == nsINode::GetLastChild()) { + return nullptr; // no more children + } + + if (!mSourcePointer) { + mSourcePointer = nsINode::GetFirstChild(); + } else { + mSourcePointer = mSourcePointer->GetNextSibling(); + } + nsIContent* child = mSourcePointer; + + // If child is a <source> element, it is the next candidate. + if (auto* source = HTMLSourceElement::FromNodeOrNull(child)) { + mSourceLoadCandidate = source; + return source; + } + } + MOZ_ASSERT_UNREACHABLE("Execution should not reach here!"); + return nullptr; +} + +void HTMLMediaElement::ChangeDelayLoadStatus(bool aDelay) { + if (mDelayingLoadEvent == aDelay) return; + + mDelayingLoadEvent = aDelay; + + LOG(LogLevel::Debug, ("%p ChangeDelayLoadStatus(%d) doc=0x%p", this, aDelay, + mLoadBlockedDoc.get())); + if (mDecoder) { + mDecoder->SetLoadInBackground(!aDelay); + } + if (aDelay) { + mLoadBlockedDoc = OwnerDoc(); + mLoadBlockedDoc->BlockOnload(); + } else { + // mLoadBlockedDoc might be null due to GC unlinking + if (mLoadBlockedDoc) { + mLoadBlockedDoc->UnblockOnload(false); + mLoadBlockedDoc = nullptr; + } + } + + // We changed mDelayingLoadEvent which can affect AddRemoveSelfReference + AddRemoveSelfReference(); +} + +already_AddRefed<nsILoadGroup> HTMLMediaElement::GetDocumentLoadGroup() { + if (!OwnerDoc()->IsActive()) { + NS_WARNING("Load group requested for media element in inactive document."); + } + return OwnerDoc()->GetDocumentLoadGroup(); +} + +nsresult HTMLMediaElement::CopyInnerTo(Element* aDest) { + nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest); + NS_ENSURE_SUCCESS(rv, rv); + if (aDest->OwnerDoc()->IsStaticDocument()) { + HTMLMediaElement* dest = static_cast<HTMLMediaElement*>(aDest); + dest->SetMediaInfo(mMediaInfo); + } + return rv; +} + +already_AddRefed<TimeRanges> HTMLMediaElement::Buffered() const { + media::TimeIntervals buffered = + mDecoder ? mDecoder->GetBuffered() : media::TimeIntervals(); + RefPtr<TimeRanges> ranges = new TimeRanges( + ToSupports(OwnerDoc()), buffered.ToMicrosecondResolution()); + return ranges.forget(); +} + +void HTMLMediaElement::SetRequestHeaders(nsIHttpChannel* aChannel) { + // Send Accept header for video and audio types only (Bug 489071) + SetAcceptHeader(aChannel); + + // Apache doesn't send Content-Length when gzip transfer encoding is used, + // which prevents us from estimating the video length (if explicit + // Content-Duration and a length spec in the container are not present either) + // and from seeking. So, disable the standard "Accept-Encoding: gzip,deflate" + // that we usually send. See bug 614760. + DebugOnly<nsresult> rv = + aChannel->SetRequestHeader("Accept-Encoding"_ns, ""_ns, false); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Set the Referrer header + // + // FIXME: Shouldn't this use the Element constructor? Though I guess it + // doesn't matter as no HTMLMediaElement supports the referrerinfo attribute. + auto referrerInfo = MakeRefPtr<ReferrerInfo>(*OwnerDoc()); + rv = aChannel->SetReferrerInfoWithoutClone(referrerInfo); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +const TimeStamp& HTMLMediaElement::LastTimeupdateDispatchTime() const { + MOZ_ASSERT(NS_IsMainThread()); + return mLastTimeUpdateDispatchTime; +} + +void HTMLMediaElement::UpdateLastTimeupdateDispatchTime() { + MOZ_ASSERT(NS_IsMainThread()); + mLastTimeUpdateDispatchTime = TimeStamp::Now(); +} + +bool HTMLMediaElement::ShouldQueueTimeupdateAsyncTask( + TimeupdateType aType) const { + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + // That means dispatching `timeupdate` is mandatorily required in the spec. + if (aType == TimeupdateType::eMandatory) { + return true; + } + + // The timeupdate only occurs when the current playback position changes. + // https://html.spec.whatwg.org/multipage/media.html#event-media-timeupdate + if (mLastCurrentTime == CurrentTime()) { + return false; + } + + // Number of milliseconds between timeupdate events as defined by spec. + if (!mQueueTimeUpdateRunnerTime.IsNull() && + TimeStamp::Now() - mQueueTimeUpdateRunnerTime < + TimeDuration::FromMilliseconds(TIMEUPDATE_MS)) { + return false; + } + return true; +} + +void HTMLMediaElement::FireTimeUpdate(TimeupdateType aType) { + NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); + + if (ShouldQueueTimeupdateAsyncTask(aType)) { + RefPtr<nsMediaEventRunner> runner = + GetEventRunner(u"timeupdate"_ns, aType == TimeupdateType::eMandatory + ? EventFlag::eMandatory + : EventFlag::eNone); + DispatchAsyncEvent(std::move(runner)); + mQueueTimeUpdateRunnerTime = TimeStamp::Now(); + mLastCurrentTime = CurrentTime(); + } + if (mFragmentEnd >= 0.0 && CurrentTime() >= mFragmentEnd) { + Pause(); + mFragmentEnd = -1.0; + mFragmentStart = -1.0; + mDecoder->SetFragmentEndTime(mFragmentEnd); + } + + // Update the cues displaying on the video. + // Here mTextTrackManager can be null if the cycle collector has unlinked + // us before our parent. In that case UnbindFromTree will call us + // when our parent is unlinked. + if (mTextTrackManager) { + mTextTrackManager->TimeMarchesOn(); + } +} + +MediaError* HTMLMediaElement::GetError() const { return mErrorSink->mError; } + +void HTMLMediaElement::GetCurrentSpec(nsCString& aString) { + // If playing a regular URL, an ObjectURL of a Blob/File, return that. + if (mLoadingSrc) { + mLoadingSrc->GetSpec(aString); + } else if (mSrcMediaSource) { + // If playing an ObjectURL, and it's a MediaSource, return the value of the + // `src` attribute. + nsAutoString src; + GetSrc(src); + CopyUTF16toUTF8(src, aString); + } else { + // Playing e.g. a MediaStream via an object URL - return an empty string + aString.Truncate(); + } +} + +double HTMLMediaElement::MozFragmentEnd() { + double duration = Duration(); + + // If there is no end fragment, or the fragment end is greater than the + // duration, return the duration. + return (mFragmentEnd < 0.0 || mFragmentEnd > duration) ? duration + : mFragmentEnd; +} + +void HTMLMediaElement::SetDefaultPlaybackRate(double aDefaultPlaybackRate, + ErrorResult& aRv) { + if (mSrcAttrStream) { + return; + } + + if (aDefaultPlaybackRate < 0) { + aRv.Throw(NS_ERROR_NOT_IMPLEMENTED); + return; + } + + double defaultPlaybackRate = ClampPlaybackRate(aDefaultPlaybackRate); + + if (mDefaultPlaybackRate == defaultPlaybackRate) { + return; + } + + mDefaultPlaybackRate = defaultPlaybackRate; + DispatchAsyncEvent(u"ratechange"_ns); +} + +void HTMLMediaElement::SetPlaybackRate(double aPlaybackRate, ErrorResult& aRv) { + if (mSrcAttrStream) { + return; + } + + // Changing the playback rate of a media that has more than two channels is + // not supported. + if (aPlaybackRate < 0) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return; + } + + if (mPlaybackRate == aPlaybackRate) { + return; + } + + mPlaybackRate = aPlaybackRate; + // Playback rate threshold above which audio is muted. + uint32_t threshold = StaticPrefs::media_audio_playbackrate_muting_threshold(); + if (mPlaybackRate != 0.0 && + (mPlaybackRate > threshold || mPlaybackRate < 1. / threshold)) { + SetMutedInternal(mMuted | MUTED_BY_INVALID_PLAYBACK_RATE); + } else { + SetMutedInternal(mMuted & ~MUTED_BY_INVALID_PLAYBACK_RATE); + } + + if (mDecoder) { + mDecoder->SetPlaybackRate(ClampPlaybackRate(mPlaybackRate)); + } + DispatchAsyncEvent(u"ratechange"_ns); +} + +void HTMLMediaElement::SetPreservesPitch(bool aPreservesPitch) { + mPreservesPitch = aPreservesPitch; + if (mDecoder) { + mDecoder->SetPreservesPitch(mPreservesPitch); + } +} + +ImageContainer* HTMLMediaElement::GetImageContainer() { + VideoFrameContainer* container = GetVideoFrameContainer(); + return container ? container->GetImageContainer() : nullptr; +} + +void HTMLMediaElement::UpdateAudioChannelPlayingState() { + if (mAudioChannelWrapper) { + mAudioChannelWrapper->UpdateAudioChannelPlayingState(); + } +} + +static const char* VisibilityString(Visibility aVisibility) { + switch (aVisibility) { + case Visibility::Untracked: { + return "Untracked"; + } + case Visibility::ApproximatelyNonVisible: { + return "ApproximatelyNonVisible"; + } + case Visibility::ApproximatelyVisible: { + return "ApproximatelyVisible"; + } + } + + return "NAN"; +} + +void HTMLMediaElement::OnVisibilityChange(Visibility aNewVisibility) { + LOG(LogLevel::Debug, + ("OnVisibilityChange(): %s\n", VisibilityString(aNewVisibility))); + + mVisibilityState = aNewVisibility; + if (StaticPrefs::media_test_video_suspend()) { + DispatchAsyncEvent(u"visibilitychanged"_ns); + } + + if (!mDecoder) { + return; + } + NotifyDecoderActivityChanges(); +} + +MediaKeys* HTMLMediaElement::GetMediaKeys() const { return mMediaKeys; } + +bool HTMLMediaElement::ContainsRestrictedContent() const { + return GetMediaKeys() != nullptr; +} + +void HTMLMediaElement::SetCDMProxyFailure(const MediaResult& aResult) { + LOG(LogLevel::Debug, ("%s", __func__)); + MOZ_ASSERT(mSetMediaKeysDOMPromise); + + ResetSetMediaKeysTempVariables(); + + mSetMediaKeysDOMPromise->MaybeReject(aResult.Code(), aResult.Message()); +} + +void HTMLMediaElement::RemoveMediaKeys() { + LOG(LogLevel::Debug, ("%s", __func__)); + // 5.2.3 Stop using the CDM instance represented by the mediaKeys attribute + // to decrypt media data and remove the association with the media element. + if (mMediaKeys) { + mMediaKeys->Unbind(); + } + mMediaKeys = nullptr; +} + +bool HTMLMediaElement::TryRemoveMediaKeysAssociation() { + MOZ_ASSERT(mMediaKeys); + LOG(LogLevel::Debug, ("%s", __func__)); + // 5.2.1 If the user agent or CDM do not support removing the association, + // let this object's attaching media keys value be false and reject promise + // with a new DOMException whose name is NotSupportedError. + // 5.2.2 If the association cannot currently be removed, let this object's + // attaching media keys value be false and reject promise with a new + // DOMException whose name is InvalidStateError. + if (mDecoder) { + RefPtr<HTMLMediaElement> self = this; + mDecoder->SetCDMProxy(nullptr) + ->Then( + AbstractMainThread(), __func__, + [self]() { + self->mSetCDMRequest.Complete(); + + self->RemoveMediaKeys(); + if (self->AttachNewMediaKeys()) { + // No incoming MediaKeys object or MediaDecoder is not + // created yet. + self->MakeAssociationWithCDMResolved(); + } + }, + [self](const MediaResult& aResult) { + self->mSetCDMRequest.Complete(); + // 5.2.4 If the preceding step failed, let this object's + // attaching media keys value be false and reject promise with + // a new DOMException whose name is the appropriate error name. + self->SetCDMProxyFailure(aResult); + }) + ->Track(mSetCDMRequest); + return false; + } + + RemoveMediaKeys(); + return true; +} + +bool HTMLMediaElement::DetachExistingMediaKeys() { + LOG(LogLevel::Debug, ("%s", __func__)); + MOZ_ASSERT(mSetMediaKeysDOMPromise); + // 5.1 If mediaKeys is not null, CDM instance represented by mediaKeys is + // already in use by another media element, and the user agent is unable + // to use it with this element, let this object's attaching media keys + // value be false and reject promise with a new DOMException whose name + // is QuotaExceededError. + if (mIncomingMediaKeys && mIncomingMediaKeys->IsBoundToMediaElement()) { + SetCDMProxyFailure(MediaResult( + NS_ERROR_DOM_MEDIA_KEY_QUOTA_EXCEEDED_ERR, + "MediaKeys object is already bound to another HTMLMediaElement")); + return false; + } + + // 5.2 If the mediaKeys attribute is not null, run the following steps: + if (mMediaKeys) { + return TryRemoveMediaKeysAssociation(); + } + return true; +} + +void HTMLMediaElement::MakeAssociationWithCDMResolved() { + LOG(LogLevel::Debug, ("%s", __func__)); + MOZ_ASSERT(mSetMediaKeysDOMPromise); + + // 5.4 Set the mediaKeys attribute to mediaKeys. + mMediaKeys = mIncomingMediaKeys; +#ifdef MOZ_WMF_CDM + if (mMediaKeys && mMediaKeys->GetCDMProxy()) { + mIsUsingWMFCDM = !!mMediaKeys->GetCDMProxy()->AsWMFCDMProxy(); + } +#endif + // 5.5 Let this object's attaching media keys value be false. + ResetSetMediaKeysTempVariables(); + // 5.6 Resolve promise. + mSetMediaKeysDOMPromise->MaybeResolveWithUndefined(); + mSetMediaKeysDOMPromise = nullptr; +} + +bool HTMLMediaElement::TryMakeAssociationWithCDM(CDMProxy* aProxy) { + LOG(LogLevel::Debug, ("%s", __func__)); + MOZ_ASSERT(aProxy); + + // 5.3.3 Queue a task to run the "Attempt to Resume Playback If Necessary" + // algorithm on the media element. + // Note: Setting the CDMProxy on the MediaDecoder will unblock playback. + if (mDecoder) { + // CDMProxy is set asynchronously in MediaFormatReader, once it's done, + // HTMLMediaElement should resolve or reject the DOM promise. + RefPtr<HTMLMediaElement> self = this; + mDecoder->SetCDMProxy(aProxy) + ->Then( + AbstractMainThread(), __func__, + [self]() { + self->mSetCDMRequest.Complete(); + self->MakeAssociationWithCDMResolved(); + }, + [self](const MediaResult& aResult) { + self->mSetCDMRequest.Complete(); + self->SetCDMProxyFailure(aResult); + }) + ->Track(mSetCDMRequest); + return false; + } + return true; +} + +bool HTMLMediaElement::AttachNewMediaKeys() { + LOG(LogLevel::Debug, + ("%s incoming MediaKeys(%p)", __func__, mIncomingMediaKeys.get())); + MOZ_ASSERT(mSetMediaKeysDOMPromise); + + // 5.3. If mediaKeys is not null, run the following steps: + if (mIncomingMediaKeys) { + auto* cdmProxy = mIncomingMediaKeys->GetCDMProxy(); + if (!cdmProxy) { + SetCDMProxyFailure(MediaResult( + NS_ERROR_DOM_INVALID_STATE_ERR, + "CDM crashed before binding MediaKeys object to HTMLMediaElement")); + return false; + } + + // 5.3.1 Associate the CDM instance represented by mediaKeys with the + // media element for decrypting media data. + if (NS_FAILED(mIncomingMediaKeys->Bind(this))) { + // 5.3.2 If the preceding step failed, run the following steps: + + // 5.3.2.1 Set the mediaKeys attribute to null. + mMediaKeys = nullptr; + // 5.3.2.2 Let this object's attaching media keys value be false. + // 5.3.2.3 Reject promise with a new DOMException whose name is + // the appropriate error name. + SetCDMProxyFailure( + MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR, + "Failed to bind MediaKeys object to HTMLMediaElement")); + return false; + } + return TryMakeAssociationWithCDM(cdmProxy); + } + return true; +} + +void HTMLMediaElement::ResetSetMediaKeysTempVariables() { + mAttachingMediaKey = false; + mIncomingMediaKeys = nullptr; +} + +already_AddRefed<Promise> HTMLMediaElement::SetMediaKeys( + mozilla::dom::MediaKeys* aMediaKeys, ErrorResult& aRv) { + LOG(LogLevel::Debug, ("%p SetMediaKeys(%p) mMediaKeys=%p mDecoder=%p", this, + aMediaKeys, mMediaKeys.get(), mDecoder.get())); + + if (MozAudioCaptured()) { + aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR); + return nullptr; + } + + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + if (!win) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + RefPtr<DetailedPromise> promise = DetailedPromise::Create( + win->AsGlobal(), aRv, "HTMLMediaElement.setMediaKeys"_ns); + if (aRv.Failed()) { + return nullptr; + } + + // 1. If mediaKeys and the mediaKeys attribute are the same object, + // return a resolved promise. + if (mMediaKeys == aMediaKeys) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + // 2. If this object's attaching media keys value is true, return a + // promise rejected with a new DOMException whose name is InvalidStateError. + if (mAttachingMediaKey) { + promise->MaybeRejectWithInvalidStateError( + "A MediaKeys object is in attaching operation."); + return promise.forget(); + } + + // 3. Let this object's attaching media keys value be true. + mAttachingMediaKey = true; + mIncomingMediaKeys = aMediaKeys; + + // 4. Let promise be a new promise. + mSetMediaKeysDOMPromise = promise; + + // 5. Run the following steps in parallel: + + // 5.1 & 5.2 & 5.3 + if (!DetachExistingMediaKeys() || !AttachNewMediaKeys()) { + return promise.forget(); + } + + // 5.4, 5.5, 5.6 + MakeAssociationWithCDMResolved(); + + // 6. Return promise. + return promise.forget(); +} + +EventHandlerNonNull* HTMLMediaElement::GetOnencrypted() { + return EventTarget::GetEventHandler(nsGkAtoms::onencrypted); +} + +void HTMLMediaElement::SetOnencrypted(EventHandlerNonNull* aCallback) { + EventTarget::SetEventHandler(nsGkAtoms::onencrypted, aCallback); +} + +EventHandlerNonNull* HTMLMediaElement::GetOnwaitingforkey() { + return EventTarget::GetEventHandler(nsGkAtoms::onwaitingforkey); +} + +void HTMLMediaElement::SetOnwaitingforkey(EventHandlerNonNull* aCallback) { + EventTarget::SetEventHandler(nsGkAtoms::onwaitingforkey, aCallback); +} + +void HTMLMediaElement::DispatchEncrypted(const nsTArray<uint8_t>& aInitData, + const nsAString& aInitDataType) { + LOG(LogLevel::Debug, ("%p DispatchEncrypted initDataType='%s'", this, + NS_ConvertUTF16toUTF8(aInitDataType).get())); + + if (mReadyState == HAVE_NOTHING) { + // Ready state not HAVE_METADATA (yet), don't dispatch encrypted now. + // Queueing for later dispatch in MetadataLoaded. + mPendingEncryptedInitData.AddInitData(aInitDataType, aInitData); + return; + } + + RefPtr<MediaEncryptedEvent> event; + if (IsCORSSameOrigin()) { + event = MediaEncryptedEvent::Constructor(this, aInitDataType, aInitData); + } else { + event = MediaEncryptedEvent::Constructor(this); + } + + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, event.forget()); + asyncDispatcher->PostDOMEvent(); +} + +bool HTMLMediaElement::IsEventAttributeNameInternal(nsAtom* aName) { + return aName == nsGkAtoms::onencrypted || + nsGenericHTMLElement::IsEventAttributeNameInternal(aName); +} + +void HTMLMediaElement::NotifyWaitingForKey() { + LOG(LogLevel::Debug, ("%p, NotifyWaitingForKey()", this)); + + // http://w3c.github.io/encrypted-media/#wait-for-key + // 7.3.4 Queue a "waitingforkey" Event + // 1. Let the media element be the specified HTMLMediaElement object. + // 2. If the media element's waiting for key value is true, abort these steps. + if (mWaitingForKey == NOT_WAITING_FOR_KEY) { + // 3. Set the media element's waiting for key value to true. + // Note: algorithm continues in UpdateReadyStateInternal() when all decoded + // data enqueued in the MDSM is consumed. + mWaitingForKey = WAITING_FOR_KEY; + // mWaitingForKey changed outside of UpdateReadyStateInternal. This may + // affect mReadyState. + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); + } +} + +AudioTrackList* HTMLMediaElement::AudioTracks() { return mAudioTrackList; } + +VideoTrackList* HTMLMediaElement::VideoTracks() { return mVideoTrackList; } + +TextTrackList* HTMLMediaElement::GetTextTracks() { + return GetOrCreateTextTrackManager()->GetTextTracks(); +} + +already_AddRefed<TextTrack> HTMLMediaElement::AddTextTrack( + TextTrackKind aKind, const nsAString& aLabel, const nsAString& aLanguage) { + return GetOrCreateTextTrackManager()->AddTextTrack( + aKind, aLabel, aLanguage, TextTrackMode::Hidden, + TextTrackReadyState::Loaded, TextTrackSource::AddTextTrack); +} + +void HTMLMediaElement::PopulatePendingTextTrackList() { + if (mTextTrackManager) { + mTextTrackManager->PopulatePendingList(); + } +} + +TextTrackManager* HTMLMediaElement::GetOrCreateTextTrackManager() { + if (!mTextTrackManager) { + mTextTrackManager = new TextTrackManager(this); + mTextTrackManager->AddListeners(); + } + return mTextTrackManager; +} + +MediaDecoderOwner::NextFrameStatus HTMLMediaElement::NextFrameStatus() { + if (mDecoder) { + return mDecoder->NextFrameStatus(); + } + if (mSrcStream) { + AutoTArray<RefPtr<MediaTrack>, 4> tracks; + GetAllEnabledMediaTracks(tracks); + if (!tracks.IsEmpty() && !mSrcStreamPlaybackEnded) { + return NEXT_FRAME_AVAILABLE; + } + return NEXT_FRAME_UNAVAILABLE; + } + return NEXT_FRAME_UNINITIALIZED; +} + +void HTMLMediaElement::SetDecoder(MediaDecoder* aDecoder) { + MOZ_ASSERT(aDecoder); // Use ShutdownDecoder() to clear. + if (mDecoder) { + ShutdownDecoder(); + } + mDecoder = aDecoder; + DDLINKCHILD("decoder", mDecoder.get()); + if (mDecoder && mForcedHidden) { + mDecoder->SetForcedHidden(mForcedHidden); + } +} + +float HTMLMediaElement::ComputedVolume() const { + return mMuted ? 0.0f + : mAudioChannelWrapper ? mAudioChannelWrapper->GetEffectiveVolume() + : static_cast<float>(mVolume); +} + +bool HTMLMediaElement::ComputedMuted() const { + return (mMuted & MUTED_BY_AUDIO_CHANNEL); +} + +bool HTMLMediaElement::IsSuspendedByInactiveDocOrDocShell() const { + return mSuspendedByInactiveDocOrDocshell; +} + +bool HTMLMediaElement::IsCurrentlyPlaying() const { + // We have playable data, but we still need to check whether data is "real" + // current data. + return mReadyState >= HAVE_CURRENT_DATA && !IsPlaybackEnded(); +} + +void HTMLMediaElement::SetAudibleState(bool aAudible) { + if (mIsAudioTrackAudible != aAudible) { + mIsAudioTrackAudible = aAudible; + NotifyAudioPlaybackChanged( + AudioChannelService::AudibleChangedReasons::eDataAudibleChanged); + } +} + +void HTMLMediaElement::NotifyAudioPlaybackChanged( + AudibleChangedReasons aReason) { + if (mAudioChannelWrapper) { + mAudioChannelWrapper->NotifyAudioPlaybackChanged(aReason); + } + // We would start the listener after media becomes audible. + const bool isAudible = IsAudible(); + if (isAudible && !mMediaControlKeyListener->IsStarted()) { + StartMediaControlKeyListenerIfNeeded(); + } + mMediaControlKeyListener->UpdateMediaAudibleState(isAudible); + // only request wake lock for audible media. + UpdateWakeLock(); +} + +void HTMLMediaElement::SetMediaInfo(const MediaInfo& aInfo) { + const bool oldHasAudio = mMediaInfo.HasAudio(); + mMediaInfo = aInfo; + if ((aInfo.HasAudio() != oldHasAudio) && mResumeDelayedPlaybackAgent) { + mResumeDelayedPlaybackAgent->UpdateAudibleState(this, IsAudible()); + } + nsILoadContext* loadContext = OwnerDoc()->GetLoadContext(); + if (HasAudio() && loadContext && !loadContext->UsePrivateBrowsing()) { + mTitleChangeObserver->Subscribe(); + UpdateStreamName(); + } else { + mTitleChangeObserver->Unsubscribe(); + } + if (mAudioChannelWrapper) { + mAudioChannelWrapper->AudioCaptureTrackChangeIfNeeded(); + } + UpdateWakeLock(); +} + +MediaInfo HTMLMediaElement::GetMediaInfo() const { return mMediaInfo; } + +FrameStatistics* HTMLMediaElement::GetFrameStatistics() const { + return mDecoder ? &(mDecoder->GetFrameStatistics()) : nullptr; +} + +void HTMLMediaElement::DispatchAsyncTestingEvent(const nsAString& aName) { + if (!StaticPrefs::media_testing_only_events()) { + return; + } + DispatchAsyncEvent(aName); +} + +void HTMLMediaElement::AudioCaptureTrackChange(bool aCapture) { + // No need to capture a silent media element. + if (!HasAudio()) { + return; + } + + if (aCapture && !mStreamWindowCapturer) { + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + if (!window) { + return; + } + + MediaTrackGraph* mtg = MediaTrackGraph::GetInstance( + MediaTrackGraph::AUDIO_THREAD_DRIVER, window, + MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, + MediaTrackGraph::DEFAULT_OUTPUT_DEVICE); + RefPtr<DOMMediaStream> stream = + CaptureStreamInternal(StreamCaptureBehavior::CONTINUE_WHEN_ENDED, + StreamCaptureType::CAPTURE_AUDIO, mtg); + mStreamWindowCapturer = + MakeUnique<MediaStreamWindowCapturer>(stream, window->WindowID()); + } else if (!aCapture && mStreamWindowCapturer) { + for (size_t i = 0; i < mOutputStreams.Length(); i++) { + if (mOutputStreams[i].mStream == mStreamWindowCapturer->mStream) { + // We own this MediaStream, it is not exposed to JS. + AutoTArray<RefPtr<MediaStreamTrack>, 2> tracks; + mStreamWindowCapturer->mStream->GetTracks(tracks); + for (auto& track : tracks) { + track->Stop(); + } + mOutputStreams.RemoveElementAt(i); + break; + } + } + mStreamWindowCapturer = nullptr; + if (mOutputStreams.IsEmpty()) { + mTracksCaptured = nullptr; + } + } +} + +void HTMLMediaElement::NotifyCueDisplayStatesChanged() { + if (!mTextTrackManager) { + return; + } + + mTextTrackManager->DispatchUpdateCueDisplay(); +} + +void HTMLMediaElement::LogVisibility(CallerAPI aAPI) { + const bool isVisible = mVisibilityState == Visibility::ApproximatelyVisible; + + LOG(LogLevel::Debug, ("%p visibility = %u, API: '%d' and 'All'", this, + isVisible, static_cast<int>(aAPI))); + + if (!isVisible) { + LOG(LogLevel::Debug, ("%p inTree = %u, API: '%d' and 'All'", this, + IsInComposedDoc(), static_cast<int>(aAPI))); + } +} + +void HTMLMediaElement::UpdateCustomPolicyAfterPlayed() { + if (mAudioChannelWrapper) { + mAudioChannelWrapper->NotifyPlayStateChanged(); + } +} + +AbstractThread* HTMLMediaElement::AbstractMainThread() const { + return AbstractThread::MainThread(); +} + +nsTArray<RefPtr<PlayPromise>> HTMLMediaElement::TakePendingPlayPromises() { + return std::move(mPendingPlayPromises); +} + +void HTMLMediaElement::NotifyAboutPlaying() { + // Stick to the DispatchAsyncEvent() call path for now because we want to + // trigger some telemetry-related codes in the DispatchAsyncEvent() method. + DispatchAsyncEvent(u"playing"_ns); +} + +already_AddRefed<PlayPromise> HTMLMediaElement::CreatePlayPromise( + ErrorResult& aRv) const { + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + + if (!win) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + RefPtr<PlayPromise> promise = PlayPromise::Create(win->AsGlobal(), aRv); + LOG(LogLevel::Debug, ("%p created PlayPromise %p", this, promise.get())); + + return promise.forget(); +} + +already_AddRefed<Promise> HTMLMediaElement::CreateDOMPromise( + ErrorResult& aRv) const { + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + + if (!win) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + return Promise::Create(win->AsGlobal(), aRv); +} + +void HTMLMediaElement::AsyncResolvePendingPlayPromises() { + if (mShuttingDown) { + return; + } + + nsCOMPtr<nsIRunnable> event = new nsResolveOrRejectPendingPlayPromisesRunner( + this, TakePendingPlayPromises()); + + GetMainThreadSerialEventTarget()->Dispatch(event.forget()); +} + +void HTMLMediaElement::AsyncRejectPendingPlayPromises(nsresult aError) { + if (!mPaused) { + mPaused = true; + DispatchAsyncEvent(u"pause"_ns); + } + + if (mShuttingDown) { + return; + } + + if (aError == NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR) { + DispatchEventsWhenPlayWasNotAllowed(); + } + + nsCOMPtr<nsIRunnable> event = new nsResolveOrRejectPendingPlayPromisesRunner( + this, TakePendingPlayPromises(), aError); + + GetMainThreadSerialEventTarget()->Dispatch(event.forget()); +} + +void HTMLMediaElement::GetEMEInfo(dom::EMEDebugInfo& aInfo) { + MOZ_ASSERT(NS_IsMainThread(), + "MediaKeys expects to be interacted with on main thread!"); + if (!mMediaKeys) { + return; + } + mMediaKeys->GetKeySystem(aInfo.mKeySystem); + mMediaKeys->GetSessionsInfo(aInfo.mSessionsInfo); +} + +void HTMLMediaElement::NotifyDecoderActivityChanges() const { + if (mDecoder) { + mDecoder->NotifyOwnerActivityChanged(IsActuallyInvisible(), + IsInComposedDoc()); + } +} + +Document* HTMLMediaElement::GetDocument() const { return OwnerDoc(); } + +bool HTMLMediaElement::IsAudible() const { + // No audio track. + if (!HasAudio()) { + return false; + } + + // Muted or the volume should not be ~0 + if (mMuted || (std::fabs(Volume()) <= 1e-7)) { + return false; + } + + return mIsAudioTrackAudible; +} + +Maybe<nsAutoString> HTMLMediaElement::GetKeySystem() const { + if (!mMediaKeys) { + return Nothing(); + } + nsAutoString keySystem; + mMediaKeys->GetKeySystem(keySystem); + return Some(keySystem); +} + +void HTMLMediaElement::ConstructMediaTracks(const MediaInfo* aInfo) { + if (!aInfo) { + return; + } + + AudioTrackList* audioList = AudioTracks(); + if (audioList && aInfo->HasAudio()) { + const TrackInfo& info = aInfo->mAudio; + RefPtr<AudioTrack> track = MediaTrackList::CreateAudioTrack( + audioList->GetOwnerGlobal(), info.mId, info.mKind, info.mLabel, + info.mLanguage, info.mEnabled); + + audioList->AddTrack(track); + } + + VideoTrackList* videoList = VideoTracks(); + if (videoList && aInfo->HasVideo()) { + const TrackInfo& info = aInfo->mVideo; + RefPtr<VideoTrack> track = MediaTrackList::CreateVideoTrack( + videoList->GetOwnerGlobal(), info.mId, info.mKind, info.mLabel, + info.mLanguage); + + videoList->AddTrack(track); + track->SetEnabledInternal(info.mEnabled, MediaTrack::FIRE_NO_EVENTS); + } +} + +void HTMLMediaElement::RemoveMediaTracks() { + if (mAudioTrackList) { + mAudioTrackList->RemoveTracks(); + } + if (mVideoTrackList) { + mVideoTrackList->RemoveTracks(); + } +} + +class MediaElementGMPCrashHelper : public GMPCrashHelper { + public: + explicit MediaElementGMPCrashHelper(HTMLMediaElement* aElement) + : mElement(aElement) { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + } + already_AddRefed<nsPIDOMWindowInner> GetPluginCrashedEventTarget() override { + MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe. + if (!mElement) { + return nullptr; + } + return do_AddRef(mElement->OwnerDoc()->GetInnerWindow()); + } + + private: + WeakPtr<HTMLMediaElement> mElement; +}; + +already_AddRefed<GMPCrashHelper> HTMLMediaElement::CreateGMPCrashHelper() { + return MakeAndAddRef<MediaElementGMPCrashHelper>(this); +} + +void HTMLMediaElement::MarkAsTainted() { + mHasSuspendTaint = true; + + if (mDecoder) { + mDecoder->SetSuspendTaint(true); + } +} + +bool HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj) { + return nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::debugger) || + nsContentUtils::CallerHasPermission(aCx, nsGkAtoms::tabs); +} + +already_AddRefed<Promise> HTMLMediaElement::SetSinkId(const nsAString& aSinkId, + ErrorResult& aRv) { + LOG(LogLevel::Info, + ("%p, setSinkId(%s)", this, NS_ConvertUTF16toUTF8(aSinkId).get())); + nsCOMPtr<nsPIDOMWindowInner> win = OwnerDoc()->GetInnerWindow(); + if (!win) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(win->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (!FeaturePolicyUtils::IsFeatureAllowed(win->GetExtantDoc(), + u"speaker-selection"_ns)) { + promise->MaybeRejectWithNotAllowedError( + "Document's Permissions Policy does not allow setSinkId()"); + } + + if (mSink.first.Equals(aSinkId)) { + promise->MaybeResolveWithUndefined(); + return promise.forget(); + } + + RefPtr<MediaDevices> mediaDevices = win->Navigator()->GetMediaDevices(aRv); + if (aRv.Failed()) { + return nullptr; + } + + nsString sinkId(aSinkId); + mediaDevices->GetSinkDevice(sinkId) + ->Then( + AbstractMainThread(), __func__, + [self = RefPtr<HTMLMediaElement>(this), + this](RefPtr<AudioDeviceInfo>&& aInfo) { + // Sink found switch output device. + MOZ_ASSERT(aInfo); + if (mDecoder) { + RefPtr<SinkInfoPromise> p = mDecoder->SetSink(aInfo)->Then( + AbstractMainThread(), __func__, + [aInfo](const GenericPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + return SinkInfoPromise::CreateAndResolve(aInfo, __func__); + } + return SinkInfoPromise::CreateAndReject( + aValue.RejectValue(), __func__); + }); + return p; + } + if (mSrcStream) { + MOZ_ASSERT(mMediaStreamRenderer); + RefPtr<SinkInfoPromise> p = + mMediaStreamRenderer->SetAudioOutputDevice(aInfo)->Then( + AbstractMainThread(), __func__, + [aInfo]( + const GenericPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + return SinkInfoPromise::CreateAndResolve(aInfo, + __func__); + } + return SinkInfoPromise::CreateAndReject( + aValue.RejectValue(), __func__); + }); + return p; + } + // No media attached to the element save it for later. + return SinkInfoPromise::CreateAndResolve(aInfo, __func__); + }, + [](nsresult res) { + // Promise is rejected, sink not found. + return SinkInfoPromise::CreateAndReject(res, __func__); + }) + ->Then(AbstractMainThread(), __func__, + [promise, self = RefPtr<HTMLMediaElement>(this), this, + sinkId](const SinkInfoPromise::ResolveOrRejectValue& aValue) { + if (aValue.IsResolve()) { + LOG(LogLevel::Info, ("%p, set sinkid=%s", this, + NS_ConvertUTF16toUTF8(sinkId).get())); + mSink = std::pair(sinkId, aValue.ResolveValue()); + promise->MaybeResolveWithUndefined(); + } else { + switch (aValue.RejectValue()) { + case NS_ERROR_ABORT: + promise->MaybeReject(NS_ERROR_DOM_ABORT_ERR); + break; + case NS_ERROR_NOT_AVAILABLE: { + promise->MaybeRejectWithNotFoundError( + "The object can not be found here."); + break; + } + default: + MOZ_ASSERT_UNREACHABLE("Invalid error."); + } + } + }); + + aRv = NS_OK; + return promise.forget(); +} + +void HTMLMediaElement::NotifyTextTrackModeChanged() { + if (mPendingTextTrackChanged) { + return; + } + mPendingTextTrackChanged = true; + AbstractMainThread()->Dispatch( + NS_NewRunnableFunction("HTMLMediaElement::NotifyTextTrackModeChanged", + [this, self = RefPtr<HTMLMediaElement>(this)]() { + mPendingTextTrackChanged = false; + if (!mTextTrackManager) { + return; + } + GetTextTracks()->CreateAndDispatchChangeEvent(); + // https://html.spec.whatwg.org/multipage/media.html#text-track-model:show-poster-flag + if (!mShowPoster) { + mTextTrackManager->TimeMarchesOn(); + } + })); +} + +void HTMLMediaElement::CreateResumeDelayedMediaPlaybackAgentIfNeeded() { + if (mResumeDelayedPlaybackAgent) { + return; + } + mResumeDelayedPlaybackAgent = + MediaPlaybackDelayPolicy::CreateResumeDelayedPlaybackAgent(this, + IsAudible()); + if (!mResumeDelayedPlaybackAgent) { + LOG(LogLevel::Debug, + ("%p Failed to create a delayed playback agant", this)); + return; + } + mResumeDelayedPlaybackAgent->GetResumePromise() + ->Then( + AbstractMainThread(), __func__, + [self = RefPtr<HTMLMediaElement>(this)]() { + LOG(LogLevel::Debug, ("%p Resume delayed Play() call", self.get())); + self->mResumePlaybackRequest.Complete(); + self->mResumeDelayedPlaybackAgent = nullptr; + IgnoredErrorResult dummy; + RefPtr<Promise> toBeIgnored = self->Play(dummy); + }, + [self = RefPtr<HTMLMediaElement>(this)]() { + LOG(LogLevel::Debug, + ("%p Can not resume delayed Play() call", self.get())); + self->mResumePlaybackRequest.Complete(); + self->mResumeDelayedPlaybackAgent = nullptr; + }) + ->Track(mResumePlaybackRequest); +} + +void HTMLMediaElement::ClearResumeDelayedMediaPlaybackAgentIfNeeded() { + if (mResumeDelayedPlaybackAgent) { + mResumePlaybackRequest.DisconnectIfExists(); + mResumeDelayedPlaybackAgent = nullptr; + } +} + +void HTMLMediaElement::NotifyMediaControlPlaybackStateChanged() { + if (!mMediaControlKeyListener->IsStarted()) { + return; + } + if (mPaused) { + mMediaControlKeyListener->NotifyMediaStoppedPlaying(); + } else { + mMediaControlKeyListener->NotifyMediaStartedPlaying(); + } +} + +bool HTMLMediaElement::IsInFullScreen() const { + return State().HasState(ElementState::FULLSCREEN); +} + +bool HTMLMediaElement::IsPlayable() const { + return (mDecoder || mSrcStream) && !HasError(); +} + +bool HTMLMediaElement::ShouldStartMediaControlKeyListener() const { + if (!IsPlayable()) { + MEDIACONTROL_LOG("Not start listener because media is not playable"); + return false; + } + + if (mSrcStream) { + MEDIACONTROL_LOG("Not listening because media is real-time"); + return false; + } + + if (IsBeingUsedInPictureInPictureMode()) { + MEDIACONTROL_LOG("Start listener because of being used in PiP mode"); + return true; + } + + if (IsInFullScreen()) { + MEDIACONTROL_LOG("Start listener because of being used in fullscreen"); + return true; + } + + // In order to filter out notification-ish sound, we use this pref to set the + // eligible media duration to prevent showing media control for those short + // sound. + if (Duration() < + StaticPrefs::media_mediacontrol_eligible_media_duration_s()) { + MEDIACONTROL_LOG("Not listening because media's duration %f is too short.", + Duration()); + return false; + } + + // This includes cases such like `video is muted`, `video has zero volume`, + // `video's audio track is still inaudible` and `tab is muted by audio channel + // (tab sound indicator)`, all these cases would make media inaudible. + // `ComputedVolume()` would return the final volume applied the affection made + // by audio channel, which is used to detect if the tab is muted by audio + // channel. + if (!IsAudible() || ComputedVolume() == 0.0f) { + MEDIACONTROL_LOG("Not listening because media is inaudible"); + return false; + } + return true; +} + +void HTMLMediaElement::StartMediaControlKeyListenerIfNeeded() { + if (!ShouldStartMediaControlKeyListener()) { + return; + } + mMediaControlKeyListener->Start(); +} + +void HTMLMediaElement::UpdateStreamName() { + MOZ_ASSERT(NS_IsMainThread()); + + nsAutoString aTitle; + OwnerDoc()->GetTitle(aTitle); + + if (mDecoder) { + mDecoder->SetStreamName(aTitle); + } +} + +void HTMLMediaElement::SetSecondaryMediaStreamRenderer( + VideoFrameContainer* aContainer, + FirstFrameVideoOutput* aFirstFrameOutput /* = nullptr */) { + MOZ_ASSERT(mSrcStream); + MOZ_ASSERT(mMediaStreamRenderer); + if (mSecondaryMediaStreamRenderer) { + mSecondaryMediaStreamRenderer->Shutdown(); + mSecondaryMediaStreamRenderer = nullptr; + } + if (aContainer) { + mSecondaryMediaStreamRenderer = MakeAndAddRef<MediaStreamRenderer>( + AbstractMainThread(), aContainer, aFirstFrameOutput, this); + if (mSrcStreamIsPlaying) { + mSecondaryMediaStreamRenderer->Start(); + } + if (mSelectedVideoStreamTrack) { + mSecondaryMediaStreamRenderer->AddTrack(mSelectedVideoStreamTrack); + } + } +} + +void HTMLMediaElement::UpdateMediaControlAfterPictureInPictureModeChanged() { + if (IsBeingUsedInPictureInPictureMode()) { + // When media enters PIP mode, we should ensure that the listener has been + // started because we always want to control PIP video. + StartMediaControlKeyListenerIfNeeded(); + if (!mMediaControlKeyListener->IsStarted()) { + MEDIACONTROL_LOG("Failed to start listener when entering PIP mode"); + } + // Updating controller PIP state no matter the listener starts or not. + mMediaControlKeyListener->SetPictureInPictureModeEnabled(true); + } else { + mMediaControlKeyListener->SetPictureInPictureModeEnabled(false); + } +} + +bool HTMLMediaElement::IsBeingUsedInPictureInPictureMode() const { + if (!IsVideo()) { + return false; + } + return static_cast<const HTMLVideoElement*>(this)->IsCloningElementVisually(); +} + +void HTMLMediaElement::NodeInfoChanged(Document* aOldDoc) { + if (mMediaSource) { + OwnerDoc()->AddMediaElementWithMSE(); + aOldDoc->RemoveMediaElementWithMSE(); + } + + nsGenericHTMLElement::NodeInfoChanged(aOldDoc); +} + +#ifdef MOZ_WMF_CDM +bool HTMLMediaElement::IsUsingWMFCDM() const { return mIsUsingWMFCDM; }; +#endif + +} // namespace mozilla::dom + +#undef LOG +#undef LOG_EVENT diff --git a/dom/html/HTMLMediaElement.h b/dom/html/HTMLMediaElement.h new file mode 100644 index 0000000000..0d35fcc85c --- /dev/null +++ b/dom/html/HTMLMediaElement.h @@ -0,0 +1,1941 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLMediaElement_h +#define mozilla_dom_HTMLMediaElement_h + +#include "nsGenericHTMLElement.h" +#include "AudioChannelService.h" +#include "MediaEventSource.h" +#include "SeekTarget.h" +#include "MediaDecoderOwner.h" +#include "MediaElementEventRunners.h" +#include "MediaPlaybackDelayPolicy.h" +#include "MediaPromiseDefs.h" +#include "TelemetryProbesReporter.h" +#include "nsCycleCollectionParticipant.h" +#include "Visibility.h" +#include "mozilla/CORSMode.h" +#include "DecoderTraits.h" +#include "mozilla/Attributes.h" +#include "mozilla/StateWatching.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/dom/DecoderDoctorNotificationBinding.h" +#include "mozilla/dom/HTMLMediaElementBinding.h" +#include "mozilla/dom/MediaDebugInfoBinding.h" +#include "mozilla/dom/MediaKeys.h" +#include "mozilla/dom/TextTrackManager.h" +#include "nsGkAtoms.h" +#include "PrincipalChangeObserver.h" +#include "nsStubMutationObserver.h" +#include "MediaSegment.h" // for PrincipalHandle, GraphTime + +#include <utility> + +// X.h on Linux #defines CurrentTime as 0L, so we have to #undef it here. +#ifdef CurrentTime +# undef CurrentTime +#endif + +// Define to output information on decoding and painting framerate +/* #define DEBUG_FRAME_RATE 1 */ + +using nsMediaNetworkState = uint16_t; +using nsMediaReadyState = uint16_t; +using SuspendTypes = uint32_t; +using AudibleChangedReasons = uint32_t; + +class nsIStreamListener; + +namespace mozilla { +class AbstractThread; +class ChannelMediaDecoder; +class DecoderDoctorDiagnostics; +class DOMMediaStream; +class ErrorResult; +class FirstFrameVideoOutput; +class MediaResource; +class MediaDecoder; +class MediaInputPort; +class MediaTrack; +class MediaTrackGraph; +class MediaStreamWindowCapturer; +struct SharedDummyTrack; +class VideoFrameContainer; +class VideoOutput; +namespace dom { +class HTMLSourceElement; +class MediaKeys; +class TextTrack; +class TimeRanges; +class WakeLock; +class MediaStreamTrack; +class MediaStreamTrackSource; +class MediaTrack; +class VideoStreamTrack; +} // namespace dom +} // namespace mozilla + +class AudioDeviceInfo; +class nsIChannel; +class nsIHttpChannel; +class nsILoadGroup; +class nsIRunnable; +class nsISerialEventTarget; +class nsITimer; +class nsRange; + +namespace mozilla::dom { + +// Number of milliseconds between timeupdate events as defined by spec +#define TIMEUPDATE_MS 250 + +class MediaError; +class MediaSource; +class PlayPromise; +class Promise; +class TextTrackList; +class AudioTrackList; +class VideoTrackList; + +enum class StreamCaptureType : uint8_t { CAPTURE_ALL_TRACKS, CAPTURE_AUDIO }; + +enum class StreamCaptureBehavior : uint8_t { + CONTINUE_WHEN_ENDED, + FINISH_WHEN_ENDED +}; + +class HTMLMediaElement : public nsGenericHTMLElement, + public MediaDecoderOwner, + public PrincipalChangeObserver<MediaStreamTrack>, + public SupportsWeakPtr, + public nsStubMutationObserver, + public TelemetryProbesReporterOwner { + public: + using TimeStamp = mozilla::TimeStamp; + using ImageContainer = mozilla::layers::ImageContainer; + using VideoFrameContainer = mozilla::VideoFrameContainer; + using MediaResource = mozilla::MediaResource; + using MediaDecoderOwner = mozilla::MediaDecoderOwner; + using MetadataTags = mozilla::MetadataTags; + + // Helper struct to keep track of the MediaStreams returned by + // mozCaptureStream(). For each OutputMediaStream, dom::MediaTracks get + // captured into MediaStreamTracks which get added to + // OutputMediaStream::mStream. + struct OutputMediaStream { + OutputMediaStream(RefPtr<DOMMediaStream> aStream, bool aCapturingAudioOnly, + bool aFinishWhenEnded); + ~OutputMediaStream(); + + RefPtr<DOMMediaStream> mStream; + nsTArray<RefPtr<MediaStreamTrack>> mLiveTracks; + const bool mCapturingAudioOnly; + const bool mFinishWhenEnded; + // If mFinishWhenEnded is true, this is the URI of the first resource + // mStream got tracks for. + nsCOMPtr<nsIURI> mFinishWhenEndedLoadingSrc; + // If mFinishWhenEnded is true, this is the first MediaStream mStream got + // tracks for. + RefPtr<DOMMediaStream> mFinishWhenEndedAttrStream; + // If mFinishWhenEnded is true, this is the MediaSource being played. + RefPtr<MediaSource> mFinishWhenEndedMediaSource; + }; + + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + CORSMode GetCORSMode() { return mCORSMode; } + + explicit HTMLMediaElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + void Init(); + + // `eMandatory`: `timeupdate` occurs according to the spec requirement. + // Eg. + // https://html.spec.whatwg.org/multipage/media.html#seeking:event-media-timeupdate + // `ePeriodic` : `timeupdate` occurs regularly and should follow the rule + // of not dispatching that event within 250 ms. Eg. + // https://html.spec.whatwg.org/multipage/media.html#offsets-into-the-media-resource:event-media-timeupdate + enum class TimeupdateType : bool { + eMandatory = false, + ePeriodic = true, + }; + + // This is used for event runner creation. Currently only timeupdate needs + // that, but it can be used to extend for other events in the future if + // necessary. + enum class EventFlag : uint8_t { + eNone = 0, + eMandatory = 1, + }; + + /** + * This is used when the browser is constructing a video element to play + * a channel that we've already started loading. The src attribute and + * <source> children are ignored. + * @param aChannel the channel to use + * @param aListener returns a stream listener that should receive + * notifications for the stream + */ + nsresult LoadWithChannel(nsIChannel* aChannel, nsIStreamListener** aListener); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLMediaElement, + nsGenericHTMLElement) + NS_IMPL_FROMNODE_HELPER(HTMLMediaElement, + IsAnyOfHTMLElements(nsGkAtoms::video, + nsGkAtoms::audio)) + + NS_DECL_ADDSIZEOFEXCLUDINGTHIS + + // EventTarget + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + + void NodeInfoChanged(Document* aOldDoc) override; + + virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + virtual void UnbindFromTree(bool aNullParent = true) override; + virtual void DoneCreatingElement() override; + + virtual bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + virtual int32_t TabIndexDefault() override; + + // Called by the video decoder object, on the main thread, + // when it has read the metadata containing video dimensions, + // etc. + virtual void MetadataLoaded(const MediaInfo* aInfo, + UniquePtr<const MetadataTags> aTags) final; + + // Called by the decoder object, on the main thread, + // when it has read the first frame of the video or audio. + void FirstFrameLoaded() final; + + // Called by the video decoder object, on the main thread, + // when the resource has a network error during loading. + void NetworkError(const MediaResult& aError) final; + + // Called by the video decoder object, on the main thread, when the + // resource has a decode error during metadata loading or decoding. + void DecodeError(const MediaResult& aError) final; + + // Called by the decoder object, on the main thread, when the + // resource has a decode issue during metadata loading or decoding, but can + // continue decoding. + void DecodeWarning(const MediaResult& aError) final; + + // Return true if error attribute is not null. + bool HasError() const final; + + // Called by the video decoder object, on the main thread, when the + // resource load has been cancelled. + void LoadAborted() final; + + // Called by the video decoder object, on the main thread, + // when the video playback has ended. + void PlaybackEnded() final; + + // Called by the video decoder object, on the main thread, + // when the resource has started seeking. + void SeekStarted() final; + + // Called by the video decoder object, on the main thread, + // when the resource has completed seeking. + void SeekCompleted() final; + + // Called by the video decoder object, on the main thread, + // when the resource has aborted seeking. + void SeekAborted() final; + + // Called by the media stream, on the main thread, when the download + // has been suspended by the cache or because the element itself + // asked the decoder to suspend the download. + void DownloadSuspended() final; + + // Called by the media stream, on the main thread, when the download + // has been resumed by the cache or because the element itself + // asked the decoder to resumed the download. + void DownloadResumed(); + + // Called to indicate the download is progressing. + void DownloadProgressed() final; + + // Called by the media decoder to indicate whether the media cache has + // suspended the channel. + void NotifySuspendedByCache(bool aSuspendedByCache) final; + + // Return true if the media element is actually invisible to users. + bool IsActuallyInvisible() const override; + + // Return true if the element is in the view port. + bool IsInViewPort() const; + + // Called by the media decoder and the video frame to get the + // ImageContainer containing the video data. + VideoFrameContainer* GetVideoFrameContainer() final; + layers::ImageContainer* GetImageContainer(); + + /** + * Call this to reevaluate whether we should start/stop due to our owner + * document being active, inactive, visible or hidden. + */ + void NotifyOwnerDocumentActivityChanged(); + + // Called when the media element enters or leaves the fullscreen. + void NotifyFullScreenChanged(); + + bool IsInFullScreen() const; + + // From PrincipalChangeObserver<MediaStreamTrack>. + void PrincipalChanged(MediaStreamTrack* aTrack) override; + + void UpdateSrcStreamVideoPrincipal(const PrincipalHandle& aPrincipalHandle); + + // Called after the MediaStream we're playing rendered a frame to aContainer + // with a different principalHandle than the previous frame. + void PrincipalHandleChangedForVideoFrameContainer( + VideoFrameContainer* aContainer, + const PrincipalHandle& aNewPrincipalHandle) override; + + // Dispatch events + void DispatchAsyncEvent(const nsAString& aName) final; + void DispatchAsyncEvent(RefPtr<nsMediaEventRunner> aRunner); + + // Triggers a recomputation of readyState. + void UpdateReadyState() override { + mWatchManager.ManualNotify(&HTMLMediaElement::UpdateReadyStateInternal); + } + + // Dispatch events that were raised while in the bfcache + nsresult DispatchPendingMediaEvents(); + + // Return true if we can activate autoplay assuming enough data has arrived. + // https://html.spec.whatwg.org/multipage/media.html#eligible-for-autoplay + bool IsEligibleForAutoplay(); + + // Notify that state has changed that might cause an autoplay element to + // start playing. + // If the element is 'autoplay' and is ready to play back (not paused, + // autoplay pref enabled, etc), it should start playing back. + void CheckAutoplayDataReady(); + + void RunAutoplay(); + + // Check if the media element had crossorigin set when loading started + bool ShouldCheckAllowOrigin(); + + // Returns true if the currently loaded resource is CORS same-origin with + // respect to the document. + bool IsCORSSameOrigin(); + + // Is the media element potentially playing as defined by the HTML 5 + // specification. + // http://www.whatwg.org/specs/web-apps/current-work/#potentially-playing + bool IsPotentiallyPlaying() const; + + // Has playback ended as defined by the HTML 5 specification. + // http://www.whatwg.org/specs/web-apps/current-work/#ended + bool IsPlaybackEnded() const; + + // principal of the currently playing resource. Anything accessing the + // contents of this element must have a principal that subsumes this + // principal. Returns null if nothing is playing. + already_AddRefed<nsIPrincipal> GetCurrentPrincipal(); + + // Return true if the loading of this resource required cross-origin + // redirects. + bool HadCrossOriginRedirects(); + + bool ShouldResistFingerprinting(RFPTarget aTarget) const override; + + // Principal of the currently playing video resource. Anything accessing the + // image container of this element must have a principal that subsumes this + // principal. If there are no live video tracks but content has been rendered + // to the image container, we return the last video principal we had. Should + // the image container be empty with no live video tracks, we return nullptr. + already_AddRefed<nsIPrincipal> GetCurrentVideoPrincipal(); + + // called to notify that the principal of the decoder's media resource has + // changed. + void NotifyDecoderPrincipalChanged() final; + + void GetEMEInfo(dom::EMEDebugInfo& aInfo); + + // Update the visual size of the media. Called from the decoder on the + // main thread when/if the size changes. + virtual void UpdateMediaSize(const nsIntSize& aSize); + + void Invalidate(ImageSizeChanged aImageSizeChanged, + const Maybe<nsIntSize>& aNewIntrinsicSize, + ForceInvalidate aForceInvalidate) override; + + // Returns the CanPlayStatus indicating if we can handle the + // full MIME type including the optional codecs parameter. + static CanPlayStatus GetCanPlay(const nsAString& aType, + DecoderDoctorDiagnostics* aDiagnostics); + + /** + * Called when a child source element is added to this media element. This + * may queue a task to run the select resource algorithm if appropriate. + */ + void NotifyAddedSource(); + + /** + * Called when there's been an error fetching the resource. This decides + * whether it's appropriate to fire an error event. + */ + void NotifyLoadError(const nsACString& aErrorDetails = nsCString()); + + /** + * Called by one of our associated MediaTrackLists (audio/video) when a + * MediaTrack is added. + */ + void NotifyMediaTrackAdded(dom::MediaTrack* aTrack); + + /** + * Called by one of our associated MediaTrackLists (audio/video) when a + * MediaTrack is removed. + */ + void NotifyMediaTrackRemoved(dom::MediaTrack* aTrack); + + /** + * Called by one of our associated MediaTrackLists (audio/video) when an + * AudioTrack is enabled or a VideoTrack is selected. + */ + void NotifyMediaTrackEnabled(dom::MediaTrack* aTrack); + + /** + * Called by one of our associated MediaTrackLists (audio/video) when an + * AudioTrack is disabled or a VideoTrack is unselected. + */ + void NotifyMediaTrackDisabled(dom::MediaTrack* aTrack); + + /** + * Returns the current load ID. Asynchronous events store the ID that was + * current when they were enqueued, and if it has changed when they come to + * fire, they consider themselves cancelled, and don't fire. + */ + uint32_t GetCurrentLoadID() const { return mCurrentLoadID; } + + /** + * Returns the load group for this media element's owner document. + * XXX XBL2 issue. + */ + already_AddRefed<nsILoadGroup> GetDocumentLoadGroup(); + + /** + * Returns true if the media has played or completed a seek. + * Used by video frame to determine whether to paint the poster. + */ + bool GetPlayedOrSeeked() const { return mHasPlayedOrSeeked; } + + nsresult CopyInnerTo(Element* aDest); + + /** + * Sets the Accept header on the HTTP channel to the required + * video or audio MIME types. + */ + virtual nsresult SetAcceptHeader(nsIHttpChannel* aChannel) = 0; + + /** + * Sets the required request headers on the HTTP channel for + * video or audio requests. + */ + void SetRequestHeaders(nsIHttpChannel* aChannel); + + /** + * Asynchronously awaits a stable state, whereupon aRunnable runs on the main + * thread. This adds an event which run aRunnable to the appshell's list of + * sections synchronous the next time control returns to the event loop. + */ + void RunInStableState(nsIRunnable* aRunnable); + + /** + * Fires a timeupdate event. If aPeriodic is true, the event will only + * be fired if we've not fired a timeupdate event (for any reason) in the + * last 250ms, as required by the spec when the current time is periodically + * increasing during playback. + */ + void FireTimeUpdate(TimeupdateType aType); + + void MaybeQueueTimeupdateEvent() final { + FireTimeUpdate(TimeupdateType::ePeriodic); + } + + const TimeStamp& LastTimeupdateDispatchTime() const; + void UpdateLastTimeupdateDispatchTime(); + + // WebIDL + + MediaError* GetError() const; + + void GetSrc(nsAString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); } + void SetSrc(const nsAString& aSrc, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::src, aSrc, aError); + } + void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal, + ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aError); + } + + void GetCurrentSrc(nsAString& aCurrentSrc); + + void GetCrossOrigin(nsAString& aResult) { + // Null for both missing and invalid defaults is ok, since we + // always parse to an enum value, so we don't need an invalid + // default, and we _want_ the missing default to be null. + GetEnumAttr(nsGkAtoms::crossorigin, nullptr, aResult); + } + void SetCrossOrigin(const nsAString& aCrossOrigin, ErrorResult& aError) { + SetOrRemoveNullableStringAttr(nsGkAtoms::crossorigin, aCrossOrigin, aError); + } + + uint16_t NetworkState() const { return mNetworkState; } + + void NotifyXPCOMShutdown() final; + + // Called by media decoder when the audible state changed or when input is + // a media stream. + void SetAudibleState(bool aAudible) final; + + // Notify agent when the MediaElement changes its audible state. + void NotifyAudioPlaybackChanged(AudibleChangedReasons aReason); + + void GetPreload(nsAString& aValue) { + if (mSrcAttrStream) { + nsGkAtoms::none->ToString(aValue); + return; + } + GetEnumAttr(nsGkAtoms::preload, nullptr, aValue); + } + void SetPreload(const nsAString& aValue, ErrorResult& aRv) { + if (mSrcAttrStream) { + return; + } + SetHTMLAttr(nsGkAtoms::preload, aValue, aRv); + } + + already_AddRefed<TimeRanges> Buffered() const; + + void Load(); + + void CanPlayType(const nsAString& aType, nsAString& aResult); + + uint16_t ReadyState() const { return mReadyState; } + + bool Seeking() const; + + double CurrentTime() const; + + void SetCurrentTime(double aCurrentTime, ErrorResult& aRv); + void SetCurrentTime(double aCurrentTime) { + SetCurrentTime(aCurrentTime, IgnoreErrors()); + } + + void FastSeek(double aTime, ErrorResult& aRv); + + already_AddRefed<Promise> SeekToNextFrame(ErrorResult& aRv); + + double Duration() const; + + bool HasAudio() const { return mMediaInfo.HasAudio(); } + + virtual bool IsVideo() const { return false; } + + bool HasVideo() const { return mMediaInfo.HasVideo(); } + + bool IsEncrypted() const override { return mIsEncrypted; } + +#ifdef MOZ_WMF_CDM + bool IsUsingWMFCDM() const override; +#endif + + bool Paused() const { return mPaused; } + + double DefaultPlaybackRate() const { + if (mSrcAttrStream) { + return 1.0; + } + return mDefaultPlaybackRate; + } + + void SetDefaultPlaybackRate(double aDefaultPlaybackRate, ErrorResult& aRv); + + double PlaybackRate() const { + if (mSrcAttrStream) { + return 1.0; + } + return mPlaybackRate; + } + + void SetPlaybackRate(double aPlaybackRate, ErrorResult& aRv); + + already_AddRefed<TimeRanges> Played(); + + already_AddRefed<TimeRanges> Seekable() const; + + bool Ended(); + + bool Autoplay() const { return GetBoolAttr(nsGkAtoms::autoplay); } + + void SetAutoplay(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::autoplay, aValue, aRv); + } + + bool Loop() const { return GetBoolAttr(nsGkAtoms::loop); } + + void SetLoop(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::loop, aValue, aRv); + } + + already_AddRefed<Promise> Play(ErrorResult& aRv); + void Play() { + IgnoredErrorResult dummy; + RefPtr<Promise> toBeIgnored = Play(dummy); + } + + void Pause(ErrorResult& aRv); + void Pause() { Pause(IgnoreErrors()); } + + bool Controls() const { return GetBoolAttr(nsGkAtoms::controls); } + + void SetControls(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::controls, aValue, aRv); + } + + double Volume() const { return mVolume; } + + void SetVolume(double aVolume, ErrorResult& aRv); + + bool Muted() const { return mMuted & MUTED_BY_CONTENT; } + void SetMuted(bool aMuted); + + bool DefaultMuted() const { return GetBoolAttr(nsGkAtoms::muted); } + + void SetDefaultMuted(bool aMuted, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::muted, aMuted, aRv); + } + + bool MozAllowCasting() const { return mAllowCasting; } + + void SetMozAllowCasting(bool aShow) { mAllowCasting = aShow; } + + bool MozIsCasting() const { return mIsCasting; } + + void SetMozIsCasting(bool aShow) { mIsCasting = aShow; } + + // Returns whether a call to Play() would be rejected with NotAllowedError. + // This assumes "worst case" for unknowns. So if prompting for permission is + // enabled and no permission is stored, this behaves as if the user would + // opt to block. + bool AllowedToPlay() const; + + already_AddRefed<MediaSource> GetMozMediaSourceObject() const; + + // Returns a promise which will be resolved after collecting debugging + // data from decoder/reader/MDSM. Used for debugging purposes. + already_AddRefed<Promise> MozRequestDebugInfo(ErrorResult& aRv); + + // Enables DecoderDoctorLogger logging. Used for debugging purposes. + static void MozEnableDebugLog(const GlobalObject&); + + // Returns a promise which will be resolved after collecting debugging + // log associated with this element. Used for debugging purposes. + already_AddRefed<Promise> MozRequestDebugLog(ErrorResult& aRv); + + // For use by mochitests. Enabling pref "media.test.video-suspend" + void SetVisible(bool aVisible); + + // For use by mochitests. Enabling pref "media.test.video-suspend" + bool HasSuspendTaint() const; + + // For use by mochitests. + bool IsVideoDecodingSuspended() const; + + // These functions return accumulated time, which are used for the telemetry + // usage. Return -1 for error. + double TotalVideoPlayTime() const; + double TotalVideoHDRPlayTime() const; + double VisiblePlayTime() const; + double InvisiblePlayTime() const; + double VideoDecodeSuspendedTime() const; + double TotalAudioPlayTime() const; + double AudiblePlayTime() const; + double InaudiblePlayTime() const; + double MutedPlayTime() const; + + // Test methods for decoder doctor. + void SetFormatDiagnosticsReportForMimeType(const nsAString& aMimeType, + DecoderDoctorReportType aType); + void SetDecodeError(const nsAString& aError, ErrorResult& aRv); + void SetAudioSinkFailedStartup(); + + // Synchronously, return the next video frame and mark the element unable to + // participate in decode suspending. + // + // This function is synchronous for cases where decoding has been suspended + // and JS needs a frame to use in, eg., nsLayoutUtils::SurfaceFromElement() + // via drawImage(). + already_AddRefed<layers::Image> GetCurrentImage(); + + already_AddRefed<DOMMediaStream> GetSrcObject() const; + void SetSrcObject(DOMMediaStream& aValue); + void SetSrcObject(DOMMediaStream* aValue); + + bool PreservesPitch() const { return mPreservesPitch; } + void SetPreservesPitch(bool aPreservesPitch); + + MediaKeys* GetMediaKeys() const; + + already_AddRefed<Promise> SetMediaKeys(MediaKeys* mediaKeys, + ErrorResult& aRv); + + mozilla::dom::EventHandlerNonNull* GetOnencrypted(); + void SetOnencrypted(mozilla::dom::EventHandlerNonNull* aCallback); + + mozilla::dom::EventHandlerNonNull* GetOnwaitingforkey(); + void SetOnwaitingforkey(mozilla::dom::EventHandlerNonNull* aCallback); + + void DispatchEncrypted(const nsTArray<uint8_t>& aInitData, + const nsAString& aInitDataType) override; + + bool IsEventAttributeNameInternal(nsAtom* aName) override; + + bool ContainsRestrictedContent() const; + + void NotifyWaitingForKey() override; + + already_AddRefed<DOMMediaStream> CaptureAudio(ErrorResult& aRv, + MediaTrackGraph* aGraph); + + already_AddRefed<DOMMediaStream> MozCaptureStream(ErrorResult& aRv); + + already_AddRefed<DOMMediaStream> MozCaptureStreamUntilEnded(ErrorResult& aRv); + + bool MozAudioCaptured() const { return mAudioCaptured; } + + void MozGetMetadata(JSContext* aCx, JS::MutableHandle<JSObject*> aResult, + ErrorResult& aRv); + + double MozFragmentEnd(); + + AudioTrackList* AudioTracks(); + + VideoTrackList* VideoTracks(); + + TextTrackList* GetTextTracks(); + + already_AddRefed<TextTrack> AddTextTrack(TextTrackKind aKind, + const nsAString& aLabel, + const nsAString& aLanguage); + + void AddTextTrack(TextTrack* aTextTrack) { + GetOrCreateTextTrackManager()->AddTextTrack(aTextTrack); + } + + void RemoveTextTrack(TextTrack* aTextTrack, bool aPendingListOnly = false) { + if (mTextTrackManager) { + mTextTrackManager->RemoveTextTrack(aTextTrack, aPendingListOnly); + } + } + + void NotifyCueAdded(TextTrackCue& aCue) { + if (mTextTrackManager) { + mTextTrackManager->NotifyCueAdded(aCue); + } + } + void NotifyCueRemoved(TextTrackCue& aCue) { + if (mTextTrackManager) { + mTextTrackManager->NotifyCueRemoved(aCue); + } + } + void NotifyCueUpdated(TextTrackCue* aCue) { + if (mTextTrackManager) { + mTextTrackManager->NotifyCueUpdated(aCue); + } + } + + void NotifyCueDisplayStatesChanged(); + + bool IsBlessed() const { return mIsBlessed; } + + // A method to check whether we are currently playing. + bool IsCurrentlyPlaying() const; + + // Returns true if the media element is being destroyed. Used in + // dormancy checks to prevent dormant processing for an element + // that will soon be gone. + bool IsBeingDestroyed(); + + virtual void OnVisibilityChange(Visibility aNewVisibility); + + // Begin testing only methods + float ComputedVolume() const; + bool ComputedMuted() const; + + // Return true if the media has been suspended media due to an inactive + // document or prohibiting by the docshell. + bool IsSuspendedByInactiveDocOrDocShell() const; + // End testing only methods + + void SetMediaInfo(const MediaInfo& aInfo); + MediaInfo GetMediaInfo() const override; + + // Gives access to the decoder's frame statistics, if present. + FrameStatistics* GetFrameStatistics() const override; + + void DispatchAsyncTestingEvent(const nsAString& aName) override; + + AbstractThread* AbstractMainThread() const final; + + // Log the usage of a {visible / invisible} video element as + // the source of {drawImage(), createPattern(), createImageBitmap() and + // captureStream()} APIs. This function can be used to collect telemetries for + // bug 1352007. + enum class CallerAPI { + DRAW_IMAGE, + CREATE_PATTERN, + CREATE_IMAGEBITMAP, + CAPTURE_STREAM, + CREATE_VIDEOFRAME, + }; + void LogVisibility(CallerAPI aAPI); + + Document* GetDocument() const override; + + already_AddRefed<GMPCrashHelper> CreateGMPCrashHelper() override; + + // Set the sink id (of the output device) that the audio will play. If aSinkId + // is empty the default device will be set. + already_AddRefed<Promise> SetSinkId(const nsAString& aSinkId, + ErrorResult& aRv); + // Get the sink id of the device that audio is being played. Initial value is + // empty and the default device is being used. + void GetSinkId(nsString& aSinkId) const { + MOZ_ASSERT(NS_IsMainThread()); + aSinkId = mSink.first; + } + + // This is used to notify MediaElementAudioSourceNode that media element is + // allowed to play when media element is used as a source for web audio, so + // that we can start AudioContext if it was not allowed to start. + RefPtr<GenericNonExclusivePromise> GetAllowedToPlayPromise(); + + bool GetShowPosterFlag() const { return mShowPoster; } + + bool IsAudible() const; + + // Return key system in use if we have one, otherwise return nothing. + Maybe<nsAutoString> GetKeySystem() const override; + + protected: + virtual ~HTMLMediaElement(); + + class AudioChannelAgentCallback; + class ChannelLoader; + class ErrorSink; + class MediaElementTrackSource; + class MediaLoadListener; + class MediaStreamRenderer; + class MediaStreamTrackListener; + class ShutdownObserver; + class TitleChangeObserver; + class MediaControlKeyListener; + + MediaDecoderOwner::NextFrameStatus NextFrameStatus(); + + void SetDecoder(MediaDecoder* aDecoder); + + void PlayInternal(bool aHandlingUserInput); + + // See spec, https://html.spec.whatwg.org/#internal-pause-steps + void PauseInternal(); + + /** Use this method to change the mReadyState member, so required + * events can be fired. + */ + void ChangeReadyState(nsMediaReadyState aState); + + /** + * Use this method to change the mNetworkState member, so required + * actions will be taken during the transition. + */ + void ChangeNetworkState(nsMediaNetworkState aState); + + /** + * The MediaElement will be responsible for creating and releasing the audio + * wakelock depending on the playing and audible state. + */ + virtual void WakeLockRelease(); + virtual void UpdateWakeLock(); + + void CreateAudioWakeLockIfNeeded(); + void ReleaseAudioWakeLockIfExists(); + RefPtr<WakeLock> mWakeLock; + + /** + * Logs a warning message to the web console to report various failures. + * aMsg is the localized message identifier, aParams is the parameters to + * be substituted into the localized message, and aParamCount is the number + * of parameters in aParams. + */ + void ReportLoadError(const char* aMsg, const nsTArray<nsString>& aParams = + nsTArray<nsString>()); + + /** + * Log message to web console. + */ + void ReportToConsole( + uint32_t aErrorFlags, const char* aMsg, + const nsTArray<nsString>& aParams = nsTArray<nsString>()) const; + + /** + * Changes mHasPlayedOrSeeked to aValue. If mHasPlayedOrSeeked changes + * we'll force a reflow so that the video frame gets reflowed to reflect + * the poster hiding or showing immediately. + */ + void SetPlayedOrSeeked(bool aValue); + + /** + * Initialize the media element for playback of aStream + */ + void SetupSrcMediaStreamPlayback(DOMMediaStream* aStream); + /** + * Stop playback on mSrcStream. + */ + void EndSrcMediaStreamPlayback(); + /** + * Ensure we're playing mSrcStream if and only if we're not paused. + */ + enum { REMOVING_SRC_STREAM = 0x1 }; + void UpdateSrcMediaStreamPlaying(uint32_t aFlags = 0); + + /** + * Ensure currentTime progresses if and only if we're potentially playing + * mSrcStream. Called by the watch manager while we're playing mSrcStream, and + * one of the inputs to the potentially playing algorithm changes. + */ + void UpdateSrcStreamPotentiallyPlaying(); + + /** + * mSrcStream's graph's CurrentTime() has been updated. It might be time to + * fire "timeupdate". + */ + void UpdateSrcStreamTime(); + + /** + * Called after a tail dispatch when playback of mSrcStream ended, to comply + * with the spec where we must start reporting true for the ended attribute + * after the event loop returns to step 1. A MediaStream could otherwise be + * manipulated to end a HTMLMediaElement synchronously. + */ + void UpdateSrcStreamReportPlaybackEnded(); + + /** + * Called by our DOMMediaStream::TrackListener when a new MediaStreamTrack has + * been added to the playback stream of |mSrcStream|. + */ + void NotifyMediaStreamTrackAdded(const RefPtr<MediaStreamTrack>& aTrack); + + /** + * Called by our DOMMediaStream::TrackListener when a MediaStreamTrack in + * |mSrcStream|'s playback stream has ended. + */ + void NotifyMediaStreamTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack); + + /** + * Convenience method to get in a single list all enabled AudioTracks and, if + * this is a video element, the selected VideoTrack. + */ + void GetAllEnabledMediaTracks(nsTArray<RefPtr<MediaTrack>>& aTracks); + + /** + * Enables or disables all tracks forwarded from mSrcStream to all + * OutputMediaStreams. We do this for muting the tracks when pausing, + * and unmuting when playing the media element again. + */ + void SetCapturedOutputStreamsEnabled(bool aEnabled); + + /** + * Returns true if output tracks should be muted, based on the state of this + * media element. + */ + enum class OutputMuteState { Muted, Unmuted }; + OutputMuteState OutputTracksMuted(); + + /** + * Sets the muted state of all output track sources. They are muted when we're + * paused and unmuted otherwise. + */ + void UpdateOutputTracksMuting(); + + /** + * Create a new MediaStreamTrack for the TrackSource corresponding to aTrack + * and add it to the DOMMediaStream in aOutputStream. This automatically sets + * the output track to enabled or disabled depending on our current playing + * state. + */ + enum class AddTrackMode { ASYNC, SYNC }; + void AddOutputTrackSourceToOutputStream( + MediaElementTrackSource* aSource, OutputMediaStream& aOutputStream, + AddTrackMode aMode = AddTrackMode::ASYNC); + + /** + * Creates output track sources when this media element is captured, tracks + * exist, playback is not ended and readyState is >= HAVE_METADATA. + */ + void UpdateOutputTrackSources(); + + /** + * Returns an DOMMediaStream containing the played contents of this + * element. When aBehavior is FINISH_WHEN_ENDED, when this element ends + * playback we will finish the stream and not play any more into it. When + * aType is CONTINUE_WHEN_ENDED, ending playback does not finish the stream. + * The stream will never finish. + * + * When aType is CAPTURE_AUDIO, we stop playout of audio and instead route it + * to the DOMMediaStream. Volume and mute state will be applied to the audio + * reaching the stream. No video tracks will be captured in this case. + * + * aGraph may be null if the stream's tracks do not need to use a + * specific graph. + */ + already_AddRefed<DOMMediaStream> CaptureStreamInternal( + StreamCaptureBehavior aFinishBehavior, + StreamCaptureType aStreamCaptureType, MediaTrackGraph* aGraph); + + /** + * Initialize a decoder as a clone of an existing decoder in another + * element. + * mLoadingSrc must already be set. + */ + nsresult InitializeDecoderAsClone(ChannelMediaDecoder* aOriginal); + + /** + * Call Load() and FinishDecoderSetup() on the decoder. It also handle + * resource cloning if DecoderType is ChannelMediaDecoder. + */ + template <typename DecoderType, typename... LoadArgs> + nsresult SetupDecoder(DecoderType* aDecoder, LoadArgs&&... aArgs); + + /** + * Initialize a decoder to load the given channel. The decoder's stream + * listener is returned via aListener. + * mLoadingSrc must already be set. + */ + nsresult InitializeDecoderForChannel(nsIChannel* aChannel, + nsIStreamListener** aListener); + + /** + * Finish setting up the decoder after Load() has been called on it. + * Called by InitializeDecoderForChannel/InitializeDecoderAsClone. + */ + nsresult FinishDecoderSetup(MediaDecoder* aDecoder); + + /** + * Call this after setting up mLoadingSrc and mDecoder. + */ + void AddMediaElementToURITable(); + /** + * Call this before modifying mLoadingSrc. + */ + void RemoveMediaElementFromURITable(); + /** + * Call this to find a media element with the same NodePrincipal and + * mLoadingSrc set to aURI, and with a decoder on which Load() has been + * called. + */ + HTMLMediaElement* LookupMediaElementURITable(nsIURI* aURI); + + /** + * Shutdown and clear mDecoder and maintain associated invariants. + */ + void ShutdownDecoder(); + /** + * Execute the initial steps of the load algorithm that ensure existing + * loads are aborted, the element is emptied, and a new load ID is + * created. + */ + void AbortExistingLoads(); + + /** + * This is the dedicated media source failure steps. + * Called when all potential resources are exhausted. Changes network + * state to NETWORK_NO_SOURCE, and sends error event with code + * MEDIA_ERR_SRC_NOT_SUPPORTED. + */ + void NoSupportedMediaSourceError( + const nsACString& aErrorDetails = nsCString()); + + /** + * Per spec, Failed with elements: Queue a task, using the DOM manipulation + * task source, to fire a simple event named error at the candidate element. + * So dispatch |QueueLoadFromSourceTask| to main thread to make sure the task + * will be executed later than loadstart event. + */ + void DealWithFailedElement(nsIContent* aSourceElement); + + /** + * Attempts to load resources from the <source> children. This is a + * substep of the resource selection algorithm. Do not call this directly, + * call QueueLoadFromSourceTask() instead. + */ + void LoadFromSourceChildren(); + + /** + * Asynchronously awaits a stable state, and then causes + * LoadFromSourceChildren() to be called on the main threads' event loop. + */ + void QueueLoadFromSourceTask(); + + /** + * Runs the media resource selection algorithm. + */ + void SelectResource(); + + /** + * A wrapper function that allows us to cleanly reset flags after a call + * to SelectResource() + */ + void SelectResourceWrapper(); + + /** + * Asynchronously awaits a stable state, and then causes SelectResource() + * to be run on the main thread's event loop. + */ + void QueueSelectResourceTask(); + + /** + * When loading a new source on an existing media element, make sure to reset + * everything that is accessible using the media element API. + */ + void ResetState(); + + /** + * The resource-fetch algorithm step of the load algorithm. + */ + MediaResult LoadResource(); + + /** + * Selects the next <source> child from which to load a resource. Called + * during the resource selection algorithm. Stores the return value in + * mSourceLoadCandidate before returning. + */ + HTMLSourceElement* GetNextSource(); + + /** + * Changes mDelayingLoadEvent, and will call BlockOnLoad()/UnblockOnLoad() + * on the owning document, so it can delay the load event firing. + */ + void ChangeDelayLoadStatus(bool aDelay); + + /** + * If we suspended downloading after the first frame, unsuspend now. + */ + void StopSuspendingAfterFirstFrame(); + + /** + * Called when our channel is redirected to another channel. + * Updates our mChannel reference to aNewChannel. + */ + nsresult OnChannelRedirect(nsIChannel* aChannel, nsIChannel* aNewChannel, + uint32_t aFlags); + + /** + * Call this to reevaluate whether we should be holding a self-reference. + */ + void AddRemoveSelfReference(); + + /** + * Called when "xpcom-shutdown" event is received. + */ + void NotifyShutdownEvent(); + + /** + * Possible values of the 'preload' attribute. + */ + enum PreloadAttrValue : uint8_t { + PRELOAD_ATTR_EMPTY, // set to "" + PRELOAD_ATTR_NONE, // set to "none" + PRELOAD_ATTR_METADATA, // set to "metadata" + PRELOAD_ATTR_AUTO // set to "auto" + }; + + /** + * The preloading action to perform. These dictate how we react to the + * preload attribute. See mPreloadAction. + */ + enum PreloadAction { + PRELOAD_UNDEFINED = 0, // not determined - used only for initialization + PRELOAD_NONE = 1, // do not preload + PRELOAD_METADATA = 2, // preload only the metadata (and first frame) + PRELOAD_ENOUGH = 3 // preload enough data to allow uninterrupted + // playback + }; + + /** + * The guts of Load(). Load() acts as a wrapper around this which sets + * mIsDoingExplicitLoad to true so that when script calls 'load()' + * preload-none will be automatically upgraded to preload-metadata. + */ + void DoLoad(); + + /** + * Suspends the load of mLoadingSrc, so that it can be resumed later + * by ResumeLoad(). This is called when we have a media with a 'preload' + * attribute value of 'none', during the resource selection algorithm. + */ + void SuspendLoad(); + + /** + * Resumes a previously suspended load (suspended by SuspendLoad(uri)). + * Will continue running the resource selection algorithm. + * Sets mPreloadAction to aAction. + */ + void ResumeLoad(PreloadAction aAction); + + /** + * Handle a change to the preload attribute. Should be called whenever the + * value (or presence) of the preload attribute changes. The change in + * attribute value may cause a change in the mPreloadAction of this + * element. If there is a change then this method will initiate any + * behaviour that is necessary to implement the action. + */ + void UpdatePreloadAction(); + + /** + * Fire progress events if needed according to the time and byte constraints + * outlined in the specification. aHaveNewProgress is true if progress has + * just been detected. Otherwise the method is called as a result of the + * progress timer. + */ + void CheckProgress(bool aHaveNewProgress); + static void ProgressTimerCallback(nsITimer* aTimer, void* aClosure); + /** + * Start timer to update download progress. + */ + void StartProgressTimer(); + /** + * Start sending progress and/or stalled events. + */ + void StartProgress(); + /** + * Stop progress information timer and events. + */ + void StopProgress(); + + /** + * Dispatches an error event to a child source element. + */ + void DispatchAsyncSourceError(nsIContent* aSourceElement); + + /** + * Resets the media element for an error condition as per aErrorCode. + * aErrorCode must be one of WebIDL HTMLMediaElement error codes. + */ + void Error(uint16_t aErrorCode, + const nsACString& aErrorDetails = nsCString()); + + /** + * Returns the URL spec of the currentSrc. + **/ + void GetCurrentSpec(nsCString& aString); + + /** + * Process any media fragment entries in the URI + */ + void ProcessMediaFragmentURI(); + + /** + * Mute or unmute the audio and change the value that the |muted| map. + */ + void SetMutedInternal(uint32_t aMuted); + /** + * Update the volume of the output audio stream to match the element's + * current mMuted/mVolume/mAudioChannelFaded state. + */ + void SetVolumeInternal(); + + /** + * Suspend or resume element playback and resource download. When we suspend + * playback, event delivery would also be suspended (and events queued) until + * the element is resumed. + */ + void SuspendOrResumeElement(bool aSuspendElement); + + // Get the HTMLMediaElement object if the decoder is being used from an + // HTML media element, and null otherwise. + HTMLMediaElement* GetMediaElement() final { return this; } + + // Return true if decoding should be paused + bool GetPaused() final { return Paused(); } + + // Seeks to aTime seconds. aSeekType can be Exact to seek to exactly the + // seek target, or PrevSyncPoint if a quicker but less precise seek is + // desired, and we'll seek to the sync point (keyframe and/or start of the + // next block of audio samples) preceeding seek target. + void Seek(double aTime, SeekTarget::Type aSeekType, ErrorResult& aRv); + + // Update the audio channel playing state + void UpdateAudioChannelPlayingState(); + + // Adds to the element's list of pending text tracks each text track + // in the element's list of text tracks whose text track mode is not disabled + // and whose text track readiness state is loading. + void PopulatePendingTextTrackList(); + + // Gets a reference to the MediaElement's TextTrackManager. If the + // MediaElement doesn't yet have one then it will create it. + TextTrackManager* GetOrCreateTextTrackManager(); + + // Recomputes ready state and fires events as necessary based on current + // state. + void UpdateReadyStateInternal(); + + // Create or destroy the captured stream. + void AudioCaptureTrackChange(bool aCapture); + + // If the network state is empty and then we would trigger DoLoad(). + void MaybeDoLoad(); + + // Anything we need to check after played success and not related with spec. + void UpdateCustomPolicyAfterPlayed(); + + // Returns a StreamCaptureType populated with the right bits, depending on the + // tracks this HTMLMediaElement has. + StreamCaptureType CaptureTypeForElement(); + + // True if this element can be captured, false otherwise. + bool CanBeCaptured(StreamCaptureType aCaptureType); + + using nsGenericHTMLElement::DispatchEvent; + // For nsAsyncEventRunner. + nsresult DispatchEvent(const nsAString& aName); + + already_AddRefed<nsMediaEventRunner> GetEventRunner( + const nsAString& aName, EventFlag aFlag = EventFlag::eNone); + + // This method moves the mPendingPlayPromises into a temperate object. So the + // mPendingPlayPromises is cleared after this method call. + nsTArray<RefPtr<PlayPromise>> TakePendingPlayPromises(); + + // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises() + // and queues a task to resolve them. + void AsyncResolvePendingPlayPromises(); + + // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises() + // and queues a task to reject them. + void AsyncRejectPendingPlayPromises(nsresult aError); + + // This method snapshots the mPendingPlayPromises by TakePendingPlayPromises() + // and queues a task to resolve them also to dispatch a "playing" event. + void NotifyAboutPlaying(); + + already_AddRefed<Promise> CreateDOMPromise(ErrorResult& aRv) const; + + // Pass information for deciding the video decode mode to decoder. + void NotifyDecoderActivityChanges() const; + + // Constructs an AudioTrack in mAudioTrackList if aInfo reports that audio is + // available, and a VideoTrack in mVideoTrackList if aInfo reports that video + // is available. + void ConstructMediaTracks(const MediaInfo* aInfo); + + // Removes all MediaTracks from mAudioTrackList and mVideoTrackList and fires + // "removetrack" on the lists accordingly. + // Note that by spec, this should not fire "removetrack". However, it appears + // other user agents do, per + // https://wpt.fyi/results/media-source/mediasource-avtracks.html. + void RemoveMediaTracks(); + + // Mark the decoder owned by the element as tainted so that the + // suspend-video-decoder is disabled. + void MarkAsTainted(); + + virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + virtual void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) override; + + bool DetachExistingMediaKeys(); + bool TryRemoveMediaKeysAssociation(); + void RemoveMediaKeys(); + bool AttachNewMediaKeys(); + bool TryMakeAssociationWithCDM(CDMProxy* aProxy); + void MakeAssociationWithCDMResolved(); + void SetCDMProxyFailure(const MediaResult& aResult); + void ResetSetMediaKeysTempVariables(); + + void PauseIfShouldNotBePlaying(); + + WatchManager<HTMLMediaElement> mWatchManager; + + // When the play is not allowed, dispatch related events which are used for + // testing or changing control UI. + void DispatchEventsWhenPlayWasNotAllowed(); + + // When the doc is blocked permanantly, we would dispatch event to notify + // front-end side to show blocking icon. + void MaybeNotifyAutoplayBlocked(); + + // Dispatch event for video control when video gets blocked in order to show + // the click-to-play icon. + void DispatchBlockEventForVideoControl(); + + // When playing state change, we have to notify MediaControl in the chrome + // process in order to keep its playing state correct. + void NotifyMediaControlPlaybackStateChanged(); + + // Clear the timer when we want to continue listening to the media control + // key events. + void ClearStopMediaControlTimerIfNeeded(); + + // Sets a secondary renderer for mSrcStream, so this media element can be + // rendered in Picture-in-Picture mode when playing a MediaStream. A null + // aContainer will unset the secondary renderer. aFirstFrameOutput allows + // for injecting a listener of the callers choice for rendering the first + // frame. + void SetSecondaryMediaStreamRenderer( + VideoFrameContainer* aContainer, + FirstFrameVideoOutput* aFirstFrameOutput = nullptr); + + // This function is used to update the status of media control when the media + // changes its status of being used in the Picture-in-Picture mode. + void UpdateMediaControlAfterPictureInPictureModeChanged(); + + // The current decoder. Load() has been called on this decoder. + // At most one of mDecoder and mSrcStream can be non-null. + RefPtr<MediaDecoder> mDecoder; + + // A reference to the VideoFrameContainer which contains the current frame + // of video to display. + RefPtr<VideoFrameContainer> mVideoFrameContainer; + + // Holds a reference to the MediaStream that has been set in the src + // attribute. + RefPtr<DOMMediaStream> mSrcAttrStream; + + // Holds the triggering principal for the src attribute. + nsCOMPtr<nsIPrincipal> mSrcAttrTriggeringPrincipal; + + // Holds a reference to the MediaStream that we're actually playing. + // At most one of mDecoder and mSrcStream can be non-null. + RefPtr<DOMMediaStream> mSrcStream; + + // The MediaStreamRenderer handles rendering of our selected video track, and + // enabled audio tracks, while mSrcStream is set. + RefPtr<MediaStreamRenderer> mMediaStreamRenderer; + + // The secondary MediaStreamRenderer handles rendering of our selected video + // track to a secondary VideoFrameContainer, while mSrcStream is set. + RefPtr<MediaStreamRenderer> mSecondaryMediaStreamRenderer; + + // True once PlaybackEnded() is called and we're playing a MediaStream. + // Reset to false if we start playing mSrcStream again. + Watchable<bool> mSrcStreamPlaybackEnded = { + false, "HTMLMediaElement::mSrcStreamPlaybackEnded"}; + + // Mirrors mSrcStreamPlaybackEnded after a tail dispatch when set to true, + // but may be be forced to false directly. To accomodate when an application + // ends playback synchronously by manipulating mSrcStream or its tracks, + // e.g., through MediaStream.removeTrack(), or MediaStreamTrack.stop(). + bool mSrcStreamReportPlaybackEnded = false; + + // Holds a reference to the stream connecting this stream to the window + // capture sink. + UniquePtr<MediaStreamWindowCapturer> mStreamWindowCapturer; + + // Holds references to the DOM wrappers for the MediaStreams that we're + // writing to. + nsTArray<OutputMediaStream> mOutputStreams; + + // Mapping for output tracks, from dom::MediaTrack ids to the + // MediaElementTrackSource that represents the source of all corresponding + // MediaStreamTracks captured from this element. + nsRefPtrHashtable<nsStringHashKey, MediaElementTrackSource> + mOutputTrackSources; + + // The currently selected video stream track. + RefPtr<VideoStreamTrack> mSelectedVideoStreamTrack; + + const RefPtr<ShutdownObserver> mShutdownObserver; + + const RefPtr<TitleChangeObserver> mTitleChangeObserver; + + // Holds a reference to the MediaSource, if any, referenced by the src + // attribute on the media element. + RefPtr<MediaSource> mSrcMediaSource; + + // Holds a reference to the MediaSource supplying data for playback. This + // may either match mSrcMediaSource or come from Source element children. + // This is set when and only when mLoadingSrc corresponds to an object url + // that resolved to a MediaSource. + RefPtr<MediaSource> mMediaSource; + + RefPtr<ChannelLoader> mChannelLoader; + + // Points to the child source elements, used to iterate through the children + // when selecting a resource to load. This is the previous sibling of the + // child considered the current 'candidate' in: + // https://html.spec.whatwg.org/multipage/media.html#concept-media-load-algorithm + // + // mSourcePointer == nullptr, we will next try to load |GetFirstChild()|. + // mSourcePointer == GetLastChild(), we've exhausted all sources, waiting + // for new elements to be appended. + nsCOMPtr<nsIContent> mSourcePointer; + + // Points to the document whose load we're blocking. This is the document + // we're bound to when loading starts. + nsCOMPtr<Document> mLoadBlockedDoc; + + // This is used to help us block/resume the event delivery. + class EventBlocker; + RefPtr<EventBlocker> mEventBlocker; + + // Media loading flags. See: + // http://www.whatwg.org/specs/web-apps/current-work/#video) + nsMediaNetworkState mNetworkState = HTMLMediaElement_Binding::NETWORK_EMPTY; + Watchable<nsMediaReadyState> mReadyState = { + HTMLMediaElement_Binding::HAVE_NOTHING, "HTMLMediaElement::mReadyState"}; + + enum LoadAlgorithmState { + // No load algorithm instance is waiting for a source to be added to the + // media in order to continue loading. + NOT_WAITING, + // We've run the load algorithm, and we tried all source children of the + // media element, and failed to load any successfully. We're waiting for + // another source element to be added to the media element, and will try + // to load any such element when its added. + WAITING_FOR_SOURCE + }; + + // The current media load ID. This is incremented every time we start a + // new load. Async events note the ID when they're first sent, and only fire + // if the ID is unchanged when they come to fire. + uint32_t mCurrentLoadID = 0; + + // Denotes the waiting state of a load algorithm instance. When the load + // algorithm is waiting for a source element child to be added, this is set + // to WAITING_FOR_SOURCE, otherwise it's NOT_WAITING. + LoadAlgorithmState mLoadWaitStatus = NOT_WAITING; + + // Current audio volume + double mVolume = 1.0; + + // True if the audio track is not silent. + bool mIsAudioTrackAudible = false; + + enum MutedReasons { + MUTED_BY_CONTENT = 0x01, + MUTED_BY_INVALID_PLAYBACK_RATE = 0x02, + MUTED_BY_AUDIO_CHANNEL = 0x04, + MUTED_BY_AUDIO_TRACK = 0x08 + }; + + uint32_t mMuted = 0; + + UniquePtr<const MetadataTags> mTags; + + // URI of the resource we're attempting to load. This stores the value we + // return in the currentSrc attribute. Use GetCurrentSrc() to access the + // currentSrc attribute. + // This is always the original URL we're trying to load --- before + // redirects etc. + nsCOMPtr<nsIURI> mLoadingSrc; + + // The triggering principal for the current source. + nsCOMPtr<nsIPrincipal> mLoadingSrcTriggeringPrincipal; + + // Stores the current preload action for this element. Initially set to + // PRELOAD_UNDEFINED, its value is changed by calling + // UpdatePreloadAction(). + PreloadAction mPreloadAction = PRELOAD_UNDEFINED; + + // Time that the last timeupdate event was queued. Read/Write from the + // main thread only. + TimeStamp mQueueTimeUpdateRunnerTime; + + // Time that the last timeupdate event was fired. Read/Write from the + // main thread only. + TimeStamp mLastTimeUpdateDispatchTime; + + // Time that the last progress event was fired. Read/Write from the + // main thread only. + TimeStamp mProgressTime; + + // Time that data was last read from the media resource. Used for + // computing if the download has stalled and to rate limit progress events + // when data is arriving slower than PROGRESS_MS. + // Read/Write from the main thread only. + TimeStamp mDataTime; + + // Media 'currentTime' value when the last timeupdate event was queued. + // Read/Write from the main thread only. + double mLastCurrentTime = 0.0; + + // Logical start time of the media resource in seconds as obtained + // from any media fragments. A negative value indicates that no + // fragment time has been set. Read/Write from the main thread only. + double mFragmentStart = -1.0; + + // Logical end time of the media resource in seconds as obtained + // from any media fragments. A negative value indicates that no + // fragment time has been set. Read/Write from the main thread only. + double mFragmentEnd = -1.0; + + // The defaultPlaybackRate attribute gives the desired speed at which the + // media resource is to play, as a multiple of its intrinsic speed. + double mDefaultPlaybackRate = 1.0; + + // The playbackRate attribute gives the speed at which the media resource + // plays, as a multiple of its intrinsic speed. If it is not equal to the + // defaultPlaybackRate, then the implication is that the user is using a + // feature such as fast forward or slow motion playback. + double mPlaybackRate = 1.0; + + // True if pitch correction is applied when playbackRate is set to a + // non-intrinsic value. + bool mPreservesPitch = true; + + // Reference to the source element last returned by GetNextSource(). + // This is the child source element which we're trying to load from. + nsCOMPtr<nsIContent> mSourceLoadCandidate; + + // Range of time played. + RefPtr<TimeRanges> mPlayed; + + // Timer used for updating progress events. + nsCOMPtr<nsITimer> mProgressTimer; + + // Encrypted Media Extension media keys. + RefPtr<MediaKeys> mMediaKeys; + RefPtr<MediaKeys> mIncomingMediaKeys; + // The dom promise is used for HTMLMediaElement::SetMediaKeys. + RefPtr<DetailedPromise> mSetMediaKeysDOMPromise; + // Used to indicate if the MediaKeys attaching operation is on-going or not. + bool mAttachingMediaKey = false; + MozPromiseRequestHolder<SetCDMPromise> mSetCDMRequest; + + // Stores the time at the start of the current 'played' range. + double mCurrentPlayRangeStart = 1.0; + + // True if loadeddata has been fired. + bool mLoadedDataFired = false; + + // One of the factors determines whether a media element with 'autoplay' + // attribute is allowed to start playing. + // https://html.spec.whatwg.org/multipage/media.html#can-autoplay-flag + bool mCanAutoplayFlag = true; + + // Playback of the video is paused either due to calling the + // 'Pause' method, or playback not yet having started. + Watchable<bool> mPaused = {true, "HTMLMediaElement::mPaused"}; + + // The following two fields are here for the private storage of the builtin + // video controls, and control 'casting' of the video to external devices + // (TVs, projectors etc.) + // True if casting is currently allowed + bool mAllowCasting = false; + // True if currently casting this video + bool mIsCasting = false; + + // Set while there are some OutputMediaStreams this media element's enabled + // and selected tracks are captured into. When set, all tracks are captured + // into the graph of this dummy track. + // NB: This is a SharedDummyTrack to allow non-default graphs (AudioContexts + // with an explicit sampleRate defined) to capture this element. When + // cross-graph tracks are supported, this can become a bool. + Watchable<RefPtr<SharedDummyTrack>> mTracksCaptured; + + // True if the sound is being captured. + bool mAudioCaptured = false; + + // If TRUE then the media element was actively playing before the currently + // in progress seeking. If FALSE then the media element is either not seeking + // or was not actively playing before the current seek. Used to decide whether + // to raise the 'waiting' event as per 4.7.1.8 in HTML 5 specification. + bool mPlayingBeforeSeek = false; + + // True if this element is suspended because the document is inactive or the + // inactive docshell is not allowing media to play. + bool mSuspendedByInactiveDocOrDocshell = false; + + // True if we're running the "load()" method. + bool mIsRunningLoadMethod = false; + + // True if we're running or waiting to run queued tasks due to an explicit + // call to "load()". + bool mIsDoingExplicitLoad = false; + + // True if we're loading the resource from the child source elements. + bool mIsLoadingFromSourceChildren = false; + + // True if we're delaying the "load" event. They are delayed until either + // an error occurs, or the first frame is loaded. + bool mDelayingLoadEvent = false; + + // True when we've got a task queued to call SelectResource(), + // or while we're running SelectResource(). + bool mIsRunningSelectResource = false; + + // True when we already have select resource call queued + bool mHaveQueuedSelectResource = false; + + // True if we suspended the decoder because we were paused, + // preloading metadata is enabled, autoplay was not enabled, and we loaded + // the first frame. + bool mSuspendedAfterFirstFrame = false; + + // True if we are allowed to suspend the decoder because we were paused, + // preloading metdata was enabled, autoplay was not enabled, and we loaded + // the first frame. + bool mAllowSuspendAfterFirstFrame = true; + + // True if we've played or completed a seek. We use this to determine + // when the poster frame should be shown. + bool mHasPlayedOrSeeked = false; + + // True if we've added a reference to ourselves to keep the element + // alive while no-one is referencing it but the element may still fire + // events of its own accord. + bool mHasSelfReference = false; + + // True if we've received a notification that the engine is shutting + // down. + bool mShuttingDown = false; + + // True if we've suspended a load in the resource selection algorithm + // due to loading a preload:none media. When true, the resource we'll + // load when the user initiates either playback or an explicit load is + // stored in mPreloadURI. + bool mSuspendedForPreloadNone = false; + + // True if we've connected mSrcStream to the media element output. + bool mSrcStreamIsPlaying = false; + + // True if we should set nsIClassOfService::UrgentStart to the channel to + // get the response ASAP for better user responsiveness. + bool mUseUrgentStartForChannel = false; + + // The CORS mode when loading the media element + CORSMode mCORSMode = CORS_NONE; + + // Info about the played media. + MediaInfo mMediaInfo; + + // True if the media has encryption information. + bool mIsEncrypted = false; + + enum WaitingForKeyState { + NOT_WAITING_FOR_KEY = 0, + WAITING_FOR_KEY = 1, + WAITING_FOR_KEY_DISPATCHED = 2 + }; + + // True when the CDM cannot decrypt the current block due to lacking a key. + // Note: the "waitingforkey" event is not dispatched until all decoded data + // has been rendered. + WaitingForKeyState mWaitingForKey = NOT_WAITING_FOR_KEY; + + // Listens for waitingForKey events from the owned decoder. + MediaEventListener mWaitingForKeyListener; + + // Init Data that needs to be sent in 'encrypted' events in MetadataLoaded(). + EncryptionInfo mPendingEncryptedInitData; + + // True if the media's channel's download has been suspended. + Watchable<bool> mDownloadSuspendedByCache = { + false, "HTMLMediaElement::mDownloadSuspendedByCache"}; + + // Disable the video playback by track selection. This flag might not be + // enough if we ever expand the ability of supporting multi-tracks video + // playback. + bool mDisableVideo = false; + + RefPtr<TextTrackManager> mTextTrackManager; + + RefPtr<AudioTrackList> mAudioTrackList; + + RefPtr<VideoTrackList> mVideoTrackList; + + UniquePtr<MediaStreamTrackListener> mMediaStreamTrackListener; + + // The principal guarding mVideoFrameContainer access when playing a + // MediaStream. + nsCOMPtr<nsIPrincipal> mSrcStreamVideoPrincipal; + + // True if the autoplay media was blocked because it hadn't loaded metadata + // yet. + bool mBlockedAsWithoutMetadata = false; + + // This promise is used to notify MediaElementAudioSourceNode that media + // element is allowed to play when MediaElement is used as a source for web + // audio. + MozPromiseHolder<GenericNonExclusivePromise> mAllowedToPlayPromise; + + // True if media has ever been blocked for autoplay, it's used to notify front + // end to show the correct blocking icon when the document goes back from + // bfcache. + bool mHasEverBeenBlockedForAutoplay = false; + + // True if we have dispatched a task for text track changed, will be unset + // when we starts processing text track changed. + // https://html.spec.whatwg.org/multipage/media.html#pending-text-track-change-notification-flag + bool mPendingTextTrackChanged = false; + + public: + // This function will be called whenever a text track that is in a media + // element's list of text tracks has its text track mode change value + void NotifyTextTrackModeChanged(); + + private: + friend class nsMediaEventRunner; + friend class nsResolveOrRejectPendingPlayPromisesRunner; + + already_AddRefed<PlayPromise> CreatePlayPromise(ErrorResult& aRv) const; + + virtual void MaybeBeginCloningVisually(){}; + + uint32_t GetPreloadDefault() const; + uint32_t GetPreloadDefaultAuto() const; + + /** + * This function is called by AfterSetAttr and OnAttrSetButNotChanged. + * It will not be called if the value is being unset. + * + * @param aNamespaceID the namespace of the attr being set + * @param aName the localname of the attribute being set + * @param aNotify Whether we plan to notify document observers. + */ + void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify); + + // True if Init() has been called after construction + bool mInitialized = false; + + // True if user has called load(), seek() or element has started playing + // before. It's *only* use for `click-to-play` blocking autoplay policy. + // In addition, we would reset this once media aborts current load. + bool mIsBlessed = false; + + // True if the first frame has been successfully loaded. + Watchable<bool> mFirstFrameLoaded = {false, + "HTMLMediaElement::mFirstFrameLoaded"}; + + // Media elements also have a default playback start position, which must + // initially be set to zero seconds. This time is used to allow the element to + // be seeked even before the media is loaded. + double mDefaultPlaybackStartPosition = 0.0; + + // True if media element has been marked as 'tainted' and can't + // participate in video decoder suspending. + bool mHasSuspendTaint = false; + + // True if media element has been forced into being considered 'hidden'. + // For use by mochitests. Enabling pref "media.test.video-suspend" + bool mForcedHidden = false; + + Visibility mVisibilityState = Visibility::Untracked; + + UniquePtr<ErrorSink> mErrorSink; + + // This wrapper will handle all audio channel related stuffs, eg. the + // operations of tab audio indicator, Fennec's media control. Note: + // mAudioChannelWrapper might be null after GC happened. + RefPtr<AudioChannelAgentCallback> mAudioChannelWrapper; + + // A list of pending play promises. The elements are pushed during the play() + // method call and are resolved/rejected during further playback steps. + nsTArray<RefPtr<PlayPromise>> mPendingPlayPromises; + + // A list of already-dispatched but not yet run + // nsResolveOrRejectPendingPlayPromisesRunners. + // Runners whose Run() method is called remove themselves from this list. + // We keep track of these because the load algorithm resolves/rejects all + // already-dispatched pending play promises. + nsTArray<nsResolveOrRejectPendingPlayPromisesRunner*> + mPendingPlayPromisesRunners; + + // A pending seek promise which is created at Seek() method call and is + // resolved/rejected at AsyncResolveSeekDOMPromiseIfExists()/ + // AsyncRejectSeekDOMPromiseIfExists() methods. + RefPtr<dom::Promise> mSeekDOMPromise; + + // Return true if the docshell is inactive and explicitly wants to stop media + // playing in that shell. + bool ShouldBeSuspendedByInactiveDocShell() const; + + // For debugging bug 1407148. + void AssertReadyStateIsNothing(); + + // Contains the unique id of the sink device and the device info. + // The initial value is ("", nullptr) and the default output device is used. + // It can contain an invalid id and info if the device has been + // unplugged. It can be set to ("", nullptr). It follows the spec attribute: + // https://w3c.github.io/mediacapture-output/#htmlmediaelement-extensions + // Read/Write from the main thread only. + std::pair<nsString, RefPtr<AudioDeviceInfo>> mSink; + + // This flag is used to control when the user agent is to show a poster frame + // for a video element instead of showing the video contents. + // https://html.spec.whatwg.org/multipage/media.html#show-poster-flag + bool mShowPoster; + + // We may delay starting playback of a media for an unvisited tab until it's + // going to foreground. We would create ResumeDelayedMediaPlaybackAgent to + // handle related operations at the time whenever delaying media playback is + // needed. + void CreateResumeDelayedMediaPlaybackAgentIfNeeded(); + void ClearResumeDelayedMediaPlaybackAgentIfNeeded(); + RefPtr<ResumeDelayedPlaybackAgent> mResumeDelayedPlaybackAgent; + MozPromiseRequestHolder<ResumeDelayedPlaybackAgent::ResumePromise> + mResumePlaybackRequest; + + // Return true if we have already a decoder or a src stream and don't have any + // error. + bool IsPlayable() const; + + // Return true if the media qualifies for being controlled by media control + // keys. + bool ShouldStartMediaControlKeyListener() const; + + // Start the listener if media fits the requirement of being able to be + // controlled be media control keys. + void StartMediaControlKeyListenerIfNeeded(); + + // It's used to listen media control key, by which we would play or pause + // media element. + RefPtr<MediaControlKeyListener> mMediaControlKeyListener; + + // Method to update audio stream name + void UpdateStreamName(); + + // Return true if the media element is being used in picture in picture mode. + bool IsBeingUsedInPictureInPictureMode() const; + + // Return true if we should queue a 'timeupdate' event runner to main thread. + bool ShouldQueueTimeupdateAsyncTask(TimeupdateType aType) const; + +#ifdef MOZ_WMF_CDM + // It's used to record telemetry probe for WMFCDM playback. + bool mIsUsingWMFCDM = false; +#endif +}; + +// Check if the context is chrome or has the debugger or tabs permission +bool HasDebuggerOrTabsPrivilege(JSContext* aCx, JSObject* aObj); + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLMediaElement_h diff --git a/dom/html/HTMLMenuElement.cpp b/dom/html/HTMLMenuElement.cpp new file mode 100644 index 0000000000..c3f323c483 --- /dev/null +++ b/dom/html/HTMLMenuElement.cpp @@ -0,0 +1,28 @@ +/* -*- 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/HTMLMenuElement.h" + +#include "mozilla/dom/HTMLMenuElementBinding.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Menu) + +namespace mozilla::dom { + +HTMLMenuElement::HTMLMenuElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLMenuElement::~HTMLMenuElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLMenuElement) + +JSObject* HTMLMenuElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLMenuElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLMenuElement.h b/dom/html/HTMLMenuElement.h new file mode 100644 index 0000000000..4ab37ce131 --- /dev/null +++ b/dom/html/HTMLMenuElement.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLMenuElement_h +#define mozilla_dom_HTMLMenuElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLMenuElement final : public nsGenericHTMLElement { + public: + explicit HTMLMenuElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLMenuElement, menu) + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLMenuElement, nsGenericHTMLElement) + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // WebIDL + bool Compact() const { return GetBoolAttr(nsGkAtoms::compact); } + void SetCompact(bool aCompact, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::compact, aCompact, aError); + } + + protected: + virtual ~HTMLMenuElement(); + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLMenuElement_h diff --git a/dom/html/HTMLMetaElement.cpp b/dom/html/HTMLMetaElement.cpp new file mode 100644 index 0000000000..b99ffabdaf --- /dev/null +++ b/dom/html/HTMLMetaElement.cpp @@ -0,0 +1,178 @@ +/* -*- 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/AsyncEventDispatcher.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/HTMLMetaElement.h" +#include "mozilla/dom/HTMLMetaElementBinding.h" +#include "mozilla/dom/nsCSPService.h" +#include "mozilla/dom/nsCSPUtils.h" +#include "mozilla/dom/ViewportMetaData.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_security.h" +#include "nsContentUtils.h" +#include "nsSandboxFlags.h" +#include "nsStyleConsts.h" +#include "nsIXMLContentSink.h" + +static mozilla::LazyLogModule gMetaElementLog("nsMetaElement"); +#define LOG(msg) MOZ_LOG(gMetaElementLog, mozilla::LogLevel::Debug, msg) +#define LOG_ENABLED() MOZ_LOG_TEST(gMetaElementLog, mozilla::LogLevel::Debug) + +NS_IMPL_NS_NEW_HTML_ELEMENT(Meta) + +namespace mozilla::dom { + +HTMLMetaElement::HTMLMetaElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLMetaElement::~HTMLMetaElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLMetaElement) + +void HTMLMetaElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (Document* document = GetUncomposedDoc()) { + if (aName == nsGkAtoms::content) { + if (const nsAttrValue* name = GetParsedAttr(nsGkAtoms::name)) { + MetaAddedOrChanged(*document, *name, ChangeKind::ContentChange); + } + CreateAndDispatchEvent(*document, u"DOMMetaChanged"_ns); + } else if (aName == nsGkAtoms::name) { + if (aOldValue) { + MetaRemoved(*document, *aOldValue, ChangeKind::NameChange); + } + if (aValue) { + MetaAddedOrChanged(*document, *aValue, ChangeKind::NameChange); + } + CreateAndDispatchEvent(*document, u"DOMMetaChanged"_ns); + } + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +nsresult HTMLMetaElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + if (!IsInUncomposedDoc()) { + return rv; + } + Document& doc = aContext.OwnerDoc(); + + bool shouldProcessMeta = true; + // We don't want to call ProcessMETATag when we are pretty print + // the document + if (doc.IsXMLDocument()) { + if (nsCOMPtr<nsIXMLContentSink> xmlSink = + do_QueryInterface(doc.GetCurrentContentSink())) { + if (xmlSink->IsPrettyPrintXML() && + xmlSink->IsPrettyPrintHasSpecialRoot()) { + shouldProcessMeta = false; + } + } + } + + if (shouldProcessMeta) { + doc.ProcessMETATag(this); + } + + if (AttrValueIs(kNameSpaceID_None, nsGkAtoms::httpEquiv, nsGkAtoms::headerCSP, + eIgnoreCase)) { + // only accept <meta http-equiv="Content-Security-Policy" content=""> if it + // appears in the <head> element. + Element* headElt = doc.GetHeadElement(); + if (headElt && IsInclusiveDescendantOf(headElt)) { + nsAutoString content; + GetContent(content); + + if (LOG_ENABLED()) { + nsAutoCString documentURIspec; + if (nsIURI* documentURI = doc.GetDocumentURI()) { + documentURI->GetAsciiSpec(documentURIspec); + } + + LOG( + ("HTMLMetaElement %p sets CSP '%s' on document=%p, " + "document-uri=%s", + this, NS_ConvertUTF16toUTF8(content).get(), &doc, + documentURIspec.get())); + } + CSP_ApplyMetaCSPToDoc(doc, content); + } + } + + if (const nsAttrValue* name = GetParsedAttr(nsGkAtoms::name)) { + MetaAddedOrChanged(doc, *name, ChangeKind::TreeChange); + } + CreateAndDispatchEvent(doc, u"DOMMetaAdded"_ns); + return rv; +} + +void HTMLMetaElement::UnbindFromTree(bool aNullParent) { + if (Document* oldDoc = GetUncomposedDoc()) { + if (const nsAttrValue* name = GetParsedAttr(nsGkAtoms::name)) { + MetaRemoved(*oldDoc, *name, ChangeKind::TreeChange); + } + CreateAndDispatchEvent(*oldDoc, u"DOMMetaRemoved"_ns); + } + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +void HTMLMetaElement::CreateAndDispatchEvent(Document&, + const nsAString& aEventName) { + AsyncEventDispatcher::RunDOMEventWhenSafe(*this, aEventName, CanBubble::eYes, + ChromeOnlyDispatch::eYes); +} + +JSObject* HTMLMetaElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLMetaElement_Binding::Wrap(aCx, this, aGivenProto); +} + +void HTMLMetaElement::MetaAddedOrChanged(Document& aDoc, + const nsAttrValue& aName, + ChangeKind aChangeKind) { + nsAutoString content; + const bool hasContent = GetAttr(nsGkAtoms::content, content); + if (aName.Equals(nsGkAtoms::viewport, eIgnoreCase)) { + if (hasContent) { + aDoc.SetMetaViewportData(MakeUnique<ViewportMetaData>(content)); + } + return; + } + + if (aName.Equals(nsGkAtoms::referrer, eIgnoreCase)) { + content = nsContentUtils::TrimWhitespace<nsContentUtils::IsHTMLWhitespace>( + content); + return aDoc.UpdateReferrerInfoFromMeta(content, + /* aPreload = */ false); + } + if (aName.Equals(nsGkAtoms::color_scheme, eIgnoreCase)) { + if (aChangeKind != ChangeKind::ContentChange) { + return aDoc.AddColorSchemeMeta(*this); + } + return aDoc.RecomputeColorScheme(); + } +} + +void HTMLMetaElement::MetaRemoved(Document& aDoc, const nsAttrValue& aName, + ChangeKind aChangeKind) { + MOZ_ASSERT(aChangeKind != ChangeKind::ContentChange, + "Content change can't trigger removal"); + if (aName.Equals(nsGkAtoms::color_scheme, eIgnoreCase)) { + return aDoc.RemoveColorSchemeMeta(*this); + } +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLMetaElement.h b/dom/html/HTMLMetaElement.h new file mode 100644 index 0000000000..e492b49e8b --- /dev/null +++ b/dom/html/HTMLMetaElement.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLMetaElement_h +#define mozilla_dom_HTMLMetaElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLMetaElement final : public nsGenericHTMLElement { + public: + explicit HTMLMetaElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLMetaElement, nsGenericHTMLElement) + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + void CreateAndDispatchEvent(Document&, const nsAString& aEventName); + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + void GetName(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); } + void SetName(const nsAString& aName, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aName, aRv); + } + void GetHttpEquiv(nsAString& aValue) { + GetHTMLAttr(nsGkAtoms::httpEquiv, aValue); + } + void SetHttpEquiv(const nsAString& aHttpEquiv, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::httpEquiv, aHttpEquiv, aRv); + } + void GetContent(nsAString& aValue) { + GetHTMLAttr(nsGkAtoms::content, aValue); + } + void SetContent(const nsAString& aContent, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::content, aContent, aRv); + } + void GetScheme(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::scheme, aValue); } + void SetScheme(const nsAString& aScheme, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::scheme, aScheme, aRv); + } + void GetMedia(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::media, aValue); } + void SetMedia(const nsAString& aMedia, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::media, aMedia, aRv); + } + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + protected: + virtual ~HTMLMetaElement(); + + private: + enum class ChangeKind : uint8_t { TreeChange, NameChange, ContentChange }; + void MetaRemoved(Document& aDoc, const nsAttrValue& aName, + ChangeKind aChangeKind); + void MetaAddedOrChanged(Document& aDoc, const nsAttrValue& aName, + ChangeKind aChangeKind); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLMetaElement_h diff --git a/dom/html/HTMLMeterElement.cpp b/dom/html/HTMLMeterElement.cpp new file mode 100644 index 0000000000..3bc025de5a --- /dev/null +++ b/dom/html/HTMLMeterElement.cpp @@ -0,0 +1,259 @@ +/* -*- 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 "HTMLMeterElement.h" +#include "mozilla/dom/HTMLMeterElementBinding.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Meter) + +namespace mozilla::dom { + +static const double kDefaultValue = 0.0; +static const double kDefaultMin = 0.0; +static const double kDefaultMax = 1.0; + +HTMLMeterElement::HTMLMeterElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLMeterElement::~HTMLMeterElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLMeterElement) + +static bool IsInterestingAttr(int32_t aNamespaceID, nsAtom* aAttribute) { + if (aNamespaceID != kNameSpaceID_None) { + return false; + } + return aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max || + aAttribute == nsGkAtoms::min || aAttribute == nsGkAtoms::low || + aAttribute == nsGkAtoms::high || aAttribute == nsGkAtoms::optimum; +} + +bool HTMLMeterElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (IsInterestingAttr(aNamespaceID, aAttribute)) { + return aResult.ParseDoubleValue(aValue); + } + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLMeterElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (IsInterestingAttr(aNameSpaceID, aName)) { + UpdateOptimumState(aNotify); + } + nsGenericHTMLElement::AfterSetAttr(aNameSpaceID, aName, aValue, aOldValue, + aSubjectPrincipal, aNotify); +} + +void HTMLMeterElement::UpdateOptimumState(bool aNotify) { + AutoStateChangeNotifier notifier(*this, aNotify); + RemoveStatesSilently(ElementState::METER_OPTIMUM_STATES); + AddStatesSilently(GetOptimumState()); +} + +double HTMLMeterElement::Min() const { + /** + * If the attribute min is defined, the minimum is this value. + * Otherwise, the minimum is the default value. + */ + const nsAttrValue* attrMin = mAttrs.GetAttr(nsGkAtoms::min); + if (attrMin && attrMin->Type() == nsAttrValue::eDoubleValue) { + return attrMin->GetDoubleValue(); + } + return kDefaultMin; +} + +double HTMLMeterElement::Max() const { + /** + * If the attribute max is defined, the maximum is this value. + * Otherwise, the maximum is the default value. + * If the maximum value is less than the minimum value, + * the maximum value is the same as the minimum value. + */ + double max; + + const nsAttrValue* attrMax = mAttrs.GetAttr(nsGkAtoms::max); + if (attrMax && attrMax->Type() == nsAttrValue::eDoubleValue) { + max = attrMax->GetDoubleValue(); + } else { + max = kDefaultMax; + } + + return std::max(max, Min()); +} + +double HTMLMeterElement::Value() const { + /** + * If the attribute value is defined, the actual value is this value. + * Otherwise, the actual value is the default value. + * If the actual value is less than the minimum value, + * the actual value is the same as the minimum value. + * If the actual value is greater than the maximum value, + * the actual value is the same as the maximum value. + */ + double value; + + const nsAttrValue* attrValue = mAttrs.GetAttr(nsGkAtoms::value); + if (attrValue && attrValue->Type() == nsAttrValue::eDoubleValue) { + value = attrValue->GetDoubleValue(); + } else { + value = kDefaultValue; + } + + double min = Min(); + + if (value <= min) { + return min; + } + + return std::min(value, Max()); +} + +double HTMLMeterElement::Position() const { + const double max = Max(); + const double min = Min(); + const double value = Value(); + + double range = max - min; + return range != 0.0 ? (value - min) / range : 1.0; +} + +double HTMLMeterElement::Low() const { + /** + * If the low value is defined, the low value is this value. + * Otherwise, the low value is the minimum value. + * If the low value is less than the minimum value, + * the low value is the same as the minimum value. + * If the low value is greater than the maximum value, + * the low value is the same as the maximum value. + */ + + double min = Min(); + + const nsAttrValue* attrLow = mAttrs.GetAttr(nsGkAtoms::low); + if (!attrLow || attrLow->Type() != nsAttrValue::eDoubleValue) { + return min; + } + + double low = attrLow->GetDoubleValue(); + + if (low <= min) { + return min; + } + + return std::min(low, Max()); +} + +double HTMLMeterElement::High() const { + /** + * If the high value is defined, the high value is this value. + * Otherwise, the high value is the maximum value. + * If the high value is less than the low value, + * the high value is the same as the low value. + * If the high value is greater than the maximum value, + * the high value is the same as the maximum value. + */ + + double max = Max(); + + const nsAttrValue* attrHigh = mAttrs.GetAttr(nsGkAtoms::high); + if (!attrHigh || attrHigh->Type() != nsAttrValue::eDoubleValue) { + return max; + } + + double high = attrHigh->GetDoubleValue(); + + if (high >= max) { + return max; + } + + return std::max(high, Low()); +} + +double HTMLMeterElement::Optimum() const { + /** + * If the optimum value is defined, the optimum value is this value. + * Otherwise, the optimum value is the midpoint between + * the minimum value and the maximum value : + * min + (max - min)/2 = (min + max)/2 + * If the optimum value is less than the minimum value, + * the optimum value is the same as the minimum value. + * If the optimum value is greater than the maximum value, + * the optimum value is the same as the maximum value. + */ + + double max = Max(); + + double min = Min(); + + const nsAttrValue* attrOptimum = mAttrs.GetAttr(nsGkAtoms::optimum); + if (!attrOptimum || attrOptimum->Type() != nsAttrValue::eDoubleValue) { + return (min + max) / 2.0; + } + + double optimum = attrOptimum->GetDoubleValue(); + + if (optimum <= min) { + return min; + } + + return std::min(optimum, max); +} + +ElementState HTMLMeterElement::GetOptimumState() const { + /* + * If the optimum value is in [minimum, low[, + * return if the value is in optimal, suboptimal or sub-suboptimal region + * + * If the optimum value is in [low, high], + * return if the value is in optimal or suboptimal region + * + * If the optimum value is in ]high, maximum], + * return if the value is in optimal, suboptimal or sub-suboptimal region + */ + double value = Value(); + double low = Low(); + double high = High(); + double optimum = Optimum(); + + if (optimum < low) { + if (value < low) { + return ElementState::OPTIMUM; + } + if (value <= high) { + return ElementState::SUB_OPTIMUM; + } + return ElementState::SUB_SUB_OPTIMUM; + } + if (optimum > high) { + if (value > high) { + return ElementState::OPTIMUM; + } + if (value >= low) { + return ElementState::SUB_OPTIMUM; + } + return ElementState::SUB_SUB_OPTIMUM; + } + // optimum in [low, high] + if (value >= low && value <= high) { + return ElementState::OPTIMUM; + } + return ElementState::SUB_OPTIMUM; +} + +JSObject* HTMLMeterElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLMeterElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLMeterElement.h b/dom/html/HTMLMeterElement.h new file mode 100644 index 0000000000..abe0521857 --- /dev/null +++ b/dom/html/HTMLMeterElement.h @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLMeterElement_h +#define mozilla_dom_HTMLMeterElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsAttrValue.h" +#include "nsAttrValueInlines.h" +#include "nsAlgorithm.h" +#include <algorithm> + +namespace mozilla::dom { + +class HTMLMeterElement final : public nsGenericHTMLElement { + public: + explicit HTMLMeterElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + // WebIDL + + /* @return the value */ + double Value() const; + /* Returns the percentage that this element should be filed based on the + * min/max/value */ + double Position() const; + void SetValue(double aValue, ErrorResult& aRv) { + SetDoubleAttr(nsGkAtoms::value, aValue, aRv); + } + + /* @return the minimum value */ + double Min() const; + void SetMin(double aValue, ErrorResult& aRv) { + SetDoubleAttr(nsGkAtoms::min, aValue, aRv); + } + + /* @return the maximum value */ + double Max() const; + void SetMax(double aValue, ErrorResult& aRv) { + SetDoubleAttr(nsGkAtoms::max, aValue, aRv); + } + + /* @return the low value */ + double Low() const; + void SetLow(double aValue, ErrorResult& aRv) { + SetDoubleAttr(nsGkAtoms::low, aValue, aRv); + } + + /* @return the high value */ + double High() const; + void SetHigh(double aValue, ErrorResult& aRv) { + SetDoubleAttr(nsGkAtoms::high, aValue, aRv); + } + + /* @return the optimum value */ + double Optimum() const; + void SetOptimum(double aValue, ErrorResult& aRv) { + SetDoubleAttr(nsGkAtoms::optimum, aValue, aRv); + } + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLMeterElement, meter); + + protected: + virtual ~HTMLMeterElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + /** + * Returns the optimum state of the element. + * ElementState::OPTIMUM if the actual value is in the optimum region. + * ElementState::SUB_OPTIMUM if the actual value is in the sub-optimal + * region. + * ElementState::SUB_SUB_OPTIMUM if the actual value is in the + * sub-sub-optimal region. + * + * @return the optimum state of the element. + */ + ElementState GetOptimumState() const; + void UpdateOptimumState(bool aNotify); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLMeterElement_h diff --git a/dom/html/HTMLModElement.cpp b/dom/html/HTMLModElement.cpp new file mode 100644 index 0000000000..ddf73bfdfe --- /dev/null +++ b/dom/html/HTMLModElement.cpp @@ -0,0 +1,28 @@ +/* -*- 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/HTMLModElement.h" +#include "mozilla/dom/HTMLModElementBinding.h" +#include "nsStyleConsts.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Mod) + +namespace mozilla::dom { + +HTMLModElement::HTMLModElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLModElement::~HTMLModElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLModElement) + +JSObject* HTMLModElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLModElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLModElement.h b/dom/html/HTMLModElement.h new file mode 100644 index 0000000000..aa1a395c91 --- /dev/null +++ b/dom/html/HTMLModElement.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLModElement_h +#define mozilla_dom_HTMLModElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" + +namespace mozilla::dom { + +class HTMLModElement final : public nsGenericHTMLElement { + public: + explicit HTMLModElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + void GetCite(nsString& aCite) { GetHTMLURIAttr(nsGkAtoms::cite, aCite); } + void SetCite(const nsAString& aCite, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::cite, aCite, aRv); + } + void GetDateTime(DOMString& aDateTime) { + GetHTMLAttr(nsGkAtoms::datetime, aDateTime); + } + void SetDateTime(const nsAString& aDateTime, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::datetime, aDateTime, aRv); + } + + protected: + virtual ~HTMLModElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLModElement_h diff --git a/dom/html/HTMLObjectElement.cpp b/dom/html/HTMLObjectElement.cpp new file mode 100644 index 0000000000..f8e5d99963 --- /dev/null +++ b/dom/html/HTMLObjectElement.cpp @@ -0,0 +1,273 @@ +/* -*- 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/BindContext.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLObjectElement.h" +#include "mozilla/dom/HTMLObjectElementBinding.h" +#include "mozilla/dom/ElementInlines.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "nsAttrValueInlines.h" +#include "nsGkAtoms.h" +#include "nsError.h" +#include "nsIContentInlines.h" +#include "nsIWidget.h" +#include "nsContentUtils.h" +#ifdef XP_MACOSX +# include "mozilla/EventDispatcher.h" +# include "mozilla/dom/Event.h" +# include "nsFocusManager.h" +#endif + +namespace mozilla::dom { + +HTMLObjectElement::HTMLObjectElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLFormControlElement(std::move(aNodeInfo), + FormControlType::Object), + mIsDoneAddingChildren(!aFromParser) { + SetIsNetworkCreated(aFromParser == FROM_PARSER_NETWORK); + + // <object> is always barred from constraint validation. + SetBarredFromConstraintValidation(true); +} + +HTMLObjectElement::~HTMLObjectElement() = default; + +bool HTMLObjectElement::IsInteractiveHTMLContent() const { + return HasAttr(nsGkAtoms::usemap) || + nsGenericHTMLFormControlElement::IsInteractiveHTMLContent(); +} + +void HTMLObjectElement::DoneAddingChildren(bool aHaveNotified) { + mIsDoneAddingChildren = true; + + // If we're already in a document, we need to trigger the load + // Otherwise, BindToTree takes care of that. + if (IsInComposedDoc()) { + StartObjectLoad(aHaveNotified, false); + } +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLObjectElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + HTMLObjectElement, nsGenericHTMLFormControlElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity) + nsObjectLoadingContent::Traverse(tmp, cb); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLObjectElement, + nsGenericHTMLFormControlElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity) + nsObjectLoadingContent::Unlink(tmp); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED( + HTMLObjectElement, nsGenericHTMLFormControlElement, nsIRequestObserver, + nsIStreamListener, nsFrameLoaderOwner, nsIObjectLoadingContent, + nsIChannelEventSink, nsIConstraintValidation) + +NS_IMPL_ELEMENT_CLONE(HTMLObjectElement) + +nsresult HTMLObjectElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLFormControlElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + // If we already have all the children, start the load. + if (IsInComposedDoc() && mIsDoneAddingChildren) { + void (HTMLObjectElement::*start)() = &HTMLObjectElement::StartObjectLoad; + nsContentUtils::AddScriptRunner( + NewRunnableMethod("dom::HTMLObjectElement::BindToTree", this, start)); + } + + return NS_OK; +} + +void HTMLObjectElement::UnbindFromTree(bool aNullParent) { + nsObjectLoadingContent::UnbindFromTree(aNullParent); + nsGenericHTMLFormControlElement::UnbindFromTree(aNullParent); +} + +void HTMLObjectElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, aNotify); + return nsGenericHTMLFormControlElement::AfterSetAttr( + aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLObjectElement::OnAttrSetButNotChanged( + int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, aNotify); + return nsGenericHTMLFormControlElement::OnAttrSetButNotChanged( + aNamespaceID, aName, aValue, aNotify); +} + +void HTMLObjectElement::AfterMaybeChangeAttr(int32_t aNamespaceID, + nsAtom* aName, bool aNotify) { + // if aNotify is false, we are coming from the parser or some such place; + // we'll get bound after all the attributes have been set, so we'll do the + // object load from BindToTree/DoneAddingChildren. + // Skip the LoadObject call in that case. + // We also don't want to start loading the object when we're not yet in + // a document, just in case that the caller wants to set additional + // attributes before inserting the node into the document. + if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::data || + !aNotify || !IsInComposedDoc() || !mIsDoneAddingChildren || + BlockEmbedOrObjectContentLoading()) { + return; + } + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "HTMLObjectElement::LoadObject", + [self = RefPtr<HTMLObjectElement>(this), aNotify]() { + if (self->IsInComposedDoc()) { + self->LoadObject(aNotify, true); + } + })); +} + +bool HTMLObjectElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + // TODO: this should probably be managed directly by IsHTMLFocusable. + // See bug 597242. + Document* doc = GetComposedDoc(); + if (!doc || IsInDesignMode()) { + if (aTabIndex) { + *aTabIndex = -1; + } + + *aIsFocusable = false; + return false; + } + + const nsAttrValue* attrVal = mAttrs.GetAttr(nsGkAtoms::tabindex); + bool isFocusable = attrVal && attrVal->Type() == nsAttrValue::eInteger; + + // This method doesn't call nsGenericHTMLFormControlElement intentionally. + // TODO: It should probably be changed when bug 597242 will be fixed. + if (IsEditingHost() || Type() == ObjectType::Document) { + if (aTabIndex) { + *aTabIndex = isFocusable ? attrVal->GetIntegerValue() : 0; + } + + *aIsFocusable = true; + return false; + } + + // TODO: this should probably be managed directly by IsHTMLFocusable. + // See bug 597242. + if (aTabIndex && isFocusable) { + *aTabIndex = attrVal->GetIntegerValue(); + *aIsFocusable = true; + } + + return false; +} + +int32_t HTMLObjectElement::TabIndexDefault() { return 0; } + +Nullable<WindowProxyHolder> HTMLObjectElement::GetContentWindow( + nsIPrincipal& aSubjectPrincipal) { + Document* doc = GetContentDocument(aSubjectPrincipal); + if (doc) { + nsPIDOMWindowOuter* win = doc->GetWindow(); + if (win) { + return WindowProxyHolder(win->GetBrowsingContext()); + } + } + + return nullptr; +} + +bool HTMLObjectElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::align) { + return ParseAlignValue(aValue, aResult); + } + if (ParseImageAttribute(aAttribute, aValue, aResult)) { + return true; + } + } + + return nsGenericHTMLFormControlElement::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +void HTMLObjectElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + MapImageAlignAttributeInto(aBuilder); + MapImageBorderAttributeInto(aBuilder); + MapImageMarginAttributeInto(aBuilder); + MapImageSizeAttributesInto(aBuilder); + MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLObjectElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = { + sCommonAttributeMap, + sImageMarginSizeAttributeMap, + sImageBorderAttributeMap, + sImageAlignAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLObjectElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +void HTMLObjectElement::StartObjectLoad(bool aNotify, bool aForce) { + // BindToTree can call us asynchronously, and we may be removed from the tree + // in the interim + if (!IsInComposedDoc() || !OwnerDoc()->IsActive() || + BlockEmbedOrObjectContentLoading()) { + return; + } + + LoadObject(aNotify, aForce); + SetIsNetworkCreated(false); +} + +uint32_t HTMLObjectElement::GetCapabilities() const { + return nsObjectLoadingContent::GetCapabilities() | eFallbackIfClassIDPresent; +} + +void HTMLObjectElement::DestroyContent() { + nsObjectLoadingContent::Destroy(); + nsGenericHTMLFormControlElement::DestroyContent(); +} + +nsresult HTMLObjectElement::CopyInnerTo(Element* aDest) { + nsresult rv = nsGenericHTMLFormControlElement::CopyInnerTo(aDest); + NS_ENSURE_SUCCESS(rv, rv); + + if (aDest->OwnerDoc()->IsStaticDocument()) { + CreateStaticClone(static_cast<HTMLObjectElement*>(aDest)); + } + + return rv; +} + +JSObject* HTMLObjectElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLObjectElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Object) diff --git a/dom/html/HTMLObjectElement.h b/dom/html/HTMLObjectElement.h new file mode 100644 index 0000000000..c627511b5c --- /dev/null +++ b/dom/html/HTMLObjectElement.h @@ -0,0 +1,205 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLObjectElement_h +#define mozilla_dom_HTMLObjectElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/ConstraintValidation.h" +#include "nsGenericHTMLElement.h" +#include "nsObjectLoadingContent.h" + +namespace mozilla::dom { + +class FormData; +template <typename T> +struct Nullable; +class WindowProxyHolder; + +class HTMLObjectElement final : public nsGenericHTMLFormControlElement, + public nsObjectLoadingContent, + public ConstraintValidation { + public: + explicit HTMLObjectElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser = NOT_FROM_PARSER); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLObjectElement, object) + int32_t TabIndexDefault() override; + + // nsObjectLoadingContent + const Element* AsElement() const final { return this; } + + // Element + bool IsInteractiveHTMLContent() const override; + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + + // Overriden nsIFormControl methods + NS_IMETHOD Reset() override { return NS_OK; } + + NS_IMETHOD SubmitNamesValues(FormData* aFormData) override { return NS_OK; } + + void DoneAddingChildren(bool aHaveNotified) override; + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + void DestroyContent() override; + + // nsObjectLoadingContent + uint32_t GetCapabilities() const override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + nsresult CopyInnerTo(Element* aDest); + + void StartObjectLoad() { StartObjectLoad(true, false); } + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLObjectElement, + nsGenericHTMLFormControlElement) + + // Web IDL binding methods + void GetData(DOMString& aValue) { + GetURIAttr(nsGkAtoms::data, nsGkAtoms::codebase, aValue); + } + void SetData(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::data, aValue, aRv); + } + void GetType(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::type, aValue); } + void SetType(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::type, aValue, aRv); + } + void GetName(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); } + void SetName(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aValue, aRv); + } + void GetUseMap(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::usemap, aValue); } + void SetUseMap(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::usemap, aValue, aRv); + } + void GetWidth(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::width, aValue); } + void SetWidth(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::width, aValue, aRv); + } + void GetHeight(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::height, aValue); } + void SetHeight(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::height, aValue, aRv); + } + using nsObjectLoadingContent::GetContentDocument; + + Nullable<WindowProxyHolder> GetContentWindow(nsIPrincipal& aSubjectPrincipal); + + using ConstraintValidation::GetValidationMessage; + using ConstraintValidation::SetCustomValidity; + void GetAlign(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::align, aValue); } + void SetAlign(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::align, aValue, aRv); + } + void GetArchive(DOMString& aValue) { + GetHTMLAttr(nsGkAtoms::archive, aValue); + } + void SetArchive(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::archive, aValue, aRv); + } + void GetCode(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::code, aValue); } + void SetCode(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::code, aValue, aRv); + } + bool Declare() { return GetBoolAttr(nsGkAtoms::declare); } + void SetDeclare(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::declare, aValue, aRv); + } + uint32_t Hspace() { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::hspace, 0); + } + void SetHspace(uint32_t aValue, ErrorResult& aRv) { + SetUnsignedIntAttr(nsGkAtoms::hspace, aValue, 0, aRv); + } + void GetStandby(DOMString& aValue) { + GetHTMLAttr(nsGkAtoms::standby, aValue); + } + void SetStandby(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::standby, aValue, aRv); + } + uint32_t Vspace() { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::vspace, 0); + } + void SetVspace(uint32_t aValue, ErrorResult& aRv) { + SetUnsignedIntAttr(nsGkAtoms::vspace, aValue, 0, aRv); + } + void GetCodeBase(DOMString& aValue) { + GetURIAttr(nsGkAtoms::codebase, nullptr, aValue); + } + void SetCodeBase(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::codebase, aValue, aRv); + } + void GetCodeType(DOMString& aValue) { + GetHTMLAttr(nsGkAtoms::codetype, aValue); + } + void SetCodeType(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::codetype, aValue, aRv); + } + void GetBorder(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::border, aValue); } + void SetBorder(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::border, aValue, aRv); + } + + Document* GetSVGDocument(nsIPrincipal& aSubjectPrincipal) { + return GetContentDocument(aSubjectPrincipal); + } + + /** + * Calls LoadObject with the correct arguments to start the plugin load. + */ + void StartObjectLoad(bool aNotify, bool aForceLoad); + + protected: + void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) override; + + private: + nsContentPolicyType GetContentPolicyType() const override { + return nsIContentPolicy::TYPE_INTERNAL_OBJECT; + } + + virtual ~HTMLObjectElement(); + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); + + /** + * This function is called by AfterSetAttr and OnAttrSetButNotChanged. + * This function will be called by AfterSetAttr whether the attribute is being + * set or unset. + * + * @param aNamespaceID the namespace of the attr being set + * @param aName the localname of the attribute being set + * @param aNotify Whether we plan to notify document observers. + */ + void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, bool aNotify); + + bool mIsDoneAddingChildren; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLObjectElement_h diff --git a/dom/html/HTMLOptGroupElement.cpp b/dom/html/HTMLOptGroupElement.cpp new file mode 100644 index 0000000000..9a160b430c --- /dev/null +++ b/dom/html/HTMLOptGroupElement.cpp @@ -0,0 +1,115 @@ +/* -*- 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/EventDispatcher.h" +#include "mozilla/Maybe.h" +#include "mozilla/dom/HTMLOptGroupElement.h" +#include "mozilla/dom/HTMLOptGroupElementBinding.h" +#include "mozilla/dom/HTMLSelectElement.h" // SafeOptionListMutation +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsIFrame.h" +#include "nsIFormControlFrame.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(OptGroup) + +namespace mozilla::dom { + +/** + * The implementation of <optgroup> + */ + +HTMLOptGroupElement::HTMLOptGroupElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + // We start off enabled + AddStatesSilently(ElementState::ENABLED); +} + +HTMLOptGroupElement::~HTMLOptGroupElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLOptGroupElement) + +void HTMLOptGroupElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + aVisitor.mCanHandle = false; + + if (nsIFrame* frame = GetPrimaryFrame()) { + // FIXME(emilio): This poking at the style of the frame is broken unless we + // flush before every event handling, which we don't really want to. + if (frame->StyleUI()->UserInput() == StyleUserInput::None) { + return; + } + } + + nsGenericHTMLElement::GetEventTargetParent(aVisitor); +} + +Element* HTMLOptGroupElement::GetSelect() { + Element* parent = nsINode::GetParentElement(); + if (!parent || !parent->IsHTMLElement(nsGkAtoms::select)) { + return nullptr; + } + return parent; +} + +void HTMLOptGroupElement::InsertChildBefore(nsIContent* aKid, + nsIContent* aBeforeThis, + bool aNotify, ErrorResult& aRv) { + const uint32_t index = + aBeforeThis ? *ComputeIndexOf(aBeforeThis) : GetChildCount(); + SafeOptionListMutation safeMutation(GetSelect(), this, aKid, index, aNotify); + nsGenericHTMLElement::InsertChildBefore(aKid, aBeforeThis, aNotify, aRv); + if (aRv.Failed()) { + safeMutation.MutationFailed(); + } +} + +void HTMLOptGroupElement::RemoveChildNode(nsIContent* aKid, bool aNotify) { + SafeOptionListMutation safeMutation(GetSelect(), this, nullptr, + *ComputeIndexOf(aKid), aNotify); + nsGenericHTMLElement::RemoveChildNode(aKid, aNotify); +} + +void HTMLOptGroupElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::disabled) { + ElementState disabledStates; + if (aValue) { + disabledStates |= ElementState::DISABLED; + } else { + disabledStates |= ElementState::ENABLED; + } + + ElementState oldDisabledStates = State() & ElementState::DISABLED_STATES; + ElementState changedStates = disabledStates ^ oldDisabledStates; + + if (!changedStates.IsEmpty()) { + ToggleStates(changedStates, aNotify); + + // All our children <option> have their :disabled state depending on our + // disabled attribute. We should make sure their state is updated. + for (nsIContent* child = nsINode::GetFirstChild(); child; + child = child->GetNextSibling()) { + if (auto optElement = HTMLOptionElement::FromNode(child)) { + optElement->OptGroupDisabledChanged(true); + } + } + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +JSObject* HTMLOptGroupElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLOptGroupElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLOptGroupElement.h b/dom/html/HTMLOptGroupElement.h new file mode 100644 index 0000000000..47596e520e --- /dev/null +++ b/dom/html/HTMLOptGroupElement.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLOptGroupElement_h +#define mozilla_dom_HTMLOptGroupElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla { +class ErrorResult; +class EventChainPreVisitor; +namespace dom { + +class HTMLOptGroupElement final : public nsGenericHTMLElement { + public: + explicit HTMLOptGroupElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLOptGroupElement, optgroup) + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLOptGroupElement, + nsGenericHTMLElement) + + // nsINode + virtual void InsertChildBefore(nsIContent* aKid, nsIContent* aBeforeThis, + bool aNotify, ErrorResult& aRv) override; + virtual void RemoveChildNode(nsIContent* aKid, bool aNotify) override; + + // nsIContent + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) override; + + bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); } + void SetDisabled(bool aValue, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::disabled, aValue, aError); + } + + void GetLabel(nsAString& aValue) const { + GetHTMLAttr(nsGkAtoms::label, aValue); + } + void SetLabel(const nsAString& aLabel, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::label, aLabel, aError); + } + + protected: + virtual ~HTMLOptGroupElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + /** + * Get the select content element that contains this option + * @param aSelectElement the select element [OUT] + */ + Element* GetSelect(); +}; + +} // namespace dom +} // namespace mozilla + +#endif /* mozilla_dom_HTMLOptGroupElement_h */ diff --git a/dom/html/HTMLOptionElement.cpp b/dom/html/HTMLOptionElement.cpp new file mode 100644 index 0000000000..733e15c609 --- /dev/null +++ b/dom/html/HTMLOptionElement.cpp @@ -0,0 +1,348 @@ +/* -*- 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/HTMLOptionElement.h" + +#include "HTMLOptGroupElement.h" +#include "mozilla/dom/HTMLOptionElementBinding.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsIFormControl.h" +#include "nsISelectControlFrame.h" + +// Notify/query select frame for selected state +#include "nsIFormControlFrame.h" +#include "mozilla/dom/Document.h" +#include "nsNodeInfoManager.h" +#include "nsCOMPtr.h" +#include "nsContentCreatorFunctions.h" +#include "mozAutoDocUpdate.h" +#include "nsTextNode.h" + +/** + * Implementation of <option> + */ + +NS_IMPL_NS_NEW_HTML_ELEMENT(Option) + +namespace mozilla::dom { + +HTMLOptionElement::HTMLOptionElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + // We start off enabled + AddStatesSilently(ElementState::ENABLED); +} + +HTMLOptionElement::~HTMLOptionElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLOptionElement) + +mozilla::dom::HTMLFormElement* HTMLOptionElement::GetForm() { + HTMLSelectElement* selectControl = GetSelect(); + return selectControl ? selectControl->GetForm() : nullptr; +} + +void HTMLOptionElement::SetSelectedInternal(bool aValue, bool aNotify) { + mSelectedChanged = true; + SetStates(ElementState::CHECKED, aValue, aNotify); +} + +void HTMLOptionElement::OptGroupDisabledChanged(bool aNotify) { + UpdateDisabledState(aNotify); +} + +void HTMLOptionElement::UpdateDisabledState(bool aNotify) { + bool isDisabled = HasAttr(nsGkAtoms::disabled); + + if (!isDisabled) { + nsIContent* parent = GetParent(); + if (auto optGroupElement = HTMLOptGroupElement::FromNodeOrNull(parent)) { + isDisabled = optGroupElement->IsDisabled(); + } + } + + ElementState disabledStates; + if (isDisabled) { + disabledStates |= ElementState::DISABLED; + } else { + disabledStates |= ElementState::ENABLED; + } + + ElementState oldDisabledStates = State() & ElementState::DISABLED_STATES; + ElementState changedStates = disabledStates ^ oldDisabledStates; + + if (!changedStates.IsEmpty()) { + ToggleStates(changedStates, aNotify); + } +} + +void HTMLOptionElement::SetSelected(bool aValue) { + // Note: The select content obj maintains all the PresState + // so defer to it to get the answer + HTMLSelectElement* selectInt = GetSelect(); + if (selectInt) { + int32_t index = Index(); + HTMLSelectElement::OptionFlags mask{ + HTMLSelectElement::OptionFlag::SetDisabled, + HTMLSelectElement::OptionFlag::Notify}; + if (aValue) { + mask += HTMLSelectElement::OptionFlag::IsSelected; + } + + // This should end up calling SetSelectedInternal + selectInt->SetOptionsSelectedByIndex(index, index, mask); + } else { + SetSelectedInternal(aValue, true); + } +} + +int32_t HTMLOptionElement::Index() { + static int32_t defaultIndex = 0; + + // Only select elements can contain a list of options. + HTMLSelectElement* selectElement = GetSelect(); + if (!selectElement) { + return defaultIndex; + } + + HTMLOptionsCollection* options = selectElement->GetOptions(); + if (!options) { + return defaultIndex; + } + + int32_t index = defaultIndex; + MOZ_ALWAYS_SUCCEEDS(options->GetOptionIndex(this, 0, true, &index)); + return index; +} + +nsChangeHint HTMLOptionElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + nsChangeHint retval = + nsGenericHTMLElement::GetAttributeChangeHint(aAttribute, aModType); + + if (aAttribute == nsGkAtoms::label) { + retval |= nsChangeHint_ReconstructFrame; + } else if (aAttribute == nsGkAtoms::text) { + retval |= NS_STYLE_HINT_REFLOW; + } + return retval; +} + +void HTMLOptionElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + nsGenericHTMLElement::BeforeSetAttr(aNamespaceID, aName, aValue, aNotify); + + if (aNamespaceID != kNameSpaceID_None || aName != nsGkAtoms::selected || + mSelectedChanged) { + return; + } + + // We just changed out selected state (since we look at the "selected" + // attribute when mSelectedChanged is false). Let's tell our select about + // it. + HTMLSelectElement* selectInt = GetSelect(); + if (!selectInt) { + // If option is a child of select, SetOptionsSelectedByIndex will set the + // selected state if needed. + SetStates(ElementState::CHECKED, !!aValue, aNotify); + return; + } + + NS_ASSERTION(!mSelectedChanged, "Shouldn't be here"); + + bool inSetDefaultSelected = mIsInSetDefaultSelected; + mIsInSetDefaultSelected = true; + + int32_t index = Index(); + HTMLSelectElement::OptionFlags mask = + HTMLSelectElement::OptionFlag::SetDisabled; + if (aValue) { + mask += HTMLSelectElement::OptionFlag::IsSelected; + } + + if (aNotify) { + mask += HTMLSelectElement::OptionFlag::Notify; + } + + // This can end up calling SetSelectedInternal if our selected state needs to + // change, which we will allow to take effect so that parts of + // SetOptionsSelectedByIndex that might depend on it working don't get + // confused. + selectInt->SetOptionsSelectedByIndex(index, index, mask); + + // Now reset our members; when we finish the attr set we'll end up with the + // rigt selected state. + mIsInSetDefaultSelected = inSetDefaultSelected; + // the selected state might have been changed by SetOptionsSelectedByIndex, + // possibly more than once; make sure our mSelectedChanged state is set back + // correctly. + mSelectedChanged = false; +} + +void HTMLOptionElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::disabled) { + UpdateDisabledState(aNotify); + } + + if (aName == nsGkAtoms::value && Selected()) { + // Since this option is selected, changing value may have changed missing + // validity state of the select element + if (HTMLSelectElement* select = GetSelect()) { + select->UpdateValueMissingValidityState(); + } + } + + if (aName == nsGkAtoms::selected) { + SetStates(ElementState::DEFAULT, !!aValue, aNotify); + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLOptionElement::GetText(nsAString& aText) { + nsAutoString text; + + nsIContent* child = nsINode::GetFirstChild(); + while (child) { + if (Text* textChild = child->GetAsText()) { + textChild->AppendTextTo(text); + } + if (child->IsHTMLElement(nsGkAtoms::script) || + child->IsSVGElement(nsGkAtoms::script)) { + child = child->GetNextNonChildNode(this); + } else { + child = child->GetNextNode(this); + } + } + + // XXX No CompressWhitespace for nsAString. Sad. + text.CompressWhitespace(true, true); + aText = text; +} + +void HTMLOptionElement::SetText(const nsAString& aText, ErrorResult& aRv) { + aRv = nsContentUtils::SetNodeTextContent(this, aText, false); +} + +nsresult HTMLOptionElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + // Our new parent might change :disabled/:enabled state. + UpdateDisabledState(false); + + return NS_OK; +} + +void HTMLOptionElement::UnbindFromTree(bool aNullParent) { + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + // Our previous parent could have been involved in :disabled/:enabled state. + UpdateDisabledState(false); +} + +// Get the select content element that contains this option +HTMLSelectElement* HTMLOptionElement::GetSelect() { + nsIContent* parent = GetParent(); + if (!parent) { + return nullptr; + } + + HTMLSelectElement* select = HTMLSelectElement::FromNode(parent); + if (select) { + return select; + } + + if (!parent->IsHTMLElement(nsGkAtoms::optgroup)) { + return nullptr; + } + + return HTMLSelectElement::FromNodeOrNull(parent->GetParent()); +} + +already_AddRefed<HTMLOptionElement> HTMLOptionElement::Option( + const GlobalObject& aGlobal, const nsAString& aText, + const Optional<nsAString>& aValue, bool aDefaultSelected, bool aSelected, + ErrorResult& aError) { + nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports()); + Document* doc; + if (!win || !(doc = win->GetExtantDoc())) { + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<mozilla::dom::NodeInfo> nodeInfo = doc->NodeInfoManager()->GetNodeInfo( + nsGkAtoms::option, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE); + + auto* nim = nodeInfo->NodeInfoManager(); + RefPtr<HTMLOptionElement> option = + new (nim) HTMLOptionElement(nodeInfo.forget()); + + if (!aText.IsEmpty()) { + // Create a new text node and append it to the option + RefPtr<nsTextNode> textContent = new (option->NodeInfo()->NodeInfoManager()) + nsTextNode(option->NodeInfo()->NodeInfoManager()); + + textContent->SetText(aText, false); + + option->AppendChildTo(textContent, false, aError); + if (aError.Failed()) { + return nullptr; + } + } + + if (aValue.WasPassed()) { + // Set the value attribute for this element. We're calling SetAttr + // directly because we want to pass aNotify == false. + aError = option->SetAttr(kNameSpaceID_None, nsGkAtoms::value, + aValue.Value(), false); + if (aError.Failed()) { + return nullptr; + } + } + + if (aDefaultSelected) { + // We're calling SetAttr directly because we want to pass + // aNotify == false. + aError = + option->SetAttr(kNameSpaceID_None, nsGkAtoms::selected, u""_ns, false); + if (aError.Failed()) { + return nullptr; + } + } + + option->SetSelected(aSelected); + option->SetSelectedChanged(false); + + return option.forget(); +} + +nsresult HTMLOptionElement::CopyInnerTo(Element* aDest) { + nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest); + NS_ENSURE_SUCCESS(rv, rv); + + if (aDest->OwnerDoc()->IsStaticDocument()) { + static_cast<HTMLOptionElement*>(aDest)->SetSelected(Selected()); + } + return NS_OK; +} + +JSObject* HTMLOptionElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLOptionElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLOptionElement.h b/dom/html/HTMLOptionElement.h new file mode 100644 index 0000000000..4d8e920a52 --- /dev/null +++ b/dom/html/HTMLOptionElement.h @@ -0,0 +1,134 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLOptionElement_h__ +#define mozilla_dom_HTMLOptionElement_h__ + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "mozilla/dom/HTMLFormElement.h" + +namespace mozilla::dom { + +class HTMLSelectElement; + +class HTMLOptionElement final : public nsGenericHTMLElement { + public: + explicit HTMLOptionElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + static already_AddRefed<HTMLOptionElement> Option( + const GlobalObject& aGlobal, const nsAString& aText, + const Optional<nsAString>& aValue, bool aDefaultSelected, bool aSelected, + ErrorResult& aError); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLOptionElement, option) + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLOptionElement, nsGenericHTMLElement) + + using mozilla::dom::Element::GetText; + + bool Selected() const { return State().HasState(ElementState::CHECKED); } + void SetSelected(bool aValue); + + void SetSelectedChanged(bool aValue) { mSelectedChanged = aValue; } + + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + + void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + void SetSelectedInternal(bool aValue, bool aNotify); + + /** + * This callback is called by an optgroup on all its option elements whenever + * its disabled state is changed so that option elements can know their + * disabled state might have changed. + */ + void OptGroupDisabledChanged(bool aNotify); + + /** + * Check our disabled content attribute and optgroup's (if it exists) disabled + * state to decide whether our disabled flag should be toggled. + */ + void UpdateDisabledState(bool aNotify); + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + nsresult CopyInnerTo(mozilla::dom::Element* aDest); + + bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); } + + void SetDisabled(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::disabled, aValue, aRv); + } + + HTMLFormElement* GetForm(); + + void GetRenderedLabel(nsAString& aLabel) { + if (!GetAttr(nsGkAtoms::label, aLabel) || aLabel.IsEmpty()) { + GetText(aLabel); + } + } + + void GetLabel(nsAString& aLabel) { + if (!GetAttr(nsGkAtoms::label, aLabel)) { + GetText(aLabel); + } + } + void SetLabel(const nsAString& aLabel, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::label, aLabel, aError); + } + + bool DefaultSelected() const { return HasAttr(nsGkAtoms::selected); } + void SetDefaultSelected(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::selected, aValue, aRv); + } + + void GetValue(nsAString& aValue) { + if (!GetAttr(nsGkAtoms::value, aValue)) { + GetText(aValue); + } + } + void SetValue(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::value, aValue, aRv); + } + + void GetText(nsAString& aText); + void SetText(const nsAString& aText, ErrorResult& aRv); + + int32_t Index(); + + protected: + virtual ~HTMLOptionElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + /** + * Get the select content element that contains this option, this + * intentionally does not return nsresult, all we care about is if + * there's a select associated with this option or not. + */ + HTMLSelectElement* GetSelect(); + + bool mSelectedChanged = false; + + // True only while we're under the SetOptionsSelectedByIndex call when our + // "selected" attribute is changing and mSelectedChanged is false. + bool mIsInSetDefaultSelected = false; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLOptionElement_h__ diff --git a/dom/html/HTMLOptionsCollection.cpp b/dom/html/HTMLOptionsCollection.cpp new file mode 100644 index 0000000000..be483641d9 --- /dev/null +++ b/dom/html/HTMLOptionsCollection.cpp @@ -0,0 +1,191 @@ +/* -*- 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/HTMLOptionsCollection.h" +#include "mozilla/dom/HTMLOptionsCollectionBinding.h" + +#include "mozilla/dom/HTMLOptionElement.h" +#include "mozilla/dom/HTMLSelectElement.h" + +namespace mozilla::dom { + +HTMLOptionsCollection::HTMLOptionsCollection(HTMLSelectElement* aSelect) + : mSelect(aSelect) {} + +nsresult HTMLOptionsCollection::GetOptionIndex(Element* aOption, + int32_t aStartIndex, + bool aForward, int32_t* aIndex) { + // NOTE: aIndex shouldn't be set if the returned value isn't NS_OK. + + int32_t index; + + // Make the common case fast + if (aStartIndex == 0 && aForward) { + index = mElements.IndexOf(aOption); + if (index == -1) { + return NS_ERROR_FAILURE; + } + + *aIndex = index; + return NS_OK; + } + + int32_t high = mElements.Length(); + int32_t step = aForward ? 1 : -1; + + for (index = aStartIndex; index < high && index > -1; index += step) { + if (mElements[index] == aOption) { + *aIndex = index; + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HTMLOptionsCollection, mElements, mSelect) + +// nsISupports + +// QueryInterface implementation for HTMLOptionsCollection +NS_INTERFACE_TABLE_HEAD(HTMLOptionsCollection) + NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY + NS_INTERFACE_TABLE(HTMLOptionsCollection, nsIHTMLCollection) + NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(HTMLOptionsCollection) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(HTMLOptionsCollection) +NS_IMPL_CYCLE_COLLECTING_RELEASE(HTMLOptionsCollection) + +JSObject* HTMLOptionsCollection::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLOptionsCollection_Binding::Wrap(aCx, this, aGivenProto); +} + +uint32_t HTMLOptionsCollection::Length() { return mElements.Length(); } + +void HTMLOptionsCollection::SetLength(uint32_t aLength, ErrorResult& aError) { + mSelect->SetLength(aLength, aError); +} + +void HTMLOptionsCollection::IndexedSetter(uint32_t aIndex, + HTMLOptionElement* aOption, + ErrorResult& aError) { + // if the new option is null, just remove this option. Note that it's safe + // to pass a too-large aIndex in here. + if (!aOption) { + mSelect->Remove(aIndex); + + // We're done. + return; + } + + // Now we're going to be setting an option in our collection + if (aIndex > mElements.Length()) { + // Fill our array with blank options up to (but not including, since we're + // about to change it) aIndex, for compat with other browsers. + SetLength(aIndex, aError); + ENSURE_SUCCESS_VOID(aError); + } + + NS_ASSERTION(aIndex <= mElements.Length(), "SetLength lied"); + + if (aIndex == mElements.Length()) { + mSelect->AppendChild(*aOption, aError); + return; + } + + // Find the option they're talking about and replace it + // hold a strong reference to follow COM rules. + RefPtr<HTMLOptionElement> refChild = ItemAsOption(aIndex); + if (!refChild) { + aError.Throw(NS_ERROR_UNEXPECTED); + return; + } + + nsCOMPtr<nsINode> parent = refChild->GetParent(); + if (!parent) { + return; + } + + parent->ReplaceChild(*aOption, *refChild, aError); +} + +int32_t HTMLOptionsCollection::SelectedIndex() { + return mSelect->SelectedIndex(); +} + +void HTMLOptionsCollection::SetSelectedIndex(int32_t aSelectedIndex) { + mSelect->SetSelectedIndex(aSelectedIndex); +} + +Element* HTMLOptionsCollection::GetElementAt(uint32_t aIndex) { + return ItemAsOption(aIndex); +} + +HTMLOptionElement* HTMLOptionsCollection::NamedGetter(const nsAString& aName, + bool& aFound) { + uint32_t count = mElements.Length(); + for (uint32_t i = 0; i < count; i++) { + HTMLOptionElement* content = mElements.ElementAt(i); + if (content && (content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, + aName, eCaseMatters) || + content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id, + aName, eCaseMatters))) { + aFound = true; + return content; + } + } + + aFound = false; + return nullptr; +} + +nsINode* HTMLOptionsCollection::GetParentObject() { return mSelect; } + +DocGroup* HTMLOptionsCollection::GetDocGroup() const { + return mSelect ? mSelect->GetDocGroup() : nullptr; +} + +void HTMLOptionsCollection::GetSupportedNames(nsTArray<nsString>& aNames) { + AutoTArray<nsAtom*, 8> atoms; + for (uint32_t i = 0; i < mElements.Length(); ++i) { + HTMLOptionElement* content = mElements.ElementAt(i); + if (content) { + // Note: HasName means the names is exposed on the document, + // which is false for options, so we don't check it here. + const nsAttrValue* val = content->GetParsedAttr(nsGkAtoms::name); + if (val && val->Type() == nsAttrValue::eAtom) { + nsAtom* name = val->GetAtomValue(); + if (!atoms.Contains(name)) { + atoms.AppendElement(name); + } + } + if (content->HasID()) { + nsAtom* id = content->GetID(); + if (!atoms.Contains(id)) { + atoms.AppendElement(id); + } + } + } + } + + uint32_t atomsLen = atoms.Length(); + nsString* names = aNames.AppendElements(atomsLen); + for (uint32_t i = 0; i < atomsLen; ++i) { + atoms[i]->ToString(names[i]); + } +} + +void HTMLOptionsCollection::Add(const HTMLOptionOrOptGroupElement& aElement, + const Nullable<HTMLElementOrLong>& aBefore, + ErrorResult& aError) { + mSelect->Add(aElement, aBefore, aError); +} + +void HTMLOptionsCollection::Remove(int32_t aIndex) { mSelect->Remove(aIndex); } + +} // namespace mozilla::dom diff --git a/dom/html/HTMLOptionsCollection.h b/dom/html/HTMLOptionsCollection.h new file mode 100644 index 0000000000..e4300c876d --- /dev/null +++ b/dom/html/HTMLOptionsCollection.h @@ -0,0 +1,150 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLOptionsCollection_h +#define mozilla_dom_HTMLOptionsCollection_h + +#include "mozilla/Attributes.h" +#include "nsIHTMLCollection.h" +#include "nsWrapperCache.h" + +#include "mozilla/dom/HTMLOptionElement.h" +#include "nsCOMPtr.h" +#include "nsError.h" +#include "nsGenericHTMLElement.h" +#include "nsTArray.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class DocGroup; +class HTMLElementOrLong; +class HTMLOptionElementOrHTMLOptGroupElement; +class HTMLSelectElement; + +/** + * The collection of options in the select (what you get back when you do + * select.options in DOM) + */ +class HTMLOptionsCollection final : public nsIHTMLCollection, + public nsWrapperCache { + typedef HTMLOptionElementOrHTMLOptGroupElement HTMLOptionOrOptGroupElement; + + public: + explicit HTMLOptionsCollection(HTMLSelectElement* aSelect); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + // nsWrapperCache + using nsWrapperCache::GetWrapper; + using nsWrapperCache::GetWrapperPreserveColor; + using nsWrapperCache::PreserveWrapper; + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + virtual ~HTMLOptionsCollection() = default; + + virtual JSObject* GetWrapperPreserveColorInternal() override { + return nsWrapperCache::GetWrapperPreserveColor(); + } + virtual void PreserveWrapperInternal( + nsISupports* aScriptObjectHolder) override { + nsWrapperCache::PreserveWrapper(aScriptObjectHolder); + } + + public: + virtual uint32_t Length() override; + virtual Element* GetElementAt(uint32_t aIndex) override; + virtual nsINode* GetParentObject() override; + DocGroup* GetDocGroup() const; + + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(HTMLOptionsCollection, + nsIHTMLCollection) + + // Helpers for HTMLSelectElement + /** + * Insert an option + * @param aOption the option to insert + * @param aIndex the index to insert at + */ + void InsertOptionAt(mozilla::dom::HTMLOptionElement* aOption, + uint32_t aIndex) { + mElements.InsertElementAt(aIndex, aOption); + } + + /** + * Remove an option + * @param aIndex the index of the option to remove + */ + void RemoveOptionAt(uint32_t aIndex) { mElements.RemoveElementAt(aIndex); } + + /** + * Get the option at the index + * @param aIndex the index + * @param aReturn the option returned [OUT] + */ + mozilla::dom::HTMLOptionElement* ItemAsOption(uint32_t aIndex) { + return mElements.SafeElementAt(aIndex, nullptr); + } + + /** + * Clears out all options + */ + void Clear() { mElements.Clear(); } + + /** + * Append an option to end of array + */ + void AppendOption(mozilla::dom::HTMLOptionElement* aOption) { + mElements.AppendElement(aOption); + } + + /** + * Finds the index of a given option element. + * If the option isn't part of the collection, return NS_ERROR_FAILURE + * without setting aIndex. + * + * @param aOption the option to get the index of + * @param aStartIndex the index to start looking at + * @param aForward TRUE to look forward, FALSE to look backward + * @return the option index + */ + nsresult GetOptionIndex(Element* aOption, int32_t aStartIndex, bool aForward, + int32_t* aIndex); + + HTMLOptionElement* GetNamedItem(const nsAString& aName) { + bool dummy; + return NamedGetter(aName, dummy); + } + HTMLOptionElement* NamedGetter(const nsAString& aName, bool& aFound); + virtual Element* GetFirstNamedElement(const nsAString& aName, + bool& aFound) override { + return NamedGetter(aName, aFound); + } + void Add(const HTMLOptionOrOptGroupElement& aElement, + const Nullable<HTMLElementOrLong>& aBefore, ErrorResult& aError); + void Remove(int32_t aIndex); + int32_t SelectedIndex(); + void SetSelectedIndex(int32_t aSelectedIndex); + void IndexedSetter(uint32_t aIndex, HTMLOptionElement* aOption, + ErrorResult& aError); + virtual void GetSupportedNames(nsTArray<nsString>& aNames) override; + void SetLength(uint32_t aLength, ErrorResult& aError); + + private: + /** The list of options (holds strong references). This is infallible, so + * various members such as InsertOptionAt are also infallible. */ + nsTArray<RefPtr<mozilla::dom::HTMLOptionElement> > mElements; + /** The select element that contains this array */ + RefPtr<HTMLSelectElement> mSelect; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_HTMLOptionsCollection_h diff --git a/dom/html/HTMLOutputElement.cpp b/dom/html/HTMLOutputElement.cpp new file mode 100644 index 0000000000..8a45a552f2 --- /dev/null +++ b/dom/html/HTMLOutputElement.cpp @@ -0,0 +1,137 @@ +/* -*- 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/HTMLOutputElement.h" + +#include "mozAutoDocUpdate.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "mozilla/dom/HTMLOutputElementBinding.h" +#include "nsContentUtils.h" +#include "nsDOMTokenList.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Output) + +namespace mozilla::dom { + +HTMLOutputElement::HTMLOutputElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLFormControlElement(std::move(aNodeInfo), + FormControlType::Output), + mValueModeFlag(eModeDefault), + mIsDoneAddingChildren(!aFromParser) { + AddMutationObserver(this); + + // <output> is always barred from constraint validation since it is not a + // submittable element. + SetBarredFromConstraintValidation(true); +} + +HTMLOutputElement::~HTMLOutputElement() = default; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLOutputElement, + nsGenericHTMLFormControlElement, mValidity, + mTokenList) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLOutputElement, + nsGenericHTMLFormControlElement, + nsIMutationObserver, + nsIConstraintValidation) + +NS_IMPL_ELEMENT_CLONE(HTMLOutputElement) + +void HTMLOutputElement::SetCustomValidity(const nsAString& aError) { + ConstraintValidation::SetCustomValidity(aError); +} + +NS_IMETHODIMP +HTMLOutputElement::Reset() { + mValueModeFlag = eModeDefault; + // We can't pass mDefaultValue, because it'll be truncated when + // the element's descendants are changed, so pass a copy instead. + const nsAutoString currentDefaultValue(mDefaultValue); + return nsContentUtils::SetNodeTextContent(this, currentDefaultValue, true); +} + +bool HTMLOutputElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::_for) { + aResult.ParseAtomArray(aValue); + return true; + } + } + + return nsGenericHTMLFormControlElement::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +void HTMLOutputElement::DoneAddingChildren(bool aHaveNotified) { + mIsDoneAddingChildren = true; + // We should update DefaultValue, after parsing is done. + DescendantsChanged(); +} + +void HTMLOutputElement::GetValue(nsAString& aValue) const { + nsContentUtils::GetNodeTextContent(this, true, aValue); +} + +void HTMLOutputElement::SetValue(const nsAString& aValue, ErrorResult& aRv) { + mValueModeFlag = eModeValue; + aRv = nsContentUtils::SetNodeTextContent(this, aValue, true); +} + +void HTMLOutputElement::SetDefaultValue(const nsAString& aDefaultValue, + ErrorResult& aRv) { + mDefaultValue = aDefaultValue; + if (mValueModeFlag == eModeDefault) { + // We can't pass mDefaultValue, because it'll be truncated when + // the element's descendants are changed. + aRv = nsContentUtils::SetNodeTextContent(this, aDefaultValue, true); + } +} + +nsDOMTokenList* HTMLOutputElement::HtmlFor() { + if (!mTokenList) { + mTokenList = new nsDOMTokenList(this, nsGkAtoms::_for); + } + return mTokenList; +} + +void HTMLOutputElement::DescendantsChanged() { + if (mIsDoneAddingChildren && mValueModeFlag == eModeDefault) { + nsContentUtils::GetNodeTextContent(this, true, mDefaultValue); + } +} + +// nsIMutationObserver + +void HTMLOutputElement::CharacterDataChanged(nsIContent* aContent, + const CharacterDataChangeInfo&) { + DescendantsChanged(); +} + +void HTMLOutputElement::ContentAppended(nsIContent* aFirstNewContent) { + DescendantsChanged(); +} + +void HTMLOutputElement::ContentInserted(nsIContent* aChild) { + DescendantsChanged(); +} + +void HTMLOutputElement::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + DescendantsChanged(); +} + +JSObject* HTMLOutputElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLOutputElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLOutputElement.h b/dom/html/HTMLOutputElement.h new file mode 100644 index 0000000000..8a658a1594 --- /dev/null +++ b/dom/html/HTMLOutputElement.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLOutputElement_h +#define mozilla_dom_HTMLOutputElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/ConstraintValidation.h" +#include "nsGenericHTMLElement.h" +#include "nsStubMutationObserver.h" + +namespace mozilla::dom { + +class FormData; + +class HTMLOutputElement final : public nsGenericHTMLFormControlElement, + public nsStubMutationObserver, + public ConstraintValidation { + public: + using ConstraintValidation::GetValidationMessage; + + explicit HTMLOutputElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser = NOT_FROM_PARSER); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + // nsIFormControl + NS_IMETHOD Reset() override; + // The output element is not submittable. + NS_IMETHOD SubmitNamesValues(FormData* aFormData) override { return NS_OK; } + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + void DoneAddingChildren(bool aHaveNotified) override; + + // This function is called when a callback function from nsIMutationObserver + // has to be used to update the defaultValue attribute. + void DescendantsChanged(); + + // nsIMutationObserver + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLOutputElement, + nsGenericHTMLFormControlElement) + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + // WebIDL + nsDOMTokenList* HtmlFor(); + + void GetName(nsAString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); } + + void SetName(const nsAString& aName, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aName, aRv); + } + + void GetType(nsAString& aType) { aType.AssignLiteral("output"); } + + void GetDefaultValue(nsAString& aDefaultValue) { + aDefaultValue = mDefaultValue; + } + + void SetDefaultValue(const nsAString& aDefaultValue, ErrorResult& aRv); + + void GetValue(nsAString& aValue) const; + void SetValue(const nsAString& aValue, ErrorResult& aRv); + + // nsIConstraintValidation::WillValidate is fine. + // nsIConstraintValidation::Validity() is fine. + // nsIConstraintValidation::GetValidationMessage() is fine. + // nsIConstraintValidation::CheckValidity() is fine. + void SetCustomValidity(const nsAString& aError); + + protected: + virtual ~HTMLOutputElement(); + + enum ValueModeFlag { eModeDefault, eModeValue }; + + ValueModeFlag mValueModeFlag; + bool mIsDoneAddingChildren; + nsString mDefaultValue; + RefPtr<nsDOMTokenList> mTokenList; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLOutputElement_h diff --git a/dom/html/HTMLParagraphElement.cpp b/dom/html/HTMLParagraphElement.cpp new file mode 100644 index 0000000000..19f732446d --- /dev/null +++ b/dom/html/HTMLParagraphElement.cpp @@ -0,0 +1,60 @@ +/* -*- 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/HTMLParagraphElement.h" +#include "mozilla/dom/HTMLParagraphElementBinding.h" + +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsStyleConsts.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Paragraph) + +namespace mozilla::dom { + +HTMLParagraphElement::~HTMLParagraphElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLParagraphElement) + +bool HTMLParagraphElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aAttribute == nsGkAtoms::align && aNamespaceID == kNameSpaceID_None) { + return ParseDivAlignValue(aValue, aResult); + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLParagraphElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLParagraphElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = { + sDivAlignAttributeMap, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLParagraphElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +JSObject* HTMLParagraphElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLParagraphElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLParagraphElement.h b/dom/html/HTMLParagraphElement.h new file mode 100644 index 0000000000..e768465760 --- /dev/null +++ b/dom/html/HTMLParagraphElement.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLParagraphElement_h +#define mozilla_dom_HTMLParagraphElement_h + +#include "mozilla/Attributes.h" + +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLParagraphElement final : public nsGenericHTMLElement { + public: + explicit HTMLParagraphElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLParagraphElement, + nsGenericHTMLElement) + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // WebIDL API + void GetAlign(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::align, aValue); } + void SetAlign(const nsAString& aValue, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::align, aValue, rv); + } + + protected: + virtual ~HTMLParagraphElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLParagraphElement_h diff --git a/dom/html/HTMLPictureElement.cpp b/dom/html/HTMLPictureElement.cpp new file mode 100644 index 0000000000..45b1e4e3e3 --- /dev/null +++ b/dom/html/HTMLPictureElement.cpp @@ -0,0 +1,79 @@ +/* -*- 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/HTMLPictureElement.h" +#include "mozilla/dom/HTMLPictureElementBinding.h" +#include "mozilla/dom/HTMLImageElement.h" +#include "mozilla/dom/HTMLSourceElement.h" + +// Expand NS_IMPL_NS_NEW_HTML_ELEMENT(Picture) to add pref check. +nsGenericHTMLElement* NS_NewHTMLPictureElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) mozilla::dom::HTMLPictureElement(nodeInfo.forget()); +} + +namespace mozilla::dom { + +HTMLPictureElement::HTMLPictureElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLPictureElement::~HTMLPictureElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLPictureElement) + +void HTMLPictureElement::RemoveChildNode(nsIContent* aKid, bool aNotify) { + MOZ_ASSERT(aKid); + + if (auto* img = HTMLImageElement::FromNode(aKid)) { + img->PictureSourceRemoved(); + } else if (auto* source = HTMLSourceElement::FromNode(aKid)) { + // Find all img siblings after this <source> to notify them of its demise + nsCOMPtr<nsIContent> nextSibling = source->GetNextSibling(); + if (nextSibling && nextSibling->GetParentNode() == this) { + do { + if (auto* img = HTMLImageElement::FromNode(nextSibling)) { + img->PictureSourceRemoved(source); + } + } while ((nextSibling = nextSibling->GetNextSibling())); + } + } + + nsGenericHTMLElement::RemoveChildNode(aKid, aNotify); +} + +void HTMLPictureElement::InsertChildBefore(nsIContent* aKid, + nsIContent* aBeforeThis, + bool aNotify, ErrorResult& aRv) { + nsGenericHTMLElement::InsertChildBefore(aKid, aBeforeThis, aNotify, aRv); + if (aRv.Failed() || !aKid) { + return; + } + + if (auto* img = HTMLImageElement::FromNode(aKid)) { + img->PictureSourceAdded(); + } else if (auto* source = HTMLSourceElement::FromNode(aKid)) { + // Find all img siblings after this <source> to notify them of its insertion + nsCOMPtr<nsIContent> nextSibling = source->GetNextSibling(); + if (nextSibling && nextSibling->GetParentNode() == this) { + do { + if (auto* img = HTMLImageElement::FromNode(nextSibling)) { + img->PictureSourceAdded(source); + } + } while ((nextSibling = nextSibling->GetNextSibling())); + } + } +} + +JSObject* HTMLPictureElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLPictureElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLPictureElement.h b/dom/html/HTMLPictureElement.h new file mode 100644 index 0000000000..5ec2a3abd0 --- /dev/null +++ b/dom/html/HTMLPictureElement.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLPictureElement_h +#define mozilla_dom_HTMLPictureElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla { +class ErrorResult; +namespace dom { + +class HTMLPictureElement final : public nsGenericHTMLElement { + public: + explicit HTMLPictureElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLPictureElement, nsGenericHTMLElement) + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + virtual void RemoveChildNode(nsIContent* aKid, bool aNotify) override; + virtual void InsertChildBefore(nsIContent* aKid, nsIContent* aBeforeThis, + bool aNotify, ErrorResult& aRv) override; + + protected: + virtual ~HTMLPictureElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_HTMLPictureElement_h diff --git a/dom/html/HTMLPreElement.cpp b/dom/html/HTMLPreElement.cpp new file mode 100644 index 0000000000..13628400d0 --- /dev/null +++ b/dom/html/HTMLPreElement.cpp @@ -0,0 +1,83 @@ +/* -*- 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/HTMLPreElement.h" +#include "mozilla/dom/HTMLPreElementBinding.h" + +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsAttrValueInlines.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Pre) + +namespace mozilla::dom { + +HTMLPreElement::~HTMLPreElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLPreElement) + +bool HTMLPreElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::width) { + return aResult.ParseIntValue(aValue); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLPreElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + // wrap: empty + if (aBuilder.GetAttr(nsGkAtoms::wrap)) { + // Equivalent to expanding `white-space: pre-wrap` + aBuilder.SetKeywordValue(eCSSProperty_white_space_collapse, + StyleWhiteSpaceCollapse::Preserve); + aBuilder.SetKeywordValue(eCSSProperty_text_wrap_mode, + StyleTextWrapMode::Wrap); + } + + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLPreElement::IsAttributeMapped(const nsAtom* aAttribute) const { + if (!mNodeInfo->Equals(nsGkAtoms::pre)) { + return nsGenericHTMLElement::IsAttributeMapped(aAttribute); + } + + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::wrap}, + {nullptr}, + }; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLPreElement::GetAttributeMappingFunction() const { + if (!mNodeInfo->Equals(nsGkAtoms::pre)) { + return nsGenericHTMLElement::GetAttributeMappingFunction(); + } + + return &MapAttributesIntoRule; +} + +JSObject* HTMLPreElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLPreElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLPreElement.h b/dom/html/HTMLPreElement.h new file mode 100644 index 0000000000..4841f2ff15 --- /dev/null +++ b/dom/html/HTMLPreElement.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLPreElement_h +#define mozilla_dom_HTMLPreElement_h + +#include "mozilla/Attributes.h" + +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLPreElement final : public nsGenericHTMLElement { + public: + explicit HTMLPreElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLPreElement, nsGenericHTMLElement) + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // WebIDL API + int32_t Width() const { return GetIntAttr(nsGkAtoms::width, 0); } + void SetWidth(int32_t aWidth, mozilla::ErrorResult& rv) { + rv = SetIntAttr(nsGkAtoms::width, aWidth); + } + + protected: + virtual ~HTMLPreElement(); + + JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*>) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLPreElement_h diff --git a/dom/html/HTMLProgressElement.cpp b/dom/html/HTMLProgressElement.cpp new file mode 100644 index 0000000000..6ad70c1f39 --- /dev/null +++ b/dom/html/HTMLProgressElement.cpp @@ -0,0 +1,87 @@ +/* -*- 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/HTMLProgressElement.h" +#include "mozilla/dom/HTMLProgressElementBinding.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Progress) + +namespace mozilla::dom { + +HTMLProgressElement::HTMLProgressElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + // We start out indeterminate + AddStatesSilently(ElementState::INDETERMINATE); +} + +HTMLProgressElement::~HTMLProgressElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLProgressElement) + +bool HTMLProgressElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::value || aAttribute == nsGkAtoms::max) { + return aResult.ParseDoubleValue(aValue); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLProgressElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::value) { + const bool indeterminate = + !aValue || aValue->Type() != nsAttrValue::eDoubleValue; + SetStates(ElementState::INDETERMINATE, indeterminate, aNotify); + } + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +double HTMLProgressElement::Value() const { + const nsAttrValue* attrValue = mAttrs.GetAttr(nsGkAtoms::value); + if (!attrValue || attrValue->Type() != nsAttrValue::eDoubleValue || + attrValue->GetDoubleValue() < 0.0) { + return 0.0; + } + + return std::min(attrValue->GetDoubleValue(), Max()); +} + +double HTMLProgressElement::Max() const { + const nsAttrValue* attrMax = mAttrs.GetAttr(nsGkAtoms::max); + if (!attrMax || attrMax->Type() != nsAttrValue::eDoubleValue || + attrMax->GetDoubleValue() <= 0.0) { + return 1.0; + } + + return attrMax->GetDoubleValue(); +} + +double HTMLProgressElement::Position() const { + if (State().HasState(ElementState::INDETERMINATE)) { + return -1.0; + } + + return Value() / Max(); +} + +JSObject* HTMLProgressElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLProgressElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLProgressElement.h b/dom/html/HTMLProgressElement.h new file mode 100644 index 0000000000..e0de536282 --- /dev/null +++ b/dom/html/HTMLProgressElement.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLProgressElement_h +#define mozilla_dom_HTMLProgressElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsAttrValue.h" +#include "nsAttrValueInlines.h" +#include <algorithm> + +namespace mozilla::dom { + +class HTMLProgressElement final : public nsGenericHTMLElement { + public: + explicit HTMLProgressElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + // WebIDL + double Value() const; + void SetValue(double aValue, ErrorResult& aRv) { + SetDoubleAttr(nsGkAtoms::value, aValue, aRv); + } + double Max() const; + void SetMax(double aValue, ErrorResult& aRv) { + // https://html.spec.whatwg.org/multipage/form-elements.html#dom-progress-max + // The max IDL attribute must reflect the content attribute of the same + // name, limited to only positive numbers. + SetDoubleAttr<Reflection::OnlyPositive>(nsGkAtoms::max, aValue, aRv); + } + double Position() const; + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLProgressElement, progress); + + protected: + virtual ~HTMLProgressElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLProgressElement_h diff --git a/dom/html/HTMLScriptElement.cpp b/dom/html/HTMLScriptElement.cpp new file mode 100644 index 0000000000..006bff23a7 --- /dev/null +++ b/dom/html/HTMLScriptElement.cpp @@ -0,0 +1,254 @@ +/* -*- 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 "nsAttrValue.h" +#include "nsAttrValueOrString.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "mozilla/dom/Document.h" +#include "nsNetUtil.h" +#include "nsContentUtils.h" +#include "nsUnicharUtils.h" // for nsCaseInsensitiveStringComparator() +#include "nsIScriptContext.h" +#include "nsIScriptGlobalObject.h" +#include "nsServiceManagerUtils.h" +#include "nsError.h" +#include "nsTArray.h" +#include "nsDOMJSUtils.h" +#include "nsIScriptError.h" +#include "nsISupportsImpl.h" +#include "nsDOMTokenList.h" +#include "mozilla/dom/FetchPriority.h" +#include "mozilla/dom/HTMLScriptElement.h" +#include "mozilla/dom/HTMLScriptElementBinding.h" +#include "mozilla/Assertions.h" +#include "mozilla/StaticPrefs_dom.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Script) + +using JS::loader::ScriptKind; + +namespace mozilla::dom { + +JSObject* HTMLScriptElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLScriptElement_Binding::Wrap(aCx, this, aGivenProto); +} + +HTMLScriptElement::HTMLScriptElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLElement(std::move(aNodeInfo)), ScriptElement(aFromParser) { + AddMutationObserver(this); +} + +HTMLScriptElement::~HTMLScriptElement() = default; + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLScriptElement, + nsGenericHTMLElement, + nsIScriptLoaderObserver, + nsIScriptElement, + nsIMutationObserver) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLScriptElement, nsGenericHTMLElement, + mBlocking) + +nsresult HTMLScriptElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInComposedDoc()) { + MaybeProcessScript(); + } + + return NS_OK; +} + +bool HTMLScriptElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::crossorigin) { + ParseCORSValue(aValue, aResult); + return true; + } + + if (aAttribute == nsGkAtoms::integrity) { + aResult.ParseStringOrAtom(aValue); + return true; + } + + if (aAttribute == nsGkAtoms::fetchpriority) { + ParseFetchPriority(aValue, aResult); + return true; + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +nsresult HTMLScriptElement::Clone(dom::NodeInfo* aNodeInfo, + nsINode** aResult) const { + *aResult = nullptr; + + HTMLScriptElement* it = new (aNodeInfo->NodeInfoManager()) + HTMLScriptElement(do_AddRef(aNodeInfo), NOT_FROM_PARSER); + + nsCOMPtr<nsINode> kungFuDeathGrip = it; + nsresult rv = const_cast<HTMLScriptElement*>(this)->CopyInnerTo(it); + NS_ENSURE_SUCCESS(rv, rv); + + // The clone should be marked evaluated if we are. + it->mAlreadyStarted = mAlreadyStarted; + it->mLineNumber = mLineNumber; + it->mMalformed = mMalformed; + + kungFuDeathGrip.swap(*aResult); + + return NS_OK; +} + +void HTMLScriptElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (nsGkAtoms::async == aName && kNameSpaceID_None == aNamespaceID) { + mForceAsync = false; + } + if (nsGkAtoms::src == aName && kNameSpaceID_None == aNamespaceID) { + mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal( + this, aValue ? aValue->GetStringValue() : EmptyString(), + aMaybeScriptedPrincipal); + } + return nsGenericHTMLElement::AfterSetAttr( + aNamespaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLScriptElement::GetInnerHTML(nsAString& aInnerHTML, + OOMReporter& aError) { + if (!nsContentUtils::GetNodeTextContent(this, false, aInnerHTML, fallible)) { + aError.ReportOOM(); + } +} + +void HTMLScriptElement::SetInnerHTML(const nsAString& aInnerHTML, + nsIPrincipal* aScriptedPrincipal, + ErrorResult& aError) { + aError = nsContentUtils::SetNodeTextContent(this, aInnerHTML, true); +} + +void HTMLScriptElement::GetText(nsAString& aValue, ErrorResult& aRv) const { + if (!nsContentUtils::GetNodeTextContent(this, false, aValue, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + } +} + +void HTMLScriptElement::SetText(const nsAString& aValue, ErrorResult& aRv) { + aRv = nsContentUtils::SetNodeTextContent(this, aValue, true); +} + +// variation of this code in SVGScriptElement - check if changes +// need to be transfered when modifying + +void HTMLScriptElement::GetScriptText(nsAString& text) const { + GetText(text, IgnoreErrors()); +} + +void HTMLScriptElement::GetScriptCharset(nsAString& charset) { + GetCharset(charset); +} + +void HTMLScriptElement::FreezeExecutionAttrs(const Document* aOwnerDoc) { + if (mFrozen) { + return; + } + + // Determine whether this is a(n) classic/module/importmap script. + DetermineKindFromType(aOwnerDoc); + + // variation of this code in SVGScriptElement - check if changes + // need to be transfered when modifying. Note that we don't use GetSrc here + // because it will return the base URL when the attr value is "". + nsAutoString src; + if (GetAttr(nsGkAtoms::src, src)) { + // Empty src should be treated as invalid URL. + if (!src.IsEmpty()) { + nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(mUri), src, + OwnerDoc(), GetBaseURI()); + + if (!mUri) { + AutoTArray<nsString, 2> params = {u"src"_ns, src}; + + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "HTML"_ns, OwnerDoc(), + nsContentUtils::eDOM_PROPERTIES, "ScriptSourceInvalidUri", params, + nullptr, u""_ns, GetScriptLineNumber(), + GetScriptColumnNumber().oneOriginValue()); + } + } else { + AutoTArray<nsString, 1> params = {u"src"_ns}; + + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "HTML"_ns, OwnerDoc(), + nsContentUtils::eDOM_PROPERTIES, "ScriptSourceEmpty", params, nullptr, + u""_ns, GetScriptLineNumber(), + GetScriptColumnNumber().oneOriginValue()); + } + + // At this point mUri will be null for invalid URLs. + mExternal = true; + } + + bool async = (mExternal || mKind == ScriptKind::eModule) && Async(); + bool defer = mExternal && Defer(); + + mDefer = !async && defer; + mAsync = async; + + mFrozen = true; +} + +CORSMode HTMLScriptElement::GetCORSMode() const { + return AttrValueToCORSMode(GetParsedAttr(nsGkAtoms::crossorigin)); +} + +FetchPriority HTMLScriptElement::GetFetchPriority() const { + return nsGenericHTMLElement::GetFetchPriority(); +} + +mozilla::dom::ReferrerPolicy HTMLScriptElement::GetReferrerPolicy() { + return GetReferrerPolicyAsEnum(); +} + +bool HTMLScriptElement::HasScriptContent() { + return (mFrozen ? mExternal : HasAttr(nsGkAtoms::src)) || + nsContentUtils::HasNonEmptyTextContent(this); +} + +// https://html.spec.whatwg.org/multipage/scripting.html#dom-script-supports +/* static */ +bool HTMLScriptElement::Supports(const GlobalObject& aGlobal, + const nsAString& aType) { + nsAutoString type(aType); + return aType.EqualsLiteral("classic") || aType.EqualsLiteral("module") || + + aType.EqualsLiteral("importmap"); +} + +nsDOMTokenList* HTMLScriptElement::Blocking() { + if (!mBlocking) { + mBlocking = + new nsDOMTokenList(this, nsGkAtoms::blocking, sSupportedBlockingValues); + } + return mBlocking; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLScriptElement.h b/dom/html/HTMLScriptElement.h new file mode 100644 index 0000000000..db09e247bc --- /dev/null +++ b/dom/html/HTMLScriptElement.h @@ -0,0 +1,167 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLScriptElement_h +#define mozilla_dom_HTMLScriptElement_h + +#include "mozilla/dom/FetchPriority.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/ScriptElement.h" +#include "nsGenericHTMLElement.h" +#include "nsStringFwd.h" + +class nsDOMTokenList; + +namespace mozilla::dom { + +class HTMLScriptElement final : public nsGenericHTMLElement, + public ScriptElement { + public: + using Element::GetText; + + HTMLScriptElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + // CC + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLScriptElement, + nsGenericHTMLElement) + + void GetInnerHTML(nsAString& aInnerHTML, OOMReporter& aError) override; + virtual void SetInnerHTML(const nsAString& aInnerHTML, + nsIPrincipal* aSubjectPrincipal, + mozilla::ErrorResult& aError) override; + + // nsIScriptElement + virtual void GetScriptText(nsAString& text) const override; + virtual void GetScriptCharset(nsAString& charset) override; + virtual void FreezeExecutionAttrs(const Document* aOwnerDoc) override; + virtual CORSMode GetCORSMode() const override; + virtual FetchPriority GetFetchPriority() const override; + virtual mozilla::dom::ReferrerPolicy GetReferrerPolicy() override; + + // nsIContent + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // Element + virtual void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + + // WebIDL + void GetText(nsAString& aValue, ErrorResult& aRv) const; + + void SetText(const nsAString& aValue, ErrorResult& aRv); + + void GetCharset(nsAString& aCharset) { + GetHTMLAttr(nsGkAtoms::charset, aCharset); + } + void SetCharset(const nsAString& aCharset, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::charset, aCharset, aRv); + } + + bool Defer() { return GetBoolAttr(nsGkAtoms::defer); } + void SetDefer(bool aDefer, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::defer, aDefer, aRv); + } + + void GetSrc(nsAString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); } + void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal, + ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, aRv); + } + + void GetType(nsAString& aType) { GetHTMLAttr(nsGkAtoms::type, aType); } + void SetType(const nsAString& aType, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::type, aType, aRv); + } + + void GetHtmlFor(nsAString& aHtmlFor) { + GetHTMLAttr(nsGkAtoms::_for, aHtmlFor); + } + void SetHtmlFor(const nsAString& aHtmlFor, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::_for, aHtmlFor, aRv); + } + + void GetEvent(nsAString& aEvent) { GetHTMLAttr(nsGkAtoms::event, aEvent); } + void SetEvent(const nsAString& aEvent, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::event, aEvent, aRv); + } + + bool Async() { return mForceAsync || GetBoolAttr(nsGkAtoms::async); } + + void SetAsync(bool aValue, ErrorResult& aRv) { + mForceAsync = false; + SetHTMLBoolAttr(nsGkAtoms::async, aValue, aRv); + } + + bool NoModule() { return GetBoolAttr(nsGkAtoms::nomodule); } + + void SetNoModule(bool aValue, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::nomodule, aValue, aRv); + } + + void GetCrossOrigin(nsAString& aResult) { + // Null for both missing and invalid defaults is ok, since we + // always parse to an enum value, so we don't need an invalid + // default, and we _want_ the missing default to be null. + GetEnumAttr(nsGkAtoms::crossorigin, nullptr, aResult); + } + void SetCrossOrigin(const nsAString& aCrossOrigin, ErrorResult& aError) { + SetOrRemoveNullableStringAttr(nsGkAtoms::crossorigin, aCrossOrigin, aError); + } + void GetIntegrity(nsAString& aIntegrity) { + GetHTMLAttr(nsGkAtoms::integrity, aIntegrity); + } + void SetIntegrity(const nsAString& aIntegrity, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::integrity, aIntegrity, aRv); + } + void SetReferrerPolicy(const nsAString& aReferrerPolicy, + ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::referrerpolicy, aReferrerPolicy, aError); + } + void GetReferrerPolicy(nsAString& aReferrerPolicy) { + GetEnumAttr(nsGkAtoms::referrerpolicy, "", aReferrerPolicy); + } + + nsDOMTokenList* Blocking(); + + // Required for the webidl-binding because `GetFetchPriority` is overloaded. + using nsGenericHTMLElement::GetFetchPriority; + + [[nodiscard]] static bool Supports(const GlobalObject& aGlobal, + const nsAString& aType); + + protected: + virtual ~HTMLScriptElement(); + + virtual bool GetAsyncState() override { return Async(); } + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // nsIScriptElement + nsIContent* GetAsContent() override { return this; } + + // ScriptElement + virtual bool HasScriptContent() override; + + RefPtr<nsDOMTokenList> mBlocking; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLScriptElement_h diff --git a/dom/html/HTMLSelectElement.cpp b/dom/html/HTMLSelectElement.cpp new file mode 100644 index 0000000000..18bf2b79b2 --- /dev/null +++ b/dom/html/HTMLSelectElement.cpp @@ -0,0 +1,1645 @@ +/* -*- 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/HTMLSelectElement.h" + +#include "mozAutoDocUpdate.h" +#include "mozilla/Attributes.h" +#include "mozilla/BasicEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/HTMLOptGroupElement.h" +#include "mozilla/dom/HTMLOptionElement.h" +#include "mozilla/dom/HTMLSelectElementBinding.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/WindowGlobalChild.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/Maybe.h" +#include "mozilla/Unused.h" +#include "nsContentCreatorFunctions.h" +#include "nsContentList.h" +#include "nsContentUtils.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsComboboxControlFrame.h" +#include "mozilla/dom/Document.h" +#include "nsIFormControlFrame.h" +#include "nsIFrame.h" +#include "nsListControlFrame.h" +#include "nsISelectControlFrame.h" +#include "nsLayoutUtils.h" +#include "mozilla/PresState.h" +#include "nsServiceManagerUtils.h" +#include "nsStyleConsts.h" +#include "nsTextNode.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(Select) + +namespace mozilla::dom { + +//---------------------------------------------------------------------- +// +// SafeOptionListMutation +// + +SafeOptionListMutation::SafeOptionListMutation(nsIContent* aSelect, + nsIContent* aParent, + nsIContent* aKid, + uint32_t aIndex, bool aNotify) + : mSelect(HTMLSelectElement::FromNodeOrNull(aSelect)), + mTopLevelMutation(false), + mNeedsRebuild(false), + mNotify(aNotify) { + if (mSelect) { + mInitialSelectedOption = mSelect->Item(mSelect->SelectedIndex()); + mTopLevelMutation = !mSelect->mMutating; + if (mTopLevelMutation) { + mSelect->mMutating = true; + } else { + // This is very unfortunate, but to handle mutation events properly, + // option list must be up-to-date before inserting or removing options. + // Fortunately this is called only if mutation event listener + // adds or removes options. + mSelect->RebuildOptionsArray(mNotify); + } + nsresult rv; + if (aKid) { + rv = mSelect->WillAddOptions(aKid, aParent, aIndex, mNotify); + } else { + rv = mSelect->WillRemoveOptions(aParent, aIndex, mNotify); + } + mNeedsRebuild = NS_FAILED(rv); + } +} + +SafeOptionListMutation::~SafeOptionListMutation() { + if (mSelect) { + if (mNeedsRebuild || (mTopLevelMutation && mGuard.Mutated(1))) { + mSelect->RebuildOptionsArray(true); + } + if (mTopLevelMutation) { + mSelect->mMutating = false; + } + if (mSelect->Item(mSelect->SelectedIndex()) != mInitialSelectedOption) { + // We must have triggered the SelectSomething() codepath, which can cause + // our validity to change. Unfortunately, our attempt to update validity + // in that case may not have worked correctly, because we actually call it + // before we have inserted the new <option>s into the DOM! Go ahead and + // update validity here as needed, because by now we know our <option>s + // are where they should be. + mSelect->UpdateValueMissingValidityState(); + mSelect->UpdateValidityElementStates(mNotify); + } +#ifdef DEBUG + mSelect->VerifyOptionsArray(); +#endif + } +} + +//---------------------------------------------------------------------- +// +// HTMLSelectElement +// + +// construction, destruction + +HTMLSelectElement::HTMLSelectElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : nsGenericHTMLFormControlElementWithState( + std::move(aNodeInfo), aFromParser, FormControlType::Select), + mOptions(new HTMLOptionsCollection(this)), + mAutocompleteAttrState(nsContentUtils::eAutocompleteAttrState_Unknown), + mAutocompleteInfoState(nsContentUtils::eAutocompleteAttrState_Unknown), + mIsDoneAddingChildren(!aFromParser), + mDisabledChanged(false), + mMutating(false), + mInhibitStateRestoration(!!(aFromParser & FROM_PARSER_FRAGMENT)), + mUserInteracted(false), + mDefaultSelectionSet(false), + mIsOpenInParentProcess(false), + mNonOptionChildren(0), + mOptGroupCount(0), + mSelectedIndex(-1) { + SetHasWeirdParserInsertionMode(); + + // DoneAddingChildren() will be called later if it's from the parser, + // otherwise it is + + // Set up our default state: enabled, optional, and valid. + AddStatesSilently(ElementState::ENABLED | ElementState::OPTIONAL_ | + ElementState::VALID); +} + +// ISupports + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLSelectElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + HTMLSelectElement, nsGenericHTMLFormControlElementWithState) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOptions) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectedOptions) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED( + HTMLSelectElement, nsGenericHTMLFormControlElementWithState) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectedOptions) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED( + HTMLSelectElement, nsGenericHTMLFormControlElementWithState, + nsIConstraintValidation) + +// nsIDOMHTMLSelectElement + +NS_IMPL_ELEMENT_CLONE(HTMLSelectElement) + +void HTMLSelectElement::SetCustomValidity(const nsAString& aError) { + ConstraintValidation::SetCustomValidity(aError); + UpdateValidityElementStates(true); +} + +// https://html.spec.whatwg.org/multipage/input.html#dom-input-showpicker +void HTMLSelectElement::ShowPicker(ErrorResult& aRv) { + // Step 1. If this is not mutable, then throw an "InvalidStateError" + // DOMException. + if (IsDisabled()) { + return aRv.ThrowInvalidStateError("This select is disabled."); + } + + // Step 2. If this's relevant settings object's origin is not same origin with + // this's relevant settings object's top-level origin, and this is a select + // element, [...], then throw a "SecurityError" DOMException. + nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow(); + WindowGlobalChild* windowGlobalChild = + window ? window->GetWindowGlobalChild() : nullptr; + if (!windowGlobalChild || !windowGlobalChild->SameOriginWithTop()) { + return aRv.ThrowSecurityError( + "Call was blocked because the current origin isn't same-origin with " + "top."); + } + + // Step 3. If this's relevant global object does not have transient + // activation, then throw a "NotAllowedError" DOMException. + if (!OwnerDoc()->HasValidTransientUserGestureActivation()) { + return aRv.ThrowNotAllowedError( + "Call was blocked due to lack of user activation."); + } + + // Step 4. If this is a select element, and this is not being rendered, then + // throw a "NotSupportedError" DOMException. + + // Flush frames so that IsRendered returns up-to-date results. + Unused << GetPrimaryFrame(FlushType::Frames); + if (!IsRendered()) { + return aRv.ThrowNotSupportedError("This select isn't being rendered."); + } + + // Step 5. Show the picker, if applicable, for this. +#if !defined(ANDROID) + if (!IsCombobox()) { + return; + } +#endif + if (!OpenInParentProcess()) { + RefPtr<Document> doc = OwnerDoc(); + nsContentUtils::DispatchChromeEvent(doc, this, u"mozshowdropdown"_ns, + CanBubble::eYes, Cancelable::eNo); + } +} + +void HTMLSelectElement::GetAutocomplete(DOMString& aValue) { + const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete); + + mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute( + attributeVal, aValue, mAutocompleteAttrState); +} + +void HTMLSelectElement::GetAutocompleteInfo(AutocompleteInfo& aInfo) { + const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete); + mAutocompleteInfoState = nsContentUtils::SerializeAutocompleteAttribute( + attributeVal, aInfo, mAutocompleteInfoState, true); +} + +void HTMLSelectElement::InsertChildBefore(nsIContent* aKid, + nsIContent* aBeforeThis, bool aNotify, + ErrorResult& aRv) { + const uint32_t index = + aBeforeThis ? *ComputeIndexOf(aBeforeThis) : GetChildCount(); + SafeOptionListMutation safeMutation(this, this, aKid, index, aNotify); + nsGenericHTMLFormControlElementWithState::InsertChildBefore(aKid, aBeforeThis, + aNotify, aRv); + if (aRv.Failed()) { + safeMutation.MutationFailed(); + } +} + +void HTMLSelectElement::RemoveChildNode(nsIContent* aKid, bool aNotify) { + SafeOptionListMutation safeMutation(this, this, nullptr, + *ComputeIndexOf(aKid), aNotify); + nsGenericHTMLFormControlElementWithState::RemoveChildNode(aKid, aNotify); +} + +void HTMLSelectElement::InsertOptionsIntoList(nsIContent* aOptions, + int32_t aListIndex, + int32_t aDepth, bool aNotify) { + MOZ_ASSERT(aDepth == 0 || aDepth == 1); + int32_t insertIndex = aListIndex; + + HTMLOptionElement* optElement = HTMLOptionElement::FromNode(aOptions); + if (optElement) { + mOptions->InsertOptionAt(optElement, insertIndex); + insertIndex++; + } else if (aDepth == 0) { + // If it's at the top level, then we just found out there are non-options + // at the top level, which will throw off the insert count + mNonOptionChildren++; + + // Deal with optgroups + if (aOptions->IsHTMLElement(nsGkAtoms::optgroup)) { + mOptGroupCount++; + + for (nsIContent* child = aOptions->GetFirstChild(); child; + child = child->GetNextSibling()) { + optElement = HTMLOptionElement::FromNode(child); + if (optElement) { + mOptions->InsertOptionAt(optElement, insertIndex); + insertIndex++; + } + } + } + } // else ignore even if optgroup; we want to ignore nested optgroups. + + // Deal with the selected list + if (insertIndex - aListIndex) { + // Fix the currently selected index + if (aListIndex <= mSelectedIndex) { + mSelectedIndex += (insertIndex - aListIndex); + OnSelectionChanged(); + } + + // Get the frame stuff for notification. No need to flush here + // since if there's no frame for the select yet the select will + // get into the right state once it's created. + nsISelectControlFrame* selectFrame = nullptr; + AutoWeakFrame weakSelectFrame; + bool didGetFrame = false; + + // Actually select the options if the added options warrant it + for (int32_t i = aListIndex; i < insertIndex; i++) { + // Notify the frame that the option is added + if (!didGetFrame || (selectFrame && !weakSelectFrame.IsAlive())) { + selectFrame = GetSelectFrame(); + weakSelectFrame = do_QueryFrame(selectFrame); + didGetFrame = true; + } + + if (selectFrame) { + selectFrame->AddOption(i); + } + + RefPtr<HTMLOptionElement> option = Item(i); + if (option && option->Selected()) { + // Clear all other options + if (!HasAttr(nsGkAtoms::multiple)) { + OptionFlags mask{OptionFlag::IsSelected, OptionFlag::ClearAll, + OptionFlag::SetDisabled, OptionFlag::Notify, + OptionFlag::InsertingOptions}; + SetOptionsSelectedByIndex(i, i, mask); + } + + // This is sort of a hack ... we need to notify that the option was + // set and change selectedIndex even though we didn't really change + // its value. + OnOptionSelected(selectFrame, i, true, false, aNotify); + } + } + + CheckSelectSomething(aNotify); + } +} + +nsresult HTMLSelectElement::RemoveOptionsFromList(nsIContent* aOptions, + int32_t aListIndex, + int32_t aDepth, + bool aNotify) { + MOZ_ASSERT(aDepth == 0 || aDepth == 1); + int32_t numRemoved = 0; + + HTMLOptionElement* optElement = HTMLOptionElement::FromNode(aOptions); + if (optElement) { + if (mOptions->ItemAsOption(aListIndex) != optElement) { + NS_ERROR("wrong option at index"); + return NS_ERROR_UNEXPECTED; + } + mOptions->RemoveOptionAt(aListIndex); + numRemoved++; + } else if (aDepth == 0) { + // Yay, one less artifact at the top level. + mNonOptionChildren--; + + // Recurse down deeper for options + if (mOptGroupCount && aOptions->IsHTMLElement(nsGkAtoms::optgroup)) { + mOptGroupCount--; + + for (nsIContent* child = aOptions->GetFirstChild(); child; + child = child->GetNextSibling()) { + optElement = HTMLOptionElement::FromNode(child); + if (optElement) { + if (mOptions->ItemAsOption(aListIndex) != optElement) { + NS_ERROR("wrong option at index"); + return NS_ERROR_UNEXPECTED; + } + mOptions->RemoveOptionAt(aListIndex); + numRemoved++; + } + } + } + } // else don't check for an optgroup; we want to ignore nested optgroups + + if (numRemoved) { + // Tell the widget we removed the options + nsISelectControlFrame* selectFrame = GetSelectFrame(); + if (selectFrame) { + nsAutoScriptBlocker scriptBlocker; + for (int32_t i = aListIndex; i < aListIndex + numRemoved; ++i) { + selectFrame->RemoveOption(i); + } + } + + // Fix the selected index + if (aListIndex <= mSelectedIndex) { + if (mSelectedIndex < (aListIndex + numRemoved)) { + // aListIndex <= mSelectedIndex < aListIndex+numRemoved + // Find a new selected index if it was one of the ones removed. + // If this is a Combobox, no other Item will be selected. + if (IsCombobox()) { + mSelectedIndex = -1; + OnSelectionChanged(); + } else { + FindSelectedIndex(aListIndex, aNotify); + } + } else { + // Shift the selected index if something in front of it was removed + // aListIndex+numRemoved <= mSelectedIndex + mSelectedIndex -= numRemoved; + OnSelectionChanged(); + } + } + + // Select something in case we removed the selected option on a + // single select + if (!CheckSelectSomething(aNotify) && mSelectedIndex == -1) { + // Update the validity state in case of we've just removed the last + // option. + UpdateValueMissingValidityState(); + UpdateValidityElementStates(aNotify); + } + } + + return NS_OK; +} + +// XXXldb Doing the processing before the content nodes have been added +// to the document (as the name of this function seems to require, and +// as the callers do), is highly unusual. Passing around unparented +// content to other parts of the app can make those things think the +// options are the root content node. +NS_IMETHODIMP +HTMLSelectElement::WillAddOptions(nsIContent* aOptions, nsIContent* aParent, + int32_t aContentIndex, bool aNotify) { + if (this != aParent && this != aParent->GetParent()) { + return NS_OK; + } + int32_t level = aParent == this ? 0 : 1; + + // Get the index where the options will be inserted + int32_t ind = -1; + if (!mNonOptionChildren) { + // If there are no artifacts, aContentIndex == ind + ind = aContentIndex; + } else { + // If there are artifacts, we have to get the index of the option the + // hard way + int32_t children = aParent->GetChildCount(); + + if (aContentIndex >= children) { + // If the content insert is after the end of the parent, then we want to + // get the next index *after* the parent and insert there. + ind = GetOptionIndexAfter(aParent); + } else { + // If the content insert is somewhere in the middle of the container, then + // we want to get the option currently at the index and insert in front of + // that. + nsIContent* currentKid = aParent->GetChildAt_Deprecated(aContentIndex); + NS_ASSERTION(currentKid, "Child not found!"); + if (currentKid) { + ind = GetOptionIndexAt(currentKid); + } else { + ind = -1; + } + } + } + + InsertOptionsIntoList(aOptions, ind, level, aNotify); + return NS_OK; +} + +NS_IMETHODIMP +HTMLSelectElement::WillRemoveOptions(nsIContent* aParent, int32_t aContentIndex, + bool aNotify) { + if (this != aParent && this != aParent->GetParent()) { + return NS_OK; + } + int32_t level = this == aParent ? 0 : 1; + + // Get the index where the options will be removed + nsIContent* currentKid = aParent->GetChildAt_Deprecated(aContentIndex); + if (currentKid) { + int32_t ind; + if (!mNonOptionChildren) { + // If there are no artifacts, aContentIndex == ind + ind = aContentIndex; + } else { + // If there are artifacts, we have to get the index of the option the + // hard way + ind = GetFirstOptionIndex(currentKid); + } + if (ind != -1) { + nsresult rv = RemoveOptionsFromList(currentKid, ind, level, aNotify); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +int32_t HTMLSelectElement::GetOptionIndexAt(nsIContent* aOptions) { + // Search this node and below. + // If not found, find the first one *after* this node. + int32_t retval = GetFirstOptionIndex(aOptions); + if (retval == -1) { + retval = GetOptionIndexAfter(aOptions); + } + + return retval; +} + +int32_t HTMLSelectElement::GetOptionIndexAfter(nsIContent* aOptions) { + // - If this is the select, the next option is the last. + // - If not, search all the options after aOptions and up to the last option + // in the parent. + // - If it's not there, search for the first option after the parent. + if (aOptions == this) { + return Length(); + } + + int32_t retval = -1; + + nsCOMPtr<nsIContent> parent = aOptions->GetParent(); + + if (parent) { + const int32_t index = parent->ComputeIndexOf_Deprecated(aOptions); + const int32_t count = static_cast<int32_t>(parent->GetChildCount()); + + retval = GetFirstChildOptionIndex(parent, index + 1, count); + + if (retval == -1) { + retval = GetOptionIndexAfter(parent); + } + } + + return retval; +} + +int32_t HTMLSelectElement::GetFirstOptionIndex(nsIContent* aOptions) { + int32_t listIndex = -1; + HTMLOptionElement* optElement = HTMLOptionElement::FromNode(aOptions); + if (optElement) { + mOptions->GetOptionIndex(optElement, 0, true, &listIndex); + return listIndex; + } + + listIndex = GetFirstChildOptionIndex(aOptions, 0, aOptions->GetChildCount()); + + return listIndex; +} + +int32_t HTMLSelectElement::GetFirstChildOptionIndex(nsIContent* aOptions, + int32_t aStartIndex, + int32_t aEndIndex) { + int32_t retval = -1; + + for (int32_t i = aStartIndex; i < aEndIndex; ++i) { + retval = GetFirstOptionIndex(aOptions->GetChildAt_Deprecated(i)); + if (retval != -1) { + break; + } + } + + return retval; +} + +nsISelectControlFrame* HTMLSelectElement::GetSelectFrame() { + nsIFormControlFrame* form_control_frame = GetFormControlFrame(false); + + nsISelectControlFrame* select_frame = nullptr; + + if (form_control_frame) { + select_frame = do_QueryFrame(form_control_frame); + } + + return select_frame; +} + +void HTMLSelectElement::Add( + const HTMLOptionElementOrHTMLOptGroupElement& aElement, + const Nullable<HTMLElementOrLong>& aBefore, ErrorResult& aRv) { + nsGenericHTMLElement& element = + aElement.IsHTMLOptionElement() ? static_cast<nsGenericHTMLElement&>( + aElement.GetAsHTMLOptionElement()) + : static_cast<nsGenericHTMLElement&>( + aElement.GetAsHTMLOptGroupElement()); + + if (aBefore.IsNull()) { + Add(element, static_cast<nsGenericHTMLElement*>(nullptr), aRv); + } else if (aBefore.Value().IsHTMLElement()) { + Add(element, &aBefore.Value().GetAsHTMLElement(), aRv); + } else { + Add(element, aBefore.Value().GetAsLong(), aRv); + } +} + +void HTMLSelectElement::Add(nsGenericHTMLElement& aElement, + nsGenericHTMLElement* aBefore, + ErrorResult& aError) { + if (!aBefore) { + Element::AppendChild(aElement, aError); + return; + } + + // Just in case we're not the parent, get the parent of the reference + // element + nsCOMPtr<nsINode> parent = aBefore->Element::GetParentNode(); + if (!parent || !parent->IsInclusiveDescendantOf(this)) { + // NOT_FOUND_ERR: Raised if before is not a descendant of the SELECT + // element. + aError.Throw(NS_ERROR_DOM_NOT_FOUND_ERR); + return; + } + + // If the before parameter is not null, we are equivalent to the + // insertBefore method on the parent of before. + nsCOMPtr<nsINode> refNode = aBefore; + parent->InsertBefore(aElement, refNode, aError); +} + +void HTMLSelectElement::Remove(int32_t aIndex) const { + if (aIndex < 0) { + return; + } + + nsCOMPtr<nsINode> option = Item(static_cast<uint32_t>(aIndex)); + if (!option) { + return; + } + + option->Remove(); +} + +void HTMLSelectElement::GetType(nsAString& aType) { + if (HasAttr(nsGkAtoms::multiple)) { + aType.AssignLiteral("select-multiple"); + } else { + aType.AssignLiteral("select-one"); + } +} + +void HTMLSelectElement::SetLength(uint32_t aLength, ErrorResult& aRv) { + constexpr uint32_t kMaxDynamicSelectLength = 100000; + + uint32_t curlen = Length(); + + if (curlen > aLength) { // Remove extra options + for (uint32_t i = curlen; i > aLength; --i) { + Remove(i - 1); + } + } else if (aLength > curlen) { + if (aLength > kMaxDynamicSelectLength) { + nsAutoString strOptionsLength; + strOptionsLength.AppendInt(aLength); + + nsAutoString strLimit; + strLimit.AppendInt(kMaxDynamicSelectLength); + + nsContentUtils::ReportToConsole( + nsIScriptError::warningFlag, "DOM"_ns, OwnerDoc(), + nsContentUtils::eDOM_PROPERTIES, + "SelectOptionsLengthAssignmentWarning", {strOptionsLength, strLimit}); + return; + } + + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + + nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::option, + getter_AddRefs(nodeInfo)); + + nsCOMPtr<nsINode> node = NS_NewHTMLOptionElement(nodeInfo.forget()); + for (uint32_t i = curlen; i < aLength; i++) { + nsINode::AppendChild(*node, aRv); + if (aRv.Failed()) { + return; + } + + if (i + 1 < aLength) { + node = node->CloneNode(true, aRv); + if (aRv.Failed()) { + return; + } + MOZ_ASSERT(node); + } + } + } +} + +/* static */ +bool HTMLSelectElement::MatchSelectedOptions(Element* aElement, + int32_t /* unused */, + nsAtom* /* unused */, + void* /* unused*/) { + HTMLOptionElement* option = HTMLOptionElement::FromNode(aElement); + return option && option->Selected(); +} + +nsIHTMLCollection* HTMLSelectElement::SelectedOptions() { + if (!mSelectedOptions) { + mSelectedOptions = new nsContentList(this, MatchSelectedOptions, nullptr, + nullptr, /* deep */ true); + } + return mSelectedOptions; +} + +void HTMLSelectElement::SetSelectedIndexInternal(int32_t aIndex, bool aNotify) { + int32_t oldSelectedIndex = mSelectedIndex; + OptionFlags mask{OptionFlag::IsSelected, OptionFlag::ClearAll, + OptionFlag::SetDisabled}; + if (aNotify) { + mask += OptionFlag::Notify; + } + + SetOptionsSelectedByIndex(aIndex, aIndex, mask); + + nsISelectControlFrame* selectFrame = GetSelectFrame(); + if (selectFrame) { + selectFrame->OnSetSelectedIndex(oldSelectedIndex, mSelectedIndex); + } + + OnSelectionChanged(); +} + +bool HTMLSelectElement::IsOptionSelectedByIndex(int32_t aIndex) const { + HTMLOptionElement* option = Item(static_cast<uint32_t>(aIndex)); + return option && option->Selected(); +} + +void HTMLSelectElement::OnOptionSelected(nsISelectControlFrame* aSelectFrame, + int32_t aIndex, bool aSelected, + bool aChangeOptionState, + bool aNotify) { + // Set the selected index + if (aSelected && (aIndex < mSelectedIndex || mSelectedIndex < 0)) { + mSelectedIndex = aIndex; + OnSelectionChanged(); + } else if (!aSelected && aIndex == mSelectedIndex) { + FindSelectedIndex(aIndex + 1, aNotify); + } + + if (aChangeOptionState) { + // Tell the option to get its bad self selected + RefPtr<HTMLOptionElement> option = Item(static_cast<uint32_t>(aIndex)); + if (option) { + option->SetSelectedInternal(aSelected, aNotify); + } + } + + // Let the frame know too + if (aSelectFrame) { + aSelectFrame->OnOptionSelected(aIndex, aSelected); + } + + UpdateSelectedOptions(); + UpdateValueMissingValidityState(); + UpdateValidityElementStates(aNotify); +} + +void HTMLSelectElement::FindSelectedIndex(int32_t aStartIndex, bool aNotify) { + mSelectedIndex = -1; + uint32_t len = Length(); + for (int32_t i = aStartIndex; i < int32_t(len); i++) { + if (IsOptionSelectedByIndex(i)) { + mSelectedIndex = i; + break; + } + } + OnSelectionChanged(); +} + +// XXX Consider splitting this into two functions for ease of reading: +// SelectOptionsByIndex(startIndex, endIndex, clearAll, checkDisabled) +// startIndex, endIndex - the range of options to turn on +// (-1, -1) will clear all indices no matter what. +// clearAll - will clear all other options unless checkDisabled is on +// and all the options attempted to be set are disabled +// (note that if it is not multiple, and an option is selected, +// everything else will be cleared regardless). +// checkDisabled - if this is TRUE, and an option is disabled, it will not be +// changed regardless of whether it is selected or not. +// Generally the UI passes TRUE and JS passes FALSE. +// (setDisabled currently is the opposite) +// DeselectOptionsByIndex(startIndex, endIndex, checkDisabled) +// startIndex, endIndex - the range of options to turn on +// (-1, -1) will clear all indices no matter what. +// checkDisabled - if this is TRUE, and an option is disabled, it will not be +// changed regardless of whether it is selected or not. +// Generally the UI passes TRUE and JS passes FALSE. +// (setDisabled currently is the opposite) +// +// XXXbz the above comment is pretty confusing. Maybe we should actually +// document the args to this function too, in addition to documenting what +// things might end up looking like? In particular, pay attention to the +// setDisabled vs checkDisabled business. +bool HTMLSelectElement::SetOptionsSelectedByIndex(int32_t aStartIndex, + int32_t aEndIndex, + OptionFlags aOptionsMask) { +#if 0 + printf("SetOption(%d-%d, %c, ClearAll=%c)\n", aStartIndex, aEndIndex, + (aOptionsMask.contains(OptionFlag::IsSelected) ? 'Y' : 'N'), + (aOptionsMask.contains(OptionFlag::ClearAll) ? 'Y' : 'N')); +#endif + // Don't bother if the select is disabled + if (!aOptionsMask.contains(OptionFlag::SetDisabled) && IsDisabled()) { + return false; + } + + // Don't bother if there are no options + uint32_t numItems = Length(); + if (numItems == 0) { + return false; + } + + // First, find out whether multiple items can be selected + bool isMultiple = Multiple(); + + // These variables tell us whether any options were selected + // or deselected. + bool optionsSelected = false; + bool optionsDeselected = false; + + nsISelectControlFrame* selectFrame = nullptr; + bool didGetFrame = false; + AutoWeakFrame weakSelectFrame; + + if (aOptionsMask.contains(OptionFlag::IsSelected)) { + // Setting selectedIndex to an out-of-bounds index means -1. (HTML5) + if (aStartIndex < 0 || AssertedCast<uint32_t>(aStartIndex) >= numItems || + aEndIndex < 0 || AssertedCast<uint32_t>(aEndIndex) >= numItems) { + aStartIndex = -1; + aEndIndex = -1; + } + + // Only select the first value if it's not multiple + if (!isMultiple) { + aEndIndex = aStartIndex; + } + + // This variable tells whether or not all of the options we attempted to + // select are disabled. If ClearAll is passed in as true, and we do not + // select anything because the options are disabled, we will not clear the + // other options. (This is to make the UI work the way one might expect.) + bool allDisabled = !aOptionsMask.contains(OptionFlag::SetDisabled); + + // + // Save a little time when clearing other options + // + int32_t previousSelectedIndex = mSelectedIndex; + + // + // Select the requested indices + // + // If index is -1, everything will be deselected (bug 28143) + if (aStartIndex != -1) { + MOZ_ASSERT(aStartIndex >= 0); + MOZ_ASSERT(aEndIndex >= 0); + // Loop through the options and select them (if they are not disabled and + // if they are not already selected). + for (uint32_t optIndex = AssertedCast<uint32_t>(aStartIndex); + optIndex <= AssertedCast<uint32_t>(aEndIndex); optIndex++) { + RefPtr<HTMLOptionElement> option = Item(optIndex); + + // Ignore disabled options. + if (!aOptionsMask.contains(OptionFlag::SetDisabled)) { + if (option && IsOptionDisabled(option)) { + continue; + } + allDisabled = false; + } + + // If the index is already selected, ignore it. On the other hand when + // the option has just been inserted we have to get in sync with it. + if (option && (aOptionsMask.contains(OptionFlag::InsertingOptions) || + !option->Selected())) { + // To notify the frame if anything gets changed. No need + // to flush here, if there's no frame yet we don't need to + // force it to be created just to notify it about a change + // in the select. + selectFrame = GetSelectFrame(); + weakSelectFrame = do_QueryFrame(selectFrame); + didGetFrame = true; + + OnOptionSelected(selectFrame, optIndex, true, !option->Selected(), + aOptionsMask.contains(OptionFlag::Notify)); + optionsSelected = true; + } + } + } + + // Next remove all other options if single select or all is clear + // If index is -1, everything will be deselected (bug 28143) + if (((!isMultiple && optionsSelected) || + (aOptionsMask.contains(OptionFlag::ClearAll) && !allDisabled) || + aStartIndex == -1) && + previousSelectedIndex != -1) { + for (uint32_t optIndex = AssertedCast<uint32_t>(previousSelectedIndex); + optIndex < numItems; optIndex++) { + if (static_cast<int32_t>(optIndex) < aStartIndex || + static_cast<int32_t>(optIndex) > aEndIndex) { + HTMLOptionElement* option = Item(optIndex); + // If the index is already deselected, ignore it. + if (option && option->Selected()) { + if (!didGetFrame || (selectFrame && !weakSelectFrame.IsAlive())) { + // To notify the frame if anything gets changed, don't + // flush, if the frame doesn't exist we don't need to + // create it just to tell it about this change. + selectFrame = GetSelectFrame(); + weakSelectFrame = do_QueryFrame(selectFrame); + + didGetFrame = true; + } + + OnOptionSelected(selectFrame, optIndex, false, true, + aOptionsMask.contains(OptionFlag::Notify)); + optionsDeselected = true; + + // Only need to deselect one option if not multiple + if (!isMultiple) { + break; + } + } + } + } + } + } else { + // If we're deselecting, loop through all selected items and deselect + // any that are in the specified range. + for (int32_t optIndex = aStartIndex; optIndex <= aEndIndex; optIndex++) { + HTMLOptionElement* option = Item(optIndex); + if (!aOptionsMask.contains(OptionFlag::SetDisabled) && + IsOptionDisabled(option)) { + continue; + } + + // If the index is already selected, ignore it. + if (option && option->Selected()) { + if (!didGetFrame || (selectFrame && !weakSelectFrame.IsAlive())) { + // To notify the frame if anything gets changed, don't + // flush, if the frame doesn't exist we don't need to + // create it just to tell it about this change. + selectFrame = GetSelectFrame(); + weakSelectFrame = do_QueryFrame(selectFrame); + + didGetFrame = true; + } + + OnOptionSelected(selectFrame, optIndex, false, true, + aOptionsMask.contains(OptionFlag::Notify)); + optionsDeselected = true; + } + } + } + + // Make sure something is selected unless we were set to -1 (none) + if (optionsDeselected && aStartIndex != -1 && + !aOptionsMask.contains(OptionFlag::NoReselect)) { + optionsSelected = + CheckSelectSomething(aOptionsMask.contains(OptionFlag::Notify)) || + optionsSelected; + } + + // Let the caller know whether anything was changed + return optionsSelected || optionsDeselected; +} + +NS_IMETHODIMP +HTMLSelectElement::IsOptionDisabled(int32_t aIndex, bool* aIsDisabled) { + *aIsDisabled = false; + RefPtr<HTMLOptionElement> option = Item(aIndex); + NS_ENSURE_TRUE(option, NS_ERROR_FAILURE); + + *aIsDisabled = IsOptionDisabled(option); + return NS_OK; +} + +bool HTMLSelectElement::IsOptionDisabled(HTMLOptionElement* aOption) const { + MOZ_ASSERT(aOption); + if (aOption->Disabled()) { + return true; + } + + // Check for disabled optgroups + // If there are no artifacts, there are no optgroups + if (mNonOptionChildren) { + for (nsCOMPtr<Element> node = + static_cast<nsINode*>(aOption)->GetParentElement(); + node; node = node->GetParentElement()) { + // If we reached the select element, we're done + if (node->IsHTMLElement(nsGkAtoms::select)) { + return false; + } + + RefPtr<HTMLOptGroupElement> optGroupElement = + HTMLOptGroupElement::FromNode(node); + + if (!optGroupElement) { + // If you put something else between you and the optgroup, you're a + // moron and you deserve not to have optgroup disabling work. + return false; + } + + if (optGroupElement->Disabled()) { + return true; + } + } + } + + return false; +} + +void HTMLSelectElement::GetValue(DOMString& aValue) const { + int32_t selectedIndex = SelectedIndex(); + if (selectedIndex < 0) { + return; + } + + RefPtr<HTMLOptionElement> option = Item(static_cast<uint32_t>(selectedIndex)); + + if (!option) { + return; + } + + option->GetValue(aValue); +} + +void HTMLSelectElement::SetValue(const nsAString& aValue) { + uint32_t length = Length(); + + for (uint32_t i = 0; i < length; i++) { + RefPtr<HTMLOptionElement> option = Item(i); + if (!option) { + continue; + } + + nsAutoString optionVal; + option->GetValue(optionVal); + if (optionVal.Equals(aValue)) { + SetSelectedIndexInternal(int32_t(i), true); + return; + } + } + // No matching option was found. + SetSelectedIndexInternal(-1, true); +} + +int32_t HTMLSelectElement::TabIndexDefault() { return 0; } + +bool HTMLSelectElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable( + aWithMouse, aIsFocusable, aTabIndex)) { + return true; + } + + *aIsFocusable = !IsDisabled(); + + return false; +} + +bool HTMLSelectElement::CheckSelectSomething(bool aNotify) { + if (mIsDoneAddingChildren) { + if (mSelectedIndex < 0 && IsCombobox()) { + return SelectSomething(aNotify); + } + } + return false; +} + +bool HTMLSelectElement::SelectSomething(bool aNotify) { + // If we're not done building the select, don't play with this yet. + if (!mIsDoneAddingChildren) { + return false; + } + + uint32_t count = Length(); + for (uint32_t i = 0; i < count; i++) { + bool disabled; + nsresult rv = IsOptionDisabled(i, &disabled); + + if (NS_FAILED(rv) || !disabled) { + SetSelectedIndexInternal(i, aNotify); + + UpdateValueMissingValidityState(); + UpdateValidityElementStates(aNotify); + + return true; + } + } + + return false; +} + +nsresult HTMLSelectElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = + nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + // If there is a disabled fieldset in the parent chain, the element is now + // barred from constraint validation. + // XXXbz is this still needed now that fieldset changes always call + // FieldSetDisabledChanged? + UpdateBarredFromConstraintValidation(); + + // And now make sure our state is up to date + UpdateValidityElementStates(false); + + return rv; +} + +void HTMLSelectElement::UnbindFromTree(bool aNullParent) { + nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent); + + // We might be no longer disabled because our parent chain changed. + // XXXbz is this still needed now that fieldset changes always call + // FieldSetDisabledChanged? + UpdateBarredFromConstraintValidation(); + + // And now make sure our state is up to date + UpdateValidityElementStates(false); +} + +void HTMLSelectElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::disabled) { + if (aNotify) { + mDisabledChanged = true; + } + } else if (aName == nsGkAtoms::multiple) { + if (!aValue && aNotify && mSelectedIndex >= 0) { + // We're changing from being a multi-select to a single-select. + // Make sure we only have one option selected before we do that. + // Note that this needs to come before we really unset the attr, + // since SetOptionsSelectedByIndex does some bail-out type + // optimization for cases when the select is not multiple that + // would lead to only a single option getting deselected. + SetSelectedIndexInternal(mSelectedIndex, aNotify); + } + } + } + + return nsGenericHTMLFormControlElementWithState::BeforeSetAttr( + aNameSpaceID, aName, aValue, aNotify); +} + +void HTMLSelectElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::disabled) { + // This *has* to be called *before* validity state check because + // UpdateBarredFromConstraintValidation and + // UpdateValueMissingValidityState depend on our disabled state. + UpdateDisabledState(aNotify); + + UpdateValueMissingValidityState(); + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(aNotify); + } else if (aName == nsGkAtoms::required) { + // This *has* to be called *before* UpdateValueMissingValidityState + // because UpdateValueMissingValidityState depends on our required + // state. + UpdateRequiredState(!!aValue, aNotify); + UpdateValueMissingValidityState(); + UpdateValidityElementStates(aNotify); + } else if (aName == nsGkAtoms::autocomplete) { + // Clear the cached @autocomplete attribute and autocompleteInfo state. + mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown; + mAutocompleteInfoState = nsContentUtils::eAutocompleteAttrState_Unknown; + } else if (aName == nsGkAtoms::multiple) { + if (!aValue && aNotify) { + // We might have become a combobox; make sure _something_ gets + // selected in that case + CheckSelectSomething(aNotify); + } + } + } + + return nsGenericHTMLFormControlElementWithState::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLSelectElement::DoneAddingChildren(bool aHaveNotified) { + mIsDoneAddingChildren = true; + + nsISelectControlFrame* selectFrame = GetSelectFrame(); + + // If we foolishly tried to restore before we were done adding + // content, restore the rest of the options proper-like + if (mRestoreState) { + RestoreStateTo(*mRestoreState); + mRestoreState = nullptr; + } + + // Notify the frame + if (selectFrame) { + selectFrame->DoneAddingChildren(true); + } + + if (!mInhibitStateRestoration) { + GenerateStateKey(); + RestoreFormControlState(); + } + + // Now that we're done, select something (if it's a single select something + // must be selected) + if (!CheckSelectSomething(false)) { + // If an option has @selected set, it will be selected during parsing but + // with an empty value. We have to make sure the select element updates it's + // validity state to take this into account. + UpdateValueMissingValidityState(); + + // And now make sure we update our content state too + UpdateValidityElementStates(aHaveNotified); + } + + mDefaultSelectionSet = true; +} + +bool HTMLSelectElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (kNameSpaceID_None == aNamespaceID) { + if (aAttribute == nsGkAtoms::size) { + return aResult.ParsePositiveIntValue(aValue); + } + if (aAttribute == nsGkAtoms::autocomplete) { + aResult.ParseAtomArray(aValue); + return true; + } + } + return nsGenericHTMLFormControlElementWithState::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +void HTMLSelectElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + nsGenericHTMLFormControlElementWithState::MapImageAlignAttributeInto( + aBuilder); + nsGenericHTMLFormControlElementWithState::MapCommonAttributesInto(aBuilder); +} + +nsChangeHint HTMLSelectElement::GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const { + nsChangeHint retval = + nsGenericHTMLFormControlElementWithState::GetAttributeChangeHint( + aAttribute, aModType); + if (aAttribute == nsGkAtoms::multiple || aAttribute == nsGkAtoms::size) { + retval |= nsChangeHint_ReconstructFrame; + } + return retval; +} + +NS_IMETHODIMP_(bool) +HTMLSelectElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = {sCommonAttributeMap, + sImageAlignAttributeMap}; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLSelectElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +bool HTMLSelectElement::IsDisabledForEvents(WidgetEvent* aEvent) { + nsIFormControlFrame* formControlFrame = GetFormControlFrame(false); + nsIFrame* formFrame = nullptr; + if (formControlFrame) { + formFrame = do_QueryFrame(formControlFrame); + } + return IsElementDisabledForEvents(aEvent, formFrame); +} + +void HTMLSelectElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + aVisitor.mCanHandle = false; + if (IsDisabledForEvents(aVisitor.mEvent)) { + return; + } + + nsGenericHTMLFormControlElementWithState::GetEventTargetParent(aVisitor); +} + +void HTMLSelectElement::UpdateValidityElementStates(bool aNotify) { + AutoStateChangeNotifier notifier(*this, aNotify); + RemoveStatesSilently(ElementState::VALIDITY_STATES); + if (!IsCandidateForConstraintValidation()) { + return; + } + + ElementState state; + if (IsValid()) { + state |= ElementState::VALID; + if (mUserInteracted) { + state |= ElementState::USER_VALID; + } + } else { + state |= ElementState::INVALID; + if (mUserInteracted) { + state |= ElementState::USER_INVALID; + } + } + + AddStatesSilently(state); +} + +void HTMLSelectElement::SaveState() { + PresState* presState = GetPrimaryPresState(); + if (!presState) { + return; + } + + SelectContentData state; + + uint32_t len = Length(); + + for (uint32_t optIndex = 0; optIndex < len; optIndex++) { + HTMLOptionElement* option = Item(optIndex); + if (option && option->Selected()) { + nsAutoString value; + option->GetValue(value); + if (value.IsEmpty()) { + state.indices().AppendElement(optIndex); + } else { + state.values().AppendElement(std::move(value)); + } + } + } + + presState->contentData() = std::move(state); + + if (mDisabledChanged) { + // We do not want to save the real disabled state but the disabled + // attribute. + presState->disabled() = HasAttr(nsGkAtoms::disabled); + presState->disabledSet() = true; + } +} + +bool HTMLSelectElement::RestoreState(PresState* aState) { + // Get the presentation state object to retrieve our stuff out of. + const PresContentData& state = aState->contentData(); + if (state.type() == PresContentData::TSelectContentData) { + RestoreStateTo(state.get_SelectContentData()); + + // Don't flush, if the frame doesn't exist yet it doesn't care if + // we're reset or not. + DispatchContentReset(); + } + + if (aState->disabledSet() && !aState->disabled()) { + SetDisabled(false, IgnoreErrors()); + } + + return false; +} + +void HTMLSelectElement::RestoreStateTo(const SelectContentData& aNewSelected) { + if (!mIsDoneAddingChildren) { + // Make a copy of the state for us to restore from in the future. + mRestoreState = MakeUnique<SelectContentData>(aNewSelected); + return; + } + + uint32_t len = Length(); + OptionFlags mask{OptionFlag::IsSelected, OptionFlag::ClearAll, + OptionFlag::SetDisabled, OptionFlag::Notify}; + + // First clear all + SetOptionsSelectedByIndex(-1, -1, mask); + + // Select by index. + for (uint32_t idx : aNewSelected.indices()) { + if (idx < len) { + SetOptionsSelectedByIndex(idx, idx, + {OptionFlag::IsSelected, + OptionFlag::SetDisabled, OptionFlag::Notify}); + } + } + + // Select by value. + for (uint32_t i = 0; i < len; ++i) { + HTMLOptionElement* option = Item(i); + if (option) { + nsAutoString value; + option->GetValue(value); + if (aNewSelected.values().Contains(value)) { + SetOptionsSelectedByIndex( + i, i, + {OptionFlag::IsSelected, OptionFlag::SetDisabled, + OptionFlag::Notify}); + } + } + } +} + +// nsIFormControl + +NS_IMETHODIMP +HTMLSelectElement::Reset() { + uint32_t numSelected = 0; + + // + // Cycle through the options array and reset the options + // + uint32_t numOptions = Length(); + + for (uint32_t i = 0; i < numOptions; i++) { + RefPtr<HTMLOptionElement> option = Item(i); + if (option) { + // + // Reset the option to its default value + // + + OptionFlags mask = {OptionFlag::SetDisabled, OptionFlag::Notify, + OptionFlag::NoReselect}; + if (option->DefaultSelected()) { + mask += OptionFlag::IsSelected; + numSelected++; + } + + SetOptionsSelectedByIndex(i, i, mask); + option->SetSelectedChanged(false); + } + } + + // + // If nothing was selected and it's not multiple, select something + // + if (numSelected == 0 && IsCombobox()) { + SelectSomething(true); + } + + OnSelectionChanged(); + SetUserInteracted(false); + + // Let the frame know we were reset + // + // Don't flush, if there's no frame yet it won't care about us being + // reset even if we forced it to be created now. + // + DispatchContentReset(); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLSelectElement::SubmitNamesValues(FormData* aFormData) { + // + // Get the name (if no name, no submit) + // + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + if (name.IsEmpty()) { + return NS_OK; + } + + // + // Submit + // + uint32_t len = Length(); + + for (uint32_t optIndex = 0; optIndex < len; optIndex++) { + HTMLOptionElement* option = Item(optIndex); + + // Don't send disabled options + if (!option || IsOptionDisabled(option)) { + continue; + } + + if (!option->Selected()) { + continue; + } + + nsString value; + option->GetValue(value); + + aFormData->AddNameValuePair(name, value); + } + + return NS_OK; +} + +void HTMLSelectElement::DispatchContentReset() { + if (nsIFormControlFrame* formControlFrame = GetFormControlFrame(false)) { + if (nsListControlFrame* listFrame = do_QueryFrame(formControlFrame)) { + listFrame->OnContentReset(); + } + } +} + +static void AddOptions(nsIContent* aRoot, HTMLOptionsCollection* aArray) { + for (nsIContent* child = aRoot->GetFirstChild(); child; + child = child->GetNextSibling()) { + HTMLOptionElement* opt = HTMLOptionElement::FromNode(child); + if (opt) { + aArray->AppendOption(opt); + } else if (child->IsHTMLElement(nsGkAtoms::optgroup)) { + for (nsIContent* grandchild = child->GetFirstChild(); grandchild; + grandchild = grandchild->GetNextSibling()) { + opt = HTMLOptionElement::FromNode(grandchild); + if (opt) { + aArray->AppendOption(opt); + } + } + } + } +} + +void HTMLSelectElement::RebuildOptionsArray(bool aNotify) { + mOptions->Clear(); + AddOptions(this, mOptions); + FindSelectedIndex(0, aNotify); +} + +bool HTMLSelectElement::IsValueMissing() const { + if (!Required()) { + return false; + } + + uint32_t length = Length(); + + for (uint32_t i = 0; i < length; ++i) { + RefPtr<HTMLOptionElement> option = Item(i); + // Check for a placeholder label option, don't count it as a valid value. + if (i == 0 && !Multiple() && Size() <= 1 && option->GetParent() == this) { + nsAutoString value; + option->GetValue(value); + if (value.IsEmpty()) { + continue; + } + } + + if (!option->Selected()) { + continue; + } + + return false; + } + + return true; +} + +void HTMLSelectElement::UpdateValueMissingValidityState() { + SetValidityState(VALIDITY_STATE_VALUE_MISSING, IsValueMissing()); +} + +nsresult HTMLSelectElement::GetValidationMessage(nsAString& aValidationMessage, + ValidityStateType aType) { + switch (aType) { + case VALIDITY_STATE_VALUE_MISSING: { + nsAutoString message; + nsresult rv = nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationSelectMissing", + OwnerDoc(), message); + aValidationMessage = message; + return rv; + } + default: { + return ConstraintValidation::GetValidationMessage(aValidationMessage, + aType); + } + } +} + +#ifdef DEBUG + +void HTMLSelectElement::VerifyOptionsArray() { + int32_t index = 0; + for (nsIContent* child = nsINode::GetFirstChild(); child; + child = child->GetNextSibling()) { + HTMLOptionElement* opt = HTMLOptionElement::FromNode(child); + if (opt) { + NS_ASSERTION(opt == mOptions->ItemAsOption(index++), + "Options collection broken"); + } else if (child->IsHTMLElement(nsGkAtoms::optgroup)) { + for (nsIContent* grandchild = child->GetFirstChild(); grandchild; + grandchild = grandchild->GetNextSibling()) { + opt = HTMLOptionElement::FromNode(grandchild); + if (opt) { + NS_ASSERTION(opt == mOptions->ItemAsOption(index++), + "Options collection broken"); + } + } + } + } +} + +#endif + +void HTMLSelectElement::UpdateBarredFromConstraintValidation() { + SetBarredFromConstraintValidation( + HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR) || IsDisabled()); +} + +void HTMLSelectElement::FieldSetDisabledChanged(bool aNotify) { + // This *has* to be called before UpdateBarredFromConstraintValidation and + // UpdateValueMissingValidityState because these two functions depend on our + // disabled state. + nsGenericHTMLFormControlElementWithState::FieldSetDisabledChanged(aNotify); + + UpdateValueMissingValidityState(); + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(aNotify); +} + +void HTMLSelectElement::OnSelectionChanged() { + if (!mDefaultSelectionSet) { + return; + } + UpdateSelectedOptions(); +} + +void HTMLSelectElement::UpdateSelectedOptions() { + if (mSelectedOptions) { + mSelectedOptions->SetDirty(); + } +} + +void HTMLSelectElement::SetUserInteracted(bool aInteracted) { + if (mUserInteracted == aInteracted) { + return; + } + mUserInteracted = aInteracted; + UpdateValidityElementStates(true); +} + +void HTMLSelectElement::SetPreviewValue(const nsAString& aValue) { + mPreviewValue = aValue; + nsContentUtils::RemoveNewlines(mPreviewValue); + nsIFormControlFrame* formControlFrame = GetFormControlFrame(false); + nsComboboxControlFrame* comboFrame = do_QueryFrame(formControlFrame); + if (comboFrame) { + comboFrame->RedisplaySelectedText(); + } +} + +void HTMLSelectElement::UserFinishedInteracting(bool aChanged) { + SetUserInteracted(true); + if (!aChanged) { + return; + } + + // Dispatch the input event. + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent(this); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + + // Dispatch the change event. + nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, u"change"_ns, + CanBubble::eYes, Cancelable::eNo); +} + +JSObject* HTMLSelectElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLSelectElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLSelectElement.h b/dom/html/HTMLSelectElement.h new file mode 100644 index 0000000000..223da65c31 --- /dev/null +++ b/dom/html/HTMLSelectElement.h @@ -0,0 +1,520 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLSelectElement_h +#define mozilla_dom_HTMLSelectElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/ConstraintValidation.h" +#include "nsGenericHTMLElement.h" + +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/UnionTypes.h" +#include "mozilla/dom/HTMLOptionsCollection.h" +#include "mozilla/EnumSet.h" +#include "nsCheapSets.h" +#include "nsCOMPtr.h" +#include "nsError.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "nsContentUtils.h" + +class nsContentList; +class nsIDOMHTMLOptionElement; +class nsIHTMLCollection; +class nsISelectControlFrame; + +namespace mozilla { + +class ErrorResult; +class EventChainPostVisitor; +class EventChainPreVisitor; +class SelectContentData; +class PresState; + +namespace dom { + +class FormData; +class HTMLSelectElement; + +class MOZ_STACK_CLASS SafeOptionListMutation { + public: + /** + * @param aSelect The select element which option list is being mutated. + * Can be null. + * @param aParent The content object which is being mutated. + * @param aKid If not null, a new child element is being inserted to + * aParent. Otherwise a child element will be removed. + * @param aIndex The index of the content object in the parent. + */ + SafeOptionListMutation(nsIContent* aSelect, nsIContent* aParent, + nsIContent* aKid, uint32_t aIndex, bool aNotify); + ~SafeOptionListMutation(); + void MutationFailed() { mNeedsRebuild = true; } + + private: + static void* operator new(size_t) noexcept(true) { return nullptr; } + static void operator delete(void*, size_t) {} + /** The select element which option list is being mutated. */ + RefPtr<HTMLSelectElement> mSelect; + /** true if the current mutation is the first one in the stack. */ + bool mTopLevelMutation; + /** true if it is known that the option list must be recreated. */ + bool mNeedsRebuild; + /** Whether we should be notifying when we make various method calls on + mSelect */ + const bool mNotify; + /** The selected option at mutation start. */ + RefPtr<HTMLOptionElement> mInitialSelectedOption; + /** Option list must be recreated if more than one mutation is detected. */ + nsMutationGuard mGuard; +}; + +/** + * Implementation of <select> + */ +class HTMLSelectElement final : public nsGenericHTMLFormControlElementWithState, + public ConstraintValidation { + public: + /** + * IsSelected whether to set the option(s) to true or false + * + * ClearAll whether to clear all other options (for example, if you + * are normal-clicking on the current option) + * + * SetDisabled whether it is permissible to set disabled options + * (for JavaScript) + * + * Notify whether to notify frames and such + * + * NoReselect no need to select something after an option is + * deselected (for reset) + * + * InsertingOptions if an option has just been inserted some bailouts can't + * be taken + */ + enum class OptionFlag : uint8_t { + IsSelected, + ClearAll, + SetDisabled, + Notify, + NoReselect, + InsertingOptions + }; + using OptionFlags = EnumSet<OptionFlag>; + + using ConstraintValidation::GetValidationMessage; + + explicit HTMLSelectElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser = NOT_FROM_PARSER); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLSelectElement, select) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + int32_t TabIndexDefault() override; + + // Element + bool IsInteractiveHTMLContent() const override { return true; } + + // WebIdl HTMLSelectElement + void GetAutocomplete(DOMString& aValue); + void SetAutocomplete(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::autocomplete, aValue, aRv); + } + + void GetAutocompleteInfo(AutocompleteInfo& aInfo); + + // Sets the user interacted flag and fires input/change events if needed. + MOZ_CAN_RUN_SCRIPT void UserFinishedInteracting(bool aChanged); + + bool Disabled() const { return GetBoolAttr(nsGkAtoms::disabled); } + void SetDisabled(bool aVal, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::disabled, aVal, aRv); + } + bool Multiple() const { return GetBoolAttr(nsGkAtoms::multiple); } + void SetMultiple(bool aVal, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::multiple, aVal, aRv); + } + + void GetName(DOMString& aValue) { GetHTMLAttr(nsGkAtoms::name, aValue); } + void SetName(const nsAString& aName, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aName, aRv); + } + bool Required() const { return State().HasState(ElementState::REQUIRED); } + void SetRequired(bool aVal, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::required, aVal, aRv); + } + uint32_t Size() const { return GetUnsignedIntAttr(nsGkAtoms::size, 0); } + void SetSize(uint32_t aSize, ErrorResult& aRv) { + SetUnsignedIntAttr(nsGkAtoms::size, aSize, 0, aRv); + } + + void GetType(nsAString& aValue); + + HTMLOptionsCollection* Options() const { return mOptions; } + uint32_t Length() const { return mOptions->Length(); } + void SetLength(uint32_t aLength, ErrorResult& aRv); + Element* IndexedGetter(uint32_t aIdx, bool& aFound) const { + return mOptions->IndexedGetter(aIdx, aFound); + } + HTMLOptionElement* Item(uint32_t aIdx) const { + return mOptions->ItemAsOption(aIdx); + } + HTMLOptionElement* NamedItem(const nsAString& aName) const { + return mOptions->GetNamedItem(aName); + } + void Add(const HTMLOptionElementOrHTMLOptGroupElement& aElement, + const Nullable<HTMLElementOrLong>& aBefore, ErrorResult& aRv); + void Remove(int32_t aIndex) const; + void IndexedSetter(uint32_t aIndex, HTMLOptionElement* aOption, + ErrorResult& aRv) { + mOptions->IndexedSetter(aIndex, aOption, aRv); + } + + static bool MatchSelectedOptions(Element* aElement, int32_t, nsAtom*, void*); + + nsIHTMLCollection* SelectedOptions(); + + int32_t SelectedIndex() const { return mSelectedIndex; } + void SetSelectedIndex(int32_t aIdx) { SetSelectedIndexInternal(aIdx, true); } + void GetValue(DOMString& aValue) const; + void SetValue(const nsAString& aValue); + + // Override SetCustomValidity so we update our state properly when it's called + // via bindings. + void SetCustomValidity(const nsAString& aError); + + void ShowPicker(ErrorResult& aRv); + + using nsINode::Remove; + + // nsINode + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + // nsIContent + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + void InsertChildBefore(nsIContent* aKid, nsIContent* aBeforeThis, + bool aNotify, ErrorResult& aRv) override; + void RemoveChildNode(nsIContent* aKid, bool aNotify) override; + + // nsGenericHTMLElement + bool IsDisabledForEvents(WidgetEvent* aEvent) override; + + // nsGenericHTMLFormElement + void SaveState() override; + bool RestoreState(PresState* aState) override; + + // Overriden nsIFormControl methods + NS_IMETHOD Reset() override; + NS_IMETHOD SubmitNamesValues(FormData* aFormData) override; + + void FieldSetDisabledChanged(bool aNotify) override; + + /** + * To be called when stuff is added under a child of the select--but *before* + * they are actually added. + * + * @param aOptions the content that was added (usually just an option, but + * could be an optgroup node with many child options) + * @param aParent the parent the options were added to (could be an optgroup) + * @param aContentIndex the index where the options are being added within the + * parent (if the parent is an optgroup, the index within the optgroup) + */ + NS_IMETHOD WillAddOptions(nsIContent* aOptions, nsIContent* aParent, + int32_t aContentIndex, bool aNotify); + + /** + * To be called when stuff is removed under a child of the select--but + * *before* they are actually removed. + * + * @param aParent the parent the option(s) are being removed from + * @param aContentIndex the index of the option(s) within the parent (if the + * parent is an optgroup, the index within the optgroup) + */ + NS_IMETHOD WillRemoveOptions(nsIContent* aParent, int32_t aContentIndex, + bool aNotify); + + /** + * Checks whether an option is disabled (even if it's part of an optgroup) + * + * @param aIndex the index of the option to check + * @return whether the option is disabled + */ + NS_IMETHOD IsOptionDisabled(int32_t aIndex, bool* aIsDisabled); + bool IsOptionDisabled(HTMLOptionElement* aOption) const; + + /** + * Sets multiple options (or just sets startIndex if select is single) + * and handles notifications and cleanup and everything under the sun. + * When this method exits, the select will be in a consistent state. i.e. + * if you set the last option to false, it will select an option anyway. + * + * @param aStartIndex the first index to set + * @param aEndIndex the last index to set (set same as first index for one + * option) + * @param aOptionsMask determines whether to set, clear all or disable + * options and whether frames are to be notified of such. + * @return whether any options were actually changed + */ + bool SetOptionsSelectedByIndex(int32_t aStartIndex, int32_t aEndIndex, + OptionFlags aOptionsMask); + + /** + * Called when an attribute is about to be changed + */ + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent) override; + void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + void DoneAddingChildren(bool aHaveNotified) override; + bool IsDoneAddingChildren() const { return mIsDoneAddingChildren; } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + HTMLSelectElement, nsGenericHTMLFormControlElementWithState) + + HTMLOptionsCollection* GetOptions() { return mOptions; } + + // ConstraintValidation + nsresult GetValidationMessage(nsAString& aValidationMessage, + ValidityStateType aType) override; + + void UpdateValueMissingValidityState(); + void UpdateValidityElementStates(bool aNotify); + /** + * Insert aElement before the node given by aBefore + */ + void Add(nsGenericHTMLElement& aElement, nsGenericHTMLElement* aBefore, + ErrorResult& aError); + void Add(nsGenericHTMLElement& aElement, int32_t aIndex, + ErrorResult& aError) { + // If item index is out of range, insert to last. + // (since beforeElement becomes null, it is inserted to last) + nsIContent* beforeContent = mOptions->GetElementAt(aIndex); + return Add(aElement, nsGenericHTMLElement::FromNodeOrNull(beforeContent), + aError); + } + + /** + * Is this a combobox? + */ + bool IsCombobox() const { return !Multiple() && Size() <= 1; } + + bool OpenInParentProcess() const { return mIsOpenInParentProcess; } + void SetOpenInParentProcess(bool aVal) { mIsOpenInParentProcess = aVal; } + + void GetPreviewValue(nsAString& aValue) { aValue = mPreviewValue; } + void SetPreviewValue(const nsAString& aValue); + + protected: + virtual ~HTMLSelectElement() = default; + + friend class SafeOptionListMutation; + + // Helper Methods + /** + * Check whether the option specified by the index is selected + * @param aIndex the index + * @return whether the option at the index is selected + */ + bool IsOptionSelectedByIndex(int32_t aIndex) const; + /** + * Starting with (and including) aStartIndex, find the first selected index + * and set mSelectedIndex to it. + * @param aStartIndex the index to start with + */ + void FindSelectedIndex(int32_t aStartIndex, bool aNotify); + /** + * Select some option if possible (generally the first non-disabled option). + * @return true if something was selected, false otherwise + */ + bool SelectSomething(bool aNotify); + /** + * Call SelectSomething(), but only if nothing is selected + * @see SelectSomething() + * @return true if something was selected, false otherwise + */ + bool CheckSelectSomething(bool aNotify); + /** + * Called to trigger notifications of frames and fixing selected index + * + * @param aSelectFrame the frame for this content (could be null) + * @param aIndex the index that was selected or deselected + * @param aSelected whether the index was selected or deselected + * @param aChangeOptionState if false, don't do anything to the + * HTMLOptionElement at aIndex. If true, change + * its selected state to aSelected. + * @param aNotify whether to notify the style system and such + */ + void OnOptionSelected(nsISelectControlFrame* aSelectFrame, int32_t aIndex, + bool aSelected, bool aChangeOptionState, bool aNotify); + /** + * Restore state to a particular state string (representing the options) + * @param aNewSelected the state string to restore to + */ + void RestoreStateTo(const SelectContentData& aNewSelected); + + // Adding options + /** + * Insert option(s) into the options[] array and perform notifications + * @param aOptions the option or optgroup being added + * @param aListIndex the index to start adding options into the list at + * @param aDepth the depth of aOptions (1=direct child of select ...) + */ + void InsertOptionsIntoList(nsIContent* aOptions, int32_t aListIndex, + int32_t aDepth, bool aNotify); + /** + * Remove option(s) from the options[] array + * @param aOptions the option or optgroup being added + * @param aListIndex the index to start removing options from the list at + * @param aDepth the depth of aOptions (1=direct child of select ...) + */ + nsresult RemoveOptionsFromList(nsIContent* aOptions, int32_t aListIndex, + int32_t aDepth, bool aNotify); + + // nsIConstraintValidation + void UpdateBarredFromConstraintValidation(); + bool IsValueMissing() const; + + /** + * Get the index of the first option at, under or following the content in + * the select, or length of options[] if none are found + * @param aOptions the content + * @return the index of the first option + */ + int32_t GetOptionIndexAt(nsIContent* aOptions); + /** + * Get the next option following the content in question (not at or under) + * (this could include siblings of the current content or siblings of the + * parent or children of siblings of the parent). + * @param aOptions the content + * @return the index of the next option after the content + */ + int32_t GetOptionIndexAfter(nsIContent* aOptions); + /** + * Get the first option index at or under the content in question. + * @param aOptions the content + * @return the index of the first option at or under the content + */ + int32_t GetFirstOptionIndex(nsIContent* aOptions); + /** + * Get the first option index under the content in question, within the + * range specified. + * @param aOptions the content + * @param aStartIndex the first child to look at + * @param aEndIndex the child *after* the last child to look at + * @return the index of the first option at or under the content + */ + int32_t GetFirstChildOptionIndex(nsIContent* aOptions, int32_t aStartIndex, + int32_t aEndIndex); + + /** + * Get the frame as an nsISelectControlFrame (MAY RETURN nullptr) + * @return the select frame, or null + */ + nsISelectControlFrame* GetSelectFrame(); + + /** + * Helper method for dispatching ContentReset notifications to list box + * frames. + */ + void DispatchContentReset(); + + /** + * Rebuilds the options array from scratch as a fallback in error cases. + */ + void RebuildOptionsArray(bool aNotify); + +#ifdef DEBUG + void VerifyOptionsArray(); +#endif + + void SetSelectedIndexInternal(int32_t aIndex, bool aNotify); + + void OnSelectionChanged(); + + /** + * Marks the selectedOptions list as dirty, so that it'll populate itself + * again. + */ + void UpdateSelectedOptions(); + + void SetUserInteracted(bool) final; + + /** The options[] array */ + RefPtr<HTMLOptionsCollection> mOptions; + nsContentUtils::AutocompleteAttrState mAutocompleteAttrState; + nsContentUtils::AutocompleteAttrState mAutocompleteInfoState; + /** false if the parser is in the middle of adding children. */ + bool mIsDoneAddingChildren : 1; + /** true if our disabled state has changed from the default **/ + bool mDisabledChanged : 1; + /** true if child nodes are being added or removed. + * Used by SafeOptionListMutation. + */ + bool mMutating : 1; + /** + * True if DoneAddingChildren will get called but shouldn't restore state. + */ + bool mInhibitStateRestoration : 1; + /** https://html.spec.whatwg.org/#user-interacted */ + bool mUserInteracted : 1; + /** True if the default selected option has been set. */ + bool mDefaultSelectionSet : 1; + /** True if we're open in the parent process */ + bool mIsOpenInParentProcess : 1; + + /** The number of non-options as children of the select */ + uint32_t mNonOptionChildren; + /** The number of optgroups anywhere under the select */ + uint32_t mOptGroupCount; + /** + * The current selected index for selectedIndex (will be the first selected + * index if multiple are selected) + */ + int32_t mSelectedIndex; + /** + * The temporary restore state in case we try to restore before parser is + * done adding options + */ + UniquePtr<SelectContentData> mRestoreState; + + /** + * The live list of selected options. + */ + RefPtr<nsContentList> mSelectedOptions; + + /** + * The current displayed preview text. + */ + nsString mPreviewValue; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_HTMLSelectElement_h diff --git a/dom/html/HTMLSharedElement.cpp b/dom/html/HTMLSharedElement.cpp new file mode 100644 index 0000000000..b18f5e3339 --- /dev/null +++ b/dom/html/HTMLSharedElement.cpp @@ -0,0 +1,223 @@ +/* -*- 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/HTMLSharedElement.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/HTMLBaseElementBinding.h" +#include "mozilla/dom/HTMLDirectoryElementBinding.h" +#include "mozilla/dom/HTMLHeadElementBinding.h" +#include "mozilla/dom/HTMLHtmlElementBinding.h" +#include "mozilla/dom/HTMLParamElementBinding.h" +#include "mozilla/dom/HTMLQuoteElementBinding.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "nsContentUtils.h" +#include "nsIContentSecurityPolicy.h" +#include "nsIURI.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Shared) + +namespace mozilla::dom { + +HTMLSharedElement::~HTMLSharedElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLSharedElement) + +void HTMLSharedElement::GetHref(nsAString& aValue) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::base), + "This should only get called for <base> elements"); + nsAutoString href; + GetAttr(nsGkAtoms::href, href); + + nsCOMPtr<nsIURI> uri; + Document* doc = OwnerDoc(); + nsContentUtils::NewURIWithDocumentCharset(getter_AddRefs(uri), href, doc, + doc->GetFallbackBaseURI()); + + if (!uri) { + aValue = href; + return; + } + + nsAutoCString spec; + uri->GetSpec(spec); + CopyUTF8toUTF16(spec, aValue); +} + +void HTMLSharedElement::DoneAddingChildren(bool aHaveNotified) { + if (mNodeInfo->Equals(nsGkAtoms::head)) { + if (nsCOMPtr<Document> doc = GetUncomposedDoc()) { + doc->OnL10nResourceContainerParsed(); + if (!doc->IsLoadedAsData()) { + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, u"DOMHeadElementParsed"_ns, + CanBubble::eYes, ChromeOnlyDispatch::eYes); + // Always run async in order to avoid running script when the content + // sink isn't expecting it. + asyncDispatcher->PostDOMEvent(); + } + } + } +} + +static void SetBaseURIUsingFirstBaseWithHref(Document* aDocument, + nsIContent* aMustMatch) { + MOZ_ASSERT(aDocument, "Need a document!"); + + for (nsIContent* child = aDocument->GetFirstChild(); child; + child = child->GetNextNode()) { + if (child->IsHTMLElement(nsGkAtoms::base) && + child->AsElement()->HasAttr(nsGkAtoms::href)) { + if (aMustMatch && child != aMustMatch) { + return; + } + + // Resolve the <base> element's href relative to our document's + // fallback base URI. + nsAutoString href; + child->AsElement()->GetAttr(nsGkAtoms::href, href); + + nsCOMPtr<nsIURI> newBaseURI; + nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(newBaseURI), href, aDocument, + aDocument->GetFallbackBaseURI()); + + // Check if CSP allows this base-uri + nsresult rv = NS_OK; + nsCOMPtr<nsIContentSecurityPolicy> csp = aDocument->GetCsp(); + if (csp && newBaseURI) { + // base-uri is only enforced if explicitly defined in the + // policy - do *not* consult default-src, see: + // http://www.w3.org/TR/CSP2/#directive-default-src + bool cspPermitsBaseURI = true; + rv = csp->Permits( + child->AsElement(), nullptr /* nsICSPEventListener */, newBaseURI, + nsIContentSecurityPolicy::BASE_URI_DIRECTIVE, true /* aSpecific */, + true /* aSendViolationReports */, &cspPermitsBaseURI); + if (NS_FAILED(rv) || !cspPermitsBaseURI) { + newBaseURI = nullptr; + } + } + aDocument->SetBaseURI(newBaseURI); + aDocument->SetChromeXHRDocBaseURI(nullptr); + return; + } + } + + aDocument->SetBaseURI(nullptr); +} + +static void SetBaseTargetUsingFirstBaseWithTarget(Document* aDocument, + nsIContent* aMustMatch) { + MOZ_ASSERT(aDocument, "Need a document!"); + + for (nsIContent* child = aDocument->GetFirstChild(); child; + child = child->GetNextNode()) { + if (child->IsHTMLElement(nsGkAtoms::base) && + child->AsElement()->HasAttr(nsGkAtoms::target)) { + if (aMustMatch && child != aMustMatch) { + return; + } + + nsString target; + child->AsElement()->GetAttr(nsGkAtoms::target, target); + aDocument->SetBaseTarget(target); + return; + } + } + + aDocument->SetBaseTarget(u""_ns); +} + +void HTMLSharedElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::href) { + // If the href attribute of a <base> tag is changing, we may need to + // update the document's base URI, which will cause all the links on the + // page to be re-resolved given the new base. + // If the href is being unset (aValue is null), we will need to find a new + // <base>. + if (mNodeInfo->Equals(nsGkAtoms::base) && IsInUncomposedDoc()) { + SetBaseURIUsingFirstBaseWithHref(GetUncomposedDoc(), + aValue ? this : nullptr); + } + } else if (aName == nsGkAtoms::target) { + // The target attribute is in pretty much the same situation as the href + // attribute, above. + if (mNodeInfo->Equals(nsGkAtoms::base) && IsInUncomposedDoc()) { + SetBaseTargetUsingFirstBaseWithTarget(GetUncomposedDoc(), + aValue ? this : nullptr); + } + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNamespaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +nsresult HTMLSharedElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + // The document stores a pointer to its base URI and base target, which we may + // need to update here. + if (mNodeInfo->Equals(nsGkAtoms::base) && IsInUncomposedDoc()) { + if (HasAttr(nsGkAtoms::href)) { + SetBaseURIUsingFirstBaseWithHref(&aContext.OwnerDoc(), this); + } + if (HasAttr(nsGkAtoms::target)) { + SetBaseTargetUsingFirstBaseWithTarget(&aContext.OwnerDoc(), this); + } + } + + return NS_OK; +} + +void HTMLSharedElement::UnbindFromTree(bool aNullParent) { + Document* doc = GetUncomposedDoc(); + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + // If we're removing a <base> from a document, we may need to update the + // document's base URI and base target + if (doc && mNodeInfo->Equals(nsGkAtoms::base)) { + if (HasAttr(nsGkAtoms::href)) { + SetBaseURIUsingFirstBaseWithHref(doc, nullptr); + } + if (HasAttr(nsGkAtoms::target)) { + SetBaseTargetUsingFirstBaseWithTarget(doc, nullptr); + } + } +} + +JSObject* HTMLSharedElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + if (mNodeInfo->Equals(nsGkAtoms::param)) { + return HTMLParamElement_Binding::Wrap(aCx, this, aGivenProto); + } + if (mNodeInfo->Equals(nsGkAtoms::base)) { + return HTMLBaseElement_Binding::Wrap(aCx, this, aGivenProto); + } + if (mNodeInfo->Equals(nsGkAtoms::dir)) { + return HTMLDirectoryElement_Binding::Wrap(aCx, this, aGivenProto); + } + if (mNodeInfo->Equals(nsGkAtoms::q) || + mNodeInfo->Equals(nsGkAtoms::blockquote)) { + return HTMLQuoteElement_Binding::Wrap(aCx, this, aGivenProto); + } + if (mNodeInfo->Equals(nsGkAtoms::head)) { + return HTMLHeadElement_Binding::Wrap(aCx, this, aGivenProto); + } + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::html)); + return HTMLHtmlElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLSharedElement.h b/dom/html/HTMLSharedElement.h new file mode 100644 index 0000000000..cb1e5ff288 --- /dev/null +++ b/dom/html/HTMLSharedElement.h @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLSharedElement_h +#define mozilla_dom_HTMLSharedElement_h + +#include "nsGenericHTMLElement.h" + +#include "nsGkAtoms.h" + +#include "mozilla/Attributes.h" +#include "mozilla/Assertions.h" + +namespace mozilla::dom { + +class HTMLSharedElement final : public nsGenericHTMLElement { + public: + explicit HTMLSharedElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + if (mNodeInfo->Equals(nsGkAtoms::head) || + mNodeInfo->Equals(nsGkAtoms::html)) { + SetHasWeirdParserInsertionMode(); + } + } + + // nsIContent + void DoneAddingChildren(bool aHaveNotified) override; + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + + void UnbindFromTree(bool aNullParent = true) override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // WebIDL API + // HTMLParamElement + void GetName(DOMString& aValue) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param)); + GetHTMLAttr(nsGkAtoms::name, aValue); + } + void SetName(const nsAString& aValue, ErrorResult& aResult) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param)); + SetHTMLAttr(nsGkAtoms::name, aValue, aResult); + } + void GetValue(DOMString& aValue) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param)); + GetHTMLAttr(nsGkAtoms::value, aValue); + } + void SetValue(const nsAString& aValue, ErrorResult& aResult) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param)); + SetHTMLAttr(nsGkAtoms::value, aValue, aResult); + } + void GetType(DOMString& aValue) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param)); + GetHTMLAttr(nsGkAtoms::type, aValue); + } + void SetType(const nsAString& aValue, ErrorResult& aResult) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param)); + SetHTMLAttr(nsGkAtoms::type, aValue, aResult); + } + void GetValueType(DOMString& aValue) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param)); + GetHTMLAttr(nsGkAtoms::valuetype, aValue); + } + void SetValueType(const nsAString& aValue, ErrorResult& aResult) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::param)); + SetHTMLAttr(nsGkAtoms::valuetype, aValue, aResult); + } + + // HTMLBaseElement + void GetTarget(DOMString& aValue) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::base)); + GetHTMLAttr(nsGkAtoms::target, aValue); + } + void SetTarget(const nsAString& aValue, ErrorResult& aResult) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::base)); + SetHTMLAttr(nsGkAtoms::target, aValue, aResult); + } + + void GetHref(nsAString& aValue); + void SetHref(const nsAString& aValue, ErrorResult& aResult) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::base)); + SetHTMLAttr(nsGkAtoms::href, aValue, aResult); + } + + // HTMLDirectoryElement + bool Compact() const { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::dir)); + return GetBoolAttr(nsGkAtoms::compact); + } + void SetCompact(bool aCompact, ErrorResult& aResult) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::dir)); + SetHTMLBoolAttr(nsGkAtoms::compact, aCompact, aResult); + } + + // HTMLQuoteElement + void GetCite(nsString& aCite) { GetHTMLURIAttr(nsGkAtoms::cite, aCite); } + + void SetCite(const nsAString& aValue, ErrorResult& aResult) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::q) || + mNodeInfo->Equals(nsGkAtoms::blockquote)); + SetHTMLAttr(nsGkAtoms::cite, aValue, aResult); + } + + // HTMLHtmlElement + void GetVersion(DOMString& aValue) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::html)); + GetHTMLAttr(nsGkAtoms::version, aValue); + } + void SetVersion(const nsAString& aValue, ErrorResult& aResult) { + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::html)); + SetHTMLAttr(nsGkAtoms::version, aValue, aResult); + } + + protected: + virtual ~HTMLSharedElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLSharedElement_h diff --git a/dom/html/HTMLSharedListElement.cpp b/dom/html/HTMLSharedListElement.cpp new file mode 100644 index 0000000000..b5a86a8154 --- /dev/null +++ b/dom/html/HTMLSharedListElement.cpp @@ -0,0 +1,148 @@ +/* -*- 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/HTMLSharedListElement.h" +#include "mozilla/dom/HTMLDListElementBinding.h" +#include "mozilla/dom/HTMLOListElementBinding.h" +#include "mozilla/dom/HTMLUListElementBinding.h" +#include "mozilla/dom/HTMLLIElement.h" + +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsGenericHTMLElement.h" +#include "nsAttrValueInlines.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(SharedList) + +namespace mozilla::dom { + +HTMLSharedListElement::~HTMLSharedListElement() = default; + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLSharedListElement, + nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLSharedListElement) + +bool HTMLSharedListElement::ParseAttribute( + int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (mNodeInfo->Equals(nsGkAtoms::ul)) { + if (aAttribute == nsGkAtoms::type) { + return aResult.ParseEnumValue(aValue, HTMLLIElement::kULTypeTable, + false); + } + } + if (mNodeInfo->Equals(nsGkAtoms::ol)) { + if (aAttribute == nsGkAtoms::type) { + return aResult.ParseEnumValue(aValue, HTMLLIElement::kOLTypeTable, + true); + } + if (aAttribute == nsGkAtoms::start) { + return aResult.ParseIntValue(aValue); + } + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLSharedListElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_list_style_type)) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::type); + if (value && value->Type() == nsAttrValue::eEnum) { + aBuilder.SetKeywordValue(eCSSProperty_list_style_type, + value->GetEnumValue()); + } + } + + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +void HTMLSharedListElement::MapOLAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_counter_reset)) { + const nsAttrValue* startAttr = aBuilder.GetAttr(nsGkAtoms::start); + bool haveStart = startAttr && startAttr->Type() == nsAttrValue::eInteger; + int32_t start = 0; + if (haveStart) { + start = startAttr->GetIntegerValue() - 1; + } + bool haveReversed = !!aBuilder.GetAttr(nsGkAtoms::reversed); + if (haveReversed) { + if (haveStart) { + start += 2; // i.e. the attr value + 1 + } else { + start = std::numeric_limits<int32_t>::min(); + } + } + if (haveStart || haveReversed) { + aBuilder.SetCounterResetListItem(start, haveReversed); + } + } + + HTMLSharedListElement::MapAttributesIntoRule(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLSharedListElement::IsAttributeMapped(const nsAtom* aAttribute) const { + if (mNodeInfo->Equals(nsGkAtoms::ul)) { + static const MappedAttributeEntry attributes[] = {{nsGkAtoms::type}, + {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); + } + + if (mNodeInfo->Equals(nsGkAtoms::ol)) { + static const MappedAttributeEntry attributes[] = {{nsGkAtoms::type}, + {nsGkAtoms::start}, + {nsGkAtoms::reversed}, + {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); + } + + return nsGenericHTMLElement::IsAttributeMapped(aAttribute); +} + +nsMapRuleToAttributesFunc HTMLSharedListElement::GetAttributeMappingFunction() + const { + if (mNodeInfo->Equals(nsGkAtoms::ul)) { + return &MapAttributesIntoRule; + } + if (mNodeInfo->Equals(nsGkAtoms::ol)) { + return &MapOLAttributesIntoRule; + } + + return nsGenericHTMLElement::GetAttributeMappingFunction(); +} + +JSObject* HTMLSharedListElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + if (mNodeInfo->Equals(nsGkAtoms::ol)) { + return HTMLOListElement_Binding::Wrap(aCx, this, aGivenProto); + } + if (mNodeInfo->Equals(nsGkAtoms::dl)) { + return HTMLDListElement_Binding::Wrap(aCx, this, aGivenProto); + } + MOZ_ASSERT(mNodeInfo->Equals(nsGkAtoms::ul)); + return HTMLUListElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLSharedListElement.h b/dom/html/HTMLSharedListElement.h new file mode 100644 index 0000000000..0b9b2e72dc --- /dev/null +++ b/dom/html/HTMLSharedListElement.h @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLSharedListElement_h +#define mozilla_dom_HTMLSharedListElement_h + +#include "mozilla/Attributes.h" + +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLSharedListElement final : public nsGenericHTMLElement { + public: + explicit HTMLSharedListElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + bool Reversed() const { return GetBoolAttr(nsGkAtoms::reversed); } + void SetReversed(bool aReversed, mozilla::ErrorResult& rv) { + SetHTMLBoolAttr(nsGkAtoms::reversed, aReversed, rv); + } + int32_t Start() const { return GetIntAttr(nsGkAtoms::start, 1); } + void SetStart(int32_t aStart, mozilla::ErrorResult& rv) { + SetHTMLIntAttr(nsGkAtoms::start, aStart, rv); + } + void GetType(DOMString& aType) { GetHTMLAttr(nsGkAtoms::type, aType); } + void SetType(const nsAString& aType, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::type, aType, rv); + } + bool Compact() const { return GetBoolAttr(nsGkAtoms::compact); } + void SetCompact(bool aCompact, mozilla::ErrorResult& rv) { + SetHTMLBoolAttr(nsGkAtoms::compact, aCompact, rv); + } + + protected: + virtual ~HTMLSharedListElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); + static void MapOLAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLSharedListElement_h diff --git a/dom/html/HTMLSlotElement.cpp b/dom/html/HTMLSlotElement.cpp new file mode 100644 index 0000000000..9fb3986e93 --- /dev/null +++ b/dom/html/HTMLSlotElement.cpp @@ -0,0 +1,371 @@ +/* -*- 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/PresShell.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLSlotElement.h" +#include "mozilla/dom/HTMLUnknownElement.h" +#include "mozilla/dom/ShadowRoot.h" +#include "mozilla/dom/Text.h" +#include "mozilla/AppShutdown.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" + +nsGenericHTMLElement* NS_NewHTMLSlotElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(std::move(aNodeInfo)); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) mozilla::dom::HTMLSlotElement(nodeInfo.forget()); +} + +namespace mozilla::dom { + +HTMLSlotElement::HTMLSlotElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLSlotElement::~HTMLSlotElement() { + for (const auto& node : mManuallyAssignedNodes) { + MOZ_ASSERT(node->AsContent()->GetManualSlotAssignment() == this); + node->AsContent()->SetManualSlotAssignment(nullptr); + } +} + +NS_IMPL_ADDREF_INHERITED(HTMLSlotElement, nsGenericHTMLElement) +NS_IMPL_RELEASE_INHERITED(HTMLSlotElement, nsGenericHTMLElement) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLSlotElement, nsGenericHTMLElement, + mAssignedNodes) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HTMLSlotElement) +NS_INTERFACE_MAP_END_INHERITING(nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLSlotElement) + +nsresult HTMLSlotElement::BindToTree(BindContext& aContext, nsINode& aParent) { + RefPtr<ShadowRoot> oldContainingShadow = GetContainingShadow(); + + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + ShadowRoot* containingShadow = GetContainingShadow(); + mInManualShadowRoot = + containingShadow && + containingShadow->SlotAssignment() == SlotAssignmentMode::Manual; + if (containingShadow && !oldContainingShadow) { + containingShadow->AddSlot(this); + } + + return NS_OK; +} + +void HTMLSlotElement::UnbindFromTree(bool aNullParent) { + RefPtr<ShadowRoot> oldContainingShadow = GetContainingShadow(); + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + if (oldContainingShadow && !GetContainingShadow()) { + oldContainingShadow->RemoveSlot(this); + } +} + +void HTMLSlotElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::name) { + if (ShadowRoot* containingShadow = GetContainingShadow()) { + containingShadow->RemoveSlot(this); + } + } + + return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue, + aNotify); +} + +void HTMLSlotElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::name) { + if (ShadowRoot* containingShadow = GetContainingShadow()) { + containingShadow->AddSlot(this); + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +/** + * Flatten assigned nodes given a slot, as in: + * https://dom.spec.whatwg.org/#find-flattened-slotables + */ +static void FlattenAssignedNodes(HTMLSlotElement* aSlot, + nsTArray<RefPtr<nsINode>>& aNodes) { + if (!aSlot->GetContainingShadow()) { + return; + } + + const nsTArray<RefPtr<nsINode>>& assignedNodes = aSlot->AssignedNodes(); + + // If assignedNodes is empty, use children of slot as fallback content. + if (assignedNodes.IsEmpty()) { + for (nsIContent* child = aSlot->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (!child->IsSlotable()) { + continue; + } + + if (auto* slot = HTMLSlotElement::FromNode(child)) { + FlattenAssignedNodes(slot, aNodes); + } else { + aNodes.AppendElement(child); + } + } + return; + } + + for (const RefPtr<nsINode>& assignedNode : assignedNodes) { + auto* slot = HTMLSlotElement::FromNode(assignedNode); + if (slot && slot->GetContainingShadow()) { + FlattenAssignedNodes(slot, aNodes); + } else { + aNodes.AppendElement(assignedNode); + } + } +} + +void HTMLSlotElement::AssignedNodes(const AssignedNodesOptions& aOptions, + nsTArray<RefPtr<nsINode>>& aNodes) { + if (aOptions.mFlatten) { + return FlattenAssignedNodes(this, aNodes); + } + + aNodes = mAssignedNodes.Clone(); +} + +void HTMLSlotElement::AssignedElements(const AssignedNodesOptions& aOptions, + nsTArray<RefPtr<Element>>& aElements) { + AutoTArray<RefPtr<nsINode>, 128> assignedNodes; + AssignedNodes(aOptions, assignedNodes); + for (const RefPtr<nsINode>& assignedNode : assignedNodes) { + if (assignedNode->IsElement()) { + aElements.AppendElement(assignedNode->AsElement()); + } + } +} + +const nsTArray<RefPtr<nsINode>>& HTMLSlotElement::AssignedNodes() const { + return mAssignedNodes; +} + +const nsTArray<nsINode*>& HTMLSlotElement::ManuallyAssignedNodes() const { + return mManuallyAssignedNodes; +} + +void HTMLSlotElement::Assign(const Sequence<OwningElementOrText>& aNodes) { + nsAutoScriptBlocker scriptBlocker; + + // no-op if the input nodes and the assigned nodes are identical + // This also works if the two 'assign' calls are like + // > slot.assign(node1, node2); + // > slot.assign(node1, node2, node1, node2); + if (!mAssignedNodes.IsEmpty() && aNodes.Length() >= mAssignedNodes.Length()) { + nsTHashMap<nsPtrHashKey<nsIContent>, size_t> nodeIndexMap; + for (size_t i = 0; i < aNodes.Length(); ++i) { + nsIContent* content; + if (aNodes[i].IsElement()) { + content = aNodes[i].GetAsElement(); + } else { + content = aNodes[i].GetAsText(); + } + MOZ_ASSERT(content); + // We only care about the first index this content appears + // in the array + nodeIndexMap.LookupOrInsert(content, i); + } + + if (nodeIndexMap.Count() == mAssignedNodes.Length()) { + bool isIdentical = true; + for (size_t i = 0; i < mAssignedNodes.Length(); ++i) { + size_t indexInInputNodes; + if (!nodeIndexMap.Get(mAssignedNodes[i]->AsContent(), + &indexInInputNodes) || + indexInInputNodes != i) { + isIdentical = false; + break; + } + } + if (isIdentical) { + return; + } + } + } + + // 1. For each node of this's manually assigned nodes, set node's manual slot + // assignment to null. + for (nsINode* node : mManuallyAssignedNodes) { + MOZ_ASSERT(node->AsContent()->GetManualSlotAssignment() == this); + node->AsContent()->SetManualSlotAssignment(nullptr); + } + + // 2. Let nodesSet be a new ordered set. + mManuallyAssignedNodes.Clear(); + + nsIContent* host = nullptr; + ShadowRoot* root = GetContainingShadow(); + + // An optimization to keep track which slots need to enqueue + // slotchange event, such that they can be enqueued later in + // tree order. + nsTHashSet<RefPtr<HTMLSlotElement>> changedSlots; + + // Clear out existing assigned nodes + if (mInManualShadowRoot) { + if (!mAssignedNodes.IsEmpty()) { + changedSlots.EnsureInserted(this); + if (root) { + root->InvalidateStyleAndLayoutOnSubtree(this); + } + ClearAssignedNodes(); + } + + MOZ_ASSERT(mAssignedNodes.IsEmpty()); + host = GetContainingShadowHost(); + } + + for (const OwningElementOrText& elementOrText : aNodes) { + nsIContent* content; + if (elementOrText.IsElement()) { + content = elementOrText.GetAsElement(); + } else { + content = elementOrText.GetAsText(); + } + + MOZ_ASSERT(content); + // XXXsmaug Should we have a helper for + // https://infra.spec.whatwg.org/#ordered-set? + if (content->GetManualSlotAssignment() != this) { + if (HTMLSlotElement* oldSlot = content->GetAssignedSlot()) { + if (changedSlots.EnsureInserted(oldSlot)) { + if (root) { + MOZ_ASSERT(oldSlot->GetContainingShadow() == root); + root->InvalidateStyleAndLayoutOnSubtree(oldSlot); + } + } + } + + if (changedSlots.EnsureInserted(this)) { + if (root) { + root->InvalidateStyleAndLayoutOnSubtree(this); + } + } + // 3.1 (HTML Spec) If content's manual slot assignment refers to a slot, + // then remove node from that slot's manually assigned nodes. 3.2 (HTML + // Spec) Set content's manual slot assignment to this. + if (HTMLSlotElement* oldSlot = content->GetManualSlotAssignment()) { + oldSlot->RemoveManuallyAssignedNode(*content); + } + content->SetManualSlotAssignment(this); + mManuallyAssignedNodes.AppendElement(content); + + if (root && host && content->GetParent() == host) { + // Equivalent to 4.2.2.4.3 (DOM Spec) `Set slot's assigned nodes to + // slottables` + root->MaybeReassignContent(*content); + } + } + } + + // The `assign slottables` step is completed already at this point, + // however we haven't fired the `slotchange` event yet because this + // needs to be done in tree order. + if (root) { + for (nsIContent* child = root->GetFirstChild(); child; + child = child->GetNextNode()) { + if (HTMLSlotElement* slot = HTMLSlotElement::FromNode(child)) { + if (changedSlots.EnsureRemoved(slot)) { + slot->EnqueueSlotChangeEvent(); + } + } + } + MOZ_ASSERT(changedSlots.IsEmpty()); + } +} + +void HTMLSlotElement::InsertAssignedNode(uint32_t aIndex, nsIContent& aNode) { + MOZ_ASSERT(!aNode.GetAssignedSlot(), "Losing track of a slot"); + mAssignedNodes.InsertElementAt(aIndex, &aNode); + aNode.SetAssignedSlot(this); + SlotAssignedNodeChanged(this, aNode); +} + +void HTMLSlotElement::AppendAssignedNode(nsIContent& aNode) { + MOZ_ASSERT(!aNode.GetAssignedSlot(), "Losing track of a slot"); + mAssignedNodes.AppendElement(&aNode); + aNode.SetAssignedSlot(this); + SlotAssignedNodeChanged(this, aNode); +} + +void HTMLSlotElement::RemoveAssignedNode(nsIContent& aNode) { + // This one runs from unlinking, so we can't guarantee that the slot pointer + // hasn't been cleared. + MOZ_ASSERT(!aNode.GetAssignedSlot() || aNode.GetAssignedSlot() == this, + "How exactly?"); + mAssignedNodes.RemoveElement(&aNode); + aNode.SetAssignedSlot(nullptr); + SlotAssignedNodeChanged(this, aNode); +} + +void HTMLSlotElement::ClearAssignedNodes() { + for (RefPtr<nsINode>& node : mAssignedNodes) { + MOZ_ASSERT(!node->AsContent()->GetAssignedSlot() || + node->AsContent()->GetAssignedSlot() == this, + "How exactly?"); + node->AsContent()->SetAssignedSlot(nullptr); + } + + mAssignedNodes.Clear(); +} + +void HTMLSlotElement::EnqueueSlotChangeEvent() { + if (mInSignalSlotList) { + return; + } + + // FIXME(bug 1459704): Need to figure out how to deal with microtasks posted + // during shutdown. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads)) { + return; + } + + DocGroup* docGroup = OwnerDoc()->GetDocGroup(); + if (!docGroup) { + return; + } + + mInSignalSlotList = true; + docGroup->SignalSlotChange(*this); +} + +void HTMLSlotElement::FireSlotChangeEvent() { + nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, u"slotchange"_ns, + CanBubble::eYes, Cancelable::eNo); +} + +void HTMLSlotElement::RemoveManuallyAssignedNode(nsIContent& aNode) { + mManuallyAssignedNodes.RemoveElement(&aNode); + RemoveAssignedNode(aNode); +} + +JSObject* HTMLSlotElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLSlotElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLSlotElement.h b/dom/html/HTMLSlotElement.h new file mode 100644 index 0000000000..fa12f0df26 --- /dev/null +++ b/dom/html/HTMLSlotElement.h @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLSlotElement_h +#define mozilla_dom_HTMLSlotElement_h + +#include "nsGenericHTMLElement.h" +#include "nsTArray.h" +#include "mozilla/dom/HTMLSlotElementBinding.h" + +namespace mozilla::dom { + +class HTMLSlotElement final : public nsGenericHTMLElement { + public: + explicit HTMLSlotElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLSlotElement, slot) + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLSlotElement, + nsGenericHTMLElement) + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // nsIContent + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent) override; + + void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + // WebIDL + void SetName(const nsAString& aName, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::name, aName, aRv); + } + + void GetName(nsAString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); } + + void AssignedNodes(const AssignedNodesOptions& aOptions, + nsTArray<RefPtr<nsINode>>& aNodes); + + void AssignedElements(const AssignedNodesOptions& aOptions, + nsTArray<RefPtr<Element>>& aNodes); + + void Assign(const Sequence<OwningElementOrText>& aNodes); + + // Helper methods + const nsTArray<RefPtr<nsINode>>& AssignedNodes() const; + const nsTArray<nsINode*>& ManuallyAssignedNodes() const; + void InsertAssignedNode(uint32_t aIndex, nsIContent&); + void AppendAssignedNode(nsIContent&); + void RemoveAssignedNode(nsIContent&); + void ClearAssignedNodes(); + + void EnqueueSlotChangeEvent(); + void RemovedFromSignalSlotList() { + MOZ_ASSERT(mInSignalSlotList); + mInSignalSlotList = false; + } + + void FireSlotChangeEvent(); + + void RemoveManuallyAssignedNode(nsIContent&); + + protected: + virtual ~HTMLSlotElement(); + JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) final; + + nsTArray<RefPtr<nsINode>> mAssignedNodes; + nsTArray<nsINode*> mManuallyAssignedNodes; + + // Whether we're in the signal slot list of our unit of related similar-origin + // browsing contexts. + // + // https://dom.spec.whatwg.org/#signal-slot-list + bool mInSignalSlotList = false; + + bool mInManualShadowRoot = false; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLSlotElement_h diff --git a/dom/html/HTMLSourceElement.cpp b/dom/html/HTMLSourceElement.cpp new file mode 100644 index 0000000000..b08d449594 --- /dev/null +++ b/dom/html/HTMLSourceElement.cpp @@ -0,0 +1,230 @@ +/* -*- 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/HTMLSourceElement.h" +#include "mozilla/dom/HTMLSourceElementBinding.h" + +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/HTMLImageElement.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/ResponsiveImageSelector.h" +#include "mozilla/dom/MediaList.h" +#include "mozilla/dom/MediaSource.h" + +#include "mozilla/dom/BlobURLProtocolHandler.h" +#include "mozilla/AttributeStyles.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/Preferences.h" + +#include "nsGkAtoms.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Source) + +namespace mozilla::dom { + +HTMLSourceElement::HTMLSourceElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLSourceElement::~HTMLSourceElement() = default; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLSourceElement, nsGenericHTMLElement, + mSrcMediaSource) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLSourceElement, + nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLSourceElement) + +bool HTMLSourceElement::MatchesCurrentMedia() { + if (mMediaList) { + return mMediaList->Matches(*OwnerDoc()); + } + + // No media specified + return true; +} + +/* static */ +bool HTMLSourceElement::WouldMatchMediaForDocument(const nsAString& aMedia, + const Document* aDocument) { + if (aMedia.IsEmpty()) { + return true; + } + + RefPtr<MediaList> mediaList = + MediaList::Create(NS_ConvertUTF16toUTF8(aMedia)); + return mediaList->Matches(*aDocument); +} + +void HTMLSourceElement::UpdateMediaList(const nsAttrValue* aValue) { + mMediaList = nullptr; + if (!aValue) { + return; + } + + NS_ConvertUTF16toUTF8 mediaStr(aValue->GetStringValue()); + mMediaList = MediaList::Create(mediaStr); +} + +bool HTMLSourceElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && + (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height)) { + return aResult.ParseHTMLDimension(aValue); + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLSourceElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::srcset) { + mSrcsetTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal( + this, aValue ? aValue->GetStringValue() : EmptyString(), + aMaybeScriptedPrincipal); + } + // If we are associated with a <picture> with a valid <img>, notify it of + // responsive parameter changes + if (aNameSpaceID == kNameSpaceID_None && + (aName == nsGkAtoms::srcset || aName == nsGkAtoms::sizes || + aName == nsGkAtoms::media || aName == nsGkAtoms::type) && + IsInPicture()) { + if (aName == nsGkAtoms::media) { + UpdateMediaList(aValue); + } + + nsString strVal = aValue ? aValue->GetStringValue() : EmptyString(); + // Find all img siblings after this <source> and notify them of the change + nsCOMPtr<nsIContent> sibling = AsContent(); + while ((sibling = sibling->GetNextSibling())) { + if (auto* img = HTMLImageElement::FromNode(sibling)) { + if (aName == nsGkAtoms::srcset) { + img->PictureSourceSrcsetChanged(this, strVal, aNotify); + } else if (aName == nsGkAtoms::sizes) { + img->PictureSourceSizesChanged(this, strVal, aNotify); + } else if (aName == nsGkAtoms::media || aName == nsGkAtoms::type) { + img->PictureSourceMediaOrTypeChanged(this, aNotify); + } + } + } + } else if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::media) { + UpdateMediaList(aValue); + } else if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::src) { + mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal( + this, aValue ? aValue->GetStringValue() : EmptyString(), + aMaybeScriptedPrincipal); + mSrcMediaSource = nullptr; + if (aValue) { + nsString srcStr = aValue->GetStringValue(); + nsCOMPtr<nsIURI> uri; + NewURIFromString(srcStr, getter_AddRefs(uri)); + if (uri && IsMediaSourceURI(uri)) { + NS_GetSourceForMediaSourceURI(uri, getter_AddRefs(mSrcMediaSource)); + } + } + } else if (aNameSpaceID == kNameSpaceID_None && + IsAttributeMappedToImages(aName) && IsInPicture()) { + BuildMappedAttributesForImage(); + + nsCOMPtr<nsIContent> sibling = AsContent(); + while ((sibling = sibling->GetNextSibling())) { + if (auto* img = HTMLImageElement::FromNode(sibling)) { + img->PictureSourceDimensionChanged(this, aNotify); + } + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +nsresult HTMLSourceElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (auto* media = HTMLMediaElement::FromNode(aParent)) { + media->NotifyAddedSource(); + } + + if (aParent.IsHTMLElement(nsGkAtoms::picture)) { + BuildMappedAttributesForImage(); + } else { + mMappedAttributesForImage = nullptr; + } + + return NS_OK; +} + +void HTMLSourceElement::UnbindFromTree(bool aNullParent) { + mMappedAttributesForImage = nullptr; + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +JSObject* HTMLSourceElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLSourceElement_Binding::Wrap(aCx, this, aGivenProto); +} + +/** + * Helper to map the image source attributes. + * Note: This will override the declaration created by the presentation + * attributes of HTMLImageElement (i.e. mapped by MapImageSizeAttributeInto). + * https://html.spec.whatwg.org/multipage/embedded-content.html#the-source-element + */ +void HTMLSourceElement::BuildMappedAttributesForImage() { + MOZ_ASSERT(NS_IsMainThread()); + + mMappedAttributesForImage = nullptr; + + Document* document = GetComposedDoc(); + if (!document) { + return; + } + + const nsAttrValue* width = mAttrs.GetAttr(nsGkAtoms::width); + const nsAttrValue* height = mAttrs.GetAttr(nsGkAtoms::height); + if (!width && !height) { + return; + } + + MappedDeclarationsBuilder builder(*this, *document); + // We should set the missing property values with auto value to make sure it + // overrides the declaration created by the presentation attributes of + // HTMLImageElement. This can make sure we compute the ratio-dependent axis + // size properly by the natural aspect-ratio of the image. + // + // Note: The spec doesn't specify this, so we follow the implementation in + // other browsers. + // Spec issue: https://github.com/whatwg/html/issues/8178. + if (width) { + MapDimensionAttributeInto(builder, eCSSProperty_width, *width); + } else { + builder.SetAutoValue(eCSSProperty_width); + } + + if (height) { + MapDimensionAttributeInto(builder, eCSSProperty_height, *height); + } else { + builder.SetAutoValue(eCSSProperty_height); + } + + if (width && height) { + DoMapAspectRatio(*width, *height, builder); + } else { + builder.SetAutoValue(eCSSProperty_aspect_ratio); + } + mMappedAttributesForImage = builder.TakeDeclarationBlock(); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLSourceElement.h b/dom/html/HTMLSourceElement.h new file mode 100644 index 0000000000..4d4a1b212d --- /dev/null +++ b/dom/html/HTMLSourceElement.h @@ -0,0 +1,157 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLSourceElement_h +#define mozilla_dom_HTMLSourceElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "mozilla/dom/HTMLMediaElement.h" + +class nsAttrValue; + +namespace mozilla::dom { + +class MediaList; + +class HTMLSourceElement final : public nsGenericHTMLElement { + public: + explicit HTMLSourceElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLSourceElement, + nsGenericHTMLElement) + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLSourceElement, source) + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // Override BindToTree() so that we can trigger a load when we add a + // child source element. + nsresult BindToTree(BindContext&, nsINode& aParent) override; + + void UnbindFromTree(bool aNullParent) override; + + // If this element's media attr matches for its owner document. Returns true + // if no media attr was set. + bool MatchesCurrentMedia(); + + // True if a source tag would match the given media attribute for the + // specified document. Used by the preloader to determine valid <source> tags + // prior to DOM creation. + static bool WouldMatchMediaForDocument(const nsAString& aMediaStr, + const Document* aDocument); + + // Return the MediaSource object if any associated with the src attribute + // when it was set. + MediaSource* GetSrcMediaSource() { return mSrcMediaSource; }; + + // WebIDL + void GetSrc(nsString& aSrc) { GetURIAttr(nsGkAtoms::src, nullptr, aSrc); } + void SetSrc(const nsAString& aSrc, nsIPrincipal* aTriggeringPrincipal, + mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::src, aSrc, aTriggeringPrincipal, rv); + } + + nsIPrincipal* GetSrcTriggeringPrincipal() const { + return mSrcTriggeringPrincipal; + } + + nsIPrincipal* GetSrcsetTriggeringPrincipal() const { + return mSrcsetTriggeringPrincipal; + } + + void GetType(DOMString& aType) { GetHTMLAttr(nsGkAtoms::type, aType); } + void SetType(const nsAString& aType, ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::type, aType, rv); + } + + void GetSrcset(DOMString& aSrcset) { + GetHTMLAttr(nsGkAtoms::srcset, aSrcset); + } + void SetSrcset(const nsAString& aSrcset, nsIPrincipal* aTriggeringPrincipal, + mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::srcset, aSrcset, aTriggeringPrincipal, rv); + } + + void GetSizes(DOMString& aSizes) { GetHTMLAttr(nsGkAtoms::sizes, aSizes); } + void SetSizes(const nsAString& aSizes, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::sizes, aSizes, rv); + } + + void GetMedia(DOMString& aMedia) { GetHTMLAttr(nsGkAtoms::media, aMedia); } + void SetMedia(const nsAString& aMedia, mozilla::ErrorResult& rv) { + SetHTMLAttr(nsGkAtoms::media, aMedia, rv); + } + + uint32_t Width() const { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::width, 0); + } + void SetWidth(uint32_t aWidth, ErrorResult& aRv) { + SetUnsignedIntAttr(nsGkAtoms::width, aWidth, 0, aRv); + } + + uint32_t Height() const { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::height, 0); + } + void SetHeight(uint32_t aHeight, ErrorResult& aRv) { + SetUnsignedIntAttr(nsGkAtoms::height, aHeight, 0, aRv); + } + + const StyleLockedDeclarationBlock* GetAttributesMappedForImage() const { + return mMappedAttributesForImage; + } + + static bool IsAttributeMappedToImages(const nsAtom* aAttribute) { + return aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height; + } + + protected: + virtual ~HTMLSourceElement(); + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + + private: + // Generates a new MediaList using the given input + void UpdateMediaList(const nsAttrValue* aValue); + + void BuildMappedAttributesForImage(); + + bool IsInPicture() const { + return GetParentElement() && + GetParentElement()->IsHTMLElement(nsGkAtoms::picture); + } + + RefPtr<MediaList> mMediaList; + RefPtr<MediaSource> mSrcMediaSource; + + // The triggering principal for the src attribute. + nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal; + + // The triggering principal for the srcset attribute. + nsCOMPtr<nsIPrincipal> mSrcsetTriggeringPrincipal; + + // The mapped attributes to HTMLImageElement if we are associated with a + // <picture> with a valid <img>. + RefPtr<StyleLockedDeclarationBlock> mMappedAttributesForImage; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLSourceElement_h diff --git a/dom/html/HTMLSpanElement.cpp b/dom/html/HTMLSpanElement.cpp new file mode 100644 index 0000000000..a7c6efe919 --- /dev/null +++ b/dom/html/HTMLSpanElement.cpp @@ -0,0 +1,23 @@ +/* -*- 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/HTMLSpanElement.h" +#include "mozilla/dom/HTMLSpanElementBinding.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Span) + +namespace mozilla::dom { + +HTMLSpanElement::~HTMLSpanElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLSpanElement) + +JSObject* HTMLSpanElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLSpanElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLSpanElement.h b/dom/html/HTMLSpanElement.h new file mode 100644 index 0000000000..311ecf2647 --- /dev/null +++ b/dom/html/HTMLSpanElement.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLSpanElement_h +#define mozilla_dom_HTMLSpanElement_h + +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLSpanElement final : public nsGenericHTMLElement { + public: + explicit HTMLSpanElement(already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLSpanElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLSpanElement_h diff --git a/dom/html/HTMLStyleElement.cpp b/dom/html/HTMLStyleElement.cpp new file mode 100644 index 0000000000..ed4c141897 --- /dev/null +++ b/dom/html/HTMLStyleElement.cpp @@ -0,0 +1,202 @@ +/* -*- 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/HTMLStyleElement.h" +#include "mozilla/dom/HTMLStyleElementBinding.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/FetchPriority.h" +#include "mozilla/dom/ReferrerInfo.h" +#include "nsUnicharUtils.h" +#include "nsThreadUtils.h" +#include "nsContentUtils.h" +#include "nsStubMutationObserver.h" +#include "nsDOMTokenList.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Style) + +namespace mozilla::dom { + +HTMLStyleElement::HTMLStyleElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + AddMutationObserver(this); +} + +HTMLStyleElement::~HTMLStyleElement() = default; + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLStyleElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLStyleElement, + nsGenericHTMLElement) + tmp->LinkStyle::Traverse(cb); + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBlocking) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLStyleElement, + nsGenericHTMLElement) + tmp->LinkStyle::Unlink(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mBlocking) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLStyleElement, + nsGenericHTMLElement, + nsIMutationObserver) + +NS_IMPL_ELEMENT_CLONE(HTMLStyleElement) + +bool HTMLStyleElement::Disabled() const { + StyleSheet* ss = GetSheet(); + return ss && ss->Disabled(); +} + +void HTMLStyleElement::SetDisabled(bool aDisabled) { + if (StyleSheet* ss = GetSheet()) { + ss->SetDisabled(aDisabled); + } +} + +void HTMLStyleElement::CharacterDataChanged(nsIContent* aContent, + const CharacterDataChangeInfo&) { + ContentChanged(aContent); +} + +void HTMLStyleElement::ContentAppended(nsIContent* aFirstNewContent) { + ContentChanged(aFirstNewContent->GetParent()); +} + +void HTMLStyleElement::ContentInserted(nsIContent* aChild) { + ContentChanged(aChild); +} + +void HTMLStyleElement::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + ContentChanged(aChild); +} + +void HTMLStyleElement::ContentChanged(nsIContent* aContent) { + mTriggeringPrincipal = nullptr; + if (nsContentUtils::IsInSameAnonymousTree(this, aContent)) { + Unused << UpdateStyleSheetInternal(nullptr, nullptr); + } +} + +nsresult HTMLStyleElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + LinkStyle::BindToTree(); + return rv; +} + +void HTMLStyleElement::UnbindFromTree(bool aNullParent) { + RefPtr<Document> oldDoc = GetUncomposedDoc(); + ShadowRoot* oldShadow = GetContainingShadow(); + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + Unused << UpdateStyleSheetInternal(oldDoc, oldShadow); +} + +void HTMLStyleElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::title || aName == nsGkAtoms::media || + aName == nsGkAtoms::type) { + Unused << UpdateStyleSheetInternal(nullptr, nullptr, ForceUpdate::Yes); + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +void HTMLStyleElement::GetInnerHTML(nsAString& aInnerHTML, + OOMReporter& aError) { + if (!nsContentUtils::GetNodeTextContent(this, false, aInnerHTML, fallible)) { + aError.ReportOOM(); + } +} + +void HTMLStyleElement::SetInnerHTML(const nsAString& aInnerHTML, + nsIPrincipal* aScriptedPrincipal, + ErrorResult& aError) { + SetTextContentInternal(aInnerHTML, aScriptedPrincipal, aError); +} + +void HTMLStyleElement::SetTextContentInternal(const nsAString& aTextContent, + nsIPrincipal* aScriptedPrincipal, + ErrorResult& aError) { + // Per spec, if we're setting text content to an empty string and don't + // already have any children, we should not trigger any mutation observers, or + // re-parse the stylesheet. + if (aTextContent.IsEmpty() && !GetFirstChild()) { + nsIPrincipal* principal = + mTriggeringPrincipal ? mTriggeringPrincipal.get() : NodePrincipal(); + if (principal == aScriptedPrincipal) { + return; + } + } + + const bool updatesWereEnabled = mUpdatesEnabled; + DisableUpdates(); + + aError = nsContentUtils::SetNodeTextContent(this, aTextContent, true); + if (updatesWereEnabled) { + mTriggeringPrincipal = aScriptedPrincipal; + Unused << EnableUpdatesAndUpdateStyleSheet(nullptr); + } +} + +void HTMLStyleElement::SetDevtoolsAsTriggeringPrincipal() { + mTriggeringPrincipal = CreateDevtoolsPrincipal(); +} + +Maybe<LinkStyle::SheetInfo> HTMLStyleElement::GetStyleSheetInfo() { + if (!IsCSSMimeTypeAttributeForStyleElement(*this)) { + return Nothing(); + } + + nsAutoString title; + nsAutoString media; + GetTitleAndMediaForElement(*this, title, media); + + return Some(SheetInfo{ + *OwnerDoc(), + this, + nullptr, + do_AddRef(mTriggeringPrincipal), + MakeAndAddRef<ReferrerInfo>(*this), + CORS_NONE, + title, + media, + /* integrity = */ u""_ns, + /* nsStyleUtil::CSPAllowsInlineStyle takes care of nonce checking for + inline styles. Bug 1607011 */ + /* nonce = */ u""_ns, + HasAlternateRel::No, + IsInline::Yes, + IsExplicitlyEnabled::No, + FetchPriority::Auto, + }); +} + +JSObject* HTMLStyleElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLStyleElement_Binding::Wrap(aCx, this, aGivenProto); +} + +nsDOMTokenList* HTMLStyleElement::Blocking() { + if (!mBlocking) { + mBlocking = + new nsDOMTokenList(this, nsGkAtoms::blocking, sSupportedBlockingValues); + } + return mBlocking; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLStyleElement.h b/dom/html/HTMLStyleElement.h new file mode 100644 index 0000000000..5815740ac5 --- /dev/null +++ b/dom/html/HTMLStyleElement.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLStyleElement_h +#define mozilla_dom_HTMLStyleElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/LinkStyle.h" +#include "nsGenericHTMLElement.h" +#include "nsStubMutationObserver.h" + +class nsDOMTokenList; + +namespace mozilla::dom { + +class HTMLStyleElement final : public nsGenericHTMLElement, + public LinkStyle, + public nsStubMutationObserver { + public: + explicit HTMLStyleElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + // CC + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLStyleElement, + nsGenericHTMLElement) + + void GetInnerHTML(nsAString& aInnerHTML, OOMReporter& aError) override; + using nsGenericHTMLElement::SetInnerHTML; + virtual void SetInnerHTML(const nsAString& aInnerHTML, + nsIPrincipal* aSubjectPrincipal, + mozilla::ErrorResult& aError) override; + virtual void SetTextContentInternal(const nsAString& aTextContent, + nsIPrincipal* aSubjectPrincipal, + mozilla::ErrorResult& aError) override; + /** + * Mark this style element with a devtools-specific principal that + * skips Content Security Policy unsafe-inline checks. This triggering + * principal will be overwritten by any callers that set textContent + * or innerHTML on this element. + */ + void SetDevtoolsAsTriggeringPrincipal(); + + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + virtual void UnbindFromTree(bool aNullParent = true) override; + virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // nsIMutationObserver + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + bool Disabled() const; + void SetDisabled(bool aDisabled); + void GetMedia(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::media, aValue); } + void SetMedia(const nsAString& aMedia, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::media, aMedia, aError); + } + void GetType(nsAString& aValue) { GetHTMLAttr(nsGkAtoms::type, aValue); } + void SetType(const nsAString& aType, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::type, aType, aError); + } + + nsDOMTokenList* Blocking(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + protected: + virtual ~HTMLStyleElement(); + + nsIContent& AsContent() final { return *this; } + const LinkStyle* AsLinkStyle() const final { return this; } + Maybe<SheetInfo> GetStyleSheetInfo() final; + + /** + * Common method to call from the various mutation observer methods. + * aContent is a content node that's either the one that changed or its + * parent; we should only respond to the change if aContent is non-anonymous. + */ + void ContentChanged(nsIContent* aContent); + + RefPtr<nsDOMTokenList> mBlocking; +}; + +} // namespace mozilla::dom + +#endif diff --git a/dom/html/HTMLSummaryElement.cpp b/dom/html/HTMLSummaryElement.cpp new file mode 100644 index 0000000000..d1fcf22598 --- /dev/null +++ b/dom/html/HTMLSummaryElement.cpp @@ -0,0 +1,118 @@ +/* -*- 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/HTMLSummaryElement.h" + +#include "mozilla/dom/HTMLDetailsElement.h" +#include "mozilla/dom/HTMLElementBinding.h" +#include "mozilla/dom/HTMLUnknownElement.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEvents.h" +#include "nsFocusManager.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Summary) + +namespace mozilla::dom { + +HTMLSummaryElement::~HTMLSummaryElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLSummaryElement) + +nsresult HTMLSummaryElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + nsresult rv = NS_OK; + if (!aVisitor.mPresContext) { + return rv; + } + + if (aVisitor.mEventStatus == nsEventStatus_eConsumeNoDefault) { + return rv; + } + + if (!IsMainSummary()) { + return rv; + } + + WidgetEvent* const event = aVisitor.mEvent; + nsCOMPtr<Element> target = + do_QueryInterface(event->GetOriginalDOMEventTarget()); + if (nsContentUtils::IsInInteractiveHTMLContent(target, this)) { + return NS_OK; + } + + if (event->HasMouseEventMessage()) { + WidgetMouseEvent* mouseEvent = event->AsMouseEvent(); + + if (mouseEvent->IsLeftClickEvent()) { + RefPtr<HTMLDetailsElement> details = GetDetails(); + MOZ_ASSERT(details, + "Expected to find details since this is the main summary!"); + + // When dispatching a synthesized mouse click event to a details element + // with 'display: none', both Chrome and Safari do not toggle the 'open' + // attribute. We had tried to be compatible with this behavior, but found + // more inconsistency in test cases in bug 1245424. So we stop doing that. + details->ToggleOpen(); + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + return NS_OK; + } + } // event->HasMouseEventMessage() + + if (event->HasKeyEventMessage() && event->IsTrusted()) { + HandleKeyboardActivation(aVisitor); + } + return rv; +} + +bool HTMLSummaryElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + bool disallowOverridingFocusability = nsGenericHTMLElement::IsHTMLFocusable( + aWithMouse, aIsFocusable, aTabIndex); + + if (disallowOverridingFocusability || !IsMainSummary()) { + return disallowOverridingFocusability; + } + + // The main summary element is focusable. + *aIsFocusable = true; + + // Give a chance to allow the subclass to override aIsFocusable. + return false; +} + +int32_t HTMLSummaryElement::TabIndexDefault() { + // Make the main summary be able to navigate via tab, and be focusable. + // See nsGenericHTMLElement::IsHTMLFocusable(). + return IsMainSummary() ? 0 : nsGenericHTMLElement::TabIndexDefault(); +} + +bool HTMLSummaryElement::IsMainSummary() const { + HTMLDetailsElement* details = GetDetails(); + if (!details) { + return false; + } + + return details->GetFirstSummary() == this || + GetContainingShadow() == details->GetShadowRoot(); +} + +HTMLDetailsElement* HTMLSummaryElement::GetDetails() const { + if (auto* details = HTMLDetailsElement::FromNodeOrNull(GetParent())) { + return details; + } + if (!HasBeenInUAWidget()) { + return nullptr; + } + return HTMLDetailsElement::FromNodeOrNull(GetContainingShadowHost()); +} + +JSObject* HTMLSummaryElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLElement_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLSummaryElement.h b/dom/html/HTMLSummaryElement.h new file mode 100644 index 0000000000..b70e8eebbb --- /dev/null +++ b/dom/html/HTMLSummaryElement.h @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLSummaryElement_h +#define mozilla_dom_HTMLSummaryElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { +class HTMLDetailsElement; + +// HTMLSummaryElement implements the <summary> tag, which is used as a summary +// or legend of the <details> tag. Please see the spec for more information. +// https://html.spec.whatwg.org/multipage/forms.html#the-details-element +// +class HTMLSummaryElement final : public nsGenericHTMLElement { + public: + using NodeInfo = mozilla::dom::NodeInfo; + + explicit HTMLSummaryElement(already_AddRefed<NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLSummaryElement, summary) + + nsresult Clone(NodeInfo*, nsINode** aResult) const override; + + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + + int32_t TabIndexDefault() override; + + // Return true if this is the first summary element child of a details or the + // default summary element. + bool IsMainSummary() const; + + // Return the details element which contains this summary. Otherwise return + // nullptr if there is no such details element. + HTMLDetailsElement* GetDetails() const; + + protected: + virtual ~HTMLSummaryElement(); + + JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLSummaryElement_h */ diff --git a/dom/html/HTMLTableCaptionElement.cpp b/dom/html/HTMLTableCaptionElement.cpp new file mode 100644 index 0000000000..8b2657b4f3 --- /dev/null +++ b/dom/html/HTMLTableCaptionElement.cpp @@ -0,0 +1,73 @@ +/* -*- 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/HTMLTableCaptionElement.h" + +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsAttrValueInlines.h" +#include "nsStyleConsts.h" +#include "mozilla/dom/HTMLTableCaptionElementBinding.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(TableCaption) + +namespace mozilla::dom { + +HTMLTableCaptionElement::~HTMLTableCaptionElement() = default; + +JSObject* HTMLTableCaptionElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTableCaptionElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_ELEMENT_CLONE(HTMLTableCaptionElement) + +static const nsAttrValue::EnumTable kCaptionAlignTable[] = { + {"top", StyleCaptionSide::Top}, + {"bottom", StyleCaptionSide::Bottom}, + {nullptr, 0}}; + +bool HTMLTableCaptionElement::ParseAttribute( + int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) { + if (aAttribute == nsGkAtoms::align && aNamespaceID == kNameSpaceID_None) { + return aResult.ParseEnumValue(aValue, kCaptionAlignTable, false); + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTableCaptionElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_caption_side)) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align); + if (value && value->Type() == nsAttrValue::eEnum) { + aBuilder.SetKeywordValue(eCSSProperty_caption_side, + value->GetEnumValue()); + } + } + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLTableCaptionElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = {{nsGkAtoms::align}, + {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLTableCaptionElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTableCaptionElement.h b/dom/html/HTMLTableCaptionElement.h new file mode 100644 index 0000000000..452ef7833d --- /dev/null +++ b/dom/html/HTMLTableCaptionElement.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLTableCaptionElement_h +#define mozilla_dom_HTMLTableCaptionElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLTableCaptionElement final : public nsGenericHTMLElement { + public: + explicit HTMLTableCaptionElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetHasWeirdParserInsertionMode(); + } + + void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLTableCaptionElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLTableCaptionElement_h */ diff --git a/dom/html/HTMLTableCellElement.cpp b/dom/html/HTMLTableCellElement.cpp new file mode 100644 index 0000000000..c260323a8b --- /dev/null +++ b/dom/html/HTMLTableCellElement.cpp @@ -0,0 +1,219 @@ +/* -*- 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/HTMLTableCellElement.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLTableElement.h" +#include "mozilla/dom/HTMLTableRowElement.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsAttrValueInlines.h" +#include "celldata.h" +#include "mozilla/dom/HTMLTableCellElementBinding.h" + +namespace { +enum class StyleCellScope : uint8_t { Row, Col, Rowgroup, Colgroup }; +} // namespace + +NS_IMPL_NS_NEW_HTML_ELEMENT(TableCell) + +namespace mozilla::dom { + +HTMLTableCellElement::~HTMLTableCellElement() = default; + +JSObject* HTMLTableCellElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTableCellElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_ELEMENT_CLONE(HTMLTableCellElement) + +// protected method +HTMLTableRowElement* HTMLTableCellElement::GetRow() const { + return HTMLTableRowElement::FromNodeOrNull(GetParent()); +} + +// protected method +HTMLTableElement* HTMLTableCellElement::GetTable() const { + nsIContent* parent = GetParent(); + if (!parent) { + return nullptr; + } + + // parent should be a row. + nsIContent* section = parent->GetParent(); + if (!section) { + return nullptr; + } + + if (section->IsHTMLElement(nsGkAtoms::table)) { + // XHTML, without a row group. + return static_cast<HTMLTableElement*>(section); + } + + // We have a row group. + nsIContent* result = section->GetParent(); + if (result && result->IsHTMLElement(nsGkAtoms::table)) { + return static_cast<HTMLTableElement*>(result); + } + + return nullptr; +} + +int32_t HTMLTableCellElement::CellIndex() const { + HTMLTableRowElement* row = GetRow(); + if (!row) { + return -1; + } + + nsIHTMLCollection* cells = row->Cells(); + if (!cells) { + return -1; + } + + uint32_t numCells = cells->Length(); + for (uint32_t i = 0; i < numCells; i++) { + if (cells->Item(i) == this) { + return i; + } + } + + return -1; +} + +StyleLockedDeclarationBlock* +HTMLTableCellElement::GetMappedAttributesInheritedFromTable() const { + if (HTMLTableElement* table = GetTable()) { + return table->GetAttributesMappedForCell(); + } + return nullptr; +} + +void HTMLTableCellElement::GetAlign(DOMString& aValue) { + if (!GetAttr(nsGkAtoms::align, aValue)) { + // There's no align attribute, ask the row for the alignment. + HTMLTableRowElement* row = GetRow(); + if (row) { + row->GetAlign(aValue); + } + } +} + +static const nsAttrValue::EnumTable kCellScopeTable[] = { + {"row", StyleCellScope::Row}, + {"col", StyleCellScope::Col}, + {"rowgroup", StyleCellScope::Rowgroup}, + {"colgroup", StyleCellScope::Colgroup}, + {nullptr, 0}}; + +void HTMLTableCellElement::GetScope(DOMString& aScope) { + GetEnumAttr(nsGkAtoms::scope, nullptr, aScope); +} + +bool HTMLTableCellElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + /* ignore these attributes, stored simply as strings + abbr, axis, ch, headers + */ + if (aAttribute == nsGkAtoms::colspan) { + aResult.ParseClampedNonNegativeInt(aValue, 1, 1, MAX_COLSPAN); + return true; + } + if (aAttribute == nsGkAtoms::rowspan) { + aResult.ParseClampedNonNegativeInt(aValue, 1, 0, MAX_ROWSPAN); + // quirks mode does not honor the special html 4 value of 0 + if (aResult.GetIntegerValue() == 0 && InNavQuirksMode(OwnerDoc())) { + aResult.SetTo(1, &aValue); + } + return true; + } + if (aAttribute == nsGkAtoms::height) { + return aResult.ParseNonzeroHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::width) { + return aResult.ParseNonzeroHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::align) { + return ParseTableCellHAlignValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::bgcolor) { + return aResult.ParseColor(aValue); + } + if (aAttribute == nsGkAtoms::scope) { + return aResult.ParseEnumValue(aValue, kCellScopeTable, false); + } + if (aAttribute == nsGkAtoms::valign) { + return ParseTableVAlignValue(aValue, aResult); + } + } + + return nsGenericHTMLElement::ParseBackgroundAttribute( + aNamespaceID, aAttribute, aValue, aResult) || + nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTableCellElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + MapImageSizeAttributesInto(aBuilder); + + if (!aBuilder.PropertyIsSet(eCSSProperty_text_wrap_mode)) { + // nowrap: enum + if (aBuilder.GetAttr(nsGkAtoms::nowrap)) { + // See if our width is not a nonzero integer width. + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::width); + nsCompatibility mode = aBuilder.Document().GetCompatibilityMode(); + if (!value || value->Type() != nsAttrValue::eInteger || + value->GetIntegerValue() == 0 || eCompatibility_NavQuirks != mode) { + aBuilder.SetKeywordValue(eCSSProperty_text_wrap_mode, + StyleTextWrapMode::Nowrap); + } + } + } + + nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapVAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLTableCellElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::align}, + {nsGkAtoms::valign}, + {nsGkAtoms::nowrap}, +#if 0 + // XXXldb If these are implemented, they might need to move to + // GetAttributeChangeHint (depending on how, and preferably not). + { nsGkAtoms::abbr }, + { nsGkAtoms::axis }, + { nsGkAtoms::headers }, + { nsGkAtoms::scope }, +#endif + {nsGkAtoms::width}, + {nsGkAtoms::height}, + {nullptr} + }; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + sBackgroundAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLTableCellElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTableCellElement.h b/dom/html/HTMLTableCellElement.h new file mode 100644 index 0000000000..cdf61f8928 --- /dev/null +++ b/dom/html/HTMLTableCellElement.h @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLTableCellElement_h +#define mozilla_dom_HTMLTableCellElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLTableElement; + +class HTMLTableCellElement final : public nsGenericHTMLElement { + public: + explicit HTMLTableCellElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetHasWeirdParserInsertionMode(); + } + + // nsISupports + NS_INLINE_DECL_REFCOUNTING_INHERITED(HTMLTableCellElement, + nsGenericHTMLElement) + + NS_IMPL_FROMNODE_HELPER(HTMLTableCellElement, + IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th)) + + uint32_t ColSpan() const { return GetUnsignedIntAttr(nsGkAtoms::colspan, 1); } + void SetColSpan(uint32_t aColSpan, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::colspan, aColSpan, 1, aError); + } + uint32_t RowSpan() const { return GetUnsignedIntAttr(nsGkAtoms::rowspan, 1); } + void SetRowSpan(uint32_t aRowSpan, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::rowspan, aRowSpan, 1, aError); + } + // already_AddRefed<nsDOMTokenList> Headers() const; + void GetHeaders(DOMString& aHeaders) { + GetHTMLAttr(nsGkAtoms::headers, aHeaders); + } + void SetHeaders(const nsAString& aHeaders, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::headers, aHeaders, aError); + } + int32_t CellIndex() const; + + void GetAbbr(DOMString& aAbbr) { GetHTMLAttr(nsGkAtoms::abbr, aAbbr); } + void SetAbbr(const nsAString& aAbbr, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::abbr, aAbbr, aError); + } + void GetScope(DOMString& aScope); + void SetScope(const nsAString& aScope, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::scope, aScope, aError); + } + void GetAlign(DOMString& aAlign); + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + void GetAxis(DOMString& aAxis) { GetHTMLAttr(nsGkAtoms::axis, aAxis); } + void SetAxis(const nsAString& aAxis, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::axis, aAxis, aError); + } + void GetHeight(DOMString& aHeight) { + GetHTMLAttr(nsGkAtoms::height, aHeight); + } + void SetHeight(const nsAString& aHeight, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::height, aHeight, aError); + } + void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); } + void SetWidth(const nsAString& aWidth, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::width, aWidth, aError); + } + void GetCh(DOMString& aCh) { GetHTMLAttr(nsGkAtoms::_char, aCh); } + void SetCh(const nsAString& aCh, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::_char, aCh, aError); + } + void GetChOff(DOMString& aChOff) { GetHTMLAttr(nsGkAtoms::charoff, aChOff); } + void SetChOff(const nsAString& aChOff, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::charoff, aChOff, aError); + } + bool NoWrap() { return GetBoolAttr(nsGkAtoms::nowrap); } + void SetNoWrap(bool aNoWrap, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::nowrap, aNoWrap, aError); + } + void GetVAlign(DOMString& aVAlign) { + GetHTMLAttr(nsGkAtoms::valign, aVAlign); + } + void SetVAlign(const nsAString& aVAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::valign, aVAlign, aError); + } + void GetBgColor(DOMString& aBgColor) { + GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor); + } + void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError); + } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + // Get mapped attributes of ancestor table, if any + StyleLockedDeclarationBlock* GetMappedAttributesInheritedFromTable() const; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLTableCellElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + HTMLTableElement* GetTable() const; + + HTMLTableRowElement* GetRow() const; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLTableCellElement_h */ diff --git a/dom/html/HTMLTableColElement.cpp b/dom/html/HTMLTableColElement.cpp new file mode 100644 index 0000000000..ae8e619a0f --- /dev/null +++ b/dom/html/HTMLTableColElement.cpp @@ -0,0 +1,102 @@ +/* -*- 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/HTMLTableColElement.h" +#include "mozilla/dom/HTMLTableColElementBinding.h" +#include "nsAttrValueInlines.h" +#include "mozilla/MappedDeclarationsBuilder.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(TableCol) + +namespace mozilla::dom { + +// use the same protection as ancient code did +// http://lxr.mozilla.org/classic/source/lib/layout/laytable.c#46 +#define MAX_COLSPAN 1000 + +HTMLTableColElement::~HTMLTableColElement() = default; + +JSObject* HTMLTableColElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTableColElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_ELEMENT_CLONE(HTMLTableColElement) + +bool HTMLTableColElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + /* ignore these attributes, stored simply as strings ch */ + if (aAttribute == nsGkAtoms::span) { + /* protection from unrealistic large colspan values */ + aResult.ParseClampedNonNegativeInt(aValue, 1, 1, MAX_COLSPAN); + return true; + } + if (aAttribute == nsGkAtoms::width) { + // Spec says to use ParseNonzeroHTMLDimension, but Chrome and Safari both + // allow 0, and we did all along too, so keep that behavior. See + // https://github.com/whatwg/html/issues/4717 + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::align) { + return ParseTableCellHAlignValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::valign) { + return ParseTableVAlignValue(aValue, aResult); + } + } + + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTableColElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty__x_span)) { + // span: int + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::span); + if (value && value->Type() == nsAttrValue::eInteger) { + int32_t val = value->GetIntegerValue(); + // Note: Do NOT use this code for table cells! The value "0" + // means something special for colspan and rowspan, but for <col + // span> and <colgroup span> it's just disallowed. + if (val > 0) { + aBuilder.SetIntValue(eCSSProperty__x_span, value->GetIntegerValue()); + } + } + } + + nsGenericHTMLElement::MapWidthAttributeInto(aBuilder); + nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapVAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLTableColElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = {{nsGkAtoms::width}, + {nsGkAtoms::align}, + {nsGkAtoms::valign}, + {nsGkAtoms::span}, + {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLTableColElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTableColElement.h b/dom/html/HTMLTableColElement.h new file mode 100644 index 0000000000..c57ce1f2a9 --- /dev/null +++ b/dom/html/HTMLTableColElement.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLTableColElement_h +#define mozilla_dom_HTMLTableColElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +class HTMLTableColElement final : public nsGenericHTMLElement { + public: + explicit HTMLTableColElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetHasWeirdParserInsertionMode(); + } + + uint32_t Span() const { return GetUnsignedIntAttr(nsGkAtoms::span, 1); } + void SetSpan(uint32_t aSpan, ErrorResult& aError) { + SetUnsignedIntAttr(nsGkAtoms::span, aSpan, 1, aError); + } + + void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + void GetCh(DOMString& aCh) { GetHTMLAttr(nsGkAtoms::_char, aCh); } + void SetCh(const nsAString& aCh, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::_char, aCh, aError); + } + void GetChOff(DOMString& aChOff) { GetHTMLAttr(nsGkAtoms::charoff, aChOff); } + void SetChOff(const nsAString& aChOff, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::charoff, aChOff, aError); + } + void GetVAlign(DOMString& aVAlign) { + GetHTMLAttr(nsGkAtoms::valign, aVAlign); + } + void SetVAlign(const nsAString& aVAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::valign, aVAlign, aError); + } + void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); } + void SetWidth(const nsAString& aWidth, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::width, aWidth, aError); + } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLTableColElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLTableColElement_h */ diff --git a/dom/html/HTMLTableElement.cpp b/dom/html/HTMLTableElement.cpp new file mode 100644 index 0000000000..97329dcef1 --- /dev/null +++ b/dom/html/HTMLTableElement.cpp @@ -0,0 +1,995 @@ +/* -*- 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/HTMLTableElement.h" +#include "mozilla/AttributeStyles.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/DeclarationBlock.h" +#include "nsAttrValueInlines.h" +#include "nsWrapperCacheInlines.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLCollectionBinding.h" +#include "mozilla/dom/HTMLTableElementBinding.h" +#include "nsContentUtils.h" +#include "nsLayoutUtils.h" +#include "jsfriendapi.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Table) + +namespace mozilla::dom { + +/* ------------------------- TableRowsCollection --------------------------- */ +/** + * This class provides a late-bound collection of rows in a table. + * mParent is NOT ref-counted to avoid circular references + */ +class TableRowsCollection final : public nsIHTMLCollection, + public nsStubMutationObserver, + public nsWrapperCache { + public: + explicit TableRowsCollection(HTMLTableElement* aParent); + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + NS_DECL_NSIMUTATIONOBSERVER_NODEWILLBEDESTROYED + + uint32_t Length() override; + Element* GetElementAt(uint32_t aIndex) override; + nsINode* GetParentObject() override { return mParent; } + + Element* GetFirstNamedElement(const nsAString& aName, bool& aFound) override; + void GetSupportedNames(nsTArray<nsString>& aNames) override; + + NS_IMETHOD ParentDestroyed(); + + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS_AMBIGUOUS(TableRowsCollection, + nsIHTMLCollection) + + // nsWrapperCache + using nsWrapperCache::GetWrapperPreserveColor; + using nsWrapperCache::PreserveWrapper; + JSObject* WrapObject(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + protected: + // Unregister ourselves as a mutation observer, and clear our internal state. + void CleanUp(); + void LastRelease() { CleanUp(); } + virtual ~TableRowsCollection() { + // we do NOT have a ref-counted reference to mParent, so do NOT + // release it! this is to avoid circular references. The + // instantiator who provided mParent is responsible for managing our + // reference for us. + CleanUp(); + } + + JSObject* GetWrapperPreserveColorInternal() override { + return nsWrapperCache::GetWrapperPreserveColor(); + } + void PreserveWrapperInternal(nsISupports* aScriptObjectHolder) override { + nsWrapperCache::PreserveWrapper(aScriptObjectHolder); + } + + // Ensure that HTMLTableElement is in a valid state. This must be called + // before inspecting the mRows object. + void EnsureInitialized(); + + // Checks if the passed-in container is interesting for the purposes of + // invalidation due to a mutation observer. + bool InterestingContainer(nsIContent* aContainer); + + // Check if the passed-in nsIContent is a <tr> within the section defined by + // `aSection`. The root of the table is considered to be part of the `<tbody>` + // section. + bool IsAppropriateRow(nsAtom* aSection, nsIContent* aContent); + + // Scan backwards starting from `aCurrent` in the table, looking for the + // previous row in the table which is within the section `aSection`. + nsIContent* PreviousRow(nsAtom* aSection, nsIContent* aCurrent); + + // Handle the insertion of the child `aChild` into the container `aContainer` + // within the tree. The container must be an `InterestingContainer`. This + // method updates the mRows, mBodyStart, and mFootStart member variables. + // + // HandleInsert returns an integer which can be passed to the next call of the + // method in a loop inserting children into the same container. This will + // optimize subsequent insertions to require less work. This can either be -1, + // in which case we don't know where to insert the next row, and When passed + // to HandleInsert, it will use `PreviousRow` to locate the index to insert. + // Or, it can be an index to insert the next <tr> in the same container at. + int32_t HandleInsert(nsIContent* aContainer, nsIContent* aChild, + int32_t aIndexGuess = -1); + + // The HTMLTableElement which this TableRowsCollection tracks the rows for. + HTMLTableElement* mParent; + + // The current state of the TableRowsCollection. mBodyStart and mFootStart are + // indices into mRows which represent the location of the first row in the + // body or foot section. If there are no rows in a section, the index points + // at the location where the first element in that section would be inserted. + nsTArray<nsCOMPtr<nsIContent>> mRows; + uint32_t mBodyStart; + uint32_t mFootStart; + bool mInitialized; +}; + +TableRowsCollection::TableRowsCollection(HTMLTableElement* aParent) + : mParent(aParent), mBodyStart(0), mFootStart(0), mInitialized(false) { + MOZ_ASSERT(mParent); +} + +void TableRowsCollection::EnsureInitialized() { + if (mInitialized) { + return; + } + mInitialized = true; + + // Initialize mRows as the TableRowsCollection is created. The mutation + // observer should keep it up to date. + // + // It should be extremely unlikely that anyone creates a TableRowsCollection + // without calling a method on it, so lazily performing this initialization + // seems unnecessary. + AutoTArray<nsCOMPtr<nsIContent>, 32> body; + AutoTArray<nsCOMPtr<nsIContent>, 32> foot; + mRows.Clear(); + + auto addRowChildren = [&](nsTArray<nsCOMPtr<nsIContent>>& aArray, + nsIContent* aNode) { + for (nsIContent* inner = aNode->nsINode::GetFirstChild(); inner; + inner = inner->GetNextSibling()) { + if (inner->IsHTMLElement(nsGkAtoms::tr)) { + aArray.AppendElement(inner); + } + } + }; + + for (nsIContent* node = mParent->nsINode::GetFirstChild(); node; + node = node->GetNextSibling()) { + if (node->IsHTMLElement(nsGkAtoms::thead)) { + addRowChildren(mRows, node); + } else if (node->IsHTMLElement(nsGkAtoms::tbody)) { + addRowChildren(body, node); + } else if (node->IsHTMLElement(nsGkAtoms::tfoot)) { + addRowChildren(foot, node); + } else if (node->IsHTMLElement(nsGkAtoms::tr)) { + body.AppendElement(node); + } + } + + mBodyStart = mRows.Length(); + mRows.AppendElements(std::move(body)); + mFootStart = mRows.Length(); + mRows.AppendElements(std::move(foot)); + + mParent->AddMutationObserver(this); +} + +void TableRowsCollection::CleanUp() { + // Unregister ourselves as a mutation observer. + if (mInitialized && mParent) { + mParent->RemoveMutationObserver(this); + } + + // Clean up all of our internal state and make it empty in case someone looks + // at us. + mRows.Clear(); + mBodyStart = 0; + mFootStart = 0; + + // We set mInitialized to true in case someone still has a reference to us, as + // we don't need to try to initialize first. + mInitialized = true; + mParent = nullptr; +} + +JSObject* TableRowsCollection::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLCollection_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TableRowsCollection, mRows) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TableRowsCollection) +NS_IMPL_CYCLE_COLLECTING_RELEASE_WITH_LAST_RELEASE(TableRowsCollection, + LastRelease()) + +NS_INTERFACE_TABLE_HEAD(TableRowsCollection) + NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY + NS_INTERFACE_TABLE(TableRowsCollection, nsIHTMLCollection, + nsIMutationObserver) + NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(TableRowsCollection) +NS_INTERFACE_MAP_END + +uint32_t TableRowsCollection::Length() { + EnsureInitialized(); + return mRows.Length(); +} + +Element* TableRowsCollection::GetElementAt(uint32_t aIndex) { + EnsureInitialized(); + if (aIndex < mRows.Length()) { + return mRows[aIndex]->AsElement(); + } + return nullptr; +} + +Element* TableRowsCollection::GetFirstNamedElement(const nsAString& aName, + bool& aFound) { + EnsureInitialized(); + aFound = false; + RefPtr<nsAtom> nameAtom = NS_Atomize(aName); + NS_ENSURE_TRUE(nameAtom, nullptr); + + for (auto& node : mRows) { + if (node->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::name, + nameAtom, eCaseMatters) || + node->AsElement()->AttrValueIs(kNameSpaceID_None, nsGkAtoms::id, + nameAtom, eCaseMatters)) { + aFound = true; + return node->AsElement(); + } + } + + return nullptr; +} + +void TableRowsCollection::GetSupportedNames(nsTArray<nsString>& aNames) { + EnsureInitialized(); + for (auto& node : mRows) { + if (node->HasID()) { + nsAtom* idAtom = node->GetID(); + MOZ_ASSERT(idAtom != nsGkAtoms::_empty, "Empty ids don't get atomized"); + nsDependentAtomString idStr(idAtom); + if (!aNames.Contains(idStr)) { + aNames.AppendElement(idStr); + } + } + + nsGenericHTMLElement* el = nsGenericHTMLElement::FromNode(node); + if (el) { + const nsAttrValue* val = el->GetParsedAttr(nsGkAtoms::name); + if (val && val->Type() == nsAttrValue::eAtom) { + nsAtom* nameAtom = val->GetAtomValue(); + MOZ_ASSERT(nameAtom != nsGkAtoms::_empty, + "Empty names don't get atomized"); + nsDependentAtomString nameStr(nameAtom); + if (!aNames.Contains(nameStr)) { + aNames.AppendElement(nameStr); + } + } + } + } +} + +NS_IMETHODIMP +TableRowsCollection::ParentDestroyed() { + CleanUp(); + return NS_OK; +} + +bool TableRowsCollection::InterestingContainer(nsIContent* aContainer) { + return mParent && aContainer && + (aContainer == mParent || + (aContainer->GetParent() == mParent && + aContainer->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tbody, + nsGkAtoms::tfoot))); +} + +bool TableRowsCollection::IsAppropriateRow(nsAtom* aSection, + nsIContent* aContent) { + if (!aContent->IsHTMLElement(nsGkAtoms::tr)) { + return false; + } + // If it's in the root, then we consider it to be in a tbody. + nsIContent* parent = aContent->GetParent(); + if (aSection == nsGkAtoms::tbody && parent == mParent) { + return true; + } + return parent->IsHTMLElement(aSection); +} + +nsIContent* TableRowsCollection::PreviousRow(nsAtom* aSection, + nsIContent* aCurrent) { + // Keep going backwards until we've found a `tr` element. We want to always + // run at least once, as we don't want to find ourselves. + // + // Each spin of the loop we step backwards one element. If we're at the top of + // a section, we step out of it into the root, and if we step onto a section + // matching `aSection`, we step into it. We keep spinning the loop until + // either we reach the first element in mParent, or find a <tr> in an + // appropriate section. + nsIContent* prev = aCurrent; + do { + nsIContent* parent = prev->GetParent(); + prev = prev->GetPreviousSibling(); + + // Ascend out of any sections we're currently in, if we've run out of + // elements. + if (!prev && parent != mParent) { + prev = parent->GetPreviousSibling(); + } + + // Descend into a section if we stepped onto one. + if (prev && prev->GetParent() == mParent && prev->IsHTMLElement(aSection)) { + prev = prev->GetLastChild(); + } + } while (prev && !IsAppropriateRow(aSection, prev)); + return prev; +} + +int32_t TableRowsCollection::HandleInsert(nsIContent* aContainer, + nsIContent* aChild, + int32_t aIndexGuess) { + if (!nsContentUtils::IsInSameAnonymousTree(mParent, aChild)) { + return aIndexGuess; // Nothing inserted, guess hasn't changed. + } + + // If we're adding a section to the root, add each of the rows in that section + // individually. + if (aContainer == mParent && + aChild->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tbody, + nsGkAtoms::tfoot)) { + // If we're entering a tbody, we can persist the index guess we were passed, + // as the newly added items are in the same section as us, however, if we're + // entering thead or tfoot we will have to re-scan. + bool isTBody = aChild->IsHTMLElement(nsGkAtoms::tbody); + int32_t indexGuess = isTBody ? aIndexGuess : -1; + + for (nsIContent* inner = aChild->GetFirstChild(); inner; + inner = inner->GetNextSibling()) { + indexGuess = HandleInsert(aChild, inner, indexGuess); + } + + return isTBody ? indexGuess : -1; + } + if (!aChild->IsHTMLElement(nsGkAtoms::tr)) { + return aIndexGuess; // Nothing inserted, guess hasn't changed. + } + + // We should have only been passed an insertion from an interesting container, + // so we can get the container we're inserting to fairly easily. + nsAtom* section = aContainer == mParent ? nsGkAtoms::tbody + : aContainer->NodeInfo()->NameAtom(); + + // Determine the default index we would to insert after if we don't find any + // previous row, and offset our section boundaries based on the section we're + // planning to insert into. + size_t index = 0; + if (section == nsGkAtoms::thead) { + mBodyStart++; + mFootStart++; + } else if (section == nsGkAtoms::tbody) { + index = mBodyStart; + mFootStart++; + } else if (section == nsGkAtoms::tfoot) { + index = mFootStart; + } else { + MOZ_ASSERT(false, "section should be one of thead, tbody, or tfoot"); + } + + // If we already have an index guess, we can skip scanning for the previous + // row. + if (aIndexGuess >= 0) { + index = aIndexGuess; + } else { + // Find the previous row in the section we're inserting into. If we find it, + // we can use it to override our insertion index. We don't need to modify + // mBodyStart or mFootStart anymore, as they have already been correctly + // updated based only on section. + nsIContent* insertAfter = PreviousRow(section, aChild); + if (insertAfter) { + // NOTE: We want to ensure that appending elements is quick, so we search + // from the end rather than from the beginning. + index = mRows.LastIndexOf(insertAfter) + 1; + MOZ_ASSERT(index != nsTArray<nsCOMPtr<nsIContent>>::NoIndex); + } + } + +#ifdef DEBUG + // Assert that we're inserting into the correct section. + if (section == nsGkAtoms::thead) { + MOZ_ASSERT(index < mBodyStart); + } else if (section == nsGkAtoms::tbody) { + MOZ_ASSERT(index >= mBodyStart); + MOZ_ASSERT(index < mFootStart); + } else if (section == nsGkAtoms::tfoot) { + MOZ_ASSERT(index >= mFootStart); + MOZ_ASSERT(index <= mRows.Length()); + } + + MOZ_ASSERT(mBodyStart <= mFootStart); + MOZ_ASSERT(mFootStart <= mRows.Length() + 1); +#endif + + mRows.InsertElementAt(index, aChild); + return index + 1; +} + +// nsIMutationObserver + +void TableRowsCollection::ContentAppended(nsIContent* aFirstNewContent) { + nsIContent* container = aFirstNewContent->GetParent(); + if (!nsContentUtils::IsInSameAnonymousTree(mParent, aFirstNewContent) || + !InterestingContainer(container)) { + return; + } + + // We usually can't guess where we need to start inserting, unless we're + // appending into mParent, in which case we can provide the guess that we + // should insert at the end of the body, which can help us avoid potentially + // expensive work in the common case. + int32_t indexGuess = mParent == container ? mFootStart : -1; + + // Insert each of the newly added content one at a time. The indexGuess should + // make insertions of a large number of elements cheaper. + for (nsIContent* content = aFirstNewContent; content; + content = content->GetNextSibling()) { + indexGuess = HandleInsert(container, content, indexGuess); + } +} + +void TableRowsCollection::ContentInserted(nsIContent* aChild) { + if (!nsContentUtils::IsInSameAnonymousTree(mParent, aChild) || + !InterestingContainer(aChild->GetParent())) { + return; + } + + HandleInsert(aChild->GetParent(), aChild); +} + +void TableRowsCollection::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + if (!nsContentUtils::IsInSameAnonymousTree(mParent, aChild) || + !InterestingContainer(aChild->GetParent())) { + return; + } + + // If the element being removed is a `tr`, we can just remove it from our + // list. It shouldn't change the order of anything. + if (aChild->IsHTMLElement(nsGkAtoms::tr)) { + size_t index = mRows.IndexOf(aChild); + if (index != nsTArray<nsCOMPtr<nsIContent>>::NoIndex) { + mRows.RemoveElementAt(index); + if (mBodyStart > index) { + mBodyStart--; + } + if (mFootStart > index) { + mFootStart--; + } + } + return; + } + + // If the element being removed is a `thead`, `tbody`, or `tfoot`, we can + // remove any `tr`s in our list which have that element as its parent node. In + // any other situation, the removal won't affect us, so we can ignore it. + if (!aChild->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tbody, + nsGkAtoms::tfoot)) { + return; + } + + size_t beforeLength = mRows.Length(); + mRows.RemoveElementsBy( + [&](nsIContent* element) { return element->GetParent() == aChild; }); + size_t removed = beforeLength - mRows.Length(); + if (aChild->IsHTMLElement(nsGkAtoms::thead)) { + // NOTE: Need to move both tbody and tfoot, as we removed from head. + mBodyStart -= removed; + mFootStart -= removed; + } else if (aChild->IsHTMLElement(nsGkAtoms::tbody)) { + // NOTE: Need to move tfoot, as we removed from body. + mFootStart -= removed; + } +} + +void TableRowsCollection::NodeWillBeDestroyed(nsINode* aNode) { + // Set mInitialized to false so CleanUp doesn't try to remove our mutation + // observer, as we're going away. CleanUp() will reset mInitialized to true as + // it returns. + mInitialized = false; + CleanUp(); +} + +/* --------------------------- HTMLTableElement ---------------------------- */ + +HTMLTableElement::HTMLTableElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetHasWeirdParserInsertionMode(); +} + +HTMLTableElement::~HTMLTableElement() { + if (mRows) { + mRows->ParentDestroyed(); + } + ReleaseInheritedAttributes(); +} + +JSObject* HTMLTableElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTableElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLTableElement) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLTableElement, + nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTBodies) + if (tmp->mRows) { + tmp->mRows->ParentDestroyed(); + } + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRows) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLTableElement, + nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTBodies) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRows) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTableElement, + nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLTableElement) + +// the DOM spec says border, cellpadding, cellSpacing are all "wstring" +// in fact, they are integers or they are meaningless. so we store them +// here as ints. + +nsIHTMLCollection* HTMLTableElement::Rows() { + if (!mRows) { + mRows = new TableRowsCollection(this); + } + + return mRows; +} + +nsIHTMLCollection* HTMLTableElement::TBodies() { + if (!mTBodies) { + // Not using NS_GetContentList because this should not be cached + mTBodies = new nsContentList(this, kNameSpaceID_XHTML, nsGkAtoms::tbody, + nsGkAtoms::tbody, false); + } + + return mTBodies; +} + +already_AddRefed<nsGenericHTMLElement> HTMLTableElement::CreateTHead() { + RefPtr<nsGenericHTMLElement> head = GetTHead(); + if (!head) { + // Create a new head rowgroup. + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::thead, + getter_AddRefs(nodeInfo)); + + head = NS_NewHTMLTableSectionElement(nodeInfo.forget()); + if (!head) { + return nullptr; + } + + nsCOMPtr<nsIContent> refNode = nullptr; + for (refNode = nsINode::GetFirstChild(); refNode; + refNode = refNode->GetNextSibling()) { + if (refNode->IsHTMLElement() && + !refNode->IsHTMLElement(nsGkAtoms::caption) && + !refNode->IsHTMLElement(nsGkAtoms::colgroup)) { + break; + } + } + + nsINode::InsertBefore(*head, refNode, IgnoreErrors()); + } + return head.forget(); +} + +void HTMLTableElement::DeleteTHead() { + RefPtr<HTMLTableSectionElement> tHead = GetTHead(); + if (tHead) { + mozilla::IgnoredErrorResult rv; + nsINode::RemoveChild(*tHead, rv); + } +} + +already_AddRefed<nsGenericHTMLElement> HTMLTableElement::CreateTFoot() { + RefPtr<nsGenericHTMLElement> foot = GetTFoot(); + if (!foot) { + // create a new foot rowgroup + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tfoot, + getter_AddRefs(nodeInfo)); + + foot = NS_NewHTMLTableSectionElement(nodeInfo.forget()); + if (!foot) { + return nullptr; + } + AppendChildTo(foot, true, IgnoreErrors()); + } + + return foot.forget(); +} + +void HTMLTableElement::DeleteTFoot() { + RefPtr<HTMLTableSectionElement> tFoot = GetTFoot(); + if (tFoot) { + mozilla::IgnoredErrorResult rv; + nsINode::RemoveChild(*tFoot, rv); + } +} + +already_AddRefed<nsGenericHTMLElement> HTMLTableElement::CreateCaption() { + RefPtr<nsGenericHTMLElement> caption = GetCaption(); + if (!caption) { + // Create a new caption. + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::caption, + getter_AddRefs(nodeInfo)); + + caption = NS_NewHTMLTableCaptionElement(nodeInfo.forget()); + if (!caption) { + return nullptr; + } + + nsCOMPtr<nsINode> firsChild = nsINode::GetFirstChild(); + nsINode::InsertBefore(*caption, firsChild, IgnoreErrors()); + } + return caption.forget(); +} + +void HTMLTableElement::DeleteCaption() { + RefPtr<HTMLTableCaptionElement> caption = GetCaption(); + if (caption) { + mozilla::IgnoredErrorResult rv; + nsINode::RemoveChild(*caption, rv); + } +} + +already_AddRefed<nsGenericHTMLElement> HTMLTableElement::CreateTBody() { + RefPtr<mozilla::dom::NodeInfo> nodeInfo = + OwnerDoc()->NodeInfoManager()->GetNodeInfo( + nsGkAtoms::tbody, nullptr, kNameSpaceID_XHTML, ELEMENT_NODE); + MOZ_ASSERT(nodeInfo); + + RefPtr<nsGenericHTMLElement> newBody = + NS_NewHTMLTableSectionElement(nodeInfo.forget()); + MOZ_ASSERT(newBody); + + nsCOMPtr<nsIContent> referenceNode = nullptr; + for (nsIContent* child = nsINode::GetLastChild(); child; + child = child->GetPreviousSibling()) { + if (child->IsHTMLElement(nsGkAtoms::tbody)) { + referenceNode = child->GetNextSibling(); + break; + } + } + + nsINode::InsertBefore(*newBody, referenceNode, IgnoreErrors()); + + return newBody.forget(); +} + +already_AddRefed<nsGenericHTMLElement> HTMLTableElement::InsertRow( + int32_t aIndex, ErrorResult& aError) { + /* get the ref row at aIndex + if there is one, + get its parent + insert the new row just before the ref row + else + get the first row group + insert the new row as its first child + */ + if (aIndex < -1) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + + nsIHTMLCollection* rows = Rows(); + uint32_t rowCount = rows->Length(); + if ((uint32_t)aIndex > rowCount && aIndex != -1) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + + // use local variable refIndex so we can remember original aIndex + uint32_t refIndex = (uint32_t)aIndex; + + RefPtr<nsGenericHTMLElement> newRow; + if (rowCount > 0) { + if (refIndex == rowCount || aIndex == -1) { + // we set refIndex to the last row so we can get the last row's + // parent we then do an AppendChild below if (rowCount<aIndex) + + refIndex = rowCount - 1; + } + + RefPtr<Element> refRow = rows->Item(refIndex); + nsCOMPtr<nsINode> parent = refRow->GetParentNode(); + + // create the row + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tr, + getter_AddRefs(nodeInfo)); + + newRow = NS_NewHTMLTableRowElement(nodeInfo.forget()); + + if (newRow) { + // If aIndex is -1 or equal to the number of rows, the new row + // is appended. + if (aIndex == -1 || uint32_t(aIndex) == rowCount) { + parent->AppendChild(*newRow, aError); + } else { + // insert the new row before the reference row we found above + parent->InsertBefore(*newRow, refRow, aError); + } + + if (aError.Failed()) { + return nullptr; + } + } + } else { + // the row count was 0, so + // find the last row group and insert there as first child + nsCOMPtr<nsIContent> rowGroup; + for (nsIContent* child = nsINode::GetLastChild(); child; + child = child->GetPreviousSibling()) { + if (child->IsHTMLElement(nsGkAtoms::tbody)) { + rowGroup = child; + break; + } + } + + if (!rowGroup) { // need to create a TBODY + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tbody, + getter_AddRefs(nodeInfo)); + + rowGroup = NS_NewHTMLTableSectionElement(nodeInfo.forget()); + if (rowGroup) { + AppendChildTo(rowGroup, true, aError); + if (aError.Failed()) { + return nullptr; + } + } + } + + if (rowGroup) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tr, + getter_AddRefs(nodeInfo)); + + newRow = NS_NewHTMLTableRowElement(nodeInfo.forget()); + if (newRow) { + HTMLTableSectionElement* section = + static_cast<HTMLTableSectionElement*>(rowGroup.get()); + nsIHTMLCollection* rows = section->Rows(); + nsCOMPtr<nsINode> refNode = rows->Item(0); + rowGroup->InsertBefore(*newRow, refNode, aError); + } + } + } + + return newRow.forget(); +} + +void HTMLTableElement::DeleteRow(int32_t aIndex, ErrorResult& aError) { + if (aIndex < -1) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + nsIHTMLCollection* rows = Rows(); + uint32_t refIndex; + if (aIndex == -1) { + refIndex = rows->Length(); + if (refIndex == 0) { + return; + } + + --refIndex; + } else { + refIndex = (uint32_t)aIndex; + } + + nsCOMPtr<nsIContent> row = rows->Item(refIndex); + if (!row) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + row->RemoveFromParent(); +} + +bool HTMLTableElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + /* ignore summary, just a string */ + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::cellspacing || + aAttribute == nsGkAtoms::cellpadding || + aAttribute == nsGkAtoms::border) { + return aResult.ParseNonNegativeIntValue(aValue); + } + if (aAttribute == nsGkAtoms::height) { + // Purposeful spec violation (spec says to use ParseNonzeroHTMLDimension) + // to stay compatible with our old behavior and other browsers. See + // https://github.com/whatwg/html/issues/4715 + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::width) { + return aResult.ParseNonzeroHTMLDimension(aValue); + } + + if (aAttribute == nsGkAtoms::align) { + return ParseTableHAlignValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::bgcolor || + aAttribute == nsGkAtoms::bordercolor) { + return aResult.ParseColor(aValue); + } + } + + return nsGenericHTMLElement::ParseBackgroundAttribute( + aNamespaceID, aAttribute, aValue, aResult) || + nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTableElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + // XXX Bug 211636: This function is used by a single style rule + // that's used to match two different type of elements -- tables, and + // table cells. (nsHTMLTableCellElement overrides + // WalkContentStyleRules so that this happens.) This violates the + // nsIStyleRule contract, since it's the same style rule object doing + // the mapping in two different ways. It's also incorrect since it's + // testing the display type of the ComputedStyle rather than checking + // which *element* it's matching (style rules should not stop matching + // when the display type is changed). + + // cellspacing + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::cellspacing); + if (value && value->Type() == nsAttrValue::eInteger && + !aBuilder.PropertyIsSet(eCSSProperty_border_spacing)) { + aBuilder.SetPixelValue(eCSSProperty_border_spacing, + float(value->GetIntegerValue())); + } + // align; Check for enumerated type (it may be another type if + // illegal) + value = aBuilder.GetAttr(nsGkAtoms::align); + if (value && value->Type() == nsAttrValue::eEnum) { + if (value->GetEnumValue() == uint8_t(StyleTextAlign::Center) || + value->GetEnumValue() == uint8_t(StyleTextAlign::MozCenter)) { + aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_left); + aBuilder.SetAutoValueIfUnset(eCSSProperty_margin_right); + } + } + + // bordercolor + value = aBuilder.GetAttr(nsGkAtoms::bordercolor); + nscolor color; + if (value && value->GetColorValue(color)) { + aBuilder.SetColorValueIfUnset(eCSSProperty_border_top_color, color); + aBuilder.SetColorValueIfUnset(eCSSProperty_border_left_color, color); + aBuilder.SetColorValueIfUnset(eCSSProperty_border_bottom_color, color); + aBuilder.SetColorValueIfUnset(eCSSProperty_border_right_color, color); + } + + // border + if (const nsAttrValue* borderValue = aBuilder.GetAttr(nsGkAtoms::border)) { + // border = 1 pixel default + int32_t borderThickness = 1; + if (borderValue->Type() == nsAttrValue::eInteger) { + borderThickness = borderValue->GetIntegerValue(); + } + + // by default, set all border sides to the specified width + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_top_width, + (float)borderThickness); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_left_width, + (float)borderThickness); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_bottom_width, + (float)borderThickness); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_right_width, + (float)borderThickness); + } + + nsGenericHTMLElement::MapImageSizeAttributesInto(aBuilder); + nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLTableElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::cellpadding}, {nsGkAtoms::cellspacing}, + {nsGkAtoms::border}, {nsGkAtoms::width}, + {nsGkAtoms::height}, + + {nsGkAtoms::bordercolor}, + + {nsGkAtoms::align}, {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + sBackgroundAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLTableElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +void HTMLTableElement::BuildInheritedAttributes() { + MOZ_ASSERT(!mTableInheritedAttributes, "potential leak, plus waste of work"); + MOZ_ASSERT(NS_IsMainThread()); + Document* document = GetComposedDoc(); + if (!document) { + return; + } + const nsAttrValue* value = GetParsedAttr(nsGkAtoms::cellpadding); + if (!value || value->Type() != nsAttrValue::eInteger) { + return; + } + // We have cellpadding. This will override our padding values if we don't + // have any set. + float pad = float(value->GetIntegerValue()); + MappedDeclarationsBuilder builder(*this, *document); + builder.SetPixelValue(eCSSProperty_padding_top, pad); + builder.SetPixelValue(eCSSProperty_padding_right, pad); + builder.SetPixelValue(eCSSProperty_padding_bottom, pad); + builder.SetPixelValue(eCSSProperty_padding_left, pad); + mTableInheritedAttributes = builder.TakeDeclarationBlock(); +} + +void HTMLTableElement::ReleaseInheritedAttributes() { + mTableInheritedAttributes = nullptr; +} + +nsresult HTMLTableElement::BindToTree(BindContext& aContext, nsINode& aParent) { + ReleaseInheritedAttributes(); + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + BuildInheritedAttributes(); + return NS_OK; +} + +void HTMLTableElement::UnbindFromTree(bool aNullParent) { + ReleaseInheritedAttributes(); + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +void HTMLTableElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) { + if (aName == nsGkAtoms::cellpadding && aNameSpaceID == kNameSpaceID_None) { + ReleaseInheritedAttributes(); + } + return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue, + aNotify); +} + +void HTMLTableElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aName == nsGkAtoms::cellpadding && aNameSpaceID == kNameSpaceID_None) { + BuildInheritedAttributes(); + // This affects our cell styles. + // TODO(emilio): Maybe GetAttributeChangeHint should also allow you to + // specify a restyle hint and this could move there? + nsLayoutUtils::PostRestyleEvent(this, RestyleHint::RestyleSubtree(), + nsChangeHint(0)); + } + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTableElement.h b/dom/html/HTMLTableElement.h new file mode 100644 index 0000000000..38d3e24a83 --- /dev/null +++ b/dom/html/HTMLTableElement.h @@ -0,0 +1,205 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLTableElement_h +#define mozilla_dom_HTMLTableElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "mozilla/dom/HTMLTableCaptionElement.h" +#include "mozilla/dom/HTMLTableSectionElement.h" + +namespace mozilla::dom { + +class TableRowsCollection; + +class HTMLTableElement final : public nsGenericHTMLElement { + public: + explicit HTMLTableElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTableElement, table) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + HTMLTableCaptionElement* GetCaption() const { + return static_cast<HTMLTableCaptionElement*>(GetChild(nsGkAtoms::caption)); + } + void SetCaption(HTMLTableCaptionElement* aCaption, ErrorResult& aError) { + DeleteCaption(); + if (aCaption) { + nsCOMPtr<nsINode> firstChild = nsINode::GetFirstChild(); + nsINode::InsertBefore(*aCaption, firstChild, aError); + } + } + + void DeleteTFoot(); + + already_AddRefed<nsGenericHTMLElement> CreateCaption(); + + void DeleteCaption(); + + HTMLTableSectionElement* GetTHead() const { + return static_cast<HTMLTableSectionElement*>(GetChild(nsGkAtoms::thead)); + } + void SetTHead(HTMLTableSectionElement* aTHead, ErrorResult& aError) { + if (aTHead && !aTHead->IsHTMLElement(nsGkAtoms::thead)) { + aError.ThrowHierarchyRequestError("New value must be a thead element."); + return; + } + + DeleteTHead(); + if (aTHead) { + nsCOMPtr<nsIContent> refNode = nullptr; + for (refNode = nsINode::GetFirstChild(); refNode; + refNode = refNode->GetNextSibling()) { + if (refNode->IsHTMLElement() && + !refNode->IsHTMLElement(nsGkAtoms::caption) && + !refNode->IsHTMLElement(nsGkAtoms::colgroup)) { + break; + } + } + + nsINode::InsertBefore(*aTHead, refNode, aError); + } + } + already_AddRefed<nsGenericHTMLElement> CreateTHead(); + + void DeleteTHead(); + + HTMLTableSectionElement* GetTFoot() const { + return static_cast<HTMLTableSectionElement*>(GetChild(nsGkAtoms::tfoot)); + } + void SetTFoot(HTMLTableSectionElement* aTFoot, ErrorResult& aError) { + if (aTFoot && !aTFoot->IsHTMLElement(nsGkAtoms::tfoot)) { + aError.ThrowHierarchyRequestError("New value must be a tfoot element."); + return; + } + + DeleteTFoot(); + if (aTFoot) { + nsINode::AppendChild(*aTFoot, aError); + } + } + already_AddRefed<nsGenericHTMLElement> CreateTFoot(); + + nsIHTMLCollection* TBodies(); + + already_AddRefed<nsGenericHTMLElement> CreateTBody(); + + nsIHTMLCollection* Rows(); + + already_AddRefed<nsGenericHTMLElement> InsertRow(int32_t aIndex, + ErrorResult& aError); + void DeleteRow(int32_t aIndex, ErrorResult& aError); + + void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + void GetBorder(DOMString& aBorder) { + GetHTMLAttr(nsGkAtoms::border, aBorder); + } + void SetBorder(const nsAString& aBorder, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::border, aBorder, aError); + } + void GetFrame(DOMString& aFrame) { GetHTMLAttr(nsGkAtoms::frame, aFrame); } + void SetFrame(const nsAString& aFrame, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::frame, aFrame, aError); + } + void GetRules(DOMString& aRules) { GetHTMLAttr(nsGkAtoms::rules, aRules); } + void SetRules(const nsAString& aRules, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::rules, aRules, aError); + } + void GetSummary(nsString& aSummary) { + GetHTMLAttr(nsGkAtoms::summary, aSummary); + } + void GetSummary(DOMString& aSummary) { + GetHTMLAttr(nsGkAtoms::summary, aSummary); + } + void SetSummary(const nsAString& aSummary, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::summary, aSummary, aError); + } + void GetWidth(DOMString& aWidth) { GetHTMLAttr(nsGkAtoms::width, aWidth); } + void SetWidth(const nsAString& aWidth, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::width, aWidth, aError); + } + void GetBgColor(DOMString& aBgColor) { + GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor); + } + void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError); + } + void GetCellPadding(DOMString& aCellPadding) { + GetHTMLAttr(nsGkAtoms::cellpadding, aCellPadding); + } + void SetCellPadding(const nsAString& aCellPadding, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::cellpadding, aCellPadding, aError); + } + void GetCellSpacing(DOMString& aCellSpacing) { + GetHTMLAttr(nsGkAtoms::cellspacing, aCellSpacing); + } + void SetCellSpacing(const nsAString& aCellSpacing, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::cellspacing, aCellSpacing, aError); + } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + /** + * Called when an attribute is about to be changed + */ + void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + /** + * Called when an attribute has just been changed + */ + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTableElement, + nsGenericHTMLElement) + StyleLockedDeclarationBlock* GetAttributesMappedForCell() const { + return mTableInheritedAttributes; + } + + protected: + virtual ~HTMLTableElement(); + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + nsIContent* GetChild(nsAtom* aTag) const { + for (nsIContent* cur = nsINode::GetFirstChild(); cur; + cur = cur->GetNextSibling()) { + if (cur->IsHTMLElement(aTag)) { + return cur; + } + } + return nullptr; + } + + RefPtr<nsContentList> mTBodies; + RefPtr<TableRowsCollection> mRows; + RefPtr<StyleLockedDeclarationBlock> mTableInheritedAttributes; + void BuildInheritedAttributes(); + void ReleaseInheritedAttributes(); + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLTableElement_h */ diff --git a/dom/html/HTMLTableRowElement.cpp b/dom/html/HTMLTableRowElement.cpp new file mode 100644 index 0000000000..af36dc5265 --- /dev/null +++ b/dom/html/HTMLTableRowElement.cpp @@ -0,0 +1,249 @@ +/* -*- 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/HTMLTableRowElement.h" +#include "mozilla/dom/HTMLTableElement.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsAttrValueInlines.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/HTMLTableRowElementBinding.h" +#include "nsContentList.h" +#include "nsContentUtils.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(TableRow) + +namespace mozilla::dom { + +HTMLTableRowElement::~HTMLTableRowElement() = default; + +JSObject* HTMLTableRowElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTableRowElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTableRowElement, nsGenericHTMLElement, + mCells) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTableRowElement, + nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLTableRowElement) + +// protected method +HTMLTableSectionElement* HTMLTableRowElement::GetSection() const { + nsIContent* parent = GetParent(); + if (parent && parent->IsAnyOfHTMLElements(nsGkAtoms::thead, nsGkAtoms::tbody, + nsGkAtoms::tfoot)) { + return static_cast<HTMLTableSectionElement*>(parent); + } + return nullptr; +} + +// protected method +HTMLTableElement* HTMLTableRowElement::GetTable() const { + nsIContent* parent = GetParent(); + if (!parent) { + return nullptr; + } + + // We may not be in a section + HTMLTableElement* table = HTMLTableElement::FromNode(parent); + if (table) { + return table; + } + + return HTMLTableElement::FromNodeOrNull(parent->GetParent()); +} + +int32_t HTMLTableRowElement::RowIndex() const { + HTMLTableElement* table = GetTable(); + if (!table) { + return -1; + } + + nsIHTMLCollection* rows = table->Rows(); + + uint32_t numRows = rows->Length(); + + for (uint32_t i = 0; i < numRows; i++) { + if (rows->GetElementAt(i) == this) { + return i; + } + } + + return -1; +} + +int32_t HTMLTableRowElement::SectionRowIndex() const { + HTMLTableSectionElement* section = GetSection(); + if (!section) { + return -1; + } + + nsCOMPtr<nsIHTMLCollection> coll = section->Rows(); + uint32_t numRows = coll->Length(); + for (uint32_t i = 0; i < numRows; i++) { + if (coll->GetElementAt(i) == this) { + return i; + } + } + + return -1; +} + +static bool IsCell(Element* aElement, int32_t aNamespaceID, nsAtom* aAtom, + void* aData) { + return aElement->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th); +} + +nsIHTMLCollection* HTMLTableRowElement::Cells() { + if (!mCells) { + mCells = new nsContentList(this, IsCell, + nullptr, // destroy func + nullptr, // closure data + false, nullptr, kNameSpaceID_XHTML, false); + } + + return mCells; +} + +already_AddRefed<nsGenericHTMLElement> HTMLTableRowElement::InsertCell( + int32_t aIndex, ErrorResult& aError) { + if (aIndex < -1) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + + // Make sure mCells is initialized. + nsIHTMLCollection* cells = Cells(); + + NS_ASSERTION(mCells, "How did that happen?"); + + nsCOMPtr<nsINode> nextSibling; + // -1 means append, so should use null nextSibling + if (aIndex != -1) { + nextSibling = cells->Item(aIndex); + // Check whether we're inserting past end of list. We want to avoid doing + // this unless we really have to, since this has to walk all our kids. If + // we have a nextSibling, we're clearly not past end of list. + if (!nextSibling) { + uint32_t cellCount = cells->Length(); + if (aIndex > int32_t(cellCount)) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + } + } + + // create the cell + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::td, + getter_AddRefs(nodeInfo)); + + RefPtr<nsGenericHTMLElement> cell = + NS_NewHTMLTableCellElement(nodeInfo.forget()); + if (!cell) { + aError.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + nsINode::InsertBefore(*cell, nextSibling, aError); + + return cell.forget(); +} + +void HTMLTableRowElement::DeleteCell(int32_t aValue, ErrorResult& aError) { + if (aValue < -1) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + nsIHTMLCollection* cells = Cells(); + + uint32_t refIndex; + if (aValue == -1) { + refIndex = cells->Length(); + if (refIndex == 0) { + return; + } + + --refIndex; + } else { + refIndex = (uint32_t)aValue; + } + + nsCOMPtr<nsINode> cell = cells->Item(refIndex); + if (!cell) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + nsINode::RemoveChild(*cell, aError); +} + +bool HTMLTableRowElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + /* + * ignore these attributes, stored simply as strings + * + * ch + */ + + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::height) { + // Per spec should be ParseNonzeroHTMLDimension, but no browsers do that. + // See https://github.com/whatwg/html/issues/4716 + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::align) { + return ParseTableCellHAlignValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::bgcolor) { + return aResult.ParseColor(aValue); + } + if (aAttribute == nsGkAtoms::valign) { + return ParseTableVAlignValue(aValue, aResult); + } + } + + return nsGenericHTMLElement::ParseBackgroundAttribute( + aNamespaceID, aAttribute, aValue, aResult) || + nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTableRowElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + nsGenericHTMLElement::MapHeightAttributeInto(aBuilder); + nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapVAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLTableRowElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::align}, {nsGkAtoms::valign}, {nsGkAtoms::height}, {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + sBackgroundAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLTableRowElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTableRowElement.h b/dom/html/HTMLTableRowElement.h new file mode 100644 index 0000000000..768aefe6d9 --- /dev/null +++ b/dom/html/HTMLTableRowElement.h @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLTableRowElement_h +#define mozilla_dom_HTMLTableRowElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +class nsContentList; + +namespace mozilla::dom { + +class HTMLTableSectionElement; + +class HTMLTableRowElement final : public nsGenericHTMLElement { + public: + explicit HTMLTableRowElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetHasWeirdParserInsertionMode(); + } + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTableRowElement, tr) + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + int32_t RowIndex() const; + int32_t SectionRowIndex() const; + nsIHTMLCollection* Cells(); + already_AddRefed<nsGenericHTMLElement> InsertCell(int32_t aIndex, + ErrorResult& aError); + void DeleteCell(int32_t aValue, ErrorResult& aError); + + void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + void GetCh(DOMString& aCh) { GetHTMLAttr(nsGkAtoms::_char, aCh); } + void SetCh(const nsAString& aCh, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::_char, aCh, aError); + } + void GetChOff(DOMString& aChOff) { GetHTMLAttr(nsGkAtoms::charoff, aChOff); } + void SetChOff(const nsAString& aChOff, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::charoff, aChOff, aError); + } + void GetVAlign(DOMString& aVAlign) { + GetHTMLAttr(nsGkAtoms::valign, aVAlign); + } + void SetVAlign(const nsAString& aVAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::valign, aVAlign, aError); + } + void GetBgColor(DOMString& aBgColor) { + GetHTMLAttr(nsGkAtoms::bgcolor, aBgColor); + } + void SetBgColor(const nsAString& aBgColor, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::bgcolor, aBgColor, aError); + } + + virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + virtual nsMapRuleToAttributesFunc GetAttributeMappingFunction() + const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTableRowElement, + nsGenericHTMLElement) + + protected: + virtual ~HTMLTableRowElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + HTMLTableSectionElement* GetSection() const; + HTMLTableElement* GetTable() const; + RefPtr<nsContentList> mCells; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLTableRowElement_h */ diff --git a/dom/html/HTMLTableSectionElement.cpp b/dom/html/HTMLTableSectionElement.cpp new file mode 100644 index 0000000000..358657b8d4 --- /dev/null +++ b/dom/html/HTMLTableSectionElement.cpp @@ -0,0 +1,177 @@ +/* -*- 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/HTMLTableSectionElement.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "nsAttrValueInlines.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/HTMLTableSectionElementBinding.h" +#include "nsContentUtils.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(TableSection) + +namespace mozilla::dom { + +// you will see the phrases "rowgroup" and "section" used interchangably + +HTMLTableSectionElement::~HTMLTableSectionElement() = default; + +JSObject* HTMLTableSectionElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTableSectionElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTableSectionElement, + nsGenericHTMLElement, mRows) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTableSectionElement, + nsGenericHTMLElement) + +NS_IMPL_ELEMENT_CLONE(HTMLTableSectionElement) + +nsIHTMLCollection* HTMLTableSectionElement::Rows() { + if (!mRows) { + mRows = new nsContentList(this, mNodeInfo->NamespaceID(), nsGkAtoms::tr, + nsGkAtoms::tr, false); + } + + return mRows; +} + +already_AddRefed<nsGenericHTMLElement> HTMLTableSectionElement::InsertRow( + int32_t aIndex, ErrorResult& aError) { + if (aIndex < -1) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + + nsIHTMLCollection* rows = Rows(); + + uint32_t rowCount = rows->Length(); + if (aIndex > (int32_t)rowCount) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + + bool doInsert = (aIndex < int32_t(rowCount)) && (aIndex != -1); + + // create the row + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nsContentUtils::QNameChanged(mNodeInfo, nsGkAtoms::tr, + getter_AddRefs(nodeInfo)); + + RefPtr<nsGenericHTMLElement> rowContent = + NS_NewHTMLTableRowElement(nodeInfo.forget()); + if (!rowContent) { + aError.Throw(NS_ERROR_OUT_OF_MEMORY); + return nullptr; + } + + if (doInsert) { + nsCOMPtr<nsINode> refNode = rows->Item(aIndex); + nsINode::InsertBefore(*rowContent, refNode, aError); + } else { + nsINode::AppendChild(*rowContent, aError); + } + return rowContent.forget(); +} + +void HTMLTableSectionElement::DeleteRow(int32_t aValue, ErrorResult& aError) { + if (aValue < -1) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + nsIHTMLCollection* rows = Rows(); + + uint32_t refIndex; + if (aValue == -1) { + refIndex = rows->Length(); + if (refIndex == 0) { + return; + } + + --refIndex; + } else { + refIndex = (uint32_t)aValue; + } + + nsCOMPtr<nsINode> row = rows->Item(refIndex); + if (!row) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + nsINode::RemoveChild(*row, aError); +} + +bool HTMLTableSectionElement::ParseAttribute( + int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + /* ignore these attributes, stored simply as strings + ch + */ + if (aAttribute == nsGkAtoms::height) { + // Per HTML spec there should be nothing special here, but all browsers + // implement height mapping to style. See + // <https://github.com/whatwg/html/issues/4718>. All browsers allow 0, so + // keep doing that. + return aResult.ParseHTMLDimension(aValue); + } + if (aAttribute == nsGkAtoms::align) { + return ParseTableCellHAlignValue(aValue, aResult); + } + if (aAttribute == nsGkAtoms::bgcolor) { + return aResult.ParseColor(aValue); + } + if (aAttribute == nsGkAtoms::valign) { + return ParseTableVAlignValue(aValue, aResult); + } + } + + return nsGenericHTMLElement::ParseBackgroundAttribute( + aNamespaceID, aAttribute, aValue, aResult) || + nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTableSectionElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + // height: value + if (!aBuilder.PropertyIsSet(eCSSProperty_height)) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::height); + if (value && value->Type() == nsAttrValue::eInteger) { + aBuilder.SetPixelValue(eCSSProperty_height, + (float)value->GetIntegerValue()); + } + } + nsGenericHTMLElement::MapDivAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapVAlignAttributeInto(aBuilder); + nsGenericHTMLElement::MapBackgroundAttributesInto(aBuilder); + nsGenericHTMLElement::MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLTableSectionElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::align}, {nsGkAtoms::valign}, {nsGkAtoms::height}, {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sCommonAttributeMap, + sBackgroundAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLTableSectionElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTableSectionElement.h b/dom/html/HTMLTableSectionElement.h new file mode 100644 index 0000000000..e1ea59622c --- /dev/null +++ b/dom/html/HTMLTableSectionElement.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLTableSectionElement_h +#define mozilla_dom_HTMLTableSectionElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsContentList.h" // For ctor. + +namespace mozilla::dom { + +class HTMLTableSectionElement final : public nsGenericHTMLElement { + public: + explicit HTMLTableSectionElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetHasWeirdParserInsertionMode(); + } + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + nsIHTMLCollection* Rows(); + already_AddRefed<nsGenericHTMLElement> InsertRow(int32_t aIndex, + ErrorResult& aError); + void DeleteRow(int32_t aValue, ErrorResult& aError); + + void GetAlign(DOMString& aAlign) { GetHTMLAttr(nsGkAtoms::align, aAlign); } + void SetAlign(const nsAString& aAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::align, aAlign, aError); + } + void GetCh(DOMString& aCh) { GetHTMLAttr(nsGkAtoms::_char, aCh); } + void SetCh(const nsAString& aCh, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::_char, aCh, aError); + } + void GetChOff(DOMString& aChOff) { GetHTMLAttr(nsGkAtoms::charoff, aChOff); } + void SetChOff(const nsAString& aChOff, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::charoff, aChOff, aError); + } + void GetVAlign(DOMString& aVAlign) { + GetHTMLAttr(nsGkAtoms::valign, aVAlign); + } + void SetVAlign(const nsAString& aVAlign, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::valign, aVAlign, aError); + } + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTableSectionElement, + nsGenericHTMLElement) + protected: + virtual ~HTMLTableSectionElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + RefPtr<nsContentList> mRows; + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLTableSectionElement_h */ diff --git a/dom/html/HTMLTemplateElement.cpp b/dom/html/HTMLTemplateElement.cpp new file mode 100644 index 0000000000..2a608f1c50 --- /dev/null +++ b/dom/html/HTMLTemplateElement.cpp @@ -0,0 +1,110 @@ +/* -*- 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/HTMLTemplateElement.h" +#include "mozilla/dom/HTMLTemplateElementBinding.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/NameSpaceConstants.h" +#include "mozilla/dom/ShadowRootBinding.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsStyleConsts.h" +#include "nsAtom.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Template) + +namespace mozilla::dom { + +static constexpr nsAttrValue::EnumTable kShadowRootModeTable[] = { + {"open", ShadowRootMode::Open}, + {"closed", ShadowRootMode::Closed}, + {nullptr, {}}}; + +const nsAttrValue::EnumTable* kShadowRootModeDefault = &kShadowRootModeTable[2]; + +HTMLTemplateElement::HTMLTemplateElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + SetHasWeirdParserInsertionMode(); + + Document* contentsOwner = OwnerDoc()->GetTemplateContentsOwner(); + if (!contentsOwner) { + MOZ_CRASH("There should always be a template contents owner."); + } + + mContent = contentsOwner->CreateDocumentFragment(); + mContent->SetHost(this); +} + +HTMLTemplateElement::~HTMLTemplateElement() { + if (mContent && mContent->GetHost() == this) { + mContent->SetHost(nullptr); + } +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTemplateElement, + nsGenericHTMLElement) + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLTemplateElement) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLTemplateElement, + nsGenericHTMLElement) + if (tmp->mContent) { + if (tmp->mContent->GetHost() == tmp) { + tmp->mContent->SetHost(nullptr); + } + tmp->mContent = nullptr; + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLTemplateElement, + nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContent) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ELEMENT_CLONE(HTMLTemplateElement) + +JSObject* HTMLTemplateElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTemplateElement_Binding::Wrap(aCx, this, aGivenProto); +} + +void HTMLTemplateElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None && aName == nsGkAtoms::shadowrootmode && + aValue && aValue->Type() == nsAttrValue::ValueType::eEnum && + !mShadowRootMode.isSome()) { + mShadowRootMode.emplace( + static_cast<ShadowRootMode>(aValue->GetEnumValue())); + } + + nsGenericHTMLElement::AfterSetAttr(aNamespaceID, aName, aValue, aOldValue, + aMaybeScriptedPrincipal, aNotify); +} + +bool HTMLTemplateElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && + aAttribute == nsGkAtoms::shadowrootmode) { + return aResult.ParseEnumValue(aValue, kShadowRootModeTable, false, nullptr); + } + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTemplateElement::SetHTMLUnsafe(const nsAString& aHTML) { + RefPtr<DocumentFragment> content = mContent; + nsContentUtils::SetHTMLUnsafe(content, this, aHTML); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTemplateElement.h b/dom/html/HTMLTemplateElement.h new file mode 100644 index 0000000000..be643d215b --- /dev/null +++ b/dom/html/HTMLTemplateElement.h @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLTemplateElement_h +#define mozilla_dom_HTMLTemplateElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "nsGenericHTMLElement.h" +#include "mozilla/dom/DocumentFragment.h" +#include "mozilla/dom/ShadowRootBinding.h" +#include "nsGkAtoms.h" + +namespace mozilla::dom { + +class HTMLTemplateElement final : public nsGenericHTMLElement { + public: + explicit HTMLTemplateElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTemplateElement, _template); + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTemplateElement, + nsGenericHTMLElement) + + void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + DocumentFragment* Content() { return mContent; } + void SetContent(DocumentFragment* aContent) { mContent = aContent; } + + void GetShadowRootMode(nsAString& aResult) const { + GetEnumAttr(nsGkAtoms::shadowrootmode, nullptr, aResult); + } + void SetShadowRootMode(const nsAString& aValue) { + SetHTMLAttr(nsGkAtoms::shadowrootmode, aValue); + } + + bool ShadowRootDelegatesFocus() { + return GetBoolAttr(nsGkAtoms::shadowrootdelegatesfocus); + } + void SetShadowRootDelegatesFocus(bool aValue) { + SetHTMLBoolAttr(nsGkAtoms::shadowrootdelegatesfocus, aValue, + IgnoredErrorResult()); + } + + MOZ_CAN_RUN_SCRIPT + void SetHTMLUnsafe(const nsAString& aHTML) final; + + protected: + virtual ~HTMLTemplateElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + RefPtr<DocumentFragment> mContent; + Maybe<ShadowRootMode> mShadowRootMode; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLTemplateElement_h diff --git a/dom/html/HTMLTextAreaElement.cpp b/dom/html/HTMLTextAreaElement.cpp new file mode 100644 index 0000000000..ce28575a4d --- /dev/null +++ b/dom/html/HTMLTextAreaElement.cpp @@ -0,0 +1,1164 @@ +/* -*- 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/HTMLTextAreaElement.h" + +#include "mozAutoDocUpdate.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/FormData.h" +#include "mozilla/dom/HTMLTextAreaElementBinding.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresState.h" +#include "mozilla/TextControlState.h" +#include "nsAttrValueInlines.h" +#include "nsBaseCommandController.h" +#include "nsContentCID.h" +#include "nsContentCreatorFunctions.h" +#include "nsError.h" +#include "nsFocusManager.h" +#include "nsIConstraintValidation.h" +#include "nsIControllers.h" +#include "mozilla/dom/Document.h" +#include "nsIFormControlFrame.h" +#include "nsIFormControl.h" +#include "nsIFrame.h" +#include "nsITextControlFrame.h" +#include "nsLayoutUtils.h" +#include "nsLinebreakConverter.h" +#include "nsPresContext.h" +#include "nsReadableUtils.h" +#include "nsStyleConsts.h" +#include "nsTextControlFrame.h" +#include "nsThreadUtils.h" +#include "nsXULControllers.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(TextArea) + +namespace mozilla::dom { + +HTMLTextAreaElement::HTMLTextAreaElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) + : TextControlElement(std::move(aNodeInfo), aFromParser, + FormControlType::Textarea), + mDoneAddingChildren(!aFromParser), + mInhibitStateRestoration(!!(aFromParser & FROM_PARSER_FRAGMENT)), + mAutocompleteAttrState(nsContentUtils::eAutocompleteAttrState_Unknown), + mState(TextControlState::Construct(this)) { + AddMutationObserver(this); + + // Set up our default state. By default we're enabled (since we're + // a control type that can be disabled but not actually disabled right now), + // optional, read-write, and valid. Also by default we don't have to show + // validity UI and so forth. + AddStatesSilently(ElementState::ENABLED | ElementState::OPTIONAL_ | + ElementState::READWRITE | ElementState::VALID | + ElementState::VALUE_EMPTY); + RemoveStatesSilently(ElementState::READONLY); +} + +HTMLTextAreaElement::~HTMLTextAreaElement() { + mState->Destroy(); + mState = nullptr; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLTextAreaElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLTextAreaElement, + TextControlElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mValidity) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControllers) + if (tmp->mState) { + tmp->mState->Traverse(cb); + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(HTMLTextAreaElement, + TextControlElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mValidity) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mControllers) + if (tmp->mState) { + tmp->mState->Unlink(); + } +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLTextAreaElement, + TextControlElement, + nsIMutationObserver, + nsIConstraintValidation) + +// nsIDOMHTMLTextAreaElement + +nsresult HTMLTextAreaElement::Clone(dom::NodeInfo* aNodeInfo, + nsINode** aResult) const { + *aResult = nullptr; + RefPtr<HTMLTextAreaElement> it = new (aNodeInfo->NodeInfoManager()) + HTMLTextAreaElement(do_AddRef(aNodeInfo)); + + nsresult rv = const_cast<HTMLTextAreaElement*>(this)->CopyInnerTo(it); + NS_ENSURE_SUCCESS(rv, rv); + + it->SetLastValueChangeWasInteractive(mLastValueChangeWasInteractive); + it.forget(aResult); + return NS_OK; +} + +// nsIContent + +void HTMLTextAreaElement::Select() { + if (FocusState() != FocusTristate::eUnfocusable) { + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + fm->SetFocus(this, nsIFocusManager::FLAG_NOSCROLL); + } + } + + SetSelectionRange(0, UINT32_MAX, mozilla::dom::Optional<nsAString>(), + IgnoreErrors()); +} + +NS_IMETHODIMP +HTMLTextAreaElement::SelectAll(nsPresContext* aPresContext) { + nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); + + if (formControlFrame) { + formControlFrame->SetFormProperty(nsGkAtoms::select, u""_ns); + } + + return NS_OK; +} + +bool HTMLTextAreaElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLFormControlElementWithState::IsHTMLFocusable( + aWithMouse, aIsFocusable, aTabIndex)) { + return true; + } + + // disabled textareas are not focusable + *aIsFocusable = !IsDisabled(); + return false; +} + +int32_t HTMLTextAreaElement::TabIndexDefault() { return 0; } + +void HTMLTextAreaElement::GetType(nsAString& aType) { + aType.AssignLiteral("textarea"); +} + +void HTMLTextAreaElement::GetValue(nsAString& aValue) { + GetValueInternal(aValue, true); + MOZ_ASSERT(aValue.FindChar(static_cast<char16_t>('\r')) == -1); +} + +void HTMLTextAreaElement::GetValueInternal(nsAString& aValue, + bool aIgnoreWrap) const { + MOZ_ASSERT(mState); + mState->GetValue(aValue, aIgnoreWrap, /* aForDisplay = */ true); +} + +nsIEditor* HTMLTextAreaElement::GetEditorForBindings() { + if (!GetPrimaryFrame()) { + GetPrimaryFrame(FlushType::Frames); + } + return GetTextEditor(); +} + +TextEditor* HTMLTextAreaElement::GetTextEditor() { + MOZ_ASSERT(mState); + return mState->GetTextEditor(); +} + +TextEditor* HTMLTextAreaElement::GetTextEditorWithoutCreation() const { + MOZ_ASSERT(mState); + return mState->GetTextEditorWithoutCreation(); +} + +nsISelectionController* HTMLTextAreaElement::GetSelectionController() { + MOZ_ASSERT(mState); + return mState->GetSelectionController(); +} + +nsFrameSelection* HTMLTextAreaElement::GetConstFrameSelection() { + MOZ_ASSERT(mState); + return mState->GetConstFrameSelection(); +} + +nsresult HTMLTextAreaElement::BindToFrame(nsTextControlFrame* aFrame) { + MOZ_ASSERT(!nsContentUtils::IsSafeToRunScript()); + MOZ_ASSERT(mState); + return mState->BindToFrame(aFrame); +} + +void HTMLTextAreaElement::UnbindFromFrame(nsTextControlFrame* aFrame) { + MOZ_ASSERT(mState); + if (aFrame) { + mState->UnbindFromFrame(aFrame); + } +} + +nsresult HTMLTextAreaElement::CreateEditor() { + MOZ_ASSERT(mState); + return mState->PrepareEditor(); +} + +void HTMLTextAreaElement::SetPreviewValue(const nsAString& aValue) { + MOZ_ASSERT(mState); + mState->SetPreviewText(aValue, true); +} + +void HTMLTextAreaElement::GetPreviewValue(nsAString& aValue) { + MOZ_ASSERT(mState); + mState->GetPreviewText(aValue); +} + +void HTMLTextAreaElement::EnablePreview() { + if (mIsPreviewEnabled) { + return; + } + + mIsPreviewEnabled = true; + // Reconstruct the frame to append an anonymous preview node + nsLayoutUtils::PostRestyleEvent(this, RestyleHint{0}, + nsChangeHint_ReconstructFrame); +} + +bool HTMLTextAreaElement::IsPreviewEnabled() { return mIsPreviewEnabled; } + +nsresult HTMLTextAreaElement::SetValueInternal( + const nsAString& aValue, const ValueSetterOptions& aOptions) { + MOZ_ASSERT(mState); + + // Need to set the value changed flag here if our value has in fact changed + // (i.e. if ValueSetterOption::SetValueChanged is in aOptions), so that + // retrieves the correct value if needed. + if (aOptions.contains(ValueSetterOption::SetValueChanged)) { + SetValueChanged(true); + } + + if (!mState->SetValue(aValue, aOptions)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +void HTMLTextAreaElement::SetValue(const nsAString& aValue, + ErrorResult& aError) { + // If the value has been set by a script, we basically want to keep the + // current change event state. If the element is ready to fire a change + // event, we should keep it that way. Otherwise, we should make sure the + // element will not fire any event because of the script interaction. + // + // NOTE: this is currently quite expensive work (too much string + // manipulation). We should probably optimize that. + nsAutoString currentValue; + GetValueInternal(currentValue, true); + + nsresult rv = SetValueInternal( + aValue, + {ValueSetterOption::ByContentAPI, ValueSetterOption::SetValueChanged, + ValueSetterOption::MoveCursorToEndIfValueChanged}); + if (NS_WARN_IF(NS_FAILED(rv))) { + aError.Throw(rv); + return; + } + + if (mFocusedValue.Equals(currentValue)) { + GetValueInternal(mFocusedValue, true); + } +} + +void HTMLTextAreaElement::SetUserInput(const nsAString& aValue, + nsIPrincipal& aSubjectPrincipal) { + SetValueInternal(aValue, {ValueSetterOption::BySetUserInputAPI, + ValueSetterOption::SetValueChanged, + ValueSetterOption::MoveCursorToEndIfValueChanged}); +} + +void HTMLTextAreaElement::SetValueChanged(bool aValueChanged) { + MOZ_ASSERT(mState); + + bool previousValue = mValueChanged; + mValueChanged = aValueChanged; + if (!aValueChanged && !mState->IsEmpty()) { + mState->EmptyValue(); + } + if (mValueChanged == previousValue) { + return; + } + UpdateTooLongValidityState(); + UpdateTooShortValidityState(); + UpdateValidityElementStates(true); +} + +void HTMLTextAreaElement::SetLastValueChangeWasInteractive( + bool aWasInteractive) { + if (aWasInteractive == mLastValueChangeWasInteractive) { + return; + } + mLastValueChangeWasInteractive = aWasInteractive; + const bool wasValid = IsValid(); + UpdateTooLongValidityState(); + UpdateTooShortValidityState(); + if (wasValid != IsValid()) { + UpdateValidityElementStates(true); + } +} + +void HTMLTextAreaElement::GetDefaultValue(nsAString& aDefaultValue, + ErrorResult& aError) const { + if (!nsContentUtils::GetNodeTextContent(this, false, aDefaultValue, + fallible)) { + aError.Throw(NS_ERROR_OUT_OF_MEMORY); + } +} + +void HTMLTextAreaElement::SetDefaultValue(const nsAString& aDefaultValue, + ErrorResult& aError) { + // setting the value of an textarea element using `.defaultValue = "foo"` + // must be interpreted as a two-step operation: + // 1. clearing all child nodes + // 2. adding a new text node with the new content + // Step 1 must therefore collapse the Selection to 0. + // Calling `SetNodeTextContent()` with an empty string will do that for us. + nsContentUtils::SetNodeTextContent(this, EmptyString(), true); + nsresult rv = nsContentUtils::SetNodeTextContent(this, aDefaultValue, true); + if (NS_SUCCEEDED(rv) && !mValueChanged) { + Reset(); + } + if (NS_FAILED(rv)) { + aError.Throw(rv); + } +} + +bool HTMLTextAreaElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::maxlength || + aAttribute == nsGkAtoms::minlength) { + return aResult.ParseNonNegativeIntValue(aValue); + } else if (aAttribute == nsGkAtoms::cols) { + aResult.ParseIntWithFallback(aValue, DEFAULT_COLS); + return true; + } else if (aAttribute == nsGkAtoms::rows) { + aResult.ParseIntWithFallback(aValue, DEFAULT_ROWS_TEXTAREA); + return true; + } else if (aAttribute == nsGkAtoms::autocomplete) { + aResult.ParseAtomArray(aValue); + return true; + } + } + return TextControlElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTextAreaElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + // wrap=off + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::wrap); + if (value && value->Type() == nsAttrValue::eString && + value->Equals(nsGkAtoms::OFF, eIgnoreCase)) { + // Equivalent to expanding `white-space; pre` + aBuilder.SetKeywordValue(eCSSProperty_white_space_collapse, + StyleWhiteSpaceCollapse::Preserve); + aBuilder.SetKeywordValue(eCSSProperty_text_wrap_mode, + StyleTextWrapMode::Nowrap); + } + + nsGenericHTMLFormControlElementWithState::MapDivAlignAttributeInto(aBuilder); + nsGenericHTMLFormControlElementWithState::MapCommonAttributesInto(aBuilder); +} + +nsChangeHint HTMLTextAreaElement::GetAttributeChangeHint( + const nsAtom* aAttribute, int32_t aModType) const { + nsChangeHint retval = + nsGenericHTMLFormControlElementWithState::GetAttributeChangeHint( + aAttribute, aModType); + + const bool isAdditionOrRemoval = + aModType == MutationEvent_Binding::ADDITION || + aModType == MutationEvent_Binding::REMOVAL; + + if (aAttribute == nsGkAtoms::rows || aAttribute == nsGkAtoms::cols) { + retval |= NS_STYLE_HINT_REFLOW; + } else if (aAttribute == nsGkAtoms::wrap) { + retval |= nsChangeHint_ReconstructFrame; + } else if (aAttribute == nsGkAtoms::placeholder && isAdditionOrRemoval) { + retval |= nsChangeHint_ReconstructFrame; + } + return retval; +} + +NS_IMETHODIMP_(bool) +HTMLTextAreaElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = {{nsGkAtoms::wrap}, + {nullptr}}; + + static const MappedAttributeEntry* const map[] = { + attributes, + sDivAlignAttributeMap, + sCommonAttributeMap, + }; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLTextAreaElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +bool HTMLTextAreaElement::IsDisabledForEvents(WidgetEvent* aEvent) { + nsIFormControlFrame* formControlFrame = GetFormControlFrame(false); + nsIFrame* formFrame = do_QueryFrame(formControlFrame); + return IsElementDisabledForEvents(aEvent, formFrame); +} + +void HTMLTextAreaElement::GetEventTargetParent(EventChainPreVisitor& aVisitor) { + aVisitor.mCanHandle = false; + if (IsDisabledForEvents(aVisitor.mEvent)) { + return; + } + + // Don't dispatch a second select event if we are already handling + // one. + if (aVisitor.mEvent->mMessage == eFormSelect) { + if (mHandlingSelect) { + return; + } + mHandlingSelect = true; + } + + if (aVisitor.mEvent->mMessage == eBlur) { + // Set mWantsPreHandleEvent and fire change event in PreHandleEvent to + // prevent it breaks event target chain creation. + aVisitor.mWantsPreHandleEvent = true; + } + + nsGenericHTMLFormControlElementWithState::GetEventTargetParent(aVisitor); +} + +nsresult HTMLTextAreaElement::PreHandleEvent(EventChainVisitor& aVisitor) { + if (aVisitor.mEvent->mMessage == eBlur) { + // Fire onchange (if necessary), before we do the blur, bug 370521. + FireChangeEventIfNeeded(); + } + return nsGenericHTMLFormControlElementWithState::PreHandleEvent(aVisitor); +} + +void HTMLTextAreaElement::FireChangeEventIfNeeded() { + nsString value; + GetValueInternal(value, true); + + // NOTE(emilio): This is not quite on the spec, but matches <input>, see + // https://github.com/whatwg/html/issues/10011 and + // https://github.com/whatwg/html/issues/10013 + if (mValueChanged) { + SetUserInteracted(true); + } + + if (mFocusedValue.Equals(value)) { + return; + } + + // Dispatch the change event. + mFocusedValue = value; + nsContentUtils::DispatchTrustedEvent(OwnerDoc(), this, u"change"_ns, + CanBubble::eYes, Cancelable::eNo); +} + +nsresult HTMLTextAreaElement::PostHandleEvent(EventChainPostVisitor& aVisitor) { + if (aVisitor.mEvent->mMessage == eFormSelect) { + mHandlingSelect = false; + } + if (aVisitor.mEvent->mMessage == eFocus) { + GetValueInternal(mFocusedValue, true); + } + return NS_OK; +} + +void HTMLTextAreaElement::DoneAddingChildren(bool aHaveNotified) { + if (!mValueChanged) { + if (!mDoneAddingChildren) { + // Reset now that we're done adding children if the content sink tried to + // sneak some text in without calling AppendChildTo. + Reset(); + } + + if (!mInhibitStateRestoration) { + GenerateStateKey(); + RestoreFormControlState(); + } + } + + mDoneAddingChildren = true; +} + +// Controllers Methods + +nsIControllers* HTMLTextAreaElement::GetControllers(ErrorResult& aError) { + if (!mControllers) { + mControllers = new nsXULControllers(); + if (!mControllers) { + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr<nsBaseCommandController> commandController = + nsBaseCommandController::CreateEditorController(); + if (!commandController) { + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + mControllers->AppendController(commandController); + + commandController = nsBaseCommandController::CreateEditingController(); + if (!commandController) { + aError.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + mControllers->AppendController(commandController); + } + + return mControllers; +} + +nsresult HTMLTextAreaElement::GetControllers(nsIControllers** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + ErrorResult error; + *aResult = GetControllers(error); + NS_IF_ADDREF(*aResult); + + return error.StealNSResult(); +} + +uint32_t HTMLTextAreaElement::GetTextLength() { + nsAutoString val; + GetValue(val); + return val.Length(); +} + +Nullable<uint32_t> HTMLTextAreaElement::GetSelectionStart(ErrorResult& aError) { + uint32_t selStart, selEnd; + GetSelectionRange(&selStart, &selEnd, aError); + return Nullable<uint32_t>(selStart); +} + +void HTMLTextAreaElement::SetSelectionStart( + const Nullable<uint32_t>& aSelectionStart, ErrorResult& aError) { + MOZ_ASSERT(mState); + mState->SetSelectionStart(aSelectionStart, aError); +} + +Nullable<uint32_t> HTMLTextAreaElement::GetSelectionEnd(ErrorResult& aError) { + uint32_t selStart, selEnd; + GetSelectionRange(&selStart, &selEnd, aError); + return Nullable<uint32_t>(selEnd); +} + +void HTMLTextAreaElement::SetSelectionEnd( + const Nullable<uint32_t>& aSelectionEnd, ErrorResult& aError) { + MOZ_ASSERT(mState); + mState->SetSelectionEnd(aSelectionEnd, aError); +} + +void HTMLTextAreaElement::GetSelectionRange(uint32_t* aSelectionStart, + uint32_t* aSelectionEnd, + ErrorResult& aRv) { + MOZ_ASSERT(mState); + return mState->GetSelectionRange(aSelectionStart, aSelectionEnd, aRv); +} + +void HTMLTextAreaElement::GetSelectionDirection(nsAString& aDirection, + ErrorResult& aError) { + MOZ_ASSERT(mState); + mState->GetSelectionDirectionString(aDirection, aError); +} + +void HTMLTextAreaElement::SetSelectionDirection(const nsAString& aDirection, + ErrorResult& aError) { + MOZ_ASSERT(mState); + mState->SetSelectionDirection(aDirection, aError); +} + +void HTMLTextAreaElement::SetSelectionRange( + uint32_t aSelectionStart, uint32_t aSelectionEnd, + const Optional<nsAString>& aDirection, ErrorResult& aError) { + MOZ_ASSERT(mState); + mState->SetSelectionRange(aSelectionStart, aSelectionEnd, aDirection, aError); +} + +void HTMLTextAreaElement::SetRangeText(const nsAString& aReplacement, + ErrorResult& aRv) { + MOZ_ASSERT(mState); + mState->SetRangeText(aReplacement, aRv); +} + +void HTMLTextAreaElement::SetRangeText(const nsAString& aReplacement, + uint32_t aStart, uint32_t aEnd, + SelectionMode aSelectMode, + ErrorResult& aRv) { + MOZ_ASSERT(mState); + mState->SetRangeText(aReplacement, aStart, aEnd, aSelectMode, aRv); +} + +void HTMLTextAreaElement::GetValueFromSetRangeText(nsAString& aValue) { + GetValueInternal(aValue, false); +} + +nsresult HTMLTextAreaElement::SetValueFromSetRangeText( + const nsAString& aValue) { + return SetValueInternal(aValue, {ValueSetterOption::ByContentAPI, + ValueSetterOption::BySetRangeTextAPI, + ValueSetterOption::SetValueChanged}); +} + +void HTMLTextAreaElement::SetDirectionFromValue(bool aNotify, + const nsAString* aKnownValue) { + nsAutoString value; + if (!aKnownValue) { + GetValue(value); + aKnownValue = &value; + } + SetDirectionalityFromValue(this, *aKnownValue, aNotify); +} + +nsresult HTMLTextAreaElement::Reset() { + nsAutoString resetVal; + GetDefaultValue(resetVal, IgnoreErrors()); + SetValueChanged(false); + SetUserInteracted(false); + + nsresult rv = SetValueInternal(resetVal, ValueSetterOption::ByInternalAPI); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLTextAreaElement::SubmitNamesValues(FormData* aFormData) { + // + // Get the name (if no name, no submit) + // + nsAutoString name; + GetAttr(nsGkAtoms::name, name); + if (name.IsEmpty()) { + return NS_OK; + } + + // + // Get the value + // + nsAutoString value; + GetValueInternal(value, false); + + // + // Submit name=value + // + const nsresult rv = aFormData->AddNameValuePair(name, value); + if (NS_FAILED(rv)) { + return rv; + } + + // Submit dirname=dir + return SubmitDirnameDir(aFormData); +} + +void HTMLTextAreaElement::SaveState() { + // Only save if value != defaultValue (bug 62713) + PresState* state = nullptr; + if (mValueChanged) { + state = GetPrimaryPresState(); + if (state) { + nsAutoString value; + GetValueInternal(value, true); + + if (NS_FAILED(nsLinebreakConverter::ConvertStringLineBreaks( + value, nsLinebreakConverter::eLinebreakPlatform, + nsLinebreakConverter::eLinebreakContent))) { + NS_ERROR("Converting linebreaks failed!"); + return; + } + + state->contentData() = + TextContentData(value, mLastValueChangeWasInteractive); + } + } + + if (mDisabledChanged) { + if (!state) { + state = GetPrimaryPresState(); + } + if (state) { + // We do not want to save the real disabled state but the disabled + // attribute. + state->disabled() = HasAttr(nsGkAtoms::disabled); + state->disabledSet() = true; + } + } +} + +bool HTMLTextAreaElement::RestoreState(PresState* aState) { + const PresContentData& state = aState->contentData(); + + if (state.type() == PresContentData::TTextContentData) { + ErrorResult rv; + SetValue(state.get_TextContentData().value(), rv); + ENSURE_SUCCESS(rv, false); + if (state.get_TextContentData().lastValueChangeWasInteractive()) { + SetLastValueChangeWasInteractive(true); + } + } + if (aState->disabledSet() && !aState->disabled()) { + SetDisabled(false, IgnoreErrors()); + } + + return false; +} + +void HTMLTextAreaElement::UpdateValidityElementStates(bool aNotify) { + AutoStateChangeNotifier notifier(*this, aNotify); + RemoveStatesSilently(ElementState::VALIDITY_STATES); + if (!IsCandidateForConstraintValidation()) { + return; + } + ElementState state; + if (IsValid()) { + state |= ElementState::VALID; + if (mUserInteracted) { + state |= ElementState::USER_VALID; + } + } else { + state |= ElementState::INVALID; + if (mUserInteracted) { + state |= ElementState::USER_INVALID; + } + } + AddStatesSilently(state); +} + +nsresult HTMLTextAreaElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = + nsGenericHTMLFormControlElementWithState::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + // Set direction based on value if dir=auto + if (HasDirAuto()) { + SetDirectionFromValue(false); + } + + // If there is a disabled fieldset in the parent chain, the element is now + // barred from constraint validation and can't suffer from value missing. + UpdateValueMissingValidityState(); + UpdateBarredFromConstraintValidation(); + + // And now make sure our state is up to date + UpdateValidityElementStates(false); + + return rv; +} + +void HTMLTextAreaElement::UnbindFromTree(bool aNullParent) { + nsGenericHTMLFormControlElementWithState::UnbindFromTree(aNullParent); + + // We might be no longer disabled because of parent chain changed. + UpdateValueMissingValidityState(); + UpdateBarredFromConstraintValidation(); + + // And now make sure our state is up to date + UpdateValidityElementStates(false); +} + +void HTMLTextAreaElement::BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + bool aNotify) { + if (aNotify && aName == nsGkAtoms::disabled && + aNameSpaceID == kNameSpaceID_None) { + mDisabledChanged = true; + } + + return nsGenericHTMLFormControlElementWithState::BeforeSetAttr( + aNameSpaceID, aName, aValue, aNotify); +} + +void HTMLTextAreaElement::CharacterDataChanged(nsIContent* aContent, + const CharacterDataChangeInfo&) { + ContentChanged(aContent); +} + +void HTMLTextAreaElement::ContentAppended(nsIContent* aFirstNewContent) { + ContentChanged(aFirstNewContent); +} + +void HTMLTextAreaElement::ContentInserted(nsIContent* aChild) { + ContentChanged(aChild); +} + +void HTMLTextAreaElement::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + ContentChanged(aChild); +} + +void HTMLTextAreaElement::ContentChanged(nsIContent* aContent) { + if (!mValueChanged && mDoneAddingChildren && + nsContentUtils::IsInSameAnonymousTree(this, aContent)) { + if (mState->IsSelectionCached()) { + // In case the content is *replaced*, i.e. by calling + // `.textContent = "foo";`, + // firstly the old content is removed, then the new content is added. + // As per wpt, this must collapse the selection to 0. + // Removing and adding of an element is routed through here, but due to + // the script runner `Reset()` is only invoked after the append operation. + // Therefore, `Reset()` would adjust the Selection to the new value, not + // to 0. + // By forcing a selection update here, the selection is reset in order to + // comply with the wpt. + auto& props = mState->GetSelectionProperties(); + nsAutoString resetVal; + GetDefaultValue(resetVal, IgnoreErrors()); + props.SetMaxLength(resetVal.Length()); + props.SetStart(props.GetStart()); + props.SetEnd(props.GetEnd()); + } + // We should wait all ranges finish handling the mutation before updating + // the anonymous subtree with a call of Reset. + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "ResetHTMLTextAreaElementIfValueHasNotChangedYet", + [self = RefPtr{this}]() { + // However, if somebody has already changed the value, we don't need + // to keep doing this. + if (!self->mValueChanged) { + self->Reset(); + } + })); + } +} + +void HTMLTextAreaElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::required || aName == nsGkAtoms::disabled || + aName == nsGkAtoms::readonly) { + if (aName == nsGkAtoms::disabled) { + // This *has* to be called *before* validity state check because + // UpdateBarredFromConstraintValidation and + // UpdateValueMissingValidityState depend on our disabled state. + UpdateDisabledState(aNotify); + } + + if (aName == nsGkAtoms::required) { + // This *has* to be called *before* UpdateValueMissingValidityState + // because UpdateValueMissingValidityState depends on our required + // state. + UpdateRequiredState(!!aValue, aNotify); + } + + if (aName == nsGkAtoms::readonly && !!aValue != !!aOldValue) { + UpdateReadOnlyState(aNotify); + } + + UpdateValueMissingValidityState(); + + // This *has* to be called *after* validity has changed. + if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) { + UpdateBarredFromConstraintValidation(); + } + UpdateValidityElementStates(aNotify); + } else if (aName == nsGkAtoms::autocomplete) { + // Clear the cached @autocomplete attribute state. + mAutocompleteAttrState = nsContentUtils::eAutocompleteAttrState_Unknown; + } else if (aName == nsGkAtoms::maxlength) { + UpdateTooLongValidityState(); + UpdateValidityElementStates(aNotify); + } else if (aName == nsGkAtoms::minlength) { + UpdateTooShortValidityState(); + UpdateValidityElementStates(aNotify); + } else if (aName == nsGkAtoms::placeholder) { + if (nsTextControlFrame* f = do_QueryFrame(GetPrimaryFrame())) { + f->PlaceholderChanged(aOldValue, aValue); + } + UpdatePlaceholderShownState(); + } else if (aName == nsGkAtoms::dir && aValue && + aValue->Equals(nsGkAtoms::_auto, eIgnoreCase)) { + SetDirectionFromValue(aNotify); + } + } + + return nsGenericHTMLFormControlElementWithState::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aSubjectPrincipal, aNotify); +} + +nsresult HTMLTextAreaElement::CopyInnerTo(Element* aDest) { + nsresult rv = nsGenericHTMLFormControlElementWithState::CopyInnerTo(aDest); + NS_ENSURE_SUCCESS(rv, rv); + + if (mValueChanged || aDest->OwnerDoc()->IsStaticDocument()) { + // Set our value on the clone. + auto* dest = static_cast<HTMLTextAreaElement*>(aDest); + + nsAutoString value; + GetValueInternal(value, true); + + // SetValueInternal handles setting mValueChanged for us. dest is a fresh + // element so setting its value can't really run script. + if (NS_WARN_IF( + NS_FAILED(rv = MOZ_KnownLive(dest)->SetValueInternal( + value, {ValueSetterOption::SetValueChanged})))) { + return rv; + } + } + + return NS_OK; +} + +bool HTMLTextAreaElement::IsMutable() const { return !IsDisabledOrReadOnly(); } + +void HTMLTextAreaElement::SetCustomValidity(const nsAString& aError) { + ConstraintValidation::SetCustomValidity(aError); + UpdateValidityElementStates(true); +} + +bool HTMLTextAreaElement::IsTooLong() { + if (!mValueChanged || !mLastValueChangeWasInteractive || + !HasAttr(nsGkAtoms::maxlength)) { + return false; + } + + int32_t maxLength = MaxLength(); + + // Maxlength of -1 means parsing error. + if (maxLength == -1) { + return false; + } + + int32_t textLength = GetTextLength(); + + return textLength > maxLength; +} + +bool HTMLTextAreaElement::IsTooShort() { + if (!mValueChanged || !mLastValueChangeWasInteractive || + !HasAttr(nsGkAtoms::minlength)) { + return false; + } + + int32_t minLength = MinLength(); + + // Minlength of -1 means parsing error. + if (minLength == -1) { + return false; + } + + int32_t textLength = GetTextLength(); + + return textLength && textLength < minLength; +} + +bool HTMLTextAreaElement::IsValueMissing() const { + if (!Required() || !IsMutable()) { + return false; + } + return IsValueEmpty(); +} + +void HTMLTextAreaElement::UpdateTooLongValidityState() { + SetValidityState(VALIDITY_STATE_TOO_LONG, IsTooLong()); +} + +void HTMLTextAreaElement::UpdateTooShortValidityState() { + SetValidityState(VALIDITY_STATE_TOO_SHORT, IsTooShort()); +} + +void HTMLTextAreaElement::UpdateValueMissingValidityState() { + SetValidityState(VALIDITY_STATE_VALUE_MISSING, IsValueMissing()); +} + +void HTMLTextAreaElement::UpdateBarredFromConstraintValidation() { + SetBarredFromConstraintValidation( + HasAttr(nsGkAtoms::readonly) || + HasFlag(ELEMENT_IS_DATALIST_OR_HAS_DATALIST_ANCESTOR) || IsDisabled()); +} + +nsresult HTMLTextAreaElement::GetValidationMessage( + nsAString& aValidationMessage, ValidityStateType aType) { + nsresult rv = NS_OK; + + switch (aType) { + case VALIDITY_STATE_TOO_LONG: { + nsAutoString message; + int32_t maxLength = MaxLength(); + int32_t textLength = GetTextLength(); + nsAutoString strMaxLength; + nsAutoString strTextLength; + + strMaxLength.AppendInt(maxLength); + strTextLength.AppendInt(textLength); + + rv = nsContentUtils::FormatMaybeLocalizedString( + message, nsContentUtils::eDOM_PROPERTIES, "FormValidationTextTooLong", + OwnerDoc(), strMaxLength, strTextLength); + aValidationMessage = message; + } break; + case VALIDITY_STATE_TOO_SHORT: { + nsAutoString message; + int32_t minLength = MinLength(); + int32_t textLength = GetTextLength(); + nsAutoString strMinLength; + nsAutoString strTextLength; + + strMinLength.AppendInt(minLength); + strTextLength.AppendInt(textLength); + + rv = nsContentUtils::FormatMaybeLocalizedString( + message, nsContentUtils::eDOM_PROPERTIES, + "FormValidationTextTooShort", OwnerDoc(), strMinLength, + strTextLength); + aValidationMessage = message; + } break; + case VALIDITY_STATE_VALUE_MISSING: { + nsAutoString message; + rv = nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationValueMissing", + OwnerDoc(), message); + aValidationMessage = message; + } break; + default: + rv = + ConstraintValidation::GetValidationMessage(aValidationMessage, aType); + } + + return rv; +} + +bool HTMLTextAreaElement::IsSingleLineTextControl() const { return false; } + +bool HTMLTextAreaElement::IsTextArea() const { return true; } + +bool HTMLTextAreaElement::IsPasswordTextControl() const { return false; } + +int32_t HTMLTextAreaElement::GetCols() { return Cols(); } + +int32_t HTMLTextAreaElement::GetWrapCols() { + nsHTMLTextWrap wrapProp; + TextControlElement::GetWrapPropertyEnum(this, wrapProp); + if (wrapProp == TextControlElement::eHTMLTextWrap_Off) { + // do not wrap when wrap=off + return 0; + } + + // Otherwise we just wrap at the given number of columns + return GetCols(); +} + +int32_t HTMLTextAreaElement::GetRows() { + const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::rows); + if (attr && attr->Type() == nsAttrValue::eInteger) { + int32_t rows = attr->GetIntegerValue(); + return (rows <= 0) ? DEFAULT_ROWS_TEXTAREA : rows; + } + + return DEFAULT_ROWS_TEXTAREA; +} + +void HTMLTextAreaElement::GetDefaultValueFromContent(nsAString& aValue, bool) { + GetDefaultValue(aValue, IgnoreErrors()); +} + +bool HTMLTextAreaElement::ValueChanged() const { return mValueChanged; } + +void HTMLTextAreaElement::GetTextEditorValue(nsAString& aValue) const { + MOZ_ASSERT(mState); + mState->GetValue(aValue, /* aIgnoreWrap = */ true, /* aForDisplay = */ true); +} + +void HTMLTextAreaElement::InitializeKeyboardEventListeners() { + MOZ_ASSERT(mState); + mState->InitializeKeyboardEventListeners(); +} + +void HTMLTextAreaElement::UpdatePlaceholderShownState() { + SetStates(ElementState::PLACEHOLDER_SHOWN, + IsValueEmpty() && HasAttr(nsGkAtoms::placeholder)); +} + +void HTMLTextAreaElement::OnValueChanged(ValueChangeKind aKind, + bool aNewValueEmpty, + const nsAString* aKnownNewValue) { + if (aKind != ValueChangeKind::Internal) { + mLastValueChangeWasInteractive = aKind == ValueChangeKind::UserInteraction; + } + + if (aNewValueEmpty != IsValueEmpty()) { + SetStates(ElementState::VALUE_EMPTY, aNewValueEmpty); + UpdatePlaceholderShownState(); + } + + // Update the validity state + const bool validBefore = IsValid(); + UpdateTooLongValidityState(); + UpdateTooShortValidityState(); + UpdateValueMissingValidityState(); + + if (HasDirAuto()) { + SetDirectionFromValue(true, aKnownNewValue); + } + + if (validBefore != IsValid()) { + UpdateValidityElementStates(true); + } +} + +bool HTMLTextAreaElement::HasCachedSelection() { + MOZ_ASSERT(mState); + return mState->IsSelectionCached(); +} + +void HTMLTextAreaElement::SetUserInteracted(bool aInteracted) { + if (mUserInteracted == aInteracted) { + return; + } + mUserInteracted = aInteracted; + UpdateValidityElementStates(true); +} + +void HTMLTextAreaElement::FieldSetDisabledChanged(bool aNotify) { + // This *has* to be called before UpdateBarredFromConstraintValidation and + // UpdateValueMissingValidityState because these two functions depend on our + // disabled state. + nsGenericHTMLFormControlElementWithState::FieldSetDisabledChanged(aNotify); + + UpdateValueMissingValidityState(); + UpdateBarredFromConstraintValidation(); + UpdateValidityElementStates(true); +} + +JSObject* HTMLTextAreaElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTextAreaElement_Binding::Wrap(aCx, this, aGivenProto); +} + +void HTMLTextAreaElement::GetAutocomplete(DOMString& aValue) { + const nsAttrValue* attributeVal = GetParsedAttr(nsGkAtoms::autocomplete); + + mAutocompleteAttrState = nsContentUtils::SerializeAutocompleteAttribute( + attributeVal, aValue, mAutocompleteAttrState); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTextAreaElement.h b/dom/html/HTMLTextAreaElement.h new file mode 100644 index 0000000000..ac3eb8bbf5 --- /dev/null +++ b/dom/html/HTMLTextAreaElement.h @@ -0,0 +1,382 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLTextAreaElement_h +#define mozilla_dom_HTMLTextAreaElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/TextControlElement.h" +#include "mozilla/TextControlState.h" +#include "mozilla/TextEditor.h" +#include "mozilla/dom/ConstraintValidation.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "mozilla/dom/HTMLInputElementBinding.h" +#include "nsIControllers.h" +#include "nsCOMPtr.h" +#include "nsGenericHTMLElement.h" +#include "nsStubMutationObserver.h" +#include "nsGkAtoms.h" + +class nsIControllers; +class nsPresContext; + +namespace mozilla { + +class EventChainPostVisitor; +class EventChainPreVisitor; +class PresState; + +namespace dom { + +class FormData; + +class HTMLTextAreaElement final : public TextControlElement, + public nsStubMutationObserver, + public ConstraintValidation { + public: + using ConstraintValidation::GetValidationMessage; + using ValueSetterOption = TextControlState::ValueSetterOption; + using ValueSetterOptions = TextControlState::ValueSetterOptions; + + explicit HTMLTextAreaElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser = NOT_FROM_PARSER); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLTextAreaElement, textarea) + + int32_t TabIndexDefault() override; + + // Element + bool IsInteractiveHTMLContent() const override { return true; } + + // nsGenericHTMLElement + bool IsDisabledForEvents(WidgetEvent* aEvent) override; + + // nsGenericHTMLFormElement + void SaveState() override; + + // FIXME: Shouldn't be a CAN_RUN_SCRIPT_BOUNDARY probably? + MOZ_CAN_RUN_SCRIPT_BOUNDARY bool RestoreState(PresState*) override; + + // nsIFormControl + MOZ_CAN_RUN_SCRIPT_BOUNDARY + NS_IMETHOD Reset() override; + NS_IMETHOD SubmitNamesValues(FormData* aFormData) override; + + void FieldSetDisabledChanged(bool aNotify) override; + + void SetLastValueChangeWasInteractive(bool); + + // TextControlElement + bool IsSingleLineTextControlOrTextArea() const override { return true; } + void SetValueChanged(bool aValueChanged) override; + bool IsSingleLineTextControl() const override; + bool IsTextArea() const override; + bool IsPasswordTextControl() const override; + int32_t GetCols() override; + int32_t GetWrapCols() override; + int32_t GetRows() override; + void GetDefaultValueFromContent(nsAString& aValue, bool aForDisplay) override; + bool ValueChanged() const override; + void GetTextEditorValue(nsAString& aValue) const override; + MOZ_CAN_RUN_SCRIPT TextEditor* GetTextEditor() override; + TextEditor* GetTextEditorWithoutCreation() const override; + nsISelectionController* GetSelectionController() override; + nsFrameSelection* GetConstFrameSelection() override; + TextControlState* GetTextControlState() const override { return mState; } + nsresult BindToFrame(nsTextControlFrame* aFrame) override; + MOZ_CAN_RUN_SCRIPT void UnbindFromFrame(nsTextControlFrame* aFrame) override; + MOZ_CAN_RUN_SCRIPT nsresult CreateEditor() override; + void SetPreviewValue(const nsAString& aValue) override; + void GetPreviewValue(nsAString& aValue) override; + void EnablePreview() override; + bool IsPreviewEnabled() override; + void InitializeKeyboardEventListeners() override; + void UpdatePlaceholderShownState(); + void OnValueChanged(ValueChangeKind, bool aNewValueEmpty, + const nsAString* aKnownNewValue) override; + void GetValueFromSetRangeText(nsAString& aValue) override; + MOZ_CAN_RUN_SCRIPT nsresult + SetValueFromSetRangeText(const nsAString& aValue) override; + bool HasCachedSelection() override; + + // nsIContent + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + nsChangeHint GetAttributeChangeHint(const nsAtom* aAttribute, + int32_t aModType) const override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + + void GetEventTargetParent(EventChainPreVisitor& aVisitor) override; + nsresult PreHandleEvent(EventChainVisitor& aVisitor) override; + nsresult PostHandleEvent(EventChainPostVisitor& aVisitor) override; + + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + + void DoneAddingChildren(bool aHaveNotified) override; + + nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult CopyInnerTo(Element* aDest); + + /** + * Called when an attribute is about to be changed + */ + void BeforeSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + + // nsIMutationObserver + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTextAreaElement, + TextControlElement) + + // nsIConstraintValidation + bool IsTooLong(); + bool IsTooShort(); + bool IsValueMissing() const; + void UpdateTooLongValidityState(); + void UpdateTooShortValidityState(); + void UpdateValueMissingValidityState(); + void UpdateBarredFromConstraintValidation(); + + // ConstraintValidation + nsresult GetValidationMessage(nsAString& aValidationMessage, + ValidityStateType aType) override; + + // Web IDL binding methods + void GetAutocomplete(DOMString& aValue); + void SetAutocomplete(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::autocomplete, aValue, aRv); + } + uint32_t Cols() { return GetUnsignedIntAttr(nsGkAtoms::cols, DEFAULT_COLS); } + void SetCols(uint32_t aCols, ErrorResult& aError) { + uint32_t cols = aCols ? aCols : DEFAULT_COLS; + SetUnsignedIntAttr(nsGkAtoms::cols, cols, DEFAULT_COLS, aError); + } + void GetDirName(nsAString& aValue) { + GetHTMLAttr(nsGkAtoms::dirname, aValue); + } + void SetDirName(const nsAString& aValue, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::dirname, aValue, aError); + } + bool Disabled() { return GetBoolAttr(nsGkAtoms::disabled); } + void SetDisabled(bool aDisabled, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::disabled, aDisabled, aError); + } + // nsGenericHTMLFormControlElementWithState::GetForm is fine + using nsGenericHTMLFormControlElementWithState::GetForm; + int32_t MaxLength() const { return GetIntAttr(nsGkAtoms::maxlength, -1); } + int32_t UsedMaxLength() const final { return MaxLength(); } + void SetMaxLength(int32_t aMaxLength, ErrorResult& aError) { + int32_t minLength = MinLength(); + if (aMaxLength < 0 || (minLength >= 0 && aMaxLength < minLength)) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + } else { + SetHTMLIntAttr(nsGkAtoms::maxlength, aMaxLength, aError); + } + } + int32_t MinLength() const { return GetIntAttr(nsGkAtoms::minlength, -1); } + void SetMinLength(int32_t aMinLength, ErrorResult& aError) { + int32_t maxLength = MaxLength(); + if (aMinLength < 0 || (maxLength >= 0 && aMinLength > maxLength)) { + aError.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + } else { + SetHTMLIntAttr(nsGkAtoms::minlength, aMinLength, aError); + } + } + void GetName(nsAString& aName) { GetHTMLAttr(nsGkAtoms::name, aName); } + void SetName(const nsAString& aName, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::name, aName, aError); + } + void GetPlaceholder(nsAString& aPlaceholder) { + GetHTMLAttr(nsGkAtoms::placeholder, aPlaceholder); + } + void SetPlaceholder(const nsAString& aPlaceholder, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::placeholder, aPlaceholder, aError); + } + bool ReadOnly() { return GetBoolAttr(nsGkAtoms::readonly); } + void SetReadOnly(bool aReadOnly, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::readonly, aReadOnly, aError); + } + bool Required() const { return State().HasState(ElementState::REQUIRED); } + + MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement, + ErrorResult& aRv); + + MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement, + uint32_t aStart, uint32_t aEnd, + SelectionMode aSelectMode, + ErrorResult& aRv); + + void SetRequired(bool aRequired, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::required, aRequired, aError); + } + uint32_t Rows() { + return GetUnsignedIntAttr(nsGkAtoms::rows, DEFAULT_ROWS_TEXTAREA); + } + void SetRows(uint32_t aRows, ErrorResult& aError) { + uint32_t rows = aRows ? aRows : DEFAULT_ROWS_TEXTAREA; + SetUnsignedIntAttr(nsGkAtoms::rows, rows, DEFAULT_ROWS_TEXTAREA, aError); + } + void GetWrap(nsAString& aWrap) { GetHTMLAttr(nsGkAtoms::wrap, aWrap); } + void SetWrap(const nsAString& aWrap, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::wrap, aWrap, aError); + } + void GetType(nsAString& aType); + void GetDefaultValue(nsAString& aDefaultValue, ErrorResult& aError) const; + void SetDefaultValue(const nsAString& aDefaultValue, ErrorResult& aError); + void GetValue(nsAString& aValue); + MOZ_CAN_RUN_SCRIPT void SetValue(const nsAString&, ErrorResult&); + + uint32_t GetTextLength(); + + // Override SetCustomValidity so we update our state properly when it's called + // via bindings. + void SetCustomValidity(const nsAString& aError); + + MOZ_CAN_RUN_SCRIPT void Select(); + Nullable<uint32_t> GetSelectionStart(ErrorResult& aError); + MOZ_CAN_RUN_SCRIPT void SetSelectionStart( + const Nullable<uint32_t>& aSelectionStart, ErrorResult& aError); + Nullable<uint32_t> GetSelectionEnd(ErrorResult& aError); + MOZ_CAN_RUN_SCRIPT void SetSelectionEnd( + const Nullable<uint32_t>& aSelectionEnd, ErrorResult& aError); + void GetSelectionDirection(nsAString& aDirection, ErrorResult& aError); + MOZ_CAN_RUN_SCRIPT void SetSelectionDirection(const nsAString& aDirection, + ErrorResult& aError); + MOZ_CAN_RUN_SCRIPT void SetSelectionRange( + uint32_t aSelectionStart, uint32_t aSelectionEnd, + const Optional<nsAString>& aDirecton, ErrorResult& aError); + nsIControllers* GetControllers(ErrorResult& aError); + // XPCOM adapter function widely used throughout code, leaving it as is. + nsresult GetControllers(nsIControllers** aResult); + + MOZ_CAN_RUN_SCRIPT nsIEditor* GetEditorForBindings(); + bool HasEditor() const { + MOZ_ASSERT(mState); + return !!mState->GetTextEditorWithoutCreation(); + } + + bool IsInputEventTarget() const { return true; } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY void SetUserInput( + const nsAString& aValue, nsIPrincipal& aSubjectPrincipal); + + protected: + MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual ~HTMLTextAreaElement(); + + // get rid of the compiler warning + using nsGenericHTMLFormControlElementWithState::IsSingleLineTextControl; + + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + nsCOMPtr<nsIControllers> mControllers; + /** https://html.spec.whatwg.org/#user-interacted */ + bool mUserInteracted = false; + /** Whether or not the value has changed since its default value was given. */ + bool mValueChanged = false; + /** Whether or not the last change to the value was made interactively by the + * user. */ + bool mLastValueChangeWasInteractive = false; + /** Whether or not we are already handling select event. */ + bool mHandlingSelect = false; + /** Whether or not we are done adding children (always true if not + created by a parser */ + bool mDoneAddingChildren; + /** Whether state restoration should be inhibited in DoneAddingChildren. */ + bool mInhibitStateRestoration; + /** Whether our disabled state has changed from the default **/ + bool mDisabledChanged = false; + bool mIsPreviewEnabled = false; + + nsContentUtils::AutocompleteAttrState mAutocompleteAttrState; + + void FireChangeEventIfNeeded(); + + nsString mFocusedValue; + + /** The state of the text editor (selection controller and the editor) **/ + TextControlState* mState; + + NS_IMETHOD SelectAll(nsPresContext* aPresContext); + /** + * Get the value, whether it is from the content or the frame. + * @param aValue the value [out] + * @param aIgnoreWrap whether to ignore the wrap attribute when getting the + * value. If this is true, linebreaks will not be inserted even if + * wrap=hard. + */ + void GetValueInternal(nsAString& aValue, bool aIgnoreWrap) const; + + /** + * Setting the value. + * + * @param aValue String to set. + * @param aOptions See TextControlState::ValueSetterOption. + */ + MOZ_CAN_RUN_SCRIPT nsresult + SetValueInternal(const nsAString& aValue, const ValueSetterOptions& aOptions); + + /** + * Common method to call from the various mutation observer methods. + * aContent is a content node that's either the one that changed or its + * parent; we should only respond to the change if aContent is non-anonymous. + */ + void ContentChanged(nsIContent* aContent); + + void AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, bool aNotify) override; + + void SetDirectionFromValue(bool aNotify, + const nsAString* aKnownValue = nullptr); + + /** + * Get the mutable state of the element. + */ + bool IsMutable() const; + + /** + * Returns whether the current value is the empty string. + * + * @return whether the current value is the empty string. + */ + bool IsValueEmpty() const { + return State().HasState(ElementState::VALUE_EMPTY); + } + + /** + * A helper to get the current selection range. Will throw on the ErrorResult + * if we have no editor state. + */ + void GetSelectionRange(uint32_t* aSelectionStart, uint32_t* aSelectionEnd, + ErrorResult& aRv); + + void SetUserInteracted(bool) final; + void UpdateValidityElementStates(bool aNotify); + + private: + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); +}; + +} // namespace dom +} // namespace mozilla + +#endif diff --git a/dom/html/HTMLTimeElement.cpp b/dom/html/HTMLTimeElement.cpp new file mode 100644 index 0000000000..fc3003ce9f --- /dev/null +++ b/dom/html/HTMLTimeElement.cpp @@ -0,0 +1,30 @@ +/* -*- 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 "HTMLTimeElement.h" +#include "mozilla/dom/HTMLTimeElementBinding.h" +#include "nsGenericHTMLElement.h" +#include "nsVariant.h" +#include "nsGkAtoms.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Time) + +namespace mozilla::dom { + +HTMLTimeElement::HTMLTimeElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + +HTMLTimeElement::~HTMLTimeElement() = default; + +NS_IMPL_ELEMENT_CLONE(HTMLTimeElement) + +JSObject* HTMLTimeElement::WrapNode(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTimeElement_Binding::Wrap(cx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTimeElement.h b/dom/html/HTMLTimeElement.h new file mode 100644 index 0000000000..ccf1dca2a9 --- /dev/null +++ b/dom/html/HTMLTimeElement.h @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLTimeElement_h +#define mozilla_dom_HTMLTimeElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" + +namespace mozilla::dom { + +class HTMLTimeElement final : public nsGenericHTMLElement { + public: + explicit HTMLTimeElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + virtual ~HTMLTimeElement(); + + // HTMLTimeElement WebIDL + void GetDateTime(DOMString& aDateTime) { + GetHTMLAttr(nsGkAtoms::datetime, aDateTime); + } + + void SetDateTime(const nsAString& aDateTime, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::datetime, aDateTime, aError); + } + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLTimeElement_h diff --git a/dom/html/HTMLTitleElement.cpp b/dom/html/HTMLTitleElement.cpp new file mode 100644 index 0000000000..776c76f7e5 --- /dev/null +++ b/dom/html/HTMLTitleElement.cpp @@ -0,0 +1,95 @@ +/* -*- 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/HTMLTitleElement.h" + +#include "mozilla/dom/HTMLTitleElementBinding.h" +#include "mozilla/ErrorResult.h" +#include "nsStyleConsts.h" +#include "mozilla/dom/Document.h" +#include "nsContentUtils.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Title) + +namespace mozilla::dom { + +HTMLTitleElement::HTMLTitleElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + AddMutationObserver(this); +} + +HTMLTitleElement::~HTMLTitleElement() = default; + +NS_IMPL_ISUPPORTS_INHERITED(HTMLTitleElement, nsGenericHTMLElement, + nsIMutationObserver) + +NS_IMPL_ELEMENT_CLONE(HTMLTitleElement) + +JSObject* HTMLTitleElement::WrapNode(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTitleElement_Binding::Wrap(cx, this, aGivenProto); +} + +void HTMLTitleElement::GetText(DOMString& aText, ErrorResult& aError) const { + if (!nsContentUtils::GetNodeTextContent(this, false, aText, fallible)) { + aError = NS_ERROR_OUT_OF_MEMORY; + } +} + +void HTMLTitleElement::SetText(const nsAString& aText, ErrorResult& aError) { + aError = nsContentUtils::SetNodeTextContent(this, aText, true); +} + +void HTMLTitleElement::CharacterDataChanged(nsIContent* aContent, + const CharacterDataChangeInfo&) { + SendTitleChangeEvent(false); +} + +void HTMLTitleElement::ContentAppended(nsIContent* aFirstNewContent) { + SendTitleChangeEvent(false); +} + +void HTMLTitleElement::ContentInserted(nsIContent* aChild) { + SendTitleChangeEvent(false); +} + +void HTMLTitleElement::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + SendTitleChangeEvent(false); +} + +nsresult HTMLTitleElement::BindToTree(BindContext& aContext, nsINode& aParent) { + // Let this fall through. + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + SendTitleChangeEvent(true); + + return NS_OK; +} + +void HTMLTitleElement::UnbindFromTree(bool aNullParent) { + SendTitleChangeEvent(false); + + // Let this fall through. + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +void HTMLTitleElement::DoneAddingChildren(bool aHaveNotified) { + if (!aHaveNotified) { + SendTitleChangeEvent(false); + } +} + +void HTMLTitleElement::SendTitleChangeEvent(bool aBound) { + Document* doc = GetUncomposedDoc(); + if (doc) { + doc->NotifyPossibleTitleChange(aBound); + } +} + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTitleElement.h b/dom/html/HTMLTitleElement.h new file mode 100644 index 0000000000..63cafa75a2 --- /dev/null +++ b/dom/html/HTMLTitleElement.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLTITLEElement_h_ +#define mozilla_dom_HTMLTITLEElement_h_ + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" +#include "nsStubMutationObserver.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class HTMLTitleElement final : public nsGenericHTMLElement, + public nsStubMutationObserver { + public: + using Element::GetText; + + explicit HTMLTitleElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + + // HTMLTitleElement + void GetText(DOMString& aText, ErrorResult& aError) const; + void SetText(const nsAString& aText, ErrorResult& aError); + + // nsIMutationObserver + NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED + NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + + virtual void UnbindFromTree(bool aNullParent = true) override; + + virtual void DoneAddingChildren(bool aHaveNotified) override; + + protected: + virtual ~HTMLTitleElement(); + + JSObject* WrapNode(JSContext* cx, JS::Handle<JSObject*> aGivenProto) final; + + private: + void SendTitleChangeEvent(bool aBound); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_HTMLTitleElement_h_ diff --git a/dom/html/HTMLTrackElement.cpp b/dom/html/HTMLTrackElement.cpp new file mode 100644 index 0000000000..5363d3e399 --- /dev/null +++ b/dom/html/HTMLTrackElement.cpp @@ -0,0 +1,517 @@ +/* -*- 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/HTMLTrackElement.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/WebVTTListener.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/dom/HTMLTrackElementBinding.h" +#include "mozilla/dom/HTMLUnknownElement.h" +#include "nsAttrValueInlines.h" +#include "nsCOMPtr.h" +#include "nsContentPolicyUtils.h" +#include "nsContentUtils.h" +#include "nsCycleCollectionParticipant.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsIContentPolicy.h" +#include "mozilla/dom/Document.h" +#include "nsILoadGroup.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIScriptError.h" +#include "nsISupportsImpl.h" +#include "nsISupportsPrimitives.h" +#include "nsNetUtil.h" +#include "nsStyleConsts.h" +#include "nsThreadUtils.h" +#include "nsVideoFrame.h" + +extern mozilla::LazyLogModule gTextTrackLog; +#define LOG(msg, ...) \ + MOZ_LOG(gTextTrackLog, LogLevel::Verbose, \ + ("TextTrackElement=%p, " msg, this, ##__VA_ARGS__)) + +// Replace the usual NS_IMPL_NS_NEW_HTML_ELEMENT(Track) so +// we can return an UnknownElement instead when pref'd off. +nsGenericHTMLElement* NS_NewHTMLTrackElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + return new (nim) mozilla::dom::HTMLTrackElement(nodeInfo.forget()); +} + +namespace mozilla::dom { + +// Map html attribute string values to TextTrackKind enums. +static constexpr nsAttrValue::EnumTable kKindTable[] = { + {"subtitles", static_cast<int16_t>(TextTrackKind::Subtitles)}, + {"captions", static_cast<int16_t>(TextTrackKind::Captions)}, + {"descriptions", static_cast<int16_t>(TextTrackKind::Descriptions)}, + {"chapters", static_cast<int16_t>(TextTrackKind::Chapters)}, + {"metadata", static_cast<int16_t>(TextTrackKind::Metadata)}, + {nullptr, 0}}; + +// Invalid values are treated as "metadata" in ParseAttribute, but if no value +// at all is specified, it's treated as "subtitles" in GetKind +static constexpr const nsAttrValue::EnumTable* kKindTableInvalidValueDefault = + &kKindTable[4]; + +class WindowDestroyObserver final : public nsIObserver { + NS_DECL_ISUPPORTS + + public: + explicit WindowDestroyObserver(HTMLTrackElement* aElement, uint64_t aWinID) + : mTrackElement(aElement), mInnerID(aWinID) { + RegisterWindowDestroyObserver(); + } + void RegisterWindowDestroyObserver() { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->AddObserver(this, "inner-window-destroyed", false); + } + } + void UnRegisterWindowDestroyObserver() { + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->RemoveObserver(this, "inner-window-destroyed"); + } + mTrackElement = nullptr; + } + NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override { + MOZ_ASSERT(NS_IsMainThread()); + if (strcmp(aTopic, "inner-window-destroyed") == 0) { + nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject); + NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); + uint64_t innerID; + nsresult rv = wrapper->GetData(&innerID); + NS_ENSURE_SUCCESS(rv, rv); + if (innerID == mInnerID) { + if (mTrackElement) { + mTrackElement->CancelChannelAndListener(); + } + UnRegisterWindowDestroyObserver(); + } + } + return NS_OK; + } + + private: + ~WindowDestroyObserver() = default; + + HTMLTrackElement* mTrackElement; + uint64_t mInnerID; +}; +NS_IMPL_ISUPPORTS(WindowDestroyObserver, nsIObserver); + +/** HTMLTrackElement */ +HTMLTrackElement::HTMLTrackElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mLoadResourceDispatched(false), + mWindowDestroyObserver(nullptr) { + nsISupports* parentObject = OwnerDoc()->GetParentObject(); + NS_ENSURE_TRUE_VOID(parentObject); + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject); + if (window) { + mWindowDestroyObserver = + new WindowDestroyObserver(this, window->WindowID()); + } +} + +HTMLTrackElement::~HTMLTrackElement() { + if (mWindowDestroyObserver) { + mWindowDestroyObserver->UnRegisterWindowDestroyObserver(); + } + CancelChannelAndListener(); +} + +NS_IMPL_ELEMENT_CLONE(HTMLTrackElement) + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLTrackElement, nsGenericHTMLElement, + mTrack, mMediaParent, mListener) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLTrackElement, + nsGenericHTMLElement) + +void HTMLTrackElement::GetKind(DOMString& aKind) const { + GetEnumAttr(nsGkAtoms::kind, kKindTable[0].tag, aKind); +} + +void HTMLTrackElement::OnChannelRedirect(nsIChannel* aChannel, + nsIChannel* aNewChannel, + uint32_t aFlags) { + NS_ASSERTION(aChannel == mChannel, "Channels should match!"); + mChannel = aNewChannel; +} + +JSObject* HTMLTrackElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLTrackElement_Binding::Wrap(aCx, this, aGivenProto); +} + +TextTrack* HTMLTrackElement::GetTrack() { + if (!mTrack) { + CreateTextTrack(); + } + return mTrack; +} + +void HTMLTrackElement::CreateTextTrack() { + nsISupports* parentObject = OwnerDoc()->GetParentObject(); + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject); + if (!parentObject) { + nsContentUtils::ReportToConsole( + nsIScriptError::errorFlag, "Media"_ns, OwnerDoc(), + nsContentUtils::eDOM_PROPERTIES, + "Using track element in non-window context"); + return; + } + + nsString label, srcLang; + GetSrclang(srcLang); + GetLabel(label); + + TextTrackKind kind; + if (const nsAttrValue* value = GetParsedAttr(nsGkAtoms::kind)) { + kind = static_cast<TextTrackKind>(value->GetEnumValue()); + } else { + kind = TextTrackKind::Subtitles; + } + + MOZ_ASSERT(!mTrack, "No need to recreate a text track!"); + mTrack = + new TextTrack(window, kind, label, srcLang, TextTrackMode::Disabled, + TextTrackReadyState::NotLoaded, TextTrackSource::Track); + mTrack->SetTrackElement(this); +} + +bool HTMLTrackElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && aAttribute == nsGkAtoms::kind) { + // Case-insensitive lookup, with the first element as the default. + return aResult.ParseEnumValue(aValue, kKindTable, false, + kKindTableInvalidValueDefault); + } + + // Otherwise call the generic implementation. + return nsGenericHTMLElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLTrackElement::SetSrc(const nsAString& aSrc, ErrorResult& aError) { + LOG("Set src=%s", NS_ConvertUTF16toUTF8(aSrc).get()); + + nsAutoString src; + if (GetAttr(nsGkAtoms::src, src) && src == aSrc) { + LOG("No need to reload for same src url"); + return; + } + + SetHTMLAttr(nsGkAtoms::src, aSrc, aError); + SetReadyState(TextTrackReadyState::NotLoaded); + if (!mMediaParent) { + return; + } + + // Stop WebVTTListener. + mListener = nullptr; + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "HTMLTrackElement::SetSrc"_ns); + mChannel = nullptr; + } + + MaybeDispatchLoadResource(); +} + +void HTMLTrackElement::MaybeClearAllCues() { + // Empty track's cue list whenever the track element's `src` attribute set, + // changed, or removed, + // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:attr-track-src + if (!mTrack) { + return; + } + mTrack->ClearAllCues(); +} + +// This function will run partial steps from `start-the-track-processing-model` +// and finish the rest of steps in `LoadResource()` during the stable state. +// https://html.spec.whatwg.org/multipage/media.html#start-the-track-processing-model +void HTMLTrackElement::MaybeDispatchLoadResource() { + MOZ_ASSERT(mTrack, "Should have already created text track!"); + + // step2, if the text track's text track mode is not set to one of hidden or + // showing, then return. + if (mTrack->Mode() == TextTrackMode::Disabled) { + LOG("Do not load resource for disable track"); + return; + } + + // step3, if the text track's track element does not have a media element as a + // parent, return. + if (!mMediaParent) { + LOG("Do not load resource for track without media element"); + return; + } + + if (ReadyState() == TextTrackReadyState::Loaded) { + LOG("Has already loaded resource"); + return; + } + + // step5, await a stable state and run the rest of steps. + if (!mLoadResourceDispatched) { + RefPtr<WebVTTListener> listener = new WebVTTListener(this); + RefPtr<Runnable> r = NewRunnableMethod<RefPtr<WebVTTListener>>( + "dom::HTMLTrackElement::LoadResource", this, + &HTMLTrackElement::LoadResource, std::move(listener)); + nsContentUtils::RunInStableState(r.forget()); + mLoadResourceDispatched = true; + } +} + +void HTMLTrackElement::LoadResource(RefPtr<WebVTTListener>&& aWebVTTListener) { + LOG("LoadResource"); + mLoadResourceDispatched = false; + + nsAutoString src; + if (!GetAttr(nsGkAtoms::src, src) || src.IsEmpty()) { + LOG("Fail to load because no src"); + SetReadyState(TextTrackReadyState::FailedToLoad); + return; + } + + nsCOMPtr<nsIURI> uri; + nsresult rv = NewURIFromString(src, getter_AddRefs(uri)); + NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv)); + LOG("Trying to load from src=%s", NS_ConvertUTF16toUTF8(src).get()); + + CancelChannelAndListener(); + + // According to + // https://www.w3.org/TR/html5/embedded-content-0.html#sourcing-out-of-band-text-tracks + // + // "8: If the track element's parent is a media element then let CORS mode + // be the state of the parent media element's crossorigin content attribute. + // Otherwise, let CORS mode be No CORS." + // + CORSMode corsMode = + mMediaParent ? AttrValueToCORSMode( + mMediaParent->GetParsedAttr(nsGkAtoms::crossorigin)) + : CORS_NONE; + + // Determine the security flag based on corsMode. + nsSecurityFlags secFlags; + if (CORS_NONE == corsMode) { + // Same-origin is required for track element. + secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT; + } else { + secFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT; + if (CORS_ANONYMOUS == corsMode) { + secFlags |= nsILoadInfo::SEC_COOKIES_SAME_ORIGIN; + } else if (CORS_USE_CREDENTIALS == corsMode) { + secFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE; + } else { + NS_WARNING("Unknown CORS mode."); + secFlags = nsILoadInfo::SEC_REQUIRE_SAME_ORIGIN_INHERITS_SEC_CONTEXT; + } + } + + mListener = std::move(aWebVTTListener); + // This will do 6. Set the text track readiness state to loading. + rv = mListener->LoadResource(); + NS_ENSURE_TRUE_VOID(NS_SUCCEEDED(rv)); + + Document* doc = OwnerDoc(); + if (!doc) { + return; + } + + // 9. End the synchronous section, continuing the remaining steps in parallel. + nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction( + "dom::HTMLTrackElement::LoadResource", + [self = RefPtr<HTMLTrackElement>(this), this, uri, secFlags]() { + if (!mListener) { + // Shutdown got called, abort. + return; + } + nsCOMPtr<nsIChannel> channel; + nsCOMPtr<nsILoadGroup> loadGroup = OwnerDoc()->GetDocumentLoadGroup(); + nsresult rv = NS_NewChannel(getter_AddRefs(channel), uri, + static_cast<Element*>(this), secFlags, + nsIContentPolicy::TYPE_INTERNAL_TRACK, + nullptr, // PerformanceStorage + loadGroup); + + if (NS_FAILED(rv)) { + LOG("create channel failed."); + SetReadyState(TextTrackReadyState::FailedToLoad); + return; + } + + channel->SetNotificationCallbacks(mListener); + + LOG("opening webvtt channel"); + rv = channel->AsyncOpen(mListener); + + if (NS_FAILED(rv)) { + SetReadyState(TextTrackReadyState::FailedToLoad); + return; + } + mChannel = channel; + }); + doc->Dispatch(runnable.forget()); +} + +nsresult HTMLTrackElement::BindToTree(BindContext& aContext, nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + LOG("Track Element bound to tree."); + auto* parent = HTMLMediaElement::FromNode(aParent); + if (!parent) { + return NS_OK; + } + + // Store our parent so we can look up its frame for display. + if (!mMediaParent) { + mMediaParent = parent; + + // TODO: separate notification for 'alternate' tracks? + mMediaParent->NotifyAddedSource(); + LOG("Track element sent notification to parent."); + + // We may already have a TextTrack at this point if GetTrack() has already + // been called. This happens, for instance, if script tries to get the + // TextTrack before its mTrackElement has been bound to the DOM tree. + if (!mTrack) { + CreateTextTrack(); + } + // As `CreateTextTrack()` might fail, so we have to check it again. + if (mTrack) { + LOG("Add text track to media parent"); + mMediaParent->AddTextTrack(mTrack); + } + MaybeDispatchLoadResource(); + } + + return NS_OK; +} + +void HTMLTrackElement::UnbindFromTree(bool aNullParent) { + if (mMediaParent && aNullParent) { + // mTrack can be null if HTMLTrackElement::LoadResource has never been + // called. + if (mTrack) { + mMediaParent->RemoveTextTrack(mTrack); + mMediaParent->UpdateReadyState(); + } + mMediaParent = nullptr; + } + + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +TextTrackReadyState HTMLTrackElement::ReadyState() const { + if (!mTrack) { + return TextTrackReadyState::NotLoaded; + } + + return mTrack->ReadyState(); +} + +void HTMLTrackElement::SetReadyState(TextTrackReadyState aReadyState) { + if (ReadyState() == aReadyState) { + return; + } + + if (mTrack) { + switch (aReadyState) { + case TextTrackReadyState::Loaded: + LOG("dispatch 'load' event"); + DispatchTrackRunnable(u"load"_ns); + break; + case TextTrackReadyState::FailedToLoad: + LOG("dispatch 'error' event"); + DispatchTrackRunnable(u"error"_ns); + break; + default: + break; + } + mTrack->SetReadyState(aReadyState); + } +} + +void HTMLTrackElement::DispatchTrackRunnable(const nsString& aEventName) { + Document* doc = OwnerDoc(); + if (!doc) { + return; + } + nsCOMPtr<nsIRunnable> runnable = NewRunnableMethod<const nsString>( + "dom::HTMLTrackElement::DispatchTrustedEvent", this, + &HTMLTrackElement::DispatchTrustedEvent, aEventName); + doc->Dispatch(runnable.forget()); +} + +void HTMLTrackElement::DispatchTrustedEvent(const nsAString& aName) { + Document* doc = OwnerDoc(); + if (!doc) { + return; + } + nsContentUtils::DispatchTrustedEvent(doc, this, aName, CanBubble::eNo, + Cancelable::eNo); +} + +void HTMLTrackElement::CancelChannelAndListener() { + if (mChannel) { + mChannel->CancelWithReason(NS_BINDING_ABORTED, + "HTMLTrackElement::CancelChannelAndListener"_ns); + mChannel->SetNotificationCallbacks(nullptr); + mChannel = nullptr; + } + + if (mListener) { + mListener->Cancel(); + mListener = nullptr; + } +} + +void HTMLTrackElement::AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && aName == nsGkAtoms::src) { + MaybeClearAllCues(); + // In spec, `start the track processing model` step10, while fetching is + // ongoing, if the track URL changes, then we have to set the `FailedToLoad` + // state. + // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:text-track-failed-to-load-3 + if (ReadyState() == TextTrackReadyState::Loading && aValue != aOldValue) { + SetReadyState(TextTrackReadyState::FailedToLoad); + } + } + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void HTMLTrackElement::DispatchTestEvent(const nsAString& aName) { + if (!StaticPrefs::media_webvtt_testing_events()) { + return; + } + DispatchTrustedEvent(aName); +} + +#undef LOG + +} // namespace mozilla::dom diff --git a/dom/html/HTMLTrackElement.h b/dom/html/HTMLTrackElement.h new file mode 100644 index 0000000000..20a239778d --- /dev/null +++ b/dom/html/HTMLTrackElement.h @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLTrackElement_h +#define mozilla_dom_HTMLTrackElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/TextTrack.h" +#include "nsCycleCollectionParticipant.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" + +class nsIContent; + +namespace mozilla::dom { + +class WebVTTListener; +class WindowDestroyObserver; +enum class TextTrackReadyState : uint8_t; + +class HTMLTrackElement final : public nsGenericHTMLElement { + public: + explicit HTMLTrackElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLTrackElement, + nsGenericHTMLElement) + + // HTMLTrackElement WebIDL + void GetKind(DOMString& aKind) const; + void SetKind(const nsAString& aKind, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::kind, aKind, aError); + } + + void GetSrc(DOMString& aSrc) const { GetHTMLURIAttr(nsGkAtoms::src, aSrc); } + + void SetSrc(const nsAString& aSrc, ErrorResult& aError); + + void GetSrclang(DOMString& aSrclang) const { + GetHTMLAttr(nsGkAtoms::srclang, aSrclang); + } + void GetSrclang(nsAString& aSrclang) const { + GetHTMLAttr(nsGkAtoms::srclang, aSrclang); + } + void SetSrclang(const nsAString& aSrclang, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::srclang, aSrclang, aError); + } + + void GetLabel(DOMString& aLabel) const { + GetHTMLAttr(nsGkAtoms::label, aLabel); + } + void GetLabel(nsAString& aLabel) const { + GetHTMLAttr(nsGkAtoms::label, aLabel); + } + void SetLabel(const nsAString& aLabel, ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::label, aLabel, aError); + } + + bool Default() const { return GetBoolAttr(nsGkAtoms::_default); } + void SetDefault(bool aDefault, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::_default, aDefault, aError); + } + + TextTrackReadyState ReadyState() const; + uint16_t ReadyStateForBindings() const { + return static_cast<uint16_t>(ReadyState()); + } + void SetReadyState(TextTrackReadyState); + + TextTrack* GetTrack(); + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + // Override ParseAttribute() to convert kind strings to enum values. + virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + // Override BindToTree() so that we can trigger a load when we become + // the child of a media element. + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + virtual void UnbindFromTree(bool aNullParent) override; + + virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + + void DispatchTrackRunnable(const nsString& aEventName); + void DispatchTrustedEvent(const nsAString& aName); + void DispatchTestEvent(const nsAString& aName); + + void CancelChannelAndListener(); + + // Only load resource for the non-disabled track with media parent. + void MaybeDispatchLoadResource(); + + protected: + virtual ~HTMLTrackElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + void OnChannelRedirect(nsIChannel* aChannel, nsIChannel* aNewChannel, + uint32_t aFlags); + + friend class TextTrackCue; + friend class WebVTTListener; + + RefPtr<TextTrack> mTrack; + nsCOMPtr<nsIChannel> mChannel; + RefPtr<HTMLMediaElement> mMediaParent; + RefPtr<WebVTTListener> mListener; + + void CreateTextTrack(); + + private: + // Open a new channel to the HTMLTrackElement's src attribute and call + // mListener's LoadResource(). + void LoadResource(RefPtr<WebVTTListener>&& aWebVTTListener); + bool mLoadResourceDispatched; + + void MaybeClearAllCues(); + + RefPtr<WindowDestroyObserver> mWindowDestroyObserver; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_HTMLTrackElement_h diff --git a/dom/html/HTMLUnknownElement.cpp b/dom/html/HTMLUnknownElement.cpp new file mode 100644 index 0000000000..875ad514e2 --- /dev/null +++ b/dom/html/HTMLUnknownElement.cpp @@ -0,0 +1,26 @@ +/* -*- 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/Document.h" +#include "mozilla/dom/HTMLUnknownElement.h" +#include "mozilla/dom/HTMLElementBinding.h" +#include "jsapi.h" + +NS_IMPL_NS_NEW_HTML_ELEMENT(Unknown) + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS_INHERITED(HTMLUnknownElement, nsGenericHTMLElement, + HTMLUnknownElement) + +JSObject* HTMLUnknownElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLUnknownElement_Binding::Wrap(aCx, this, aGivenProto); +} + +NS_IMPL_ELEMENT_CLONE(HTMLUnknownElement) + +} // namespace mozilla::dom diff --git a/dom/html/HTMLUnknownElement.h b/dom/html/HTMLUnknownElement.h new file mode 100644 index 0000000000..3bed35a4f6 --- /dev/null +++ b/dom/html/HTMLUnknownElement.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_HTMLUnknownElement_h +#define mozilla_dom_HTMLUnknownElement_h + +#include "mozilla/Attributes.h" +#include "nsGenericHTMLElement.h" + +namespace mozilla::dom { + +#define NS_HTMLUNKNOWNELEMENT_IID \ + { \ + 0xc09e665b, 0x3876, 0x40dd, { \ + 0x85, 0x28, 0x44, 0xc2, 0x3f, 0xd4, 0x58, 0xf2 \ + } \ + } + +class HTMLUnknownElement final : public nsGenericHTMLElement { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_HTMLUNKNOWNELEMENT_IID) + + NS_DECL_ISUPPORTS_INHERITED + + explicit HTMLUnknownElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) {} + + virtual nsresult Clone(dom::NodeInfo*, nsINode** aResult) const override; + + protected: + virtual ~HTMLUnknownElement() = default; + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(HTMLUnknownElement, NS_HTMLUNKNOWNELEMENT_IID) + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HTMLUnknownElement_h */ diff --git a/dom/html/HTMLVideoElement.cpp b/dom/html/HTMLVideoElement.cpp new file mode 100644 index 0000000000..effb21706c --- /dev/null +++ b/dom/html/HTMLVideoElement.cpp @@ -0,0 +1,681 @@ +/* -*- 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/HTMLVideoElement.h" + +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/dom/HTMLVideoElementBinding.h" +#include "nsGenericHTMLElement.h" +#include "nsGkAtoms.h" +#include "nsSize.h" +#include "nsError.h" +#include "nsIHttpChannel.h" +#include "nsNodeInfoManager.h" +#include "plbase64.h" +#include "prlock.h" +#include "nsRFPService.h" +#include "nsThreadUtils.h" +#include "ImageContainer.h" +#include "VideoFrameContainer.h" +#include "VideoOutput.h" + +#include "FrameStatistics.h" +#include "MediaError.h" +#include "MediaDecoder.h" +#include "MediaDecoderStateMachine.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/WakeLock.h" +#include "mozilla/dom/power/PowerManagerService.h" +#include "mozilla/dom/Performance.h" +#include "mozilla/dom/TimeRanges.h" +#include "mozilla/dom/VideoPlaybackQuality.h" +#include "mozilla/dom/VideoStreamTrack.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/Unused.h" + +#include <algorithm> +#include <limits> + +extern mozilla::LazyLogModule gMediaElementLog; +#define LOG(msg, ...) \ + MOZ_LOG(gMediaElementLog, LogLevel::Debug, \ + ("HTMLVideoElement=%p, " msg, this, ##__VA_ARGS__)) + +nsGenericHTMLElement* NS_NewHTMLVideoElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); + auto* nim = nodeInfo->NodeInfoManager(); + mozilla::dom::HTMLVideoElement* element = + new (nim) mozilla::dom::HTMLVideoElement(nodeInfo.forget()); + element->Init(); + return element; +} + +namespace mozilla::dom { + +nsresult HTMLVideoElement::Clone(mozilla::dom::NodeInfo* aNodeInfo, + nsINode** aResult) const { + *aResult = nullptr; + RefPtr<mozilla::dom::NodeInfo> ni(aNodeInfo); + auto* nim = ni->NodeInfoManager(); + HTMLVideoElement* it = new (nim) HTMLVideoElement(ni.forget()); + it->Init(); + nsCOMPtr<nsINode> kungFuDeathGrip = it; + nsresult rv = const_cast<HTMLVideoElement*>(this)->CopyInnerTo(it); + if (NS_SUCCEEDED(rv)) { + kungFuDeathGrip.swap(*aResult); + } + return rv; +} + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(HTMLVideoElement, + HTMLMediaElement) + +NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLVideoElement) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HTMLVideoElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualCloneTarget) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualCloneTargetPromise) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mVisualCloneSource) + tmp->mSecondaryVideoOutput = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(HTMLMediaElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(HTMLVideoElement, + HTMLMediaElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualCloneTarget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualCloneTargetPromise) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVisualCloneSource) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +HTMLVideoElement::HTMLVideoElement(already_AddRefed<NodeInfo>&& aNodeInfo) + : HTMLMediaElement(std::move(aNodeInfo)), + mVideoWatchManager(this, AbstractThread::MainThread()) { + DecoderDoctorLogger::LogConstruction(this); +} + +HTMLVideoElement::~HTMLVideoElement() { + mVideoWatchManager.Shutdown(); + DecoderDoctorLogger::LogDestruction(this); +} + +void HTMLVideoElement::UpdateMediaSize(const nsIntSize& aSize) { + HTMLMediaElement::UpdateMediaSize(aSize); + // If we have a clone target, we should update its size as well. + if (mVisualCloneTarget) { + Maybe<nsIntSize> newSize = Some(aSize); + mVisualCloneTarget->Invalidate(ImageSizeChanged::Yes, newSize, + ForceInvalidate::Yes); + } +} + +Maybe<CSSIntSize> HTMLVideoElement::GetVideoSize() const { + if (!mMediaInfo.HasVideo()) { + return Nothing(); + } + + if (mDisableVideo) { + return Nothing(); + } + + CSSIntSize size; + switch (mMediaInfo.mVideo.mRotation) { + case VideoRotation::kDegree_90: + case VideoRotation::kDegree_270: { + size.width = mMediaInfo.mVideo.mDisplay.height; + size.height = mMediaInfo.mVideo.mDisplay.width; + break; + } + case VideoRotation::kDegree_0: + case VideoRotation::kDegree_180: + default: { + size.height = mMediaInfo.mVideo.mDisplay.height; + size.width = mMediaInfo.mVideo.mDisplay.width; + break; + } + } + return Some(size); +} + +void HTMLVideoElement::Invalidate(ImageSizeChanged aImageSizeChanged, + const Maybe<nsIntSize>& aNewIntrinsicSize, + ForceInvalidate aForceInvalidate) { + HTMLMediaElement::Invalidate(aImageSizeChanged, aNewIntrinsicSize, + aForceInvalidate); + if (mVisualCloneTarget) { + VideoFrameContainer* container = + mVisualCloneTarget->GetVideoFrameContainer(); + if (container) { + container->Invalidate(); + } + } +} + +bool HTMLVideoElement::ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height) { + return aResult.ParseHTMLDimension(aValue); + } + + return HTMLMediaElement::ParseAttribute(aNamespaceID, aAttribute, aValue, + aMaybeScriptedPrincipal, aResult); +} + +void HTMLVideoElement::MapAttributesIntoRule( + MappedDeclarationsBuilder& aBuilder) { + MapImageSizeAttributesInto(aBuilder, MapAspectRatio::Yes); + MapCommonAttributesInto(aBuilder); +} + +NS_IMETHODIMP_(bool) +HTMLVideoElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry attributes[] = { + {nsGkAtoms::width}, {nsGkAtoms::height}, {nullptr}}; + + static const MappedAttributeEntry* const map[] = {attributes, + sCommonAttributeMap}; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc HTMLVideoElement::GetAttributeMappingFunction() + const { + return &MapAttributesIntoRule; +} + +void HTMLVideoElement::UnbindFromTree(bool aNullParent) { + if (mVisualCloneSource) { + mVisualCloneSource->EndCloningVisually(); + } else if (mVisualCloneTarget) { + AsyncEventDispatcher::RunDOMEventWhenSafe( + *this, u"MozStopPictureInPicture"_ns, CanBubble::eNo, + ChromeOnlyDispatch::eYes); + EndCloningVisually(); + } + + HTMLMediaElement::UnbindFromTree(aNullParent); +} + +nsresult HTMLVideoElement::SetAcceptHeader(nsIHttpChannel* aChannel) { + nsAutoCString value( + "video/webm," + "video/ogg," + "video/*;q=0.9," + "application/ogg;q=0.7," + "audio/*;q=0.6,*/*;q=0.5"); + + return aChannel->SetRequestHeader("Accept"_ns, value, false); +} + +bool HTMLVideoElement::IsInteractiveHTMLContent() const { + return HasAttr(nsGkAtoms::controls) || + HTMLMediaElement::IsInteractiveHTMLContent(); +} + +gfx::IntSize HTMLVideoElement::GetVideoIntrinsicDimensions() { + const auto& sz = mMediaInfo.mVideo.mDisplay; + + // Prefer the size of the container as it's more up to date. + return ToMaybeRef(mVideoFrameContainer.get()) + .map([&](auto& aVFC) { return aVFC.CurrentIntrinsicSize().valueOr(sz); }) + .valueOr(sz); +} + +uint32_t HTMLVideoElement::VideoWidth() { + if (!HasVideo()) { + return 0; + } + gfx::IntSize size = GetVideoIntrinsicDimensions(); + if (mMediaInfo.mVideo.mRotation == VideoRotation::kDegree_90 || + mMediaInfo.mVideo.mRotation == VideoRotation::kDegree_270) { + return size.height; + } + return size.width; +} + +uint32_t HTMLVideoElement::VideoHeight() { + if (!HasVideo()) { + return 0; + } + gfx::IntSize size = GetVideoIntrinsicDimensions(); + if (mMediaInfo.mVideo.mRotation == VideoRotation::kDegree_90 || + mMediaInfo.mVideo.mRotation == VideoRotation::kDegree_270) { + return size.width; + } + return size.height; +} + +uint32_t HTMLVideoElement::MozParsedFrames() const { + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread."); + if (!IsVideoStatsEnabled()) { + return 0; + } + + if (OwnerDoc()->ShouldResistFingerprinting( + RFPTarget::VideoElementMozFrames)) { + return nsRFPService::GetSpoofedTotalFrames(TotalPlayTime()); + } + + return mDecoder ? mDecoder->GetFrameStatistics().GetParsedFrames() : 0; +} + +uint32_t HTMLVideoElement::MozDecodedFrames() const { + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread."); + if (!IsVideoStatsEnabled()) { + return 0; + } + + if (OwnerDoc()->ShouldResistFingerprinting( + RFPTarget::VideoElementMozFrames)) { + return nsRFPService::GetSpoofedTotalFrames(TotalPlayTime()); + } + + return mDecoder ? mDecoder->GetFrameStatistics().GetDecodedFrames() : 0; +} + +uint32_t HTMLVideoElement::MozPresentedFrames() { + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread."); + if (!IsVideoStatsEnabled()) { + return 0; + } + + if (OwnerDoc()->ShouldResistFingerprinting( + RFPTarget::VideoElementMozFrames)) { + return nsRFPService::GetSpoofedPresentedFrames(TotalPlayTime(), + VideoWidth(), VideoHeight()); + } + + return mDecoder ? mDecoder->GetFrameStatistics().GetPresentedFrames() : 0; +} + +uint32_t HTMLVideoElement::MozPaintedFrames() { + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread."); + if (!IsVideoStatsEnabled()) { + return 0; + } + + if (OwnerDoc()->ShouldResistFingerprinting( + RFPTarget::VideoElementMozFrames)) { + return nsRFPService::GetSpoofedPresentedFrames(TotalPlayTime(), + VideoWidth(), VideoHeight()); + } + + layers::ImageContainer* container = GetImageContainer(); + return container ? container->GetPaintCount() : 0; +} + +double HTMLVideoElement::MozFrameDelay() { + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread."); + + if (!IsVideoStatsEnabled() || OwnerDoc()->ShouldResistFingerprinting( + RFPTarget::VideoElementMozFrameDelay)) { + return 0.0; + } + + VideoFrameContainer* container = GetVideoFrameContainer(); + // Hide negative delays. Frame timing tweaks in the compositor (e.g. + // adding a bias value to prevent multiple dropped/duped frames when + // frame times are aligned with composition times) may produce apparent + // negative delay, but we shouldn't report that. + return container ? std::max(0.0, container->GetFrameDelay()) : 0.0; +} + +bool HTMLVideoElement::MozHasAudio() const { + MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread."); + return HasAudio(); +} + +JSObject* HTMLVideoElement::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLVideoElement_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed<VideoPlaybackQuality> +HTMLVideoElement::GetVideoPlaybackQuality() { + DOMHighResTimeStamp creationTime = 0; + uint32_t totalFrames = 0; + uint32_t droppedFrames = 0; + + if (IsVideoStatsEnabled()) { + if (nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow()) { + Performance* perf = window->GetPerformance(); + if (perf) { + creationTime = perf->Now(); + } + } + + if (mDecoder) { + if (OwnerDoc()->ShouldResistFingerprinting( + RFPTarget::VideoElementPlaybackQuality)) { + totalFrames = nsRFPService::GetSpoofedTotalFrames(TotalPlayTime()); + droppedFrames = nsRFPService::GetSpoofedDroppedFrames( + TotalPlayTime(), VideoWidth(), VideoHeight()); + } else { + FrameStatistics* stats = &mDecoder->GetFrameStatistics(); + if (sizeof(totalFrames) >= sizeof(stats->GetParsedFrames())) { + totalFrames = stats->GetTotalFrames(); + droppedFrames = stats->GetDroppedFrames(); + } else { + uint64_t total = stats->GetTotalFrames(); + const auto maxNumber = std::numeric_limits<uint32_t>::max(); + if (total <= maxNumber) { + totalFrames = uint32_t(total); + droppedFrames = uint32_t(stats->GetDroppedFrames()); + } else { + // Too big number(s) -> Resize everything to fit in 32 bits. + double ratio = double(maxNumber) / double(total); + totalFrames = maxNumber; // === total * ratio + droppedFrames = uint32_t(double(stats->GetDroppedFrames()) * ratio); + } + } + } + if (!StaticPrefs::media_video_dropped_frame_stats_enabled()) { + droppedFrames = 0; + } + } + } + + RefPtr<VideoPlaybackQuality> playbackQuality = + new VideoPlaybackQuality(this, creationTime, totalFrames, droppedFrames); + return playbackQuality.forget(); +} + +void HTMLVideoElement::WakeLockRelease() { + HTMLMediaElement::WakeLockRelease(); + ReleaseVideoWakeLockIfExists(); +} + +void HTMLVideoElement::UpdateWakeLock() { + HTMLMediaElement::UpdateWakeLock(); + if (!mPaused) { + CreateVideoWakeLockIfNeeded(); + } else { + ReleaseVideoWakeLockIfExists(); + } +} + +bool HTMLVideoElement::ShouldCreateVideoWakeLock() const { + if (!StaticPrefs::media_video_wakelock()) { + return false; + } + // Only request wake lock for video with audio or video from media + // stream, because non-stream video without audio is often used as a + // background image. + // + // Some web conferencing sites route audio outside the video element, + // and would not be detected unless we check for media stream, so do + // that below. + // + // Media streams generally aren't used as background images, though if + // they were we'd get false positives. If this is an issue, we could + // check for media stream AND document has audio playing (but that was + // tricky to do). + return HasVideo() && (mSrcStream || HasAudio()); +} + +void HTMLVideoElement::CreateVideoWakeLockIfNeeded() { + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return; + } + if (!mScreenWakeLock && ShouldCreateVideoWakeLock()) { + RefPtr<power::PowerManagerService> pmService = + power::PowerManagerService::GetInstance(); + NS_ENSURE_TRUE_VOID(pmService); + + ErrorResult rv; + mScreenWakeLock = pmService->NewWakeLock(u"video-playing"_ns, + OwnerDoc()->GetInnerWindow(), rv); + } +} + +void HTMLVideoElement::ReleaseVideoWakeLockIfExists() { + if (mScreenWakeLock) { + ErrorResult rv; + mScreenWakeLock->Unlock(rv); + rv.SuppressException(); + mScreenWakeLock = nullptr; + return; + } +} + +bool HTMLVideoElement::SetVisualCloneTarget( + RefPtr<HTMLVideoElement> aVisualCloneTarget, + RefPtr<Promise> aVisualCloneTargetPromise) { + MOZ_DIAGNOSTIC_ASSERT( + !aVisualCloneTarget || aVisualCloneTarget->IsInComposedDoc(), + "Can't set the clone target to a disconnected video " + "element."); + MOZ_DIAGNOSTIC_ASSERT(!mVisualCloneSource, + "Can't clone a video element that is already a clone."); + if (!aVisualCloneTarget || + (aVisualCloneTarget->IsInComposedDoc() && !mVisualCloneSource)) { + mVisualCloneTarget = std::move(aVisualCloneTarget); + mVisualCloneTargetPromise = std::move(aVisualCloneTargetPromise); + return true; + } + return false; +} + +bool HTMLVideoElement::SetVisualCloneSource( + RefPtr<HTMLVideoElement> aVisualCloneSource) { + MOZ_DIAGNOSTIC_ASSERT( + !aVisualCloneSource || aVisualCloneSource->IsInComposedDoc(), + "Can't set the clone source to a disconnected video " + "element."); + MOZ_DIAGNOSTIC_ASSERT(!mVisualCloneTarget, + "Can't clone a video element that is already a " + "clone."); + if (!aVisualCloneSource || + (aVisualCloneSource->IsInComposedDoc() && !mVisualCloneTarget)) { + mVisualCloneSource = std::move(aVisualCloneSource); + return true; + } + return false; +} + +/* static */ +bool HTMLVideoElement::IsVideoStatsEnabled() { + return StaticPrefs::media_video_stats_enabled(); +} + +double HTMLVideoElement::TotalPlayTime() const { + double total = 0.0; + + if (mPlayed) { + uint32_t timeRangeCount = mPlayed->Length(); + + for (uint32_t i = 0; i < timeRangeCount; i++) { + double begin = mPlayed->Start(i); + double end = mPlayed->End(i); + total += end - begin; + } + + if (mCurrentPlayRangeStart != -1.0) { + double now = CurrentTime(); + if (mCurrentPlayRangeStart != now) { + total += now - mCurrentPlayRangeStart; + } + } + } + + return total; +} + +already_AddRefed<Promise> HTMLVideoElement::CloneElementVisually( + HTMLVideoElement& aTargetVideo, ErrorResult& aRv) { + MOZ_ASSERT(IsInComposedDoc(), + "Can't clone a video that's not bound to a DOM tree."); + MOZ_ASSERT(aTargetVideo.IsInComposedDoc(), + "Can't clone to a video that's not bound to a DOM tree."); + if (!IsInComposedDoc() || !aTargetVideo.IsInComposedDoc()) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); + if (!win) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return nullptr; + } + + RefPtr<Promise> promise = Promise::Create(win->AsGlobal(), aRv); + if (aRv.Failed()) { + return nullptr; + } + + // Do we already have a visual clone target? If so, shut it down. + if (mVisualCloneTarget) { + EndCloningVisually(); + } + + // If there's a poster set on the target video, clear it, otherwise + // it'll display over top of the cloned frames. + aTargetVideo.UnsetHTMLAttr(nsGkAtoms::poster, aRv); + if (aRv.Failed()) { + return nullptr; + } + + if (!SetVisualCloneTarget(&aTargetVideo, promise)) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + if (!aTargetVideo.SetVisualCloneSource(this)) { + mVisualCloneTarget = nullptr; + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + aTargetVideo.SetMediaInfo(mMediaInfo); + + if (IsInComposedDoc() && !StaticPrefs::media_cloneElementVisually_testing()) { + NotifyUAWidgetSetupOrChange(); + } + + MaybeBeginCloningVisually(); + + return promise.forget(); +} + +void HTMLVideoElement::StopCloningElementVisually() { + if (mVisualCloneTarget) { + EndCloningVisually(); + } +} + +void HTMLVideoElement::MaybeBeginCloningVisually() { + if (!mVisualCloneTarget) { + return; + } + + if (mDecoder) { + mDecoder->SetSecondaryVideoContainer( + mVisualCloneTarget->GetVideoFrameContainer()); + NotifyDecoderActivityChanges(); + UpdateMediaControlAfterPictureInPictureModeChanged(); + } else if (mSrcStream) { + VideoFrameContainer* container = + mVisualCloneTarget->GetVideoFrameContainer(); + if (container) { + mSecondaryVideoOutput = MakeRefPtr<FirstFrameVideoOutput>( + container, AbstractThread::MainThread()); + mVideoWatchManager.Watch( + mSecondaryVideoOutput->mFirstFrameRendered, + &HTMLVideoElement::OnSecondaryVideoOutputFirstFrameRendered); + SetSecondaryMediaStreamRenderer(container, mSecondaryVideoOutput); + } + UpdateMediaControlAfterPictureInPictureModeChanged(); + } +} + +void HTMLVideoElement::EndCloningVisually() { + MOZ_ASSERT(mVisualCloneTarget); + + if (mDecoder) { + mDecoder->SetSecondaryVideoContainer(nullptr); + NotifyDecoderActivityChanges(); + } else if (mSrcStream) { + if (mSecondaryVideoOutput) { + mVideoWatchManager.Unwatch( + mSecondaryVideoOutput->mFirstFrameRendered, + &HTMLVideoElement::OnSecondaryVideoOutputFirstFrameRendered); + mSecondaryVideoOutput = nullptr; + } + SetSecondaryMediaStreamRenderer(nullptr); + } + + Unused << mVisualCloneTarget->SetVisualCloneSource(nullptr); + Unused << SetVisualCloneTarget(nullptr); + + UpdateMediaControlAfterPictureInPictureModeChanged(); + + if (IsInComposedDoc() && !StaticPrefs::media_cloneElementVisually_testing()) { + NotifyUAWidgetSetupOrChange(); + } +} + +void HTMLVideoElement::OnSecondaryVideoContainerInstalled( + const RefPtr<VideoFrameContainer>& aSecondaryContainer) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT_IF(mVisualCloneTargetPromise, mVisualCloneTarget); + if (!mVisualCloneTargetPromise) { + // Clone target was unset. + return; + } + + VideoFrameContainer* container = mVisualCloneTarget->GetVideoFrameContainer(); + if (NS_WARN_IF(container != aSecondaryContainer)) { + // Not the right container. + return; + } + + NS_DispatchToCurrentThread(NewRunnableMethod( + "Promise::MaybeResolveWithUndefined", mVisualCloneTargetPromise, + &Promise::MaybeResolveWithUndefined)); + mVisualCloneTargetPromise = nullptr; +} + +void HTMLVideoElement::OnSecondaryVideoOutputFirstFrameRendered() { + OnSecondaryVideoContainerInstalled( + mVisualCloneTarget->GetVideoFrameContainer()); +} + +void HTMLVideoElement::OnVisibilityChange(Visibility aNewVisibility) { + HTMLMediaElement::OnVisibilityChange(aNewVisibility); + + // See the alternative part after step 4, but we only pause/resume invisible + // autoplay for non-audible video, which is different from the spec. This + // behavior seems aiming to reduce the power consumption without interering + // users, and Chrome and Safari also chose to do that only for non-audible + // video, so we want to match them in order to reduce webcompat issue. + // https://html.spec.whatwg.org/multipage/media.html#ready-states:eligible-for-autoplay-2 + if (!HasAttr(nsGkAtoms::autoplay) || IsAudible()) { + return; + } + + if (aNewVisibility == Visibility::ApproximatelyVisible && mPaused && + IsEligibleForAutoplay() && AllowedToPlay()) { + LOG("resume invisible paused autoplay video"); + RunAutoplay(); + } + + // We need to consider the Pip window as well, which won't reflect in the + // visibility event. + if ((aNewVisibility == Visibility::ApproximatelyNonVisible && + !IsCloningElementVisually()) && + mCanAutoplayFlag) { + LOG("pause non-audible autoplay video when it's invisible"); + PauseInternal(); + mCanAutoplayFlag = true; + return; + } +} + +} // namespace mozilla::dom + +#undef LOG diff --git a/dom/html/HTMLVideoElement.h b/dom/html/HTMLVideoElement.h new file mode 100644 index 0000000000..eda62d759a --- /dev/null +++ b/dom/html/HTMLVideoElement.h @@ -0,0 +1,203 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HTMLVideoElement_h +#define mozilla_dom_HTMLVideoElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/StaticPrefs_media.h" +#include "Units.h" + +namespace mozilla { + +class FrameStatistics; + +namespace dom { + +class WakeLock; +class VideoPlaybackQuality; + +class HTMLVideoElement final : public HTMLMediaElement { + class SecondaryVideoOutput; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLVideoElement, HTMLMediaElement) + + typedef mozilla::dom::NodeInfo NodeInfo; + + explicit HTMLVideoElement(already_AddRefed<NodeInfo>&& aNodeInfo); + + NS_IMPL_FROMNODE_HTML_WITH_TAG(HTMLVideoElement, video) + + using HTMLMediaElement::GetPaused; + + void Invalidate(ImageSizeChanged aImageSizeChanged, + const Maybe<nsIntSize>& aNewIntrinsicSize, + ForceInvalidate aForceInvalidate) override; + + virtual bool IsVideo() const override { return true; } + + virtual bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + + nsresult Clone(NodeInfo*, nsINode** aResult) const override; + + void UnbindFromTree(bool aNullParent = true) override; + + mozilla::Maybe<mozilla::CSSIntSize> GetVideoSize() const; + + void UpdateMediaSize(const nsIntSize& aSize) override; + + nsresult SetAcceptHeader(nsIHttpChannel* aChannel) override; + + // Element + bool IsInteractiveHTMLContent() const override; + + // WebIDL + + uint32_t Width() const { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::width, 0); + } + + void SetWidth(uint32_t aValue, ErrorResult& aRv) { + SetUnsignedIntAttr(nsGkAtoms::width, aValue, 0, aRv); + } + + uint32_t Height() const { + return GetDimensionAttrAsUnsignedInt(nsGkAtoms::height, 0); + } + + void SetHeight(uint32_t aValue, ErrorResult& aRv) { + SetUnsignedIntAttr(nsGkAtoms::height, aValue, 0, aRv); + } + + uint32_t VideoWidth(); + + uint32_t VideoHeight(); + + VideoRotation RotationDegrees() const { return mMediaInfo.mVideo.mRotation; } + + bool HasAlpha() const { return mMediaInfo.mVideo.HasAlpha(); } + + void GetPoster(nsAString& aValue) { + GetURIAttr(nsGkAtoms::poster, nullptr, aValue); + } + void SetPoster(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::poster, aValue, aRv); + } + + uint32_t MozParsedFrames() const; + + uint32_t MozDecodedFrames() const; + + uint32_t MozPresentedFrames(); + + uint32_t MozPaintedFrames(); + + double MozFrameDelay(); + + bool MozHasAudio() const; + + already_AddRefed<VideoPlaybackQuality> GetVideoPlaybackQuality(); + + already_AddRefed<Promise> CloneElementVisually(HTMLVideoElement& aTarget, + ErrorResult& rv); + + void StopCloningElementVisually(); + + bool IsCloningElementVisually() const { return !!mVisualCloneTarget; } + + void OnSecondaryVideoContainerInstalled( + const RefPtr<VideoFrameContainer>& aSecondaryContainer) override; + + void OnSecondaryVideoOutputFirstFrameRendered(); + + void OnVisibilityChange(Visibility aNewVisibility) override; + + bool DisablePictureInPicture() const { + return GetBoolAttr(nsGkAtoms::disablepictureinpicture); + } + + void SetDisablePictureInPicture(bool aValue, ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::disablepictureinpicture, aValue, aError); + } + + protected: + virtual ~HTMLVideoElement(); + + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + /** + * We create video wakelock when the video is playing and release it when + * video pauses. Note, the actual platform wakelock will automatically be + * released when the page is in the background, so we don't need to check the + * video's visibility by ourselves. + */ + void WakeLockRelease() override; + void UpdateWakeLock() override; + + bool ShouldCreateVideoWakeLock() const; + void CreateVideoWakeLockIfNeeded(); + void ReleaseVideoWakeLockIfExists(); + + gfx::IntSize GetVideoIntrinsicDimensions(); + + RefPtr<WakeLock> mScreenWakeLock; + + WatchManager<HTMLVideoElement> mVideoWatchManager; + + private: + bool SetVisualCloneTarget( + RefPtr<HTMLVideoElement> aVisualCloneTarget, + RefPtr<Promise> aVisualCloneTargetPromise = nullptr); + bool SetVisualCloneSource(RefPtr<HTMLVideoElement> aVisualCloneSource); + + // For video elements, we can clone the frames being played to + // a secondary video element. If we're doing that, we hold a + // reference to the video element we're cloning to in + // mVisualCloneSource. + // + // Please don't set this to non-nullptr values directly - use + // SetVisualCloneTarget() instead. + RefPtr<HTMLVideoElement> mVisualCloneTarget; + // Set when mVisualCloneTarget is set, and resolved (and unset) when the + // secondary container has been applied to the underlying resource. + RefPtr<Promise> mVisualCloneTargetPromise; + // Set when beginning to clone visually and we are playing a MediaStream. + // This is the output wrapping the VideoFrameContainer of mVisualCloneTarget, + // so we can render its first frame, and resolve mVisualCloneTargetPromise as + // we do. + RefPtr<FirstFrameVideoOutput> mSecondaryVideoOutput; + // If this video is the clone target of another video element, + // then mVisualCloneSource points to that originating video + // element. + // + // Please don't set this to non-nullptr values directly - use + // SetVisualCloneTarget() instead. + RefPtr<HTMLVideoElement> mVisualCloneSource; + + static void MapAttributesIntoRule(MappedDeclarationsBuilder&); + + static bool IsVideoStatsEnabled(); + double TotalPlayTime() const; + + virtual void MaybeBeginCloningVisually() override; + void EndCloningVisually(); +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_HTMLVideoElement_h diff --git a/dom/html/ImageDocument.cpp b/dom/html/ImageDocument.cpp new file mode 100644 index 0000000000..2e037284a6 --- /dev/null +++ b/dom/html/ImageDocument.cpp @@ -0,0 +1,795 @@ +/* -*- 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 "ImageDocument.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/ComputedStyle.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/ImageDocumentBinding.h" +#include "mozilla/dom/HTMLImageElement.h" +#include "mozilla/dom/MouseEvent.h" +#include "mozilla/LoadInfo.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_browser.h" +#include "nsICSSDeclaration.h" +#include "nsObjectLoadingContent.h" +#include "nsRect.h" +#include "nsIImageLoadingContent.h" +#include "nsGenericHTMLElement.h" +#include "nsDocShell.h" +#include "DocumentInlines.h" +#include "ImageBlocker.h" +#include "nsDOMTokenList.h" +#include "nsIDOMEventListener.h" +#include "nsIFrame.h" +#include "nsGkAtoms.h" +#include "imgIRequest.h" +#include "imgIContainer.h" +#include "imgINotificationObserver.h" +#include "nsPresContext.h" +#include "nsIChannel.h" +#include "nsIContentPolicy.h" +#include "nsContentPolicyUtils.h" +#include "nsPIDOMWindow.h" +#include "nsError.h" +#include "nsURILoader.h" +#include "nsIDocShell.h" +#include "nsIDocumentViewer.h" +#include "nsThreadUtils.h" +#include "nsIScrollableFrame.h" +#include "nsContentUtils.h" +#include "mozilla/Preferences.h" +#include <algorithm> + +namespace mozilla::dom { + +class ImageListener : public MediaDocumentStreamListener { + public: + // NS_DECL_NSIREQUESTOBSERVER + // We only implement OnStartRequest; OnStopRequest is + // implemented by MediaDocumentStreamListener + NS_IMETHOD OnStartRequest(nsIRequest* aRequest) override; + + explicit ImageListener(ImageDocument* aDocument); + virtual ~ImageListener(); +}; + +ImageListener::ImageListener(ImageDocument* aDocument) + : MediaDocumentStreamListener(aDocument) {} + +ImageListener::~ImageListener() = default; + +NS_IMETHODIMP +ImageListener::OnStartRequest(nsIRequest* request) { + NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE); + + ImageDocument* imgDoc = static_cast<ImageDocument*>(mDocument.get()); + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request); + if (!channel) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsPIDOMWindowOuter> domWindow = imgDoc->GetWindow(); + NS_ENSURE_TRUE(domWindow, NS_ERROR_UNEXPECTED); + + // This is an image being loaded as a document, so it's not going to be + // detected by the ImageBlocker. However we don't want to call + // NS_CheckContentLoadPolicy (with an TYPE_INTERNAL_IMAGE) here, as it would + // e.g. make this image load be detectable by CSP. + nsCOMPtr<nsIURI> channelURI; + channel->GetURI(getter_AddRefs(channelURI)); + if (image::ImageBlocker::ShouldBlock(channelURI)) { + request->Cancel(NS_ERROR_CONTENT_BLOCKED); + return NS_OK; + } + + if (!imgDoc->mObservingImageLoader) { + NS_ENSURE_TRUE(imgDoc->mImageContent, NS_ERROR_UNEXPECTED); + imgDoc->mImageContent->AddNativeObserver(imgDoc); + imgDoc->mObservingImageLoader = true; + imgDoc->mImageContent->LoadImageWithChannel(channel, + getter_AddRefs(mNextStream)); + } + + return MediaDocumentStreamListener::OnStartRequest(request); +} + +ImageDocument::ImageDocument() + : mVisibleWidth(0.0), + mVisibleHeight(0.0), + mImageWidth(0), + mImageHeight(0), + mImageIsResized(false), + mShouldResize(false), + mFirstResize(false), + mObservingImageLoader(false), + mTitleUpdateInProgress(false), + mHasCustomTitle(false), + mIsInObjectOrEmbed(false), + mOriginalZoomLevel(1.0), + mOriginalResolution(1.0) {} + +ImageDocument::~ImageDocument() = default; + +NS_IMPL_CYCLE_COLLECTION_INHERITED(ImageDocument, MediaDocument, mImageContent) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(ImageDocument, MediaDocument, + imgINotificationObserver, + nsIDOMEventListener) + +nsresult ImageDocument::Init(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) { + nsresult rv = MediaDocument::Init(aPrincipal, aPartitionedPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + mShouldResize = StaticPrefs::browser_enable_automatic_image_resizing(); + mFirstResize = true; + + return NS_OK; +} + +JSObject* ImageDocument::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ImageDocument_Binding::Wrap(aCx, this, aGivenProto); +} + +nsresult ImageDocument::StartDocumentLoad( + const char* aCommand, nsIChannel* aChannel, nsILoadGroup* aLoadGroup, + nsISupports* aContainer, nsIStreamListener** aDocListener, bool aReset) { + nsresult rv = MediaDocument::StartDocumentLoad( + aCommand, aChannel, aLoadGroup, aContainer, aDocListener, aReset); + if (NS_FAILED(rv)) { + return rv; + } + + mOriginalZoomLevel = IsSiteSpecific() ? 1.0 : GetZoomLevel(); + CheckFullZoom(); + mOriginalResolution = GetResolution(); + + if (BrowsingContext* context = GetBrowsingContext()) { + mIsInObjectOrEmbed = context->IsEmbedderTypeObjectOrEmbed(); + } + + NS_ASSERTION(aDocListener, "null aDocListener"); + *aDocListener = new ImageListener(this); + NS_ADDREF(*aDocListener); + + return NS_OK; +} + +void ImageDocument::Destroy() { + if (RefPtr<HTMLImageElement> img = std::move(mImageContent)) { + // Remove our event listener from the image content. + img->RemoveEventListener(u"load"_ns, this, false); + img->RemoveEventListener(u"click"_ns, this, false); + + // Break reference cycle with mImageContent, if we have one + if (mObservingImageLoader) { + img->RemoveNativeObserver(this); + } + } + + MediaDocument::Destroy(); +} + +void ImageDocument::SetScriptGlobalObject( + nsIScriptGlobalObject* aScriptGlobalObject) { + // If the script global object is changing, we need to unhook our event + // listeners on the window. + nsCOMPtr<EventTarget> target; + if (mScriptGlobalObject && aScriptGlobalObject != mScriptGlobalObject) { + target = do_QueryInterface(mScriptGlobalObject); + target->RemoveEventListener(u"resize"_ns, this, false); + target->RemoveEventListener(u"keypress"_ns, this, false); + } + + // Set the script global object on the superclass before doing + // anything that might require it.... + MediaDocument::SetScriptGlobalObject(aScriptGlobalObject); + + if (aScriptGlobalObject) { + if (!InitialSetupHasBeenDone()) { + MOZ_ASSERT(!GetRootElement(), "Where did the root element come from?"); + // Create synthetic document +#ifdef DEBUG + nsresult rv = +#endif + CreateSyntheticDocument(); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create synthetic document"); + + target = mImageContent; + target->AddEventListener(u"load"_ns, this, false); + target->AddEventListener(u"click"_ns, this, false); + } + + target = do_QueryInterface(aScriptGlobalObject); + target->AddEventListener(u"resize"_ns, this, false); + target->AddEventListener(u"keypress"_ns, this, false); + + if (!InitialSetupHasBeenDone()) { + LinkStylesheet(u"resource://content-accessible/ImageDocument.css"_ns); + if (!nsContentUtils::IsChildOfSameType(this)) { + LinkStylesheet(nsLiteralString( + u"resource://content-accessible/TopLevelImageDocument.css")); + } + InitialSetupDone(); + } + } +} + +void ImageDocument::OnPageShow(bool aPersisted, + EventTarget* aDispatchStartTarget, + bool aOnlySystemGroup) { + if (aPersisted) { + mOriginalZoomLevel = IsSiteSpecific() ? 1.0 : GetZoomLevel(); + CheckFullZoom(); + mOriginalResolution = GetResolution(); + } + RefPtr<ImageDocument> kungFuDeathGrip(this); + UpdateSizeFromLayout(); + + MediaDocument::OnPageShow(aPersisted, aDispatchStartTarget, aOnlySystemGroup); +} + +void ImageDocument::ShrinkToFit() { + if (!mImageContent) { + return; + } + if (GetZoomLevel() != mOriginalZoomLevel && mImageIsResized && + !nsContentUtils::IsChildOfSameType(this)) { + // If we're zoomed, so that we don't maintain the invariant that + // mImageIsResized if and only if its displayed width/height fit in + // mVisibleWidth/mVisibleHeight, then we may need to switch to/from the + // overflowingVertical class here, because our viewport size may have + // changed and we don't plan to adjust the image size to compensate. Since + // mImageIsResized it has a "height" attribute set, and we can just get the + // displayed image height by getting .height on the HTMLImageElement. + // + // Hold strong ref, because Height() can run script. + RefPtr<HTMLImageElement> img = mImageContent; + uint32_t imageHeight = img->Height(); + nsDOMTokenList* classList = img->ClassList(); + if (imageHeight > mVisibleHeight) { + classList->Add(u"overflowingVertical"_ns, IgnoreErrors()); + } else { + classList->Remove(u"overflowingVertical"_ns, IgnoreErrors()); + } + return; + } + if (GetResolution() != mOriginalResolution && mImageIsResized) { + // Don't resize if resolution has changed, e.g., through pinch-zooming on + // Android. + return; + } + + // Keep image content alive while changing the attributes. + RefPtr<HTMLImageElement> image = mImageContent; + + uint32_t newWidth = std::max(1, NSToCoordFloor(GetRatio() * mImageWidth)); + uint32_t newHeight = std::max(1, NSToCoordFloor(GetRatio() * mImageHeight)); + image->SetWidth(newWidth, IgnoreErrors()); + image->SetHeight(newHeight, IgnoreErrors()); + + // The view might have been scrolled when zooming in, scroll back to the + // origin now that we're showing a shrunk-to-window version. + ScrollImageTo(0, 0); + + if (!mImageContent) { + // ScrollImageTo flush destroyed our content. + return; + } + + SetModeClass(eShrinkToFit); + + mImageIsResized = true; + + UpdateTitleAndCharset(); +} + +void ImageDocument::ScrollImageTo(int32_t aX, int32_t aY) { + RefPtr<PresShell> presShell = GetPresShell(); + if (!presShell) { + return; + } + + nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable(); + if (!sf) { + return; + } + + float ratio = GetRatio(); + // Don't try to scroll image if the document is not visible (mVisibleWidth or + // mVisibleHeight is zero). + if (ratio <= 0.0) { + return; + } + nsRect portRect = sf->GetScrollPortRect(); + sf->ScrollTo( + nsPoint( + nsPresContext::CSSPixelsToAppUnits(aX / ratio) - portRect.width / 2, + nsPresContext::CSSPixelsToAppUnits(aY / ratio) - portRect.height / 2), + ScrollMode::Instant); +} + +void ImageDocument::RestoreImage() { + if (!mImageContent) { + return; + } + // Keep image content alive while changing the attributes. + RefPtr<HTMLImageElement> imageContent = mImageContent; + imageContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::width, true); + imageContent->UnsetAttr(kNameSpaceID_None, nsGkAtoms::height, true); + + if (mIsInObjectOrEmbed) { + SetModeClass(eIsInObjectOrEmbed); + } else if (ImageIsOverflowing()) { + if (!ImageIsOverflowingVertically()) { + SetModeClass(eOverflowingHorizontalOnly); + } else { + SetModeClass(eOverflowingVertical); + } + } else { + SetModeClass(eNone); + } + + mImageIsResized = false; + + UpdateTitleAndCharset(); +} + +void ImageDocument::NotifyPossibleTitleChange(bool aBoundTitleElement) { + if (!mHasCustomTitle && !mTitleUpdateInProgress) { + mHasCustomTitle = true; + } + + Document::NotifyPossibleTitleChange(aBoundTitleElement); +} + +void ImageDocument::Notify(imgIRequest* aRequest, int32_t aType, + const nsIntRect* aData) { + if (aType == imgINotificationObserver::SIZE_AVAILABLE) { + nsCOMPtr<imgIContainer> image; + aRequest->GetImage(getter_AddRefs(image)); + return OnSizeAvailable(aRequest, image); + } + + // Run this using a script runner because HAS_TRANSPARENCY notifications can + // come during painting and this will trigger invalidation. + if (aType == imgINotificationObserver::HAS_TRANSPARENCY) { + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod("dom::ImageDocument::OnHasTransparency", this, + &ImageDocument::OnHasTransparency); + nsContentUtils::AddScriptRunner(runnable); + } + + if (aType == imgINotificationObserver::LOAD_COMPLETE) { + uint32_t reqStatus; + aRequest->GetImageStatus(&reqStatus); + nsresult status = + reqStatus & imgIRequest::STATUS_ERROR ? NS_ERROR_FAILURE : NS_OK; + return OnLoadComplete(aRequest, status); + } +} + +void ImageDocument::OnHasTransparency() { + if (!mImageContent || nsContentUtils::IsChildOfSameType(this)) { + return; + } + + nsDOMTokenList* classList = mImageContent->ClassList(); + classList->Add(u"transparent"_ns, IgnoreErrors()); +} + +void ImageDocument::SetModeClass(eModeClasses mode) { + nsDOMTokenList* classList = mImageContent->ClassList(); + + if (mode == eShrinkToFit) { + classList->Add(u"shrinkToFit"_ns, IgnoreErrors()); + } else { + classList->Remove(u"shrinkToFit"_ns, IgnoreErrors()); + } + + if (mode == eOverflowingVertical) { + classList->Add(u"overflowingVertical"_ns, IgnoreErrors()); + } else { + classList->Remove(u"overflowingVertical"_ns, IgnoreErrors()); + } + + if (mode == eOverflowingHorizontalOnly) { + classList->Add(u"overflowingHorizontalOnly"_ns, IgnoreErrors()); + } else { + classList->Remove(u"overflowingHorizontalOnly"_ns, IgnoreErrors()); + } + + if (mode == eIsInObjectOrEmbed) { + classList->Add(u"isInObjectOrEmbed"_ns, IgnoreErrors()); + } +} + +void ImageDocument::OnSizeAvailable(imgIRequest* aRequest, + imgIContainer* aImage) { + int32_t oldWidth = mImageWidth; + int32_t oldHeight = mImageHeight; + + // Styles have not yet been applied, so we don't know the final size. For now, + // default to the image's intrinsic size. + aImage->GetWidth(&mImageWidth); + aImage->GetHeight(&mImageHeight); + + // Multipart images send size available for each part; ignore them if it + // doesn't change our size. (We may not even support changing size in + // multipart images in the future.) + if (oldWidth == mImageWidth && oldHeight == mImageHeight) { + return; + } + + nsCOMPtr<nsIRunnable> runnable = + NewRunnableMethod("dom::ImageDocument::DefaultCheckOverflowing", this, + &ImageDocument::DefaultCheckOverflowing); + nsContentUtils::AddScriptRunner(runnable); + UpdateTitleAndCharset(); +} + +void ImageDocument::OnLoadComplete(imgIRequest* aRequest, nsresult aStatus) { + UpdateTitleAndCharset(); + + // mImageContent can be null if the document is already destroyed + if (NS_FAILED(aStatus) && mImageContent) { + nsAutoCString src; + mDocumentURI->GetSpec(src); + AutoTArray<nsString, 1> formatString; + CopyUTF8toUTF16(src, *formatString.AppendElement()); + nsAutoString errorMsg; + FormatStringFromName("InvalidImage", formatString, errorMsg); + + mImageContent->SetAttr(kNameSpaceID_None, nsGkAtoms::alt, errorMsg, false); + } + + MaybeSendResultToEmbedder(aStatus); +} + +NS_IMETHODIMP +ImageDocument::HandleEvent(Event* aEvent) { + nsAutoString eventType; + aEvent->GetType(eventType); + if (eventType.EqualsLiteral("resize")) { + CheckOverflowing(false); + CheckFullZoom(); + } else if (eventType.EqualsLiteral("click") && + StaticPrefs::browser_enable_click_image_resizing() && + !mIsInObjectOrEmbed) { + ResetZoomLevel(); + mShouldResize = true; + if (mImageIsResized) { + int32_t x = 0, y = 0; + MouseEvent* event = aEvent->AsMouseEvent(); + if (event) { + RefPtr<HTMLImageElement> img = mImageContent; + x = event->ClientX() - img->OffsetLeft(); + y = event->ClientY() - img->OffsetTop(); + } + mShouldResize = false; + RestoreImage(); + FlushPendingNotifications(FlushType::Layout); + ScrollImageTo(x, y); + } else if (ImageIsOverflowing()) { + ShrinkToFit(); + } + } else if (eventType.EqualsLiteral("load")) { + UpdateSizeFromLayout(); + } + + return NS_OK; +} + +void ImageDocument::UpdateSizeFromLayout() { + // Pull an updated size from the content frame to account for any size + // change due to CSS properties like |image-orientation|. + if (!mImageContent) { + return; + } + + // Need strong ref, because GetPrimaryFrame can run script. + RefPtr<HTMLImageElement> imageContent = mImageContent; + nsIFrame* contentFrame = imageContent->GetPrimaryFrame(FlushType::Frames); + if (!contentFrame) { + return; + } + + nsIntSize oldSize(mImageWidth, mImageHeight); + IntrinsicSize newSize = contentFrame->GetIntrinsicSize(); + + if (newSize.width) { + mImageWidth = nsPresContext::AppUnitsToFloatCSSPixels(*newSize.width); + } + if (newSize.height) { + mImageHeight = nsPresContext::AppUnitsToFloatCSSPixels(*newSize.height); + } + + // Ensure that our information about overflow is up-to-date if needed. + if (mImageWidth != oldSize.width || mImageHeight != oldSize.height) { + CheckOverflowing(false); + } +} + +void ImageDocument::UpdateRemoteStyle(StyleImageRendering aImageRendering) { + if (!mImageContent) { + return; + } + + // Using ScriptRunner to avoid doing DOM mutation at an unexpected time. + if (!nsContentUtils::IsSafeToRunScript()) { + return nsContentUtils::AddScriptRunner( + NewRunnableMethod<StyleImageRendering>( + "UpdateRemoteStyle", this, &ImageDocument::UpdateRemoteStyle, + aImageRendering)); + } + + nsCOMPtr<nsICSSDeclaration> style = mImageContent->Style(); + switch (aImageRendering) { + case StyleImageRendering::Auto: + case StyleImageRendering::Smooth: + case StyleImageRendering::Optimizequality: + style->SetProperty("image-rendering"_ns, "auto"_ns, ""_ns, + IgnoreErrors()); + break; + case StyleImageRendering::Optimizespeed: + case StyleImageRendering::Pixelated: + style->SetProperty("image-rendering"_ns, "pixelated"_ns, ""_ns, + IgnoreErrors()); + break; + case StyleImageRendering::CrispEdges: + style->SetProperty("image-rendering"_ns, "crisp-edges"_ns, ""_ns, + IgnoreErrors()); + break; + } +} + +nsresult ImageDocument::CreateSyntheticDocument() { + // Synthesize an html document that refers to the image + nsresult rv = MediaDocument::CreateSyntheticDocument(); + NS_ENSURE_SUCCESS(rv, rv); + + // Add the image element + RefPtr<Element> body = GetBodyElement(); + if (!body) { + NS_WARNING("no body on image document!"); + return NS_ERROR_FAILURE; + } + + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::img, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + RefPtr<Element> image = NS_NewHTMLImageElement(nodeInfo.forget()); + mImageContent = HTMLImageElement::FromNodeOrNull(image); + if (!mImageContent) { + return NS_ERROR_OUT_OF_MEMORY; + } + + nsAutoCString src; + mDocumentURI->GetSpec(src); + + NS_ConvertUTF8toUTF16 srcString(src); + // Make sure not to start the image load from here... + mImageContent->SetLoadingEnabled(false); + mImageContent->SetAttr(kNameSpaceID_None, nsGkAtoms::src, srcString, false); + mImageContent->SetAttr(kNameSpaceID_None, nsGkAtoms::alt, srcString, false); + + if (mIsInObjectOrEmbed) { + SetModeClass(eIsInObjectOrEmbed); + } + + body->AppendChildTo(mImageContent, false, IgnoreErrors()); + mImageContent->SetLoadingEnabled(true); + + return NS_OK; +} + +void ImageDocument::DefaultCheckOverflowing() { + CheckOverflowing(StaticPrefs::browser_enable_automatic_image_resizing()); +} + +nsresult ImageDocument::CheckOverflowing(bool changeState) { + const bool imageWasOverflowing = ImageIsOverflowing(); + const bool imageWasOverflowingVertically = ImageIsOverflowingVertically(); + + { + nsPresContext* context = GetPresContext(); + if (!context) { + return NS_OK; + } + + nsRect visibleArea = context->GetVisibleArea(); + + mVisibleWidth = nsPresContext::AppUnitsToFloatCSSPixels(visibleArea.width); + mVisibleHeight = + nsPresContext::AppUnitsToFloatCSSPixels(visibleArea.height); + } + + const bool windowBecameBigEnough = + imageWasOverflowing && !ImageIsOverflowing(); + const bool verticalOverflowChanged = + imageWasOverflowingVertically != ImageIsOverflowingVertically(); + + if (changeState || mShouldResize || mFirstResize || windowBecameBigEnough || + verticalOverflowChanged) { + if (mIsInObjectOrEmbed) { + SetModeClass(eIsInObjectOrEmbed); + } else if (ImageIsOverflowing() && (changeState || mShouldResize)) { + ShrinkToFit(); + } else if (mImageIsResized || mFirstResize || windowBecameBigEnough) { + RestoreImage(); + } else if (!mImageIsResized && verticalOverflowChanged) { + if (ImageIsOverflowingVertically()) { + SetModeClass(eOverflowingVertical); + } else { + SetModeClass(eOverflowingHorizontalOnly); + } + } + } + mFirstResize = false; + return NS_OK; +} + +void ImageDocument::UpdateTitleAndCharset() { + if (mHasCustomTitle) { + return; + } + + AutoRestore<bool> restore(mTitleUpdateInProgress); + mTitleUpdateInProgress = true; + + nsAutoCString typeStr; + nsCOMPtr<imgIRequest> imageRequest; + if (mImageContent) { + mImageContent->GetRequest(nsIImageLoadingContent::CURRENT_REQUEST, + getter_AddRefs(imageRequest)); + } + + if (imageRequest) { + nsCString mimeType; + imageRequest->GetMimeType(getter_Copies(mimeType)); + ToUpperCase(mimeType); + nsCString::const_iterator start, end; + mimeType.BeginReading(start); + mimeType.EndReading(end); + nsCString::const_iterator iter = end; + if (FindInReadable("IMAGE/"_ns, start, iter) && iter != end) { + // strip out "X-" if any + if (*iter == 'X') { + ++iter; + if (iter != end && *iter == '-') { + ++iter; + if (iter == end) { + // looks like "IMAGE/X-" is the type?? Bail out of here. + mimeType.BeginReading(iter); + } + } else { + --iter; + } + } + typeStr = Substring(iter, end); + } else { + typeStr = mimeType; + } + } + + nsAutoString status; + if (mImageIsResized) { + AutoTArray<nsString, 1> formatString; + formatString.AppendElement()->AppendInt(NSToCoordFloor(GetRatio() * 100)); + + FormatStringFromName("ScaledImage", formatString, status); + } + + static const char* const formatNames[4] = { + "ImageTitleWithNeitherDimensionsNorFile", + "ImageTitleWithoutDimensions", + "ImageTitleWithDimensions2", + "ImageTitleWithDimensions2AndFile", + }; + + MediaDocument::UpdateTitleAndCharset(typeStr, mChannel, formatNames, + mImageWidth, mImageHeight, status); +} + +bool ImageDocument::IsSiteSpecific() { + return !ShouldResistFingerprinting(RFPTarget::SiteSpecificZoom) && + StaticPrefs::browser_zoom_siteSpecific(); +} + +void ImageDocument::ResetZoomLevel() { + if (nsContentUtils::IsChildOfSameType(this)) { + return; + } + + if (RefPtr<BrowsingContext> bc = GetBrowsingContext()) { + // Resetting the zoom level on a discarded browsing context has no effect. + Unused << bc->SetFullZoom(mOriginalZoomLevel); + } +} + +float ImageDocument::GetZoomLevel() { + if (BrowsingContext* bc = GetBrowsingContext()) { + return bc->FullZoom(); + } + return mOriginalZoomLevel; +} + +void ImageDocument::CheckFullZoom() { + nsDOMTokenList* classList = + mImageContent ? mImageContent->ClassList() : nullptr; + + if (!classList) { + return; + } + + classList->Toggle(u"fullZoomOut"_ns, + dom::Optional<bool>(GetZoomLevel() > mOriginalZoomLevel), + IgnoreErrors()); + classList->Toggle(u"fullZoomIn"_ns, + dom::Optional<bool>(GetZoomLevel() < mOriginalZoomLevel), + IgnoreErrors()); +} + +float ImageDocument::GetResolution() { + if (PresShell* presShell = GetPresShell()) { + return presShell->GetResolution(); + } + return mOriginalResolution; +} + +void ImageDocument::MaybeSendResultToEmbedder(nsresult aResult) { + if (!mIsInObjectOrEmbed) { + return; + } + + BrowsingContext* context = GetBrowsingContext(); + + if (!context) { + return; + } + + if (context->GetParent() && context->GetParent()->IsInProcess()) { + if (Element* embedder = context->GetEmbedderElement()) { + if (nsCOMPtr<nsIObjectLoadingContent> objectLoadingContent = + do_QueryInterface(embedder)) { + NS_DispatchToMainThread(NS_NewRunnableFunction( + "nsObjectLoadingContent::SubdocumentImageLoadComplete", + [objectLoadingContent, aResult]() { + static_cast<nsObjectLoadingContent*>(objectLoadingContent.get()) + ->SubdocumentImageLoadComplete(aResult); + })); + } + return; + } + } + + if (BrowserChild* browserChild = + BrowserChild::GetFrom(context->GetDocShell())) { + browserChild->SendImageLoadComplete(aResult); + } +} +} // namespace mozilla::dom + +nsresult NS_NewImageDocument(mozilla::dom::Document** aResult, + nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) { + auto* doc = new mozilla::dom::ImageDocument(); + NS_ADDREF(doc); + + nsresult rv = doc->Init(aPrincipal, aPartitionedPrincipal); + if (NS_FAILED(rv)) { + NS_RELEASE(doc); + } + + *aResult = doc; + + return rv; +} diff --git a/dom/html/ImageDocument.h b/dom/html/ImageDocument.h new file mode 100644 index 0000000000..891658264c --- /dev/null +++ b/dom/html/ImageDocument.h @@ -0,0 +1,160 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef mozilla_dom_ImageDocument_h +#define mozilla_dom_ImageDocument_h + +#include "mozilla/Attributes.h" +#include "imgINotificationObserver.h" +#include "mozilla/dom/MediaDocument.h" +#include "nsIDOMEventListener.h" + +namespace mozilla { +enum class StyleImageRendering : uint8_t; +struct IntrinsicSize; +} // namespace mozilla + +namespace mozilla::dom { +class HTMLImageElement; + +class ImageDocument final : public MediaDocument, + public imgINotificationObserver, + public nsIDOMEventListener { + public: + ImageDocument(); + + NS_DECL_ISUPPORTS_INHERITED + + enum MediaDocumentKind MediaDocumentKind() const override { + return MediaDocumentKind::Image; + } + + nsresult Init(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) override; + + nsresult StartDocumentLoad(const char* aCommand, nsIChannel* aChannel, + nsILoadGroup* aLoadGroup, nsISupports* aContainer, + nsIStreamListener** aDocListener, + bool aReset = true) override; + + void SetScriptGlobalObject(nsIScriptGlobalObject*) override; + void Destroy() override; + void OnPageShow(bool aPersisted, EventTarget* aDispatchStartTarget, + bool aOnlySystemGroup = false) override; + + NS_DECL_IMGINOTIFICATIONOBSERVER + + // nsIDOMEventListener + NS_DECL_NSIDOMEVENTLISTENER + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ImageDocument, MediaDocument) + + friend class ImageListener; + + void DefaultCheckOverflowing(); + + // WebIDL API + JSObject* WrapNode(JSContext*, JS::Handle<JSObject*> aGivenProto) override; + + bool ImageIsOverflowing() const { + return ImageIsOverflowingHorizontally() || ImageIsOverflowingVertically(); + } + + bool ImageIsOverflowingVertically() const { + return mImageHeight > mVisibleHeight; + } + + bool ImageIsOverflowingHorizontally() const { + return mImageWidth > mVisibleWidth; + } + + bool ImageIsResized() const { return mImageIsResized; } + // ShrinkToFit is called from xpidl methods and we don't have a good + // way to mark those MOZ_CAN_RUN_SCRIPT yet. + MOZ_CAN_RUN_SCRIPT_BOUNDARY void ShrinkToFit(); + void RestoreImage(); + + void NotifyPossibleTitleChange(bool aBoundTitleElement) override; + + void UpdateRemoteStyle(StyleImageRendering aImageRendering); + + protected: + virtual ~ImageDocument(); + + nsresult CreateSyntheticDocument() override; + + nsresult CheckOverflowing(bool changeState); + + void UpdateTitleAndCharset(); + + void ScrollImageTo(int32_t aX, int32_t aY); + + float GetRatio() const { + return std::min(mVisibleWidth / mImageWidth, mVisibleHeight / mImageHeight); + } + + bool IsSiteSpecific(); + + void ResetZoomLevel(); + float GetZoomLevel(); + void CheckFullZoom(); + float GetResolution(); + + void UpdateSizeFromLayout(); + + enum eModeClasses { + eNone, + eShrinkToFit, + eOverflowingVertical, // And maybe horizontal too. + eOverflowingHorizontalOnly, + eIsInObjectOrEmbed + }; + void SetModeClass(eModeClasses mode); + + void OnSizeAvailable(imgIRequest* aRequest, imgIContainer* aImage); + void OnLoadComplete(imgIRequest* aRequest, nsresult aStatus); + void OnHasTransparency(); + + void MaybeSendResultToEmbedder(nsresult aResult); + + RefPtr<HTMLImageElement> mImageContent; + + float mVisibleWidth; + float mVisibleHeight; + int32_t mImageWidth; + int32_t mImageHeight; + + // mImageIsResized is true if the image is currently resized + bool mImageIsResized; + // mShouldResize is true if the image should be resized when it doesn't fit + // mImageIsResized cannot be true when this is false, but mImageIsResized + // can be false when this is true + bool mShouldResize; + bool mFirstResize; + // mObservingImageLoader is true while the observer is set. + bool mObservingImageLoader; + bool mTitleUpdateInProgress; + bool mHasCustomTitle; + + // True iff embedder is either <object> or <embed>. + bool mIsInObjectOrEmbed; + + float mOriginalZoomLevel; + float mOriginalResolution; +}; + +inline ImageDocument* Document::AsImageDocument() { + MOZ_ASSERT(IsImageDocument()); + return static_cast<ImageDocument*>(this); +} + +inline const ImageDocument* Document::AsImageDocument() const { + MOZ_ASSERT(IsImageDocument()); + return static_cast<const ImageDocument*>(this); +} + +} // namespace mozilla::dom + +#endif /* mozilla_dom_ImageDocument_h */ diff --git a/dom/html/MediaDocument.cpp b/dom/html/MediaDocument.cpp new file mode 100644 index 0000000000..40662e6d4c --- /dev/null +++ b/dom/html/MediaDocument.cpp @@ -0,0 +1,411 @@ +/* -*- 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 "MediaDocument.h" +#include "nsGkAtoms.h" +#include "nsRect.h" +#include "nsPresContext.h" +#include "nsViewManager.h" +#include "nsITextToSubURI.h" +#include "nsIURL.h" +#include "nsIDocShell.h" +#include "nsCharsetSource.h" // kCharsetFrom* macro definition +#include "nsNodeInfoManager.h" +#include "nsContentUtils.h" +#include "nsDocElementCreatedNotificationRunner.h" +#include "mozilla/Encoding.h" +#include "mozilla/PresShell.h" +#include "mozilla/Components.h" +#include "nsServiceManagerUtils.h" +#include "nsIPrincipal.h" +#include "nsIMultiPartChannel.h" +#include "nsProxyRelease.h" + +namespace mozilla::dom { + +MediaDocumentStreamListener::MediaDocumentStreamListener( + MediaDocument* aDocument) + : mDocument(aDocument) {} + +MediaDocumentStreamListener::~MediaDocumentStreamListener() { + if (mDocument && !NS_IsMainThread()) { + nsCOMPtr<nsIEventTarget> mainTarget(do_GetMainThread()); + NS_ProxyRelease("MediaDocumentStreamListener::mDocument", mainTarget, + mDocument.forget()); + } +} + +NS_IMPL_ISUPPORTS(MediaDocumentStreamListener, nsIRequestObserver, + nsIStreamListener, nsIThreadRetargetableStreamListener) + +NS_IMETHODIMP +MediaDocumentStreamListener::OnStartRequest(nsIRequest* request) { + NS_ENSURE_TRUE(mDocument, NS_ERROR_FAILURE); + + mDocument->StartLayout(); + + if (mNextStream) { + return mNextStream->OnStartRequest(request); + } + + return NS_ERROR_PARSED_DATA_CACHED; +} + +NS_IMETHODIMP +MediaDocumentStreamListener::OnStopRequest(nsIRequest* request, + nsresult status) { + nsresult rv = NS_OK; + if (mNextStream) { + rv = mNextStream->OnStopRequest(request, status); + } + + // Don't release mDocument here if we're in the middle of a multipart + // response. + bool lastPart = true; + nsCOMPtr<nsIMultiPartChannel> mpchan(do_QueryInterface(request)); + if (mpchan) { + mpchan->GetIsLastPart(&lastPart); + } + + if (lastPart) { + mDocument = nullptr; + } + return rv; +} + +NS_IMETHODIMP +MediaDocumentStreamListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, + uint32_t count) { + if (mNextStream) { + return mNextStream->OnDataAvailable(request, inStr, sourceOffset, count); + } + + return NS_OK; +} + +NS_IMETHODIMP +MediaDocumentStreamListener::OnDataFinished(nsresult aStatus) { + if (!mNextStream) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable = + do_QueryInterface(mNextStream); + if (retargetable) { + return retargetable->OnDataFinished(aStatus); + } + + return NS_OK; +} + +NS_IMETHODIMP +MediaDocumentStreamListener::CheckListenerChain() { + nsCOMPtr<nsIThreadRetargetableStreamListener> retargetable = + do_QueryInterface(mNextStream); + if (retargetable) { + return retargetable->CheckListenerChain(); + } + return NS_ERROR_NO_INTERFACE; +} + +// default format names for MediaDocument. +const char* const MediaDocument::sFormatNames[4] = { + "MediaTitleWithNoInfo", // eWithNoInfo + "MediaTitleWithFile", // eWithFile + "", // eWithDim + "" // eWithDimAndFile +}; + +MediaDocument::MediaDocument() : mDidInitialDocumentSetup(false) { + mCompatMode = eCompatibility_FullStandards; +} +MediaDocument::~MediaDocument() = default; + +nsresult MediaDocument::Init(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) { + nsresult rv = nsHTMLDocument::Init(aPrincipal, aPartitionedPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + mIsSyntheticDocument = true; + + return NS_OK; +} + +nsresult MediaDocument::StartDocumentLoad( + const char* aCommand, nsIChannel* aChannel, nsILoadGroup* aLoadGroup, + nsISupports* aContainer, nsIStreamListener** aDocListener, bool aReset) { + nsresult rv = Document::StartDocumentLoad(aCommand, aChannel, aLoadGroup, + aContainer, aDocListener, aReset); + if (NS_FAILED(rv)) { + return rv; + } + + // We try to set the charset of the current document to that of the + // 'genuine' (as opposed to an intervening 'chrome') parent document + // that may be in a different window/tab. Even if we fail here, + // we just return NS_OK because another attempt is made in + // |UpdateTitleAndCharset| and the worst thing possible is a mangled + // filename in the titlebar and the file picker. + + // Note that we + // exclude UTF-8 as 'invalid' because UTF-8 is likely to be the charset + // of a chrome document that has nothing to do with the actual content + // whose charset we want to know. Even if "the actual content" is indeed + // in UTF-8, we don't lose anything because the default empty value is + // considered synonymous with UTF-8. + + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aContainer)); + + // not being able to set the charset is not critical. + NS_ENSURE_TRUE(docShell, NS_OK); + + const Encoding* encoding; + int32_t source; + nsCOMPtr<nsIPrincipal> principal; + // opening in a new tab + docShell->GetParentCharset(encoding, &source, getter_AddRefs(principal)); + + if (encoding && encoding != UTF_8_ENCODING && + NodePrincipal()->Equals(principal)) { + SetDocumentCharacterSetSource(source); + SetDocumentCharacterSet(WrapNotNull(encoding)); + } + + return NS_OK; +} + +void MediaDocument::InitialSetupDone() { + MOZ_ASSERT(GetReadyStateEnum() == Document::READYSTATE_LOADING, + "Bad readyState: we should still be doing our initial load"); + mDidInitialDocumentSetup = true; + nsContentUtils::AddScriptRunner( + new nsDocElementCreatedNotificationRunner(this)); + SetReadyStateInternal(Document::READYSTATE_INTERACTIVE); +} + +nsresult MediaDocument::CreateSyntheticDocument() { + MOZ_ASSERT(!InitialSetupHasBeenDone()); + + // Synthesize an empty html document + + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::html, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + RefPtr<nsGenericHTMLElement> root = NS_NewHTMLHtmlElement(nodeInfo.forget()); + NS_ENSURE_TRUE(root, NS_ERROR_OUT_OF_MEMORY); + + NS_ASSERTION(GetChildCount() == 0, "Shouldn't have any kids"); + ErrorResult rv; + AppendChildTo(root, false, rv); + if (rv.Failed()) { + return rv.StealNSResult(); + } + + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::head, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + // Create a <head> so our title has somewhere to live + RefPtr<nsGenericHTMLElement> head = NS_NewHTMLHeadElement(nodeInfo.forget()); + NS_ENSURE_TRUE(head, NS_ERROR_OUT_OF_MEMORY); + + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::meta, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + RefPtr<nsGenericHTMLElement> metaContent = + NS_NewHTMLMetaElement(nodeInfo.forget()); + NS_ENSURE_TRUE(metaContent, NS_ERROR_OUT_OF_MEMORY); + metaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::name, u"viewport"_ns, + true); + + metaContent->SetAttr(kNameSpaceID_None, nsGkAtoms::content, + u"width=device-width; height=device-height;"_ns, true); + head->AppendChildTo(metaContent, false, IgnoreErrors()); + + root->AppendChildTo(head, false, IgnoreErrors()); + + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::body, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + RefPtr<nsGenericHTMLElement> body = NS_NewHTMLBodyElement(nodeInfo.forget()); + NS_ENSURE_TRUE(body, NS_ERROR_OUT_OF_MEMORY); + + root->AppendChildTo(body, false, IgnoreErrors()); + + return NS_OK; +} + +nsresult MediaDocument::StartLayout() { + mMayStartLayout = true; + RefPtr<PresShell> presShell = GetPresShell(); + // Don't mess with the presshell if someone has already handled + // its initial reflow. + if (presShell && !presShell->DidInitialize()) { + nsresult rv = presShell->Initialize(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +void MediaDocument::GetFileName(nsAString& aResult, nsIChannel* aChannel) { + aResult.Truncate(); + + if (aChannel) { + aChannel->GetContentDispositionFilename(aResult); + if (!aResult.IsEmpty()) return; + } + + nsCOMPtr<nsIURL> url = do_QueryInterface(mDocumentURI); + if (!url) return; + + nsAutoCString fileName; + url->GetFileName(fileName); + if (fileName.IsEmpty()) return; + + // Now that the charset is set in |StartDocumentLoad| to the charset of + // the document viewer instead of a bogus value ("windows-1252" set in + // |Document|'s ctor), the priority is given to the current charset. + // This is necessary to deal with a media document being opened in a new + // window or a new tab. + if (mCharacterSetSource == kCharsetUninitialized) { + // resort to UTF-8 + SetDocumentCharacterSet(UTF_8_ENCODING); + } + + nsresult rv; + nsCOMPtr<nsITextToSubURI> textToSubURI = + do_GetService(NS_ITEXTTOSUBURI_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + // UnEscapeURIForUI always succeeds + textToSubURI->UnEscapeURIForUI(fileName, aResult); + } else { + CopyUTF8toUTF16(fileName, aResult); + } +} + +nsresult MediaDocument::LinkStylesheet(const nsAString& aStylesheet) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::link, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + RefPtr<nsGenericHTMLElement> link = NS_NewHTMLLinkElement(nodeInfo.forget()); + NS_ENSURE_TRUE(link, NS_ERROR_OUT_OF_MEMORY); + + link->SetAttr(kNameSpaceID_None, nsGkAtoms::rel, u"stylesheet"_ns, true); + + link->SetAttr(kNameSpaceID_None, nsGkAtoms::href, aStylesheet, true); + + ErrorResult rv; + Element* head = GetHeadElement(); + head->AppendChildTo(link, false, rv); + return rv.StealNSResult(); +} + +nsresult MediaDocument::LinkScript(const nsAString& aScript) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::script, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + RefPtr<nsGenericHTMLElement> script = + NS_NewHTMLScriptElement(nodeInfo.forget()); + NS_ENSURE_TRUE(script, NS_ERROR_OUT_OF_MEMORY); + + script->SetAttr(kNameSpaceID_None, nsGkAtoms::type, u"text/javascript"_ns, + true); + + script->SetAttr(kNameSpaceID_None, nsGkAtoms::src, aScript, true); + + ErrorResult rv; + Element* head = GetHeadElement(); + head->AppendChildTo(script, false, rv); + return rv.StealNSResult(); +} + +void MediaDocument::FormatStringFromName(const char* aName, + const nsTArray<nsString>& aParams, + nsAString& aResult) { + bool spoofLocale = nsContentUtils::SpoofLocaleEnglish() && !AllowsL10n(); + if (!spoofLocale) { + if (!mStringBundle) { + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::components::StringBundle::Service(); + if (stringService) { + stringService->CreateBundle(NSMEDIADOCUMENT_PROPERTIES_URI, + getter_AddRefs(mStringBundle)); + } + } + if (mStringBundle) { + mStringBundle->FormatStringFromName(aName, aParams, aResult); + } + } else { + if (!mStringBundleEnglish) { + nsCOMPtr<nsIStringBundleService> stringService = + mozilla::components::StringBundle::Service(); + if (stringService) { + stringService->CreateBundle(NSMEDIADOCUMENT_PROPERTIES_URI_en_US, + getter_AddRefs(mStringBundleEnglish)); + } + } + if (mStringBundleEnglish) { + mStringBundleEnglish->FormatStringFromName(aName, aParams, aResult); + } + } +} + +void MediaDocument::UpdateTitleAndCharset(const nsACString& aTypeStr, + nsIChannel* aChannel, + const char* const* aFormatNames, + int32_t aWidth, int32_t aHeight, + const nsAString& aStatus) { + nsAutoString fileStr; + GetFileName(fileStr, aChannel); + + NS_ConvertASCIItoUTF16 typeStr(aTypeStr); + nsAutoString title; + + // if we got a valid size (not all media have a size) + if (aWidth != 0 && aHeight != 0) { + nsAutoString widthStr; + nsAutoString heightStr; + widthStr.AppendInt(aWidth); + heightStr.AppendInt(aHeight); + // If we got a filename, display it + if (!fileStr.IsEmpty()) { + AutoTArray<nsString, 4> formatStrings = {fileStr, typeStr, widthStr, + heightStr}; + FormatStringFromName(aFormatNames[eWithDimAndFile], formatStrings, title); + } else { + AutoTArray<nsString, 3> formatStrings = {typeStr, widthStr, heightStr}; + FormatStringFromName(aFormatNames[eWithDim], formatStrings, title); + } + } else { + // If we got a filename, display it + if (!fileStr.IsEmpty()) { + AutoTArray<nsString, 2> formatStrings = {fileStr, typeStr}; + FormatStringFromName(aFormatNames[eWithFile], formatStrings, title); + } else { + AutoTArray<nsString, 1> formatStrings = {typeStr}; + FormatStringFromName(aFormatNames[eWithNoInfo], formatStrings, title); + } + } + + // set it on the document + if (aStatus.IsEmpty()) { + IgnoredErrorResult ignored; + SetTitle(title, ignored); + } else { + nsAutoString titleWithStatus; + AutoTArray<nsString, 2> formatStrings; + formatStrings.AppendElement(title); + formatStrings.AppendElement(aStatus); + FormatStringFromName("TitleWithStatus", formatStrings, titleWithStatus); + SetTitle(titleWithStatus, IgnoreErrors()); + } +} + +} // namespace mozilla::dom diff --git a/dom/html/MediaDocument.h b/dom/html/MediaDocument.h new file mode 100644 index 0000000000..31c1658c97 --- /dev/null +++ b/dom/html/MediaDocument.h @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_MediaDocument_h +#define mozilla_dom_MediaDocument_h + +#include "mozilla/Attributes.h" +#include "nsHTMLDocument.h" +#include "nsGenericHTMLElement.h" +#include "nsIStreamListener.h" +#include "nsIStringBundle.h" +#include "nsIThreadRetargetableStreamListener.h" + +#define NSMEDIADOCUMENT_PROPERTIES_URI \ + "chrome://global/locale/layout/MediaDocument.properties" + +#define NSMEDIADOCUMENT_PROPERTIES_URI_en_US \ + "resource://gre/res/locale/layout/MediaDocument.properties" + +namespace mozilla::dom { + +class MediaDocument : public nsHTMLDocument { + public: + MediaDocument(); + virtual ~MediaDocument(); + + // Subclasses need to override this. + enum MediaDocumentKind MediaDocumentKind() const override = 0; + + virtual nsresult Init(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) override; + + virtual nsresult StartDocumentLoad(const char* aCommand, nsIChannel* aChannel, + nsILoadGroup* aLoadGroup, + nsISupports* aContainer, + nsIStreamListener** aDocListener, + bool aReset = true) override; + + virtual bool WillIgnoreCharsetOverride() override { return true; } + + protected: + // Hook to be called once our initial document setup is done. Subclasses + // should call this from SetScriptGlobalObject when setup hasn't been done + // yet, a non-null script global is being set, and they have finished whatever + // setup work they plan to do for an initial load. + void InitialSetupDone(); + + // Check whether initial setup has been done. + [[nodiscard]] bool InitialSetupHasBeenDone() const { + return mDidInitialDocumentSetup; + } + + virtual nsresult CreateSyntheticDocument(); + + friend class MediaDocumentStreamListener; + virtual nsresult StartLayout(); + + void GetFileName(nsAString& aResult, nsIChannel* aChannel); + + nsresult LinkStylesheet(const nsAString& aStylesheet); + nsresult LinkScript(const nsAString& aScript); + + void FormatStringFromName(const char* aName, + const nsTArray<nsString>& aParams, + nsAString& aResult); + + // |aFormatNames[]| needs to have four elements in the following order: + // a format name with neither dimension nor file, a format name with + // filename but w/o dimension, a format name with dimension but w/o filename, + // a format name with both of them. For instance, it can have + // "ImageTitleWithNeitherDimensionsNorFile", "ImageTitleWithoutDimensions", + // "ImageTitleWithDimesions2", "ImageTitleWithDimensions2AndFile". + // + // Also see MediaDocument.properties if you want to define format names + // for a new subclass. aWidth and aHeight are pixels for |ImageDocument|, + // but could be in other units for other 'media', in which case you have to + // define format names accordingly. + void UpdateTitleAndCharset(const nsACString& aTypeStr, nsIChannel* aChannel, + const char* const* aFormatNames = sFormatNames, + int32_t aWidth = 0, int32_t aHeight = 0, + const nsAString& aStatus = u""_ns); + + nsCOMPtr<nsIStringBundle> mStringBundle; + nsCOMPtr<nsIStringBundle> mStringBundleEnglish; + static const char* const sFormatNames[4]; + + private: + enum { eWithNoInfo, eWithFile, eWithDim, eWithDimAndFile }; + + // A boolean that indicates whether we did our initial document setup. This + // will be false initially, become true when we finish setting up the document + // during initial load and stay true thereafter. + bool mDidInitialDocumentSetup; +}; + +class MediaDocumentStreamListener : public nsIThreadRetargetableStreamListener { + protected: + virtual ~MediaDocumentStreamListener(); + + public: + explicit MediaDocumentStreamListener(MediaDocument* aDocument); + + NS_DECL_THREADSAFE_ISUPPORTS + + NS_DECL_NSIREQUESTOBSERVER + + NS_DECL_NSISTREAMLISTENER + + NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER + + void DropDocumentRef() { mDocument = nullptr; } + + RefPtr<MediaDocument> mDocument; + nsCOMPtr<nsIStreamListener> mNextStream; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_MediaDocument_h */ diff --git a/dom/html/MediaError.cpp b/dom/html/MediaError.cpp new file mode 100644 index 0000000000..819e1745c6 --- /dev/null +++ b/dom/html/MediaError.cpp @@ -0,0 +1,85 @@ +/* -*- 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/MediaError.h" + +#include <string> +#include <unordered_set> + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MediaErrorBinding.h" +#include "nsContentUtils.h" +#include "nsIScriptError.h" +#include "jsapi.h" +#include "js/Warnings.h" // JS::WarnASCII + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaError, mParent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaError) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaError) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaError) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +MediaError::MediaError(HTMLMediaElement* aParent, uint16_t aCode, + const nsACString& aMessage) + : mParent(aParent), mCode(aCode), mMessage(aMessage) {} + +void MediaError::GetMessage(nsAString& aResult) const { + // When fingerprinting resistance is enabled, only messages in this list + // can be returned to content script. + // FIXME: An unordered_set seems overkill for this. + static const std::unordered_set<std::string> whitelist = { + "404: Not Found" + // TODO + }; + + const bool shouldBlank = whitelist.find(mMessage.get()) == whitelist.end(); + + if (shouldBlank) { + // Print a warning message to JavaScript console to alert developers of + // a non-whitelisted error message. + nsAutoCString message = + nsLiteralCString( + "This error message will be blank when " + "privacy.resistFingerprinting = true." + " If it is really necessary, please add it to the whitelist in" + " MediaError::GetMessage: ") + + mMessage; + Document* ownerDoc = mParent->OwnerDoc(); + AutoJSAPI api; + if (api.Init(ownerDoc->GetScopeObject())) { + // We prefer this API because it can also print to our debug log and + // try server's log viewer. + JS::WarnASCII(api.cx(), "%s", message.get()); + } else { + // If failed to use JS::WarnASCII, fall back to + // nsContentUtils::ReportToConsoleNonLocalized, which can only print to + // JavaScript console. + nsContentUtils::ReportToConsoleNonLocalized( + NS_ConvertASCIItoUTF16(message), nsIScriptError::warningFlag, + "MediaError"_ns, ownerDoc); + } + + if (!nsContentUtils::IsCallerChrome() && + ownerDoc->ShouldResistFingerprinting(RFPTarget::MediaError)) { + aResult.Truncate(); + return; + } + } + + CopyUTF8toUTF16(mMessage, aResult); +} + +JSObject* MediaError::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MediaError_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/MediaError.h b/dom/html/MediaError.h new file mode 100644 index 0000000000..aac1df118d --- /dev/null +++ b/dom/html/MediaError.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_MediaError_h +#define mozilla_dom_MediaError_h + +#include "mozilla/dom/HTMLMediaElement.h" +#include "nsWrapperCache.h" +#include "nsISupports.h" +#include "mozilla/Attributes.h" + +namespace mozilla::dom { + +class MediaError final : public nsISupports, public nsWrapperCache { + ~MediaError() = default; + + public: + MediaError(HTMLMediaElement* aParent, uint16_t aCode, + const nsACString& aMessage = nsCString()); + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaError) + + HTMLMediaElement* GetParentObject() const { return mParent; } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + uint16_t Code() const { return mCode; } + + void GetMessage(nsAString& aResult) const; + + private: + RefPtr<HTMLMediaElement> mParent; + + // Error code + const uint16_t mCode; + // Error details; + const nsCString mMessage; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_MediaError_h diff --git a/dom/html/PlayPromise.cpp b/dom/html/PlayPromise.cpp new file mode 100644 index 0000000000..ee33b3ad68 --- /dev/null +++ b/dom/html/PlayPromise.cpp @@ -0,0 +1,84 @@ +/* -*- 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/PlayPromise.h" +#include "mozilla/Logging.h" +#include "mozilla/Telemetry.h" + +extern mozilla::LazyLogModule gMediaElementLog; + +#define PLAY_PROMISE_LOG(msg, ...) \ + MOZ_LOG(gMediaElementLog, LogLevel::Debug, (msg, ##__VA_ARGS__)) + +namespace mozilla::dom { + +PlayPromise::PlayPromise(nsIGlobalObject* aGlobal) : Promise(aGlobal) {} + +PlayPromise::~PlayPromise() { + if (!mFulfilled && PromiseObj()) { + MaybeReject(NS_ERROR_DOM_ABORT_ERR); + } +} + +/* static */ +already_AddRefed<PlayPromise> PlayPromise::Create(nsIGlobalObject* aGlobal, + ErrorResult& aRv) { + RefPtr<PlayPromise> promise = new PlayPromise(aGlobal); + promise->CreateWrapper(aRv); + return aRv.Failed() ? nullptr : promise.forget(); +} + +/* static */ +void PlayPromise::ResolvePromisesWithUndefined( + const PlayPromiseArr& aPromises) { + for (const auto& promise : aPromises) { + promise->MaybeResolveWithUndefined(); + } +} + +/* static */ +void PlayPromise::RejectPromises(const PlayPromiseArr& aPromises, + nsresult aError) { + for (const auto& promise : aPromises) { + promise->MaybeReject(aError); + } +} + +void PlayPromise::MaybeResolveWithUndefined() { + if (mFulfilled) { + return; + } + mFulfilled = true; + PLAY_PROMISE_LOG("PlayPromise %p resolved with undefined", this); + Promise::MaybeResolveWithUndefined(); +} + +static const char* ToPlayResultStr(nsresult aReason) { + switch (aReason) { + case NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR: + return "NotAllowedErr"; + case NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR: + return "SrcNotSupportedErr"; + case NS_ERROR_DOM_MEDIA_ABORT_ERR: + return "PauseAbortErr"; + case NS_ERROR_DOM_ABORT_ERR: + return "AbortErr"; + default: + return "UnknownErr"; + } +} + +void PlayPromise::MaybeReject(nsresult aReason) { + if (mFulfilled) { + return; + } + mFulfilled = true; + PLAY_PROMISE_LOG("PlayPromise %p rejected with 0x%x (%s)", this, + static_cast<uint32_t>(aReason), ToPlayResultStr(aReason)); + Promise::MaybeReject(aReason); +} + +} // namespace mozilla::dom diff --git a/dom/html/PlayPromise.h b/dom/html/PlayPromise.h new file mode 100644 index 0000000000..9684f926df --- /dev/null +++ b/dom/html/PlayPromise.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __PlayPromise_h__ +#define __PlayPromise_h__ + +#include "mozilla/dom/Promise.h" +#include "mozilla/Telemetry.h" + +namespace mozilla::dom { + +// Decorates a DOM Promise to report telemetry as to whether it was resolved +// or rejected and why. +class PlayPromise : public Promise { + public: + static already_AddRefed<PlayPromise> Create(nsIGlobalObject* aGlobal, + ErrorResult& aRv); + + using PlayPromiseArr = nsTArray<RefPtr<PlayPromise>>; + static void ResolvePromisesWithUndefined(const PlayPromiseArr& aPromises); + static void RejectPromises(const PlayPromiseArr& aPromises, nsresult aError); + + ~PlayPromise(); + void MaybeResolveWithUndefined(); + void MaybeReject(nsresult aReason); + + private: + explicit PlayPromise(nsIGlobalObject* aGlobal); + bool mFulfilled = false; +}; + +} // namespace mozilla::dom + +#endif // __PlayPromise_h__ diff --git a/dom/html/RadioNodeList.cpp b/dom/html/RadioNodeList.cpp new file mode 100644 index 0000000000..a6c0d55929 --- /dev/null +++ b/dom/html/RadioNodeList.cpp @@ -0,0 +1,60 @@ +/* -*- 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/RadioNodeList.h" + +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/RadioNodeListBinding.h" +#include "js/TypeDecls.h" + +#include "HTMLInputElement.h" + +namespace mozilla::dom { + +/* virtual */ +JSObject* RadioNodeList::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return RadioNodeList_Binding::Wrap(aCx, this, aGivenProto); +} + +HTMLInputElement* GetAsRadio(nsIContent* node) { + auto* el = HTMLInputElement::FromNode(node); + if (el && el->ControlType() == FormControlType::InputRadio) { + return el; + } + return nullptr; +} + +void RadioNodeList::GetValue(nsString& retval, CallerType aCallerType) { + for (uint32_t i = 0; i < Length(); i++) { + HTMLInputElement* maybeRadio = GetAsRadio(Item(i)); + if (maybeRadio && maybeRadio->Checked()) { + maybeRadio->GetValue(retval, aCallerType); + return; + } + } + retval.Truncate(); +} + +void RadioNodeList::SetValue(const nsAString& value, CallerType aCallerType) { + for (uint32_t i = 0; i < Length(); i++) { + HTMLInputElement* maybeRadio = GetAsRadio(Item(i)); + if (!maybeRadio) { + continue; + } + + nsString curval = nsString(); + maybeRadio->GetValue(curval, aCallerType); + if (curval.Equals(value)) { + maybeRadio->SetChecked(true); + return; + } + } +} + +NS_IMPL_ISUPPORTS_INHERITED(RadioNodeList, nsSimpleContentList, RadioNodeList) + +} // namespace mozilla::dom diff --git a/dom/html/RadioNodeList.h b/dom/html/RadioNodeList.h new file mode 100644 index 0000000000..965bbb007c --- /dev/null +++ b/dom/html/RadioNodeList.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_RadioNodeList_h +#define mozilla_dom_RadioNodeList_h + +#include "nsContentList.h" +#include "nsCOMPtr.h" +#include "HTMLFormElement.h" +#include "mozilla/dom/BindingDeclarations.h" + +#define MOZILLA_DOM_RADIONODELIST_IMPLEMENTATION_IID \ + { \ + 0xbba7f3e8, 0xf3b5, 0x42e5, { \ + 0x82, 0x08, 0xa6, 0x8b, 0xe0, 0xbc, 0x22, 0x19 \ + } \ + } + +namespace mozilla::dom { + +class RadioNodeList final : public nsSimpleContentList { + public: + explicit RadioNodeList(HTMLFormElement* aForm) : nsSimpleContentList(aForm) {} + + virtual JSObject* WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) override; + void GetValue(nsString& retval, CallerType aCallerType); + void SetValue(const nsAString& value, CallerType aCallerType); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECLARE_STATIC_IID_ACCESSOR(MOZILLA_DOM_RADIONODELIST_IMPLEMENTATION_IID) + private: + ~RadioNodeList() = default; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(RadioNodeList, + MOZILLA_DOM_RADIONODELIST_IMPLEMENTATION_IID) + +} // namespace mozilla::dom + +#endif // mozilla_dom_RadioNodeList_h diff --git a/dom/html/TextControlElement.h b/dom/html/TextControlElement.h new file mode 100644 index 0000000000..abca471953 --- /dev/null +++ b/dom/html/TextControlElement.h @@ -0,0 +1,249 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_TextControlElement_h +#define mozilla_TextControlElement_h + +#include "mozilla/dom/FromParser.h" +#include "mozilla/dom/NodeInfo.h" +#include "nsGenericHTMLElement.h" + +class nsIContent; +class nsISelectionController; +class nsFrameSelection; +class nsTextControlFrame; + +namespace mozilla { + +class ErrorResult; +class TextControlState; +class TextEditor; + +/** + * This abstract class is used for the text control frame to get the editor and + * selection controller objects, and some helper properties. + */ +class TextControlElement : public nsGenericHTMLFormControlElementWithState { + public: + TextControlElement(already_AddRefed<dom::NodeInfo>&& aNodeInfo, + dom::FromParser aFromParser, FormControlType aType) + : nsGenericHTMLFormControlElementWithState(std::move(aNodeInfo), + aFromParser, aType){}; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED( + TextControlElement, nsGenericHTMLFormControlElementWithState) + + /** + * Return true always, i.e., even if this is an <input> but the type is not + * for a single line text control, this returns true. Use + * IsSingleLineTextControlOrTextArea() if you want to know whether this may + * work with a TextEditor. + */ + bool IsTextControlElement() const final { return true; } + + virtual bool IsSingleLineTextControlOrTextArea() const = 0; + + NS_IMPL_FROMNODE_HELPER(TextControlElement, IsTextControlElement()) + + /** + * Tell the control that value has been deliberately changed (or not). + */ + virtual void SetValueChanged(bool) = 0; + + /** + * Find out whether this is a single line text control. (text or password) + * @return whether this is a single line text control + */ + virtual bool IsSingleLineTextControl() const = 0; + + /** + * Find out whether this control is a textarea. + * @return whether this is a textarea text control + */ + virtual bool IsTextArea() const = 0; + + /** + * Find out whether this is a password control (input type=password) + * @return whether this is a password ontrol + */ + virtual bool IsPasswordTextControl() const = 0; + + /** + * Get the cols attribute (if textarea) or a default + * @return the number of columns to use + */ + virtual int32_t GetCols() = 0; + + /** + * Get the column index to wrap at, or -1 if we shouldn't wrap + */ + virtual int32_t GetWrapCols() = 0; + + /** + * Get the rows attribute (if textarea) or a default + * @return the number of rows to use + */ + virtual int32_t GetRows() = 0; + + /** + * Get the default value of the text control + */ + virtual void GetDefaultValueFromContent(nsAString& aValue, + bool aForDisplay) = 0; + + /** + * Return true if the value of the control has been changed. + */ + virtual bool ValueChanged() const = 0; + + /** + * Returns the used maxlength attribute value. + */ + virtual int32_t UsedMaxLength() const = 0; + + /** + * Get the current value of the text editor. + * + * @param aValue the buffer to retrieve the value in + */ + virtual void GetTextEditorValue(nsAString& aValue) const = 0; + + /** + * Get the editor object associated with the text editor. + * The return value is null if the control does not support an editor + * (for example, if it is a checkbox.) + * Note that GetTextEditor() creates editor if it hasn't been created yet. + * If you need editor only when the editor is there, you should use + * GetTextEditorWithoutCreation(). + */ + MOZ_CAN_RUN_SCRIPT virtual TextEditor* GetTextEditor() = 0; + virtual TextEditor* GetTextEditorWithoutCreation() const = 0; + + /** + * Get the selection controller object associated with the text editor. + * The return value is null if the control does not support an editor + * (for example, if it is a checkbox.) + */ + virtual nsISelectionController* GetSelectionController() = 0; + + virtual nsFrameSelection* GetConstFrameSelection() = 0; + + virtual TextControlState* GetTextControlState() const = 0; + + /** + * Binds a frame to the text control. This is performed when a frame + * is created for the content node. + * Be aware, this must be called with script blocker. + */ + virtual nsresult BindToFrame(nsTextControlFrame* aFrame) = 0; + + /** + * Unbinds a frame from the text control. This is performed when a frame + * belonging to a content node is destroyed. + */ + MOZ_CAN_RUN_SCRIPT virtual void UnbindFromFrame( + nsTextControlFrame* aFrame) = 0; + + /** + * Creates an editor for the text control. This should happen when + * a frame has been created for the text control element, but the created + * editor may outlive the frame itself. + */ + MOZ_CAN_RUN_SCRIPT virtual nsresult CreateEditor() = 0; + + /** + * Update preview value for the text control. + */ + virtual void SetPreviewValue(const nsAString& aValue) = 0; + + /** + * Get the current preview value for text control. + */ + virtual void GetPreviewValue(nsAString& aValue) = 0; + + /** + * Enable preview for text control. + */ + virtual void EnablePreview() = 0; + + /** + * Find out whether this control enables preview for form autofoll. + */ + virtual bool IsPreviewEnabled() = 0; + + /** + * Initialize the keyboard event listeners. + */ + virtual void InitializeKeyboardEventListeners() = 0; + + enum class ValueChangeKind { + Internal, + Script, + UserInteraction, + }; + + /** + * Callback called whenever the value is changed. + * + * aKnownNewValue can be used to avoid value lookups if present (might be + * null, if the caller doesn't know the specific value that got set). + */ + virtual void OnValueChanged(ValueChangeKind, bool aNewValueEmpty, + const nsAString* aKnownNewValue) = 0; + + void OnValueChanged(ValueChangeKind aKind, const nsAString& aNewValue) { + return OnValueChanged(aKind, aNewValue.IsEmpty(), &aNewValue); + } + + /** + * Helpers for value manipulation from SetRangeText. + */ + virtual void GetValueFromSetRangeText(nsAString& aValue) = 0; + MOZ_CAN_RUN_SCRIPT virtual nsresult SetValueFromSetRangeText( + const nsAString& aValue) = 0; + + static const int32_t DEFAULT_COLS = 20; + static const int32_t DEFAULT_ROWS = 1; + static const int32_t DEFAULT_ROWS_TEXTAREA = 2; + static const int32_t DEFAULT_UNDO_CAP = 1000; + + // wrap can be one of these three values. + typedef enum { + eHTMLTextWrap_Off = 1, // "off" + eHTMLTextWrap_Hard = 2, // "hard" + eHTMLTextWrap_Soft = 3 // the default + } nsHTMLTextWrap; + + static bool GetWrapPropertyEnum(nsIContent* aContent, + nsHTMLTextWrap& aWrapProp); + + /** + * Does the editor have a selection cache? + * + * Note that this function has the side effect of making the editor for input + * elements be initialized eagerly. + */ + virtual bool HasCachedSelection() = 0; + + static already_AddRefed<TextControlElement> + GetTextControlElementFromEditingHost(nsIContent* aHost); + + protected: + virtual ~TextControlElement() = default; + + // The focusability state of this form control. eUnfocusable means that it + // shouldn't be focused at all, eInactiveWindow means it's in an inactive + // window, eActiveWindow means it's in an active window. + enum class FocusTristate { eUnfocusable, eInactiveWindow, eActiveWindow }; + + // Get our focus state. + FocusTristate FocusState(); +}; + +} // namespace mozilla + +#endif // mozilla_TextControlElement_h diff --git a/dom/html/TextControlState.cpp b/dom/html/TextControlState.cpp new file mode 100644 index 0000000000..3e2c06c53a --- /dev/null +++ b/dom/html/TextControlState.cpp @@ -0,0 +1,3087 @@ +/* -*- 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 "TextControlState.h" +#include "mozilla/Attributes.h" +#include "mozilla/CaretAssociationHint.h" +#include "mozilla/IMEContentObserver.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/TextInputListener.h" + +#include "nsCOMPtr.h" +#include "nsView.h" +#include "nsCaret.h" +#include "nsLayoutCID.h" +#include "nsITextControlFrame.h" +#include "nsContentCreatorFunctions.h" +#include "nsTextControlFrame.h" +#include "nsIControllers.h" +#include "nsIControllerContext.h" +#include "nsAttrValue.h" +#include "nsAttrValueInlines.h" +#include "nsGenericHTMLElement.h" +#include "nsIDOMEventListener.h" +#include "nsIWidget.h" +#include "nsIDocumentEncoder.h" +#include "nsPIDOMWindow.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/EventListenerManager.h" +#include "nsContentUtils.h" +#include "mozilla/Preferences.h" +#include "nsTextNode.h" +#include "nsIController.h" +#include "nsIScrollableFrame.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/InputEventOptions.h" +#include "mozilla/NativeKeyBindingsType.h" +#include "mozilla/PresShell.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/HTMLTextAreaElement.h" +#include "mozilla/dom/Text.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_ui.h" +#include "nsFrameSelection.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Telemetry.h" +#include "mozilla/ShortcutKeys.h" +#include "mozilla/KeyEventHandler.h" +#include "mozilla/dom/KeyboardEvent.h" +#include "mozilla/ScrollTypes.h" + +namespace mozilla { + +using namespace dom; +using ValueSetterOption = TextControlState::ValueSetterOption; +using ValueSetterOptions = TextControlState::ValueSetterOptions; +using SelectionDirection = nsITextControlFrame::SelectionDirection; + +/***************************************************************************** + * TextControlElement + *****************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_CLASS(TextControlElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED( + TextControlElement, nsGenericHTMLFormControlElementWithState) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED( + TextControlElement, nsGenericHTMLFormControlElementWithState) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0( + TextControlElement, nsGenericHTMLFormControlElementWithState) + +/*static*/ +bool TextControlElement::GetWrapPropertyEnum( + nsIContent* aContent, TextControlElement::nsHTMLTextWrap& aWrapProp) { + // soft is the default; "physical" defaults to soft as well because all other + // browsers treat it that way and there is no real reason to maintain physical + // and virtual as separate entities if no one else does. Only hard and off + // do anything different. + aWrapProp = eHTMLTextWrap_Soft; // the default + + if (!aContent->IsHTMLElement()) { + return false; + } + + static mozilla::dom::Element::AttrValuesArray strings[] = { + nsGkAtoms::HARD, nsGkAtoms::OFF, nullptr}; + switch (aContent->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::wrap, strings, eIgnoreCase)) { + case 0: + aWrapProp = eHTMLTextWrap_Hard; + break; + case 1: + aWrapProp = eHTMLTextWrap_Off; + break; + } + + return true; +} + +/*static*/ +already_AddRefed<TextControlElement> +TextControlElement::GetTextControlElementFromEditingHost(nsIContent* aHost) { + if (!aHost) { + return nullptr; + } + + RefPtr<TextControlElement> parent = + TextControlElement::FromNodeOrNull(aHost->GetParent()); + return parent.forget(); +} + +TextControlElement::FocusTristate TextControlElement::FocusState() { + // We can't be focused if we aren't in a (composed) document + Document* doc = GetComposedDoc(); + if (!doc) { + return FocusTristate::eUnfocusable; + } + + // first see if we are disabled or not. If disabled then do nothing. + if (IsDisabled()) { + return FocusTristate::eUnfocusable; + } + + return IsInActiveTab(doc) ? FocusTristate::eActiveWindow + : FocusTristate::eInactiveWindow; +} + +using ValueChangeKind = TextControlElement::ValueChangeKind; + +MOZ_CAN_RUN_SCRIPT inline nsresult SetEditorFlagsIfNecessary( + EditorBase& aEditorBase, uint32_t aFlags) { + if (aEditorBase.Flags() == aFlags) { + return NS_OK; + } + return aEditorBase.SetFlags(aFlags); +} + +/***************************************************************************** + * mozilla::AutoInputEventSuppresser + *****************************************************************************/ + +class MOZ_STACK_CLASS AutoInputEventSuppresser final { + public: + explicit AutoInputEventSuppresser(TextEditor* aTextEditor) + : mTextEditor(aTextEditor), + // To protect against a reentrant call to SetValue, we check whether + // another SetValue is already happening for this editor. If it is, + // we must wait until we unwind to re-enable oninput events. + mOuterTransaction(aTextEditor->IsSuppressingDispatchingInputEvent()) { + MOZ_ASSERT(mTextEditor); + mTextEditor->SuppressDispatchingInputEvent(true); + } + ~AutoInputEventSuppresser() { + mTextEditor->SuppressDispatchingInputEvent(mOuterTransaction); + } + + private: + RefPtr<TextEditor> mTextEditor; + bool mOuterTransaction; +}; + +/***************************************************************************** + * mozilla::RestoreSelectionState + *****************************************************************************/ + +class RestoreSelectionState : public Runnable { + public: + RestoreSelectionState(TextControlState* aState, nsTextControlFrame* aFrame) + : Runnable("RestoreSelectionState"), + mFrame(aFrame), + mTextControlState(aState) {} + + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + if (!mTextControlState) { + return NS_OK; + } + + AutoHideSelectionChanges hideSelectionChanges( + mFrame->GetConstFrameSelection()); + + if (mFrame) { + // EnsureEditorInitialized and SetSelectionRange leads to + // Selection::AddRangeAndSelectFramesAndNotifyListeners which flushes + // Layout - need to block script to avoid nested PrepareEditor calls (bug + // 642800). + nsAutoScriptBlocker scriptBlocker; + mFrame->EnsureEditorInitialized(); + TextControlState::SelectionProperties& properties = + mTextControlState->GetSelectionProperties(); + if (properties.IsDirty()) { + mFrame->SetSelectionRange(properties.GetStart(), properties.GetEnd(), + properties.GetDirection()); + } + } + + if (mTextControlState) { + mTextControlState->FinishedRestoringSelection(); + } + return NS_OK; + } + + // Let the text editor tell us we're no longer relevant - avoids use of + // AutoWeakFrame + void Revoke() { + mFrame = nullptr; + mTextControlState = nullptr; + } + + private: + nsTextControlFrame* mFrame; + TextControlState* mTextControlState; +}; + +/***************************************************************************** + * mozilla::AutoRestoreEditorState + *****************************************************************************/ + +class MOZ_RAII AutoRestoreEditorState final { + public: + MOZ_CAN_RUN_SCRIPT explicit AutoRestoreEditorState(TextEditor* aTextEditor) + : mTextEditor(aTextEditor), + mSavedFlags(mTextEditor->Flags()), + mSavedMaxLength(mTextEditor->MaxTextLength()), + mSavedEchoingPasswordPrevented( + mTextEditor->EchoingPasswordPrevented()) { + MOZ_ASSERT(mTextEditor); + + // EditorBase::SetFlags() is a virtual method. Even though it does nothing + // if new flags and current flags are same, the calling cost causes + // appearing the method in profile. So, this class should check if it's + // necessary to call. + uint32_t flags = mSavedFlags; + flags &= ~nsIEditor::eEditorReadonlyMask; + if (mSavedFlags != flags) { + // It's aTextEditor and whose lifetime must be guaranteed by the caller. + MOZ_KnownLive(mTextEditor)->SetFlags(flags); + } + mTextEditor->PreventToEchoPassword(); + mTextEditor->SetMaxTextLength(-1); + } + + MOZ_CAN_RUN_SCRIPT ~AutoRestoreEditorState() { + if (!mSavedEchoingPasswordPrevented) { + mTextEditor->AllowToEchoPassword(); + } + mTextEditor->SetMaxTextLength(mSavedMaxLength); + // mTextEditor's lifetime must be guaranteed by owner of the instance + // since the constructor is marked as `MOZ_CAN_RUN_SCRIPT` and this is + // a stack only class. + SetEditorFlagsIfNecessary(MOZ_KnownLive(*mTextEditor), mSavedFlags); + } + + private: + TextEditor* mTextEditor; + uint32_t mSavedFlags; + int32_t mSavedMaxLength; + bool mSavedEchoingPasswordPrevented; +}; + +/***************************************************************************** + * mozilla::AutoDisableUndo + *****************************************************************************/ + +class MOZ_RAII AutoDisableUndo final { + public: + explicit AutoDisableUndo(TextEditor* aTextEditor) + : mTextEditor(aTextEditor), mNumberOfMaximumTransactions(0) { + MOZ_ASSERT(mTextEditor); + + mNumberOfMaximumTransactions = + mTextEditor ? mTextEditor->NumberOfMaximumTransactions() : 0; + DebugOnly<bool> disabledUndoRedo = mTextEditor->DisableUndoRedo(); + NS_WARNING_ASSERTION(disabledUndoRedo, + "Failed to disable undo/redo transactions"); + } + + ~AutoDisableUndo() { + // Don't change enable/disable of undo/redo if it's enabled after + // it's disabled by the constructor because we shouldn't change + // the maximum undo/redo count to the old value. + if (mTextEditor->IsUndoRedoEnabled()) { + return; + } + // If undo/redo was enabled, mNumberOfMaximumTransactions is -1 or lager + // than 0. Only when it's 0, it was disabled. + if (mNumberOfMaximumTransactions) { + DebugOnly<bool> enabledUndoRedo = + mTextEditor->EnableUndoRedo(mNumberOfMaximumTransactions); + NS_WARNING_ASSERTION(enabledUndoRedo, + "Failed to enable undo/redo transactions"); + } else { + DebugOnly<bool> disabledUndoRedo = mTextEditor->DisableUndoRedo(); + NS_WARNING_ASSERTION(disabledUndoRedo, + "Failed to disable undo/redo transactions"); + } + } + + private: + TextEditor* mTextEditor; + int32_t mNumberOfMaximumTransactions; +}; + +static bool SuppressEventHandlers(nsPresContext* aPresContext) { + bool suppressHandlers = false; + + if (aPresContext) { + // Right now we only suppress event handlers and controller manipulation + // when in a print preview or print context! + + // In the current implementation, we only paginate when + // printing or in print preview. + + suppressHandlers = aPresContext->IsPaginated(); + } + + return suppressHandlers; +} + +/***************************************************************************** + * mozilla::TextInputSelectionController + *****************************************************************************/ + +class TextInputSelectionController final : public nsSupportsWeakReference, + public nsISelectionController { + ~TextInputSelectionController() = default; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TextInputSelectionController, + nsISelectionController) + + TextInputSelectionController(PresShell* aPresShell, nsIContent* aLimiter); + + void SetScrollableFrame(nsIScrollableFrame* aScrollableFrame); + nsFrameSelection* GetConstFrameSelection() { return mFrameSelection; } + // Will return null if !mFrameSelection. + Selection* GetSelection(SelectionType aSelectionType); + + // NSISELECTIONCONTROLLER INTERFACES + NS_IMETHOD SetDisplaySelection(int16_t toggle) override; + NS_IMETHOD GetDisplaySelection(int16_t* _retval) override; + NS_IMETHOD SetSelectionFlags(int16_t aInEnable) override; + NS_IMETHOD GetSelectionFlags(int16_t* aOutEnable) override; + NS_IMETHOD GetSelectionFromScript(RawSelectionType aRawSelectionType, + Selection** aSelection) override; + Selection* GetSelection(RawSelectionType aRawSelectionType) override; + NS_IMETHOD ScrollSelectionIntoView(RawSelectionType aRawSelectionType, + int16_t aRegion, int16_t aFlags) override; + NS_IMETHOD RepaintSelection(RawSelectionType aRawSelectionType) override; + nsresult RepaintSelection(nsPresContext* aPresContext, + SelectionType aSelectionType); + NS_IMETHOD SetCaretEnabled(bool enabled) override; + NS_IMETHOD SetCaretReadOnly(bool aReadOnly) override; + NS_IMETHOD GetCaretEnabled(bool* _retval) override; + NS_IMETHOD GetCaretVisible(bool* _retval) override; + NS_IMETHOD SetCaretVisibilityDuringSelection(bool aVisibility) override; + NS_IMETHOD PhysicalMove(int16_t aDirection, int16_t aAmount, + bool aExtend) override; + NS_IMETHOD CharacterMove(bool aForward, bool aExtend) override; + NS_IMETHOD WordMove(bool aForward, bool aExtend) override; + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD LineMove(bool aForward, + bool aExtend) override; + NS_IMETHOD IntraLineMove(bool aForward, bool aExtend) override; + MOZ_CAN_RUN_SCRIPT + NS_IMETHOD PageMove(bool aForward, bool aExtend) override; + NS_IMETHOD CompleteScroll(bool aForward) override; + MOZ_CAN_RUN_SCRIPT NS_IMETHOD CompleteMove(bool aForward, + bool aExtend) override; + NS_IMETHOD ScrollPage(bool aForward) override; + NS_IMETHOD ScrollLine(bool aForward) override; + NS_IMETHOD ScrollCharacter(bool aRight) override; + void SelectionWillTakeFocus() override; + void SelectionWillLoseFocus() override; + + private: + RefPtr<nsFrameSelection> mFrameSelection; + nsIScrollableFrame* mScrollFrame; + nsWeakPtr mPresShellWeak; +}; + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TextInputSelectionController) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TextInputSelectionController) +NS_INTERFACE_TABLE_HEAD(TextInputSelectionController) + NS_INTERFACE_TABLE(TextInputSelectionController, nsISelectionController, + nsISelectionDisplay, nsISupportsWeakReference) + NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(TextInputSelectionController) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_WEAK(TextInputSelectionController, mFrameSelection) + +TextInputSelectionController::TextInputSelectionController( + PresShell* aPresShell, nsIContent* aLimiter) + : mScrollFrame(nullptr) { + if (aPresShell) { + bool accessibleCaretEnabled = + PresShell::AccessibleCaretEnabled(aLimiter->OwnerDoc()->GetDocShell()); + mFrameSelection = + new nsFrameSelection(aPresShell, aLimiter, accessibleCaretEnabled); + mPresShellWeak = do_GetWeakReference(aPresShell); + } +} + +void TextInputSelectionController::SetScrollableFrame( + nsIScrollableFrame* aScrollableFrame) { + mScrollFrame = aScrollableFrame; + if (!mScrollFrame && mFrameSelection) { + mFrameSelection->DisconnectFromPresShell(); + mFrameSelection = nullptr; + } +} + +Selection* TextInputSelectionController::GetSelection( + SelectionType aSelectionType) { + if (!mFrameSelection) { + return nullptr; + } + + return mFrameSelection->GetSelection(aSelectionType); +} + +NS_IMETHODIMP +TextInputSelectionController::SetDisplaySelection(int16_t aToggle) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + mFrameSelection->SetDisplaySelection(aToggle); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::GetDisplaySelection(int16_t* aToggle) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + *aToggle = mFrameSelection->GetDisplaySelection(); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::SetSelectionFlags(int16_t aToggle) { + return NS_OK; // stub this out. not used in input +} + +NS_IMETHODIMP +TextInputSelectionController::GetSelectionFlags(int16_t* aOutEnable) { + *aOutEnable = nsISelectionDisplay::DISPLAY_TEXT; + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::GetSelectionFromScript( + RawSelectionType aRawSelectionType, Selection** aSelection) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + + *aSelection = + mFrameSelection->GetSelection(ToSelectionType(aRawSelectionType)); + + // GetSelection() fails only when aRawSelectionType is invalid value. + if (!(*aSelection)) { + return NS_ERROR_INVALID_ARG; + } + + NS_ADDREF(*aSelection); + return NS_OK; +} + +Selection* TextInputSelectionController::GetSelection( + RawSelectionType aRawSelectionType) { + return GetSelection(ToSelectionType(aRawSelectionType)); +} + +NS_IMETHODIMP +TextInputSelectionController::ScrollSelectionIntoView( + RawSelectionType aRawSelectionType, int16_t aRegion, int16_t aFlags) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->ScrollSelectionIntoView( + ToSelectionType(aRawSelectionType), aRegion, aFlags); +} + +NS_IMETHODIMP +TextInputSelectionController::RepaintSelection( + RawSelectionType aRawSelectionType) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->RepaintSelection(ToSelectionType(aRawSelectionType)); +} + +nsresult TextInputSelectionController::RepaintSelection( + nsPresContext* aPresContext, SelectionType aSelectionType) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->RepaintSelection(aSelectionType); +} + +NS_IMETHODIMP +TextInputSelectionController::SetCaretEnabled(bool enabled) { + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak); + if (!presShell) { + return NS_ERROR_FAILURE; + } + + // tell the pres shell to enable the caret, rather than settings its + // visibility directly. this way the presShell's idea of caret visibility is + // maintained. + presShell->SetCaretEnabled(enabled); + + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::SetCaretReadOnly(bool aReadOnly) { + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv; + RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak, &rv); + if (!presShell) { + return NS_ERROR_FAILURE; + } + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (!caret) { + return NS_ERROR_FAILURE; + } + + if (!mFrameSelection) { + return NS_ERROR_FAILURE; + } + + Selection* selection = mFrameSelection->GetSelection(SelectionType::eNormal); + if (selection) { + caret->SetCaretReadOnly(aReadOnly); + } + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::GetCaretEnabled(bool* _retval) { + return GetCaretVisible(_retval); +} + +NS_IMETHODIMP +TextInputSelectionController::GetCaretVisible(bool* _retval) { + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv; + RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak, &rv); + if (!presShell) { + return NS_ERROR_FAILURE; + } + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (!caret) { + return NS_ERROR_FAILURE; + } + *_retval = caret->IsVisible(); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::SetCaretVisibilityDuringSelection( + bool aVisibility) { + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv; + RefPtr<PresShell> presShell = do_QueryReferent(mPresShellWeak, &rv); + if (!presShell) { + return NS_ERROR_FAILURE; + } + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (!caret) { + return NS_ERROR_FAILURE; + } + Selection* selection = mFrameSelection->GetSelection(SelectionType::eNormal); + if (selection) { + caret->SetVisibilityDuringSelection(aVisibility); + } + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::PhysicalMove(int16_t aDirection, int16_t aAmount, + bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->PhysicalMove(aDirection, aAmount, aExtend); +} + +NS_IMETHODIMP +TextInputSelectionController::CharacterMove(bool aForward, bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->CharacterMove(aForward, aExtend); +} + +NS_IMETHODIMP +TextInputSelectionController::WordMove(bool aForward, bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->WordMove(aForward, aExtend); +} + +NS_IMETHODIMP +TextInputSelectionController::LineMove(bool aForward, bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + nsresult result = frameSelection->LineMove(aForward, aExtend); + if (NS_FAILED(result)) { + result = CompleteMove(aForward, aExtend); + } + return result; +} + +NS_IMETHODIMP +TextInputSelectionController::IntraLineMove(bool aForward, bool aExtend) { + if (!mFrameSelection) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + return frameSelection->IntraLineMove(aForward, aExtend); +} + +NS_IMETHODIMP +TextInputSelectionController::PageMove(bool aForward, bool aExtend) { + // expected behavior for PageMove is to scroll AND move the caret + // and to remain relative position of the caret in view. see Bug 4302. + if (mScrollFrame) { + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + nsIFrame* scrollFrame = do_QueryFrame(mScrollFrame); + // We won't scroll parent scrollable element of mScrollFrame. Therefore, + // this may be handled when mScrollFrame is completely outside of the view. + // In such case, user may be confused since they might have wanted to + // scroll a parent scrollable element. For making clearer which element + // handles PageDown/PageUp, we should move selection into view even if + // selection is not changed. + return frameSelection->PageMove(aForward, aExtend, scrollFrame, + nsFrameSelection::SelectionIntoView::Yes); + } + // Similarly, if there is no scrollable frame, we should move the editor + // frame into the view for making it clearer which element handles + // PageDown/PageUp. + return ScrollSelectionIntoView( + nsISelectionController::SELECTION_NORMAL, + nsISelectionController::SELECTION_FOCUS_REGION, + nsISelectionController::SCROLL_SYNCHRONOUS | + nsISelectionController::SCROLL_FOR_CARET_MOVE); +} + +NS_IMETHODIMP +TextInputSelectionController::CompleteScroll(bool aForward) { + if (!mScrollFrame) { + return NS_ERROR_NOT_INITIALIZED; + } + + mScrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), ScrollUnit::WHOLE, + ScrollMode::Instant); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::CompleteMove(bool aForward, bool aExtend) { + if (NS_WARN_IF(!mFrameSelection)) { + return NS_ERROR_NULL_POINTER; + } + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + + // grab the parent / root DIV for this text widget + nsIContent* parentDIV = frameSelection->GetLimiter(); + if (!parentDIV) { + return NS_ERROR_UNEXPECTED; + } + + // make the caret be either at the very beginning (0) or the very end + int32_t offset = 0; + CaretAssociationHint hint = CaretAssociationHint::Before; + if (aForward) { + offset = parentDIV->GetChildCount(); + + // Prevent the caret from being placed after the last + // BR node in the content tree! + + if (offset > 0) { + nsIContent* child = parentDIV->GetLastChild(); + + if (child->IsHTMLElement(nsGkAtoms::br)) { + --offset; + hint = CaretAssociationHint::After; // for Bug 106855 + } + } + } + + const RefPtr<nsIContent> pinnedParentDIV{parentDIV}; + const nsFrameSelection::FocusMode focusMode = + aExtend ? nsFrameSelection::FocusMode::kExtendSelection + : nsFrameSelection::FocusMode::kCollapseToNewPoint; + frameSelection->HandleClick(pinnedParentDIV, offset, offset, focusMode, hint); + + // if we got this far, attempt to scroll no matter what the above result is + return CompleteScroll(aForward); +} + +NS_IMETHODIMP +TextInputSelectionController::ScrollPage(bool aForward) { + if (!mScrollFrame) { + return NS_ERROR_NOT_INITIALIZED; + } + + mScrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), ScrollUnit::PAGES, + ScrollMode::Smooth); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::ScrollLine(bool aForward) { + if (!mScrollFrame) { + return NS_ERROR_NOT_INITIALIZED; + } + + mScrollFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), ScrollUnit::LINES, + ScrollMode::Smooth); + return NS_OK; +} + +NS_IMETHODIMP +TextInputSelectionController::ScrollCharacter(bool aRight) { + if (!mScrollFrame) { + return NS_ERROR_NOT_INITIALIZED; + } + + mScrollFrame->ScrollBy(nsIntPoint(aRight ? 1 : -1, 0), ScrollUnit::LINES, + ScrollMode::Smooth); + return NS_OK; +} + +void TextInputSelectionController::SelectionWillTakeFocus() { + if (mFrameSelection) { + if (PresShell* shell = mFrameSelection->GetPresShell()) { + shell->FrameSelectionWillTakeFocus(*mFrameSelection); + } + } +} + +void TextInputSelectionController::SelectionWillLoseFocus() { + if (mFrameSelection) { + if (PresShell* shell = mFrameSelection->GetPresShell()) { + shell->FrameSelectionWillLoseFocus(*mFrameSelection); + } + } +} + +/***************************************************************************** + * mozilla::TextInputListener + *****************************************************************************/ + +TextInputListener::TextInputListener(TextControlElement* aTxtCtrlElement) + : mFrame(nullptr), + mTxtCtrlElement(aTxtCtrlElement), + mTextControlState(aTxtCtrlElement ? aTxtCtrlElement->GetTextControlState() + : nullptr), + mSelectionWasCollapsed(true), + mHadUndoItems(false), + mHadRedoItems(false), + mSettingValue(false), + mSetValueChanged(true), + mListeningToSelectionChange(false) {} + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TextInputListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TextInputListener) + +NS_INTERFACE_MAP_BEGIN(TextInputListener) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIDOMEventListener) + NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(TextInputListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION_CLASS(TextInputListener) +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TextInputListener) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TextInputListener) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +void TextInputListener::OnSelectionChange(Selection& aSelection, + int16_t aReason) { + if (!mListeningToSelectionChange) { + return; + } + + AutoWeakFrame weakFrame = mFrame; + + // Fire the select event + // The specs don't exactly say when we should fire the select event. + // IE: Whenever you add/remove a character to/from the selection. Also + // each time for select all. Also if you get to the end of the text + // field you will get new event for each keypress or a continuous + // stream of events if you use the mouse. IE will fire select event + // when the selection collapses to nothing if you are holding down + // the shift or mouse button. + // Mozilla: If we have non-empty selection we will fire a new event for each + // keypress (or mouseup) if the selection changed. Mozilla will also + // create the event each time select all is called, even if + // everything was previously selected, because technically select all + // will first collapse and then extend. Mozilla will never create an + // event if the selection collapses to nothing. + // FYI: If you want to skip dispatching eFormSelect event and if there are no + // event listeners, you can refer + // nsPIDOMWindow::HasFormSelectEventListeners(), but be careful about + // some C++ event handlers, e.g., HTMLTextAreaElement::PostHandleEvent(). + bool collapsed = aSelection.IsCollapsed(); + if (!collapsed && (aReason & (nsISelectionListener::MOUSEUP_REASON | + nsISelectionListener::KEYPRESS_REASON | + nsISelectionListener::SELECTALL_REASON))) { + if (nsCOMPtr<nsIContent> content = mFrame->GetContent()) { + if (nsCOMPtr<Document> doc = content->GetComposedDoc()) { + if (RefPtr<PresShell> presShell = doc->GetPresShell()) { + nsEventStatus status = nsEventStatus_eIgnore; + WidgetEvent event(true, eFormSelect); + + presShell->HandleEventWithTarget(&event, mFrame, content, &status); + } + } + } + } + + // if the collapsed state did not change, don't fire notifications + if (collapsed == mSelectionWasCollapsed) { + return; + } + + mSelectionWasCollapsed = collapsed; + + if (!weakFrame.IsAlive() || !mFrame || + !nsContentUtils::IsFocusedContent(mFrame->GetContent())) { + return; + } + + UpdateTextInputCommands(u"select"_ns); +} + +MOZ_CAN_RUN_SCRIPT +static void DoCommandCallback(Command aCommand, void* aData) { + nsTextControlFrame* frame = static_cast<nsTextControlFrame*>(aData); + nsIContent* content = frame->GetContent(); + + nsCOMPtr<nsIControllers> controllers; + HTMLInputElement* input = HTMLInputElement::FromNode(content); + if (input) { + input->GetControllers(getter_AddRefs(controllers)); + } else { + HTMLTextAreaElement* textArea = HTMLTextAreaElement::FromNode(content); + + if (textArea) { + textArea->GetControllers(getter_AddRefs(controllers)); + } + } + + if (!controllers) { + NS_WARNING("Could not get controllers"); + return; + } + + const char* commandStr = WidgetKeyboardEvent::GetCommandStr(aCommand); + + nsCOMPtr<nsIController> controller; + controllers->GetControllerForCommand(commandStr, getter_AddRefs(controller)); + if (!controller) { + return; + } + + bool commandEnabled; + if (NS_WARN_IF(NS_FAILED( + controller->IsCommandEnabled(commandStr, &commandEnabled)))) { + return; + } + if (commandEnabled) { + controller->DoCommand(commandStr); + } +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +TextInputListener::HandleEvent(Event* aEvent) { + if (aEvent->DefaultPrevented()) { + return NS_OK; + } + + if (!aEvent->IsTrusted()) { + return NS_OK; + } + + RefPtr<KeyboardEvent> keyEvent = aEvent->AsKeyboardEvent(); + if (!keyEvent) { + return NS_ERROR_UNEXPECTED; + } + + WidgetKeyboardEvent* widgetKeyEvent = + aEvent->WidgetEventPtr()->AsKeyboardEvent(); + if (!widgetKeyEvent) { + return NS_ERROR_UNEXPECTED; + } + + { + auto* input = HTMLInputElement::FromNode(mTxtCtrlElement); + if (input && input->StepsInputValue(*widgetKeyEvent)) { + // As an special case, don't handle key events that would step the value + // of our <input type=number>. + return NS_OK; + } + } + + auto ExecuteOurShortcutKeys = [&](TextControlElement& aTextControlElement) + MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> bool { + KeyEventHandler* keyHandlers = ShortcutKeys::GetHandlers( + aTextControlElement.IsTextArea() ? HandlerType::eTextArea + : HandlerType::eInput); + + RefPtr<nsAtom> eventTypeAtom = + ShortcutKeys::ConvertEventToDOMEventType(widgetKeyEvent); + for (KeyEventHandler* handler = keyHandlers; handler; + handler = handler->GetNextHandler()) { + if (!handler->EventTypeEquals(eventTypeAtom)) { + continue; + } + + if (!handler->KeyEventMatched(keyEvent, 0, IgnoreModifierState())) { + continue; + } + + // XXX Do we execute only one handler even if the handler neither stops + // propagation nor prevents default of the event? + nsresult rv = handler->ExecuteHandler(&aTextControlElement, aEvent); + if (NS_SUCCEEDED(rv)) { + return true; + } + } + return false; + }; + + auto ExecuteNativeKeyBindings = + [&](TextControlElement& aTextControlElement) + MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION -> bool { + if (widgetKeyEvent->mMessage != eKeyPress) { + return false; + } + + NativeKeyBindingsType nativeKeyBindingsType = + aTextControlElement.IsTextArea() + ? NativeKeyBindingsType::MultiLineEditor + : NativeKeyBindingsType::SingleLineEditor; + + nsIWidget* widget = widgetKeyEvent->mWidget; + // If the event is created by chrome script, the widget is nullptr. + if (MOZ_UNLIKELY(!widget)) { + widget = mFrame->GetNearestWidget(); + if (MOZ_UNLIKELY(NS_WARN_IF(!widget))) { + return false; + } + } + + // WidgetKeyboardEvent::ExecuteEditCommands() requires non-nullptr mWidget. + // If the event is created by chrome script, it is nullptr but we need to + // execute native key bindings. Therefore, we need to set widget to + // WidgetEvent::mWidget temporarily. + AutoRestore<nsCOMPtr<nsIWidget>> saveWidget(widgetKeyEvent->mWidget); + widgetKeyEvent->mWidget = widget; + if (widgetKeyEvent->ExecuteEditCommands(nativeKeyBindingsType, + DoCommandCallback, mFrame)) { + aEvent->PreventDefault(); + return true; + } + return false; + }; + + OwningNonNull<TextControlElement> textControlElement(*mTxtCtrlElement); + if (StaticPrefs:: + ui_key_textcontrol_prefer_native_key_bindings_over_builtin_shortcut_key_definitions()) { + if (!ExecuteNativeKeyBindings(textControlElement)) { + ExecuteOurShortcutKeys(textControlElement); + } + } else { + if (!ExecuteOurShortcutKeys(textControlElement)) { + ExecuteNativeKeyBindings(textControlElement); + } + } + return NS_OK; +} + +nsresult TextInputListener::OnEditActionHandled(TextEditor& aTextEditor) { + if (mFrame) { + // XXX Do we still need this or can we just remove the mFrame and + // frame.IsAlive() conditions below? + AutoWeakFrame weakFrame = mFrame; + + // Update the undo / redo menus + // + size_t numUndoItems = aTextEditor.NumberOfUndoItems(); + size_t numRedoItems = aTextEditor.NumberOfRedoItems(); + if ((numUndoItems && !mHadUndoItems) || (!numUndoItems && mHadUndoItems) || + (numRedoItems && !mHadRedoItems) || (!numRedoItems && mHadRedoItems)) { + // Modify the menu if undo or redo items are different + UpdateTextInputCommands(u"undo"_ns); + + mHadUndoItems = numUndoItems != 0; + mHadRedoItems = numRedoItems != 0; + } + + if (weakFrame.IsAlive()) { + HandleValueChanged(aTextEditor); + } + } + + return mTextControlState ? mTextControlState->OnEditActionHandled() : NS_OK; +} + +void TextInputListener::HandleValueChanged(TextEditor& aTextEditor) { + // Make sure we know we were changed (do NOT set this to false if there are + // no undo items; JS could change the value and we'd still need to save it) + if (mSetValueChanged) { + mTxtCtrlElement->SetValueChanged(true); + } + + if (!mSettingValue) { + // NOTE(emilio): execCommand might get here even though it might not be a + // "proper" user-interactive change. Might be worth reconsidering which + // ValueChangeKind are we passing down. + mTxtCtrlElement->OnValueChanged(ValueChangeKind::UserInteraction, + aTextEditor.IsEmpty(), nullptr); + if (mTextControlState) { + mTextControlState->ClearLastInteractiveValue(); + } + } +} + +nsresult TextInputListener::UpdateTextInputCommands( + const nsAString& aCommandsToUpdate) { + nsIContent* content = mFrame->GetContent(); + if (NS_WARN_IF(!content)) { + return NS_ERROR_FAILURE; + } + nsCOMPtr<Document> doc = content->GetComposedDoc(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + nsPIDOMWindowOuter* domWindow = doc->GetWindow(); + if (NS_WARN_IF(!domWindow)) { + return NS_ERROR_FAILURE; + } + domWindow->UpdateCommands(aCommandsToUpdate); + return NS_OK; +} + +/***************************************************************************** + * mozilla::AutoTextControlHandlingState + * + * This class is temporarily created in the stack and can manage nested + * handling state of TextControlState. While this instance exists, lifetime of + * TextControlState which created the instance is guaranteed. In other words, + * you can use this class as "kungFuDeathGrip" for TextControlState. + *****************************************************************************/ + +enum class TextControlAction { + CommitComposition, + Destructor, + PrepareEditor, + SetRangeText, + SetSelectionRange, + SetValue, + UnbindFromFrame, + Unlink, +}; + +class MOZ_STACK_CLASS AutoTextControlHandlingState { + public: + AutoTextControlHandlingState() = delete; + explicit AutoTextControlHandlingState(const AutoTextControlHandlingState&) = + delete; + AutoTextControlHandlingState(AutoTextControlHandlingState&&) = delete; + void operator=(AutoTextControlHandlingState&) = delete; + void operator=(const AutoTextControlHandlingState&) = delete; + + /** + * Generic constructor. If TextControlAction does not require additional + * data, must use this constructor. + */ + MOZ_CAN_RUN_SCRIPT AutoTextControlHandlingState( + TextControlState& aTextControlState, TextControlAction aTextControlAction) + : mParent(aTextControlState.mHandlingState), + mTextControlState(aTextControlState), + mTextCtrlElement(aTextControlState.mTextCtrlElement), + mTextInputListener(aTextControlState.mTextListener), + mTextControlAction(aTextControlAction) { + MOZ_ASSERT(aTextControlAction != TextControlAction::SetValue, + "Use specific constructor"); + MOZ_DIAGNOSTIC_ASSERT_IF( + !aTextControlState.mTextListener, + !aTextControlState.mBoundFrame || !aTextControlState.mTextEditor); + mTextControlState.mHandlingState = this; + if (Is(TextControlAction::CommitComposition)) { + MOZ_ASSERT(mParent); + MOZ_ASSERT(mParent->Is(TextControlAction::SetValue)); + // If we're trying to commit composition before handling SetValue, + // the parent old values will be outdated so that we need to clear + // them. + mParent->InvalidateOldValue(); + } + } + + /** + * TextControlAction::SetValue specific constructor. Current setting value + * must be specified and the creator should check whether we succeeded to + * allocate memory for line breaker conversion. + */ + MOZ_CAN_RUN_SCRIPT AutoTextControlHandlingState( + TextControlState& aTextControlState, TextControlAction aTextControlAction, + const nsAString& aSettingValue, const nsAString* aOldValue, + const ValueSetterOptions& aOptions, ErrorResult& aRv) + : mParent(aTextControlState.mHandlingState), + mTextControlState(aTextControlState), + mTextCtrlElement(aTextControlState.mTextCtrlElement), + mTextInputListener(aTextControlState.mTextListener), + mSettingValue(aSettingValue), + mOldValue(aOldValue), + mValueSetterOptions(aOptions), + mTextControlAction(aTextControlAction) { + MOZ_ASSERT(aTextControlAction == TextControlAction::SetValue, + "Use generic constructor"); + MOZ_DIAGNOSTIC_ASSERT_IF( + !aTextControlState.mTextListener, + !aTextControlState.mBoundFrame || !aTextControlState.mTextEditor); + mTextControlState.mHandlingState = this; + if (!nsContentUtils::PlatformToDOMLineBreaks(mSettingValue, fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + // Update all setting value's new value because older value shouldn't + // overwrite newer value. + if (mParent) { + // If SetValue is nested, parents cannot trust their old value anymore. + // So, we need to clear them. + mParent->UpdateSettingValueAndInvalidateOldValue(mSettingValue); + } + } + + MOZ_CAN_RUN_SCRIPT ~AutoTextControlHandlingState() { + mTextControlState.mHandlingState = mParent; + if (!mParent && mTextControlStateDestroyed) { + mTextControlState.DeleteOrCacheForReuse(); + } + if (!mTextControlStateDestroyed && mPrepareEditorLater) { + MOZ_ASSERT(nsContentUtils::IsSafeToRunScript()); + MOZ_ASSERT(Is(TextControlAction::SetValue)); + mTextControlState.PrepareEditor(&mSettingValue); + } + } + + void OnDestroyTextControlState() { + if (IsHandling(TextControlAction::Destructor)) { + // Do nothing since mTextContrlState.DeleteOrCacheForReuse() has + // already been called. + return; + } + mTextControlStateDestroyed = true; + if (mParent) { + mParent->OnDestroyTextControlState(); + } + } + + void PrepareEditorLater() { + MOZ_ASSERT(IsHandling(TextControlAction::SetValue)); + MOZ_ASSERT(!IsHandling(TextControlAction::PrepareEditor)); + // Look for the top most SetValue. + AutoTextControlHandlingState* settingValue = nullptr; + for (AutoTextControlHandlingState* handlingSomething = this; + handlingSomething; handlingSomething = handlingSomething->mParent) { + if (handlingSomething->Is(TextControlAction::SetValue)) { + settingValue = handlingSomething; + } + } + settingValue->mPrepareEditorLater = true; + } + + /** + * WillSetValueWithTextEditor() is called when TextControlState sets + * value with its mTextEditor. + */ + void WillSetValueWithTextEditor() { + MOZ_ASSERT(Is(TextControlAction::SetValue)); + MOZ_ASSERT(mTextControlState.mBoundFrame); + mTextControlFrame = mTextControlState.mBoundFrame; + // If we'reemulating user input, we don't need to manage mTextInputListener + // by ourselves since everything should be handled by TextEditor as normal + // user input. + if (mValueSetterOptions.contains(ValueSetterOption::BySetUserInputAPI)) { + return; + } + // Otherwise, if we're setting the value programatically, we need to manage + // mTextInputListener by ourselves since TextEditor users special path + // for the performance. + mTextInputListener->SettingValue(true); + mTextInputListener->SetValueChanged( + mValueSetterOptions.contains(ValueSetterOption::SetValueChanged)); + mEditActionHandled = false; + // Even if falling back to `TextControlState::SetValueWithoutTextEditor()` + // due to editor destruction, it shouldn't dispatch "beforeinput" event + // anymore. Therefore, we should mark that we've already dispatched + // "beforeinput" event. + WillDispatchBeforeInputEvent(); + } + + /** + * WillDispatchBeforeInputEvent() is called immediately before dispatching + * "beforeinput" event in `TextControlState`. + */ + void WillDispatchBeforeInputEvent() { + mBeforeInputEventHasBeenDispatched = true; + } + + /** + * OnEditActionHandled() is called when the TextEditor handles something + * and immediately before dispatching "input" event. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult OnEditActionHandled() { + MOZ_ASSERT(!mEditActionHandled); + mEditActionHandled = true; + if (!Is(TextControlAction::SetValue)) { + return NS_OK; + } + if (!mValueSetterOptions.contains(ValueSetterOption::BySetUserInputAPI)) { + mTextInputListener->SetValueChanged(true); + mTextInputListener->SettingValue( + mParent && mParent->IsHandling(TextControlAction::SetValue)); + } + if (!IsOriginalTextControlFrameAlive()) { + return SetValueWithoutTextEditorAgain() ? NS_OK : NS_ERROR_OUT_OF_MEMORY; + } + // The new value never includes line breaks caused by hard-wrap. + // So, mCachedValue can always cache the new value. + nsITextControlFrame* textControlFrame = + do_QueryFrame(mTextControlFrame.GetFrame()); + return static_cast<nsTextControlFrame*>(textControlFrame) + ->CacheValue(mSettingValue, fallible) + ? NS_OK + : NS_ERROR_OUT_OF_MEMORY; + } + + /** + * SetValueWithoutTextEditorAgain() should be called if the frame for + * mTextControlState was destroyed during setting value. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool SetValueWithoutTextEditorAgain() { + MOZ_ASSERT(!IsOriginalTextControlFrameAlive()); + // If the frame was destroyed because of a flush somewhere inside + // TextEditor, mBoundFrame here will be nullptr. But it's also + // possible for the frame to go away because of another reason (such + // as deleting the existing selection -- see bug 574558), in which + // case we don't need to reset the value here. + if (mTextControlState.mBoundFrame) { + return true; + } + // XXX It's odd to drop flags except + // ValueSetterOption::SetValueChanged. + // Probably, this intended to drop ValueSetterOption::BySetUserInputAPI + // and ValueSetterOption::ByContentAPI, but other flags are added later. + ErrorResult error; + AutoTextControlHandlingState handlingSetValueWithoutEditor( + mTextControlState, TextControlAction::SetValue, mSettingValue, + mOldValue, mValueSetterOptions & ValueSetterOption::SetValueChanged, + error); + if (error.Failed()) { + MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY)); + error.SuppressException(); + return false; + } + return mTextControlState.SetValueWithoutTextEditor( + handlingSetValueWithoutEditor); + } + + bool IsTextControlStateDestroyed() const { + return mTextControlStateDestroyed; + } + bool IsOriginalTextControlFrameAlive() const { + return const_cast<AutoTextControlHandlingState*>(this) + ->mTextControlFrame.IsAlive(); + } + bool HasEditActionHandled() const { return mEditActionHandled; } + bool HasBeforeInputEventDispatched() const { + return mBeforeInputEventHasBeenDispatched; + } + bool Is(TextControlAction aTextControlAction) const { + return mTextControlAction == aTextControlAction; + } + bool IsHandling(TextControlAction aTextControlAction) const { + if (mTextControlAction == aTextControlAction) { + return true; + } + return mParent && mParent->IsHandling(aTextControlAction); + } + TextControlElement* GetTextControlElement() const { return mTextCtrlElement; } + TextInputListener* GetTextInputListener() const { return mTextInputListener; } + const ValueSetterOptions& ValueSetterOptionsRef() const { + MOZ_ASSERT(Is(TextControlAction::SetValue)); + return mValueSetterOptions; + } + const nsAString* GetOldValue() const { + MOZ_ASSERT(Is(TextControlAction::SetValue)); + return mOldValue; + } + const nsString& GetSettingValue() const { + MOZ_ASSERT(IsHandling(TextControlAction::SetValue)); + if (mTextControlAction == TextControlAction::SetValue) { + return mSettingValue; + } + return mParent->GetSettingValue(); + } + + private: + void UpdateSettingValueAndInvalidateOldValue(const nsString& aSettingValue) { + if (mTextControlAction == TextControlAction::SetValue) { + mSettingValue = aSettingValue; + } + mOldValue = nullptr; + if (mParent) { + mParent->UpdateSettingValueAndInvalidateOldValue(aSettingValue); + } + } + void InvalidateOldValue() { + mOldValue = nullptr; + if (mParent) { + mParent->InvalidateOldValue(); + } + } + + AutoTextControlHandlingState* const mParent; + TextControlState& mTextControlState; + // mTextControlFrame should be set immediately before calling methods + // which may destroy the frame. Then, you can check whether the frame + // was destroyed/replaced. + AutoWeakFrame mTextControlFrame; + // mTextCtrlElement grabs TextControlState::mTextCtrlElement since + // if the text control element releases mTextControlState, only this + // can guarantee the instance of the text control element. + RefPtr<TextControlElement> const mTextCtrlElement; + // mTextInputListener grabs TextControlState::mTextListener because if + // TextControlState is unbind from the frame, it's released. + RefPtr<TextInputListener> const mTextInputListener; + nsAutoString mSettingValue; + const nsAString* mOldValue = nullptr; + ValueSetterOptions mValueSetterOptions; + TextControlAction const mTextControlAction; + bool mTextControlStateDestroyed = false; + bool mEditActionHandled = false; + bool mPrepareEditorLater = false; + bool mBeforeInputEventHasBeenDispatched = false; +}; + +/***************************************************************************** + * mozilla::TextControlState + *****************************************************************************/ + +/** + * For avoiding allocation cost of the instance, we should reuse instances + * as far as possible. + * + * FYI: `25` is just a magic number considered without enough investigation, + * but at least, this value must not make damage for footprint. + * Feel free to change it if you find better number. + */ +static constexpr size_t kMaxCountOfCacheToReuse = 25; +static AutoTArray<void*, kMaxCountOfCacheToReuse>* sReleasedInstances = nullptr; +static bool sHasShutDown = false; + +TextControlState::TextControlState(TextControlElement* aOwningElement) + : mTextCtrlElement(aOwningElement), + mEverInited(false), + mEditorInitialized(false), + mValueTransferInProgress(false), + mSelectionCached(true) +// When adding more member variable initializations here, add the same +// also to ::Construct. +{ + MOZ_COUNT_CTOR(TextControlState); + static_assert(sizeof(*this) <= 128, + "Please keep small TextControlState as far as possible"); +} + +TextControlState* TextControlState::Construct( + TextControlElement* aOwningElement) { + void* mem; + if (sReleasedInstances && !sReleasedInstances->IsEmpty()) { + mem = sReleasedInstances->PopLastElement(); + } else { + mem = moz_xmalloc(sizeof(TextControlState)); + } + + return new (mem) TextControlState(aOwningElement); +} + +TextControlState::~TextControlState() { + MOZ_ASSERT(!mHandlingState); + MOZ_COUNT_DTOR(TextControlState); + AutoTextControlHandlingState handlingDesctructor( + *this, TextControlAction::Destructor); + Clear(); +} + +void TextControlState::Shutdown() { + sHasShutDown = true; + if (sReleasedInstances) { + for (void* mem : *sReleasedInstances) { + free(mem); + } + delete sReleasedInstances; + } +} + +void TextControlState::Destroy() { + // If we're handling something, we should be deleted later. + if (mHandlingState) { + mHandlingState->OnDestroyTextControlState(); + return; + } + DeleteOrCacheForReuse(); + // Note that this instance may have already been deleted here. Don't touch + // any members. +} + +void TextControlState::DeleteOrCacheForReuse() { + MOZ_ASSERT(!IsBusy()); + + void* mem = this; + this->~TextControlState(); + + // If we can cache this instance, we should do it instead of deleting it. + if (!sHasShutDown && (!sReleasedInstances || sReleasedInstances->Length() < + kMaxCountOfCacheToReuse)) { + // Put this instance to the cache. Note that now, the array may be full, + // but it's not problem to cache more instances than kMaxCountOfCacheToReuse + // because it just requires reallocation cost of the array buffer. + if (!sReleasedInstances) { + sReleasedInstances = new AutoTArray<void*, kMaxCountOfCacheToReuse>; + } + sReleasedInstances->AppendElement(mem); + } else { + free(mem); + } +} + +nsresult TextControlState::OnEditActionHandled() { + return mHandlingState ? mHandlingState->OnEditActionHandled() : NS_OK; +} + +Element* TextControlState::GetRootNode() { + return mBoundFrame ? mBoundFrame->GetRootNode() : nullptr; +} + +Element* TextControlState::GetPreviewNode() { + return mBoundFrame ? mBoundFrame->GetPreviewNode() : nullptr; +} + +void TextControlState::Clear() { + MOZ_ASSERT(mHandlingState); + MOZ_ASSERT(mHandlingState->Is(TextControlAction::Destructor) || + mHandlingState->Is(TextControlAction::Unlink)); + if (mTextEditor) { + mTextEditor->SetTextInputListener(nullptr); + } + + if (mBoundFrame) { + // Oops, we still have a frame! + // This should happen when the type of a text input control is being changed + // to something which is not a text control. In this case, we should + // pretend that a frame is being destroyed, and clean up after ourselves + // properly. + UnbindFromFrame(mBoundFrame); + mTextEditor = nullptr; + } else { + // If we have a bound frame around, UnbindFromFrame will call DestroyEditor + // for us. + DestroyEditor(); + MOZ_DIAGNOSTIC_ASSERT(!mBoundFrame || !mTextEditor); + } + mTextListener = nullptr; +} + +void TextControlState::Unlink() { + AutoTextControlHandlingState handlingUnlink(*this, TextControlAction::Unlink); + UnlinkInternal(); +} + +void TextControlState::UnlinkInternal() { + MOZ_ASSERT(mHandlingState); + MOZ_ASSERT(mHandlingState->Is(TextControlAction::Unlink)); + TextControlState* tmp = this; + tmp->Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelCon) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextEditor) +} + +void TextControlState::Traverse(nsCycleCollectionTraversalCallback& cb) { + TextControlState* tmp = this; + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelCon) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTextEditor) +} + +nsFrameSelection* TextControlState::GetConstFrameSelection() { + return mSelCon ? mSelCon->GetConstFrameSelection() : nullptr; +} + +TextEditor* TextControlState::GetTextEditor() { + // Note that if the instance is destroyed in PrepareEditor(), it returns + // NS_ERROR_NOT_INITIALIZED so that we don't need to create kungFuDeathGrip + // in this hot path. + if (!mTextEditor && NS_WARN_IF(NS_FAILED(PrepareEditor()))) { + return nullptr; + } + return mTextEditor; +} + +TextEditor* TextControlState::GetTextEditorWithoutCreation() const { + return mTextEditor; +} + +nsISelectionController* TextControlState::GetSelectionController() const { + return mSelCon; +} + +// Helper class, used below in BindToFrame(). +class PrepareEditorEvent : public Runnable { + public: + PrepareEditorEvent(TextControlState& aState, nsIContent* aOwnerContent, + const nsAString& aCurrentValue) + : Runnable("PrepareEditorEvent"), + mState(&aState), + mOwnerContent(aOwnerContent), + mCurrentValue(aCurrentValue) { + aState.mValueTransferInProgress = true; + } + + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + if (NS_WARN_IF(!mState)) { + return NS_ERROR_NULL_POINTER; + } + + // Transfer the saved value to the editor if we have one + const nsAString* value = nullptr; + if (!mCurrentValue.IsEmpty()) { + value = &mCurrentValue; + } + + nsAutoScriptBlocker scriptBlocker; + + mState->PrepareEditor(value); + + mState->mValueTransferInProgress = false; + + return NS_OK; + } + + private: + WeakPtr<TextControlState> mState; + nsCOMPtr<nsIContent> mOwnerContent; // strong reference + nsAutoString mCurrentValue; +}; + +nsresult TextControlState::BindToFrame(nsTextControlFrame* aFrame) { + MOZ_ASSERT( + !nsContentUtils::IsSafeToRunScript(), + "TextControlState::BindToFrame() has to be called with script blocker"); + NS_ASSERTION(aFrame, "The frame to bind to should be valid"); + if (!aFrame) { + return NS_ERROR_INVALID_ARG; + } + + NS_ASSERTION(!mBoundFrame, "Cannot bind twice, need to unbind first"); + if (mBoundFrame) { + return NS_ERROR_FAILURE; + } + + // If we'll need to transfer our current value to the editor, save it before + // binding to the frame. + nsAutoString currentValue; + if (mTextEditor) { + GetValue(currentValue, true, /* aForDisplay = */ false); + } + + mBoundFrame = aFrame; + + Element* rootNode = aFrame->GetRootNode(); + MOZ_ASSERT(rootNode); + + PresShell* presShell = aFrame->PresContext()->GetPresShell(); + MOZ_ASSERT(presShell); + + // Create a SelectionController + mSelCon = new TextInputSelectionController(presShell, rootNode); + MOZ_ASSERT(!mTextListener, "Should not overwrite the object"); + mTextListener = new TextInputListener(mTextCtrlElement); + + mTextListener->SetFrame(mBoundFrame); + + // Editor will override this as needed from InitializeSelection. + mSelCon->SetDisplaySelection(nsISelectionController::SELECTION_HIDDEN); + + // Get the caret and make it a selection listener. + // FYI: It's safe to use raw pointer for calling + // Selection::AddSelectionListner() because it only appends the listener + // to its internal array. + Selection* selection = mSelCon->GetSelection(SelectionType::eNormal); + if (selection) { + RefPtr<nsCaret> caret = presShell->GetCaret(); + if (caret) { + selection->AddSelectionListener(caret); + } + mTextListener->StartToListenToSelectionChange(); + } + + // If an editor exists from before, prepare it for usage + if (mTextEditor) { + if (NS_WARN_IF(!mTextCtrlElement)) { + return NS_ERROR_FAILURE; + } + + // Set the correct direction on the newly created root node + if (mTextEditor->IsRightToLeft()) { + rootNode->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, u"rtl"_ns, false); + } else if (mTextEditor->IsLeftToRight()) { + rootNode->SetAttr(kNameSpaceID_None, nsGkAtoms::dir, u"ltr"_ns, false); + } else { + // otherwise, inherit the content node's direction + } + + nsContentUtils::AddScriptRunner( + new PrepareEditorEvent(*this, mTextCtrlElement, currentValue)); + } + + return NS_OK; +} + +struct MOZ_STACK_CLASS PreDestroyer { + void Init(TextEditor* aTextEditor) { mTextEditor = aTextEditor; } + ~PreDestroyer() { + if (mTextEditor) { + // In this case, we don't need to restore the unmasked range of password + // editor. + UniquePtr<PasswordMaskData> passwordMaskData = mTextEditor->PreDestroy(); + } + } + void Swap(RefPtr<TextEditor>& aTextEditor) { + return mTextEditor.swap(aTextEditor); + } + + private: + RefPtr<TextEditor> mTextEditor; +}; + +nsresult TextControlState::PrepareEditor(const nsAString* aValue) { + if (!mBoundFrame) { + // Cannot create an editor without a bound frame. + // Don't return a failure code, because js callers can't handle that. + return NS_OK; + } + + if (mEditorInitialized) { + // Do not initialize the editor multiple times. + return NS_OK; + } + + AutoHideSelectionChanges hideSelectionChanges(GetConstFrameSelection()); + + if (mHandlingState) { + // Don't attempt to initialize recursively! + if (mHandlingState->IsHandling(TextControlAction::PrepareEditor)) { + return NS_ERROR_NOT_INITIALIZED; + } + // Reschedule creating editor later if we're setting value. + if (mHandlingState->IsHandling(TextControlAction::SetValue)) { + mHandlingState->PrepareEditorLater(); + return NS_ERROR_NOT_INITIALIZED; + } + } + + MOZ_ASSERT(mTextCtrlElement); + + AutoTextControlHandlingState preparingEditor( + *this, TextControlAction::PrepareEditor); + + // Note that we don't check mTextEditor here, because we might already have + // one around, in which case we don't create a new one, and we'll just tie + // the required machinery to it. + + nsPresContext* presContext = mBoundFrame->PresContext(); + PresShell* presShell = presContext->GetPresShell(); + + // Setup the editor flags + + // Spell check is diabled at creation time. It is enabled once + // the editor comes into focus. + uint32_t editorFlags = nsIEditor::eEditorSkipSpellCheck; + + if (IsSingleLineTextControl()) { + editorFlags |= nsIEditor::eEditorSingleLineMask; + } + if (IsPasswordTextControl()) { + editorFlags |= nsIEditor::eEditorPasswordMask; + } + + bool shouldInitializeEditor = false; + RefPtr<TextEditor> newTextEditor; // the editor that we might create + PreDestroyer preDestroyer; + if (!mTextEditor) { + shouldInitializeEditor = true; + + // Create an editor + newTextEditor = new TextEditor(); + preDestroyer.Init(newTextEditor); + + // Make sure we clear out the non-breaking space before we initialize the + // editor + nsresult rv = mBoundFrame->UpdateValueDisplay(true, true); + if (NS_FAILED(rv)) { + NS_WARNING("nsTextControlFrame::UpdateValueDisplay() failed"); + return rv; + } + } else { + if (aValue || !mEditorInitialized) { + // Set the correct value in the root node + nsresult rv = + mBoundFrame->UpdateValueDisplay(true, !mEditorInitialized, aValue); + if (NS_FAILED(rv)) { + NS_WARNING("nsTextControlFrame::UpdateValueDisplay() failed"); + return rv; + } + } + + newTextEditor = mTextEditor; // just pretend that we have a new editor! + + // Don't lose application flags in the process. + if (newTextEditor->IsMailEditor()) { + editorFlags |= nsIEditor::eEditorMailMask; + } + } + + // Get the current value of the textfield from the content. + // Note that if we've created a new editor, mTextEditor is null at this stage, + // so we will get the real value from the content. + nsAutoString defaultValue; + if (aValue) { + defaultValue = *aValue; + } else { + GetValue(defaultValue, true, /* aForDisplay = */ true); + } + + if (!mEditorInitialized) { + // Now initialize the editor. + // + // NOTE: Conversion of '\n' to <BR> happens inside the + // editor's Init() call. + + // Get the DOM document + nsCOMPtr<Document> doc = presShell->GetDocument(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + + // What follows is a bit of a hack. The editor uses the public DOM APIs + // for its content manipulations, and it causes it to fail some security + // checks deep inside when initializing. So we explictly make it clear that + // we're native code. + // Note that any script that's directly trying to access our value + // has to be going through some scriptable object to do that and that + // already does the relevant security checks. + AutoNoJSAPI nojsapi; + + RefPtr<Element> anonymousDivElement = GetRootNode(); + if (NS_WARN_IF(!anonymousDivElement) || NS_WARN_IF(!mSelCon)) { + return NS_ERROR_FAILURE; + } + OwningNonNull<TextInputSelectionController> selectionController(*mSelCon); + UniquePtr<PasswordMaskData> passwordMaskData; + if (editorFlags & nsIEditor::eEditorPasswordMask) { + if (mPasswordMaskData) { + passwordMaskData = std::move(mPasswordMaskData); + } else { + passwordMaskData = MakeUnique<PasswordMaskData>(); + } + } else { + mPasswordMaskData = nullptr; + } + nsresult rv = + newTextEditor->Init(*doc, *anonymousDivElement, selectionController, + editorFlags, std::move(passwordMaskData)); + if (NS_FAILED(rv)) { + NS_WARNING("TextEditor::Init() failed"); + return rv; + } + } + + // Initialize the controller for the editor + + nsresult rv = NS_OK; + if (!SuppressEventHandlers(presContext)) { + nsCOMPtr<nsIControllers> controllers; + if (auto* inputElement = HTMLInputElement::FromNode(mTextCtrlElement)) { + nsresult rv = inputElement->GetControllers(getter_AddRefs(controllers)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } else { + auto* textAreaElement = HTMLTextAreaElement::FromNode(mTextCtrlElement); + if (!textAreaElement) { + return NS_ERROR_FAILURE; + } + + nsresult rv = + textAreaElement->GetControllers(getter_AddRefs(controllers)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (controllers) { + // XXX Oddly, nsresult value is overwritten in the following loop, and + // only the last result or `found` decides the value. + uint32_t numControllers; + bool found = false; + rv = controllers->GetControllerCount(&numControllers); + for (uint32_t i = 0; i < numControllers; i++) { + nsCOMPtr<nsIController> controller; + rv = controllers->GetControllerAt(i, getter_AddRefs(controller)); + if (NS_SUCCEEDED(rv) && controller) { + nsCOMPtr<nsIControllerContext> editController = + do_QueryInterface(controller); + if (editController) { + editController->SetCommandContext( + static_cast<nsIEditor*>(newTextEditor)); + found = true; + } + } + } + if (!found) { + rv = NS_ERROR_FAILURE; + } + } + } + + // Initialize the plaintext editor + if (shouldInitializeEditor) { + const int32_t wrapCols = GetWrapCols(); + MOZ_ASSERT(wrapCols >= 0); + newTextEditor->SetWrapColumn(wrapCols); + } + + // Set max text field length + newTextEditor->SetMaxTextLength(mTextCtrlElement->UsedMaxLength()); + + editorFlags = newTextEditor->Flags(); + + // Check if the readonly/disabled attributes are set. + if (mTextCtrlElement->IsDisabledOrReadOnly()) { + editorFlags |= nsIEditor::eEditorReadonlyMask; + } + + SetEditorFlagsIfNecessary(*newTextEditor, editorFlags); + + if (shouldInitializeEditor) { + // Hold on to the newly created editor + preDestroyer.Swap(mTextEditor); + } + + // If we have a default value, insert it under the div we created + // above, but be sure to use the editor so that '*' characters get + // displayed for password fields, etc. SetValue() will call the + // editor for us. + + if (!defaultValue.IsEmpty()) { + // XXX rv may store error code which indicates there is no controller. + // However, we overwrite it only in this case. + rv = SetEditorFlagsIfNecessary(*newTextEditor, editorFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Now call SetValue() which will make the necessary editor calls to set + // the default value. Make sure to turn off undo before setting the default + // value, and turn it back on afterwards. This will make sure we can't undo + // past the default value. + // So, we use ValueSetterOption::ByInternalAPI only that it will turn off + // undo. + + if (NS_WARN_IF(!SetValue(defaultValue, ValueSetterOption::ByInternalAPI))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // Now restore the original editor flags. + rv = SetEditorFlagsIfNecessary(*newTextEditor, editorFlags); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + // When the default value is empty, we don't call SetValue(). That means that + // we have not notified IMEContentObserver of the empty value when the + // <textarea> is not dirty (i.e., the default value is mirrored into the + // anonymous subtree asynchronously) and the value was changed during a + // reframe (i.e., while IMEContentObserver was not observing the mutation of + // the anonymous subtree). Therefore, we notify IMEContentObserver here in + // that case. + else if (mTextCtrlElement && mTextCtrlElement->IsTextArea() && + !mTextCtrlElement->ValueChanged()) { + MOZ_ASSERT(defaultValue.IsEmpty()); + IMEContentObserver* observer = GetIMEContentObserver(); + if (observer && observer->WasInitializedWith(*newTextEditor)) { + observer->OnTextControlValueChangedWhileNotObservable(defaultValue); + } + } + + DebugOnly<bool> enabledUndoRedo = + newTextEditor->EnableUndoRedo(TextControlElement::DEFAULT_UNDO_CAP); + NS_WARNING_ASSERTION(enabledUndoRedo, + "Failed to enable undo/redo transaction"); + + if (!mEditorInitialized) { + newTextEditor->PostCreate(); + mEverInited = true; + mEditorInitialized = true; + } + + if (mTextListener) { + newTextEditor->SetTextInputListener(mTextListener); + } + + // Restore our selection after being bound to a new frame + if (mSelectionCached) { + if (mRestoringSelection) { // paranoia + mRestoringSelection->Revoke(); + } + mRestoringSelection = new RestoreSelectionState(this, mBoundFrame); + if (mRestoringSelection) { + nsContentUtils::AddScriptRunner(mRestoringSelection); + } + } + + // The selection cache is no longer going to be valid. + // + // XXXbz Shouldn't we do this at the point when we're actually about to + // restore the properties or something? As things stand, if UnbindFromFrame + // happens before our RestoreSelectionState runs, it looks like we'll lose our + // selection info, because we will think we don't have it cached and try to + // read it from the selection controller, which will not have it yet. + mSelectionCached = false; + + return preparingEditor.IsTextControlStateDestroyed() + ? NS_ERROR_NOT_INITIALIZED + : rv; +} + +void TextControlState::FinishedRestoringSelection() { + mRestoringSelection = nullptr; +} + +void TextControlState::SyncUpSelectionPropertiesBeforeDestruction() { + if (mBoundFrame) { + UnbindFromFrame(mBoundFrame); + } +} + +void TextControlState::SetSelectionProperties( + TextControlState::SelectionProperties& aProps) { + if (mBoundFrame) { + mBoundFrame->SetSelectionRange(aProps.GetStart(), aProps.GetEnd(), + aProps.GetDirection()); + // The instance may have already been deleted here. + } else { + mSelectionProperties = aProps; + } +} + +void TextControlState::GetSelectionRange(uint32_t* aSelectionStart, + uint32_t* aSelectionEnd, + ErrorResult& aRv) { + MOZ_ASSERT(aSelectionStart); + MOZ_ASSERT(aSelectionEnd); + MOZ_ASSERT(IsSelectionCached() || GetSelectionController(), + "How can we not have a cached selection if we have no selection " + "controller?"); + + // Note that we may have both IsSelectionCached() _and_ + // GetSelectionController() if we haven't initialized our editor yet. + if (IsSelectionCached()) { + const SelectionProperties& props = GetSelectionProperties(); + *aSelectionStart = props.GetStart(); + *aSelectionEnd = props.GetEnd(); + return; + } + + Selection* sel = mSelCon->GetSelection(SelectionType::eNormal); + if (NS_WARN_IF(!sel)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + Element* root = GetRootNode(); + if (NS_WARN_IF(!root)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + nsContentUtils::GetSelectionInTextControl(sel, root, *aSelectionStart, + *aSelectionEnd); +} + +SelectionDirection TextControlState::GetSelectionDirection(ErrorResult& aRv) { + MOZ_ASSERT(IsSelectionCached() || GetSelectionController(), + "How can we not have a cached selection if we have no selection " + "controller?"); + + // Note that we may have both IsSelectionCached() _and_ + // GetSelectionController() if we haven't initialized our editor yet. + if (IsSelectionCached()) { + return GetSelectionProperties().GetDirection(); + } + + Selection* sel = mSelCon->GetSelection(SelectionType::eNormal); + if (NS_WARN_IF(!sel)) { + aRv.Throw(NS_ERROR_FAILURE); + return SelectionDirection::Forward; + } + + nsDirection direction = sel->GetDirection(); + if (direction == eDirNext) { + return SelectionDirection::Forward; + } + + MOZ_ASSERT(direction == eDirPrevious); + return SelectionDirection::Backward; +} + +void TextControlState::SetSelectionRange(uint32_t aStart, uint32_t aEnd, + SelectionDirection aDirection, + ErrorResult& aRv, + ScrollAfterSelection aScroll) { + MOZ_ASSERT(IsSelectionCached() || mBoundFrame, + "How can we have a non-cached selection but no frame?"); + + AutoTextControlHandlingState handlingSetSelectionRange( + *this, TextControlAction::SetSelectionRange); + + if (aStart > aEnd) { + aStart = aEnd; + } + + if (!IsSelectionCached()) { + MOZ_ASSERT(mBoundFrame, "Our frame should still be valid"); + aRv = mBoundFrame->SetSelectionRange(aStart, aEnd, aDirection); + if (aRv.Failed() || + handlingSetSelectionRange.IsTextControlStateDestroyed()) { + return; + } + if (aScroll == ScrollAfterSelection::Yes && mBoundFrame) { + // mBoundFrame could be gone if selection listeners flushed layout for + // example. + mBoundFrame->ScrollSelectionIntoViewAsync(); + } + return; + } + + SelectionProperties& props = GetSelectionProperties(); + if (!props.HasMaxLength()) { + // A clone without a dirty value flag may not have a max length yet + nsAutoString value; + GetValue(value, false, /* aForDisplay = */ true); + props.SetMaxLength(value.Length()); + } + + bool changed = props.SetStart(aStart); + changed |= props.SetEnd(aEnd); + changed |= props.SetDirection(aDirection); + + if (!changed) { + return; + } + + // It sure would be nice if we had an existing Element* or so to work with. + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(mTextCtrlElement, eFormSelect, CanBubble::eYes); + asyncDispatcher->PostDOMEvent(); + + // SelectionChangeEventDispatcher covers this when !IsSelectionCached(). + // XXX(krosylight): Shouldn't it fire before select event? + // Currently Gecko and Blink both fire selectionchange after select. + if (IsSelectionCached() && + StaticPrefs::dom_select_events_textcontrols_selectionchange_enabled()) { + asyncDispatcher = new AsyncEventDispatcher( + mTextCtrlElement, eSelectionChange, CanBubble::eYes); + asyncDispatcher->PostDOMEvent(); + } +} + +void TextControlState::SetSelectionStart(const Nullable<uint32_t>& aStart, + ErrorResult& aRv) { + uint32_t start = 0; + if (!aStart.IsNull()) { + start = aStart.Value(); + } + + uint32_t ignored, end; + GetSelectionRange(&ignored, &end, aRv); + if (aRv.Failed()) { + return; + } + + SelectionDirection dir = GetSelectionDirection(aRv); + if (aRv.Failed()) { + return; + } + + if (end < start) { + end = start; + } + + SetSelectionRange(start, end, dir, aRv); + // The instance may have already been deleted here. +} + +void TextControlState::SetSelectionEnd(const Nullable<uint32_t>& aEnd, + ErrorResult& aRv) { + uint32_t end = 0; + if (!aEnd.IsNull()) { + end = aEnd.Value(); + } + + uint32_t start, ignored; + GetSelectionRange(&start, &ignored, aRv); + if (aRv.Failed()) { + return; + } + + SelectionDirection dir = GetSelectionDirection(aRv); + if (aRv.Failed()) { + return; + } + + SetSelectionRange(start, end, dir, aRv); + // The instance may have already been deleted here. +} + +static void DirectionToName(SelectionDirection dir, nsAString& aDirection) { + switch (dir) { + case SelectionDirection::None: + // TODO(mbrodesser): this should be supported, see + // https://bugzilla.mozilla.org/show_bug.cgi?id=1541454. + NS_WARNING("We don't actually support this... how did we get it?"); + return aDirection.AssignLiteral("none"); + case SelectionDirection::Forward: + return aDirection.AssignLiteral("forward"); + case SelectionDirection::Backward: + return aDirection.AssignLiteral("backward"); + } + MOZ_ASSERT_UNREACHABLE("Invalid SelectionDirection value"); +} + +void TextControlState::GetSelectionDirectionString(nsAString& aDirection, + ErrorResult& aRv) { + SelectionDirection dir = GetSelectionDirection(aRv); + if (aRv.Failed()) { + return; + } + DirectionToName(dir, aDirection); +} + +static SelectionDirection DirectionStringToSelectionDirection( + const nsAString& aDirection) { + if (aDirection.EqualsLiteral("backward")) { + return SelectionDirection::Backward; + } + // We don't support directionless selections, see bug 1541454. + return SelectionDirection::Forward; +} + +void TextControlState::SetSelectionDirection(const nsAString& aDirection, + ErrorResult& aRv) { + SelectionDirection dir = DirectionStringToSelectionDirection(aDirection); + + uint32_t start, end; + GetSelectionRange(&start, &end, aRv); + if (aRv.Failed()) { + return; + } + + SetSelectionRange(start, end, dir, aRv); + // The instance may have already been deleted here. +} + +static SelectionDirection DirectionStringToSelectionDirection( + const Optional<nsAString>& aDirection) { + if (!aDirection.WasPassed()) { + // We don't support directionless selections. + return SelectionDirection::Forward; + } + + return DirectionStringToSelectionDirection(aDirection.Value()); +} + +void TextControlState::SetSelectionRange(uint32_t aSelectionStart, + uint32_t aSelectionEnd, + const Optional<nsAString>& aDirection, + ErrorResult& aRv, + ScrollAfterSelection aScroll) { + SelectionDirection dir = DirectionStringToSelectionDirection(aDirection); + + SetSelectionRange(aSelectionStart, aSelectionEnd, dir, aRv, aScroll); + // The instance may have already been deleted here. +} + +void TextControlState::SetRangeText(const nsAString& aReplacement, + ErrorResult& aRv) { + uint32_t start, end; + GetSelectionRange(&start, &end, aRv); + if (aRv.Failed()) { + return; + } + + SetRangeText(aReplacement, start, end, SelectionMode::Preserve, aRv, + Some(start), Some(end)); + // The instance may have already been deleted here. +} + +void TextControlState::SetRangeText(const nsAString& aReplacement, + uint32_t aStart, uint32_t aEnd, + SelectionMode aSelectMode, ErrorResult& aRv, + const Maybe<uint32_t>& aSelectionStart, + const Maybe<uint32_t>& aSelectionEnd) { + if (aStart > aEnd) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return; + } + + AutoTextControlHandlingState handlingSetRangeText( + *this, TextControlAction::SetRangeText); + + nsAutoString value; + mTextCtrlElement->GetValueFromSetRangeText(value); + uint32_t inputValueLength = value.Length(); + + if (aStart > inputValueLength) { + aStart = inputValueLength; + } + + if (aEnd > inputValueLength) { + aEnd = inputValueLength; + } + + uint32_t selectionStart, selectionEnd; + if (!aSelectionStart) { + MOZ_ASSERT(!aSelectionEnd); + GetSelectionRange(&selectionStart, &selectionEnd, aRv); + if (aRv.Failed()) { + return; + } + } else { + MOZ_ASSERT(aSelectionEnd); + selectionStart = *aSelectionStart; + selectionEnd = *aSelectionEnd; + } + + // Batch selectionchanges from SetValueFromSetRangeText and SetSelectionRange + Selection* selection = + mSelCon ? mSelCon->GetSelection(SelectionType::eNormal) : nullptr; + SelectionBatcher selectionBatcher( + selection, __FUNCTION__, + nsISelectionListener::JS_REASON); // no-op if nullptr + + MOZ_ASSERT(aStart <= aEnd); + value.Replace(aStart, aEnd - aStart, aReplacement); + nsresult rv = + MOZ_KnownLive(mTextCtrlElement)->SetValueFromSetRangeText(value); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + uint32_t newEnd = aStart + aReplacement.Length(); + int32_t delta = aReplacement.Length() - (aEnd - aStart); + + switch (aSelectMode) { + case SelectionMode::Select: + selectionStart = aStart; + selectionEnd = newEnd; + break; + case SelectionMode::Start: + selectionStart = selectionEnd = aStart; + break; + case SelectionMode::End: + selectionStart = selectionEnd = newEnd; + break; + case SelectionMode::Preserve: + if (selectionStart > aEnd) { + selectionStart += delta; + } else if (selectionStart > aStart) { + selectionStart = aStart; + } + + if (selectionEnd > aEnd) { + selectionEnd += delta; + } else if (selectionEnd > aStart) { + selectionEnd = newEnd; + } + break; + default: + MOZ_ASSERT_UNREACHABLE("Unknown mode!"); + } + + SetSelectionRange(selectionStart, selectionEnd, Optional<nsAString>(), aRv); + if (IsSelectionCached()) { + // SetValueFromSetRangeText skipped SetMaxLength, set it here properly + GetSelectionProperties().SetMaxLength(value.Length()); + } +} + +void TextControlState::DestroyEditor() { + // notify the editor that we are going away + if (mEditorInitialized) { + // FYI: TextEditor checks whether it's destroyed or not immediately after + // changes the DOM tree or selection so that it's safe to call + // PreDestroy() here even while we're handling actions with + // mTextEditor. + MOZ_ASSERT(!mPasswordMaskData); + RefPtr<TextEditor> textEditor = mTextEditor; + mPasswordMaskData = textEditor->PreDestroy(); + MOZ_ASSERT_IF(mPasswordMaskData, !mPasswordMaskData->mTimer); + mEditorInitialized = false; + } +} + +void TextControlState::UnbindFromFrame(nsTextControlFrame* aFrame) { + if (NS_WARN_IF(!mBoundFrame)) { + return; + } + + // If it was, however, it should be unbounded from the same frame. + MOZ_ASSERT(aFrame == mBoundFrame, "Unbinding from the wrong frame"); + if (aFrame && aFrame != mBoundFrame) { + return; + } + + AutoTextControlHandlingState handlingUnbindFromFrame( + *this, TextControlAction::UnbindFromFrame); + + if (mSelCon) { + mSelCon->SelectionWillLoseFocus(); + } + + // We need to start storing the value outside of the editor if we're not + // going to use it anymore, so retrieve it for now. + nsAutoString value; + GetValue(value, true, /* aForDisplay = */ false); + + if (mRestoringSelection) { + mRestoringSelection->Revoke(); + mRestoringSelection = nullptr; + } + + // Save our selection state if needed. + // Note that GetSelectionRange will attempt to work with our selection + // controller, so we should make sure we do it before we start doing things + // like destroying our editor (if we have one), tearing down the selection + // controller, and so forth. + if (!IsSelectionCached()) { + // Go ahead and cache it now. + uint32_t start = 0, end = 0; + GetSelectionRange(&start, &end, IgnoreErrors()); + + nsITextControlFrame::SelectionDirection direction = + GetSelectionDirection(IgnoreErrors()); + + SelectionProperties& props = GetSelectionProperties(); + props.SetMaxLength(value.Length()); + props.SetStart(start); + props.SetEnd(end); + props.SetDirection(direction); + mSelectionCached = true; + } + + // Destroy our editor + DestroyEditor(); + + // Clean up the controller + if (!SuppressEventHandlers(mBoundFrame->PresContext())) { + nsCOMPtr<nsIControllers> controllers; + if (auto* inputElement = HTMLInputElement::FromNode(mTextCtrlElement)) { + inputElement->GetControllers(getter_AddRefs(controllers)); + } else { + auto* textAreaElement = HTMLTextAreaElement::FromNode(mTextCtrlElement); + if (textAreaElement) { + textAreaElement->GetControllers(getter_AddRefs(controllers)); + } + } + + if (controllers) { + uint32_t numControllers; + nsresult rv = controllers->GetControllerCount(&numControllers); + NS_ASSERTION((NS_SUCCEEDED(rv)), + "bad result in gfx text control destructor"); + for (uint32_t i = 0; i < numControllers; i++) { + nsCOMPtr<nsIController> controller; + rv = controllers->GetControllerAt(i, getter_AddRefs(controller)); + if (NS_SUCCEEDED(rv) && controller) { + nsCOMPtr<nsIControllerContext> editController = + do_QueryInterface(controller); + if (editController) { + editController->SetCommandContext(nullptr); + } + } + } + } + } + + if (mSelCon) { + if (mTextListener) { + mTextListener->EndListeningToSelectionChange(); + } + + mSelCon->SetScrollableFrame(nullptr); + mSelCon = nullptr; + } + + if (mTextListener) { + mTextListener->SetFrame(nullptr); + + EventListenerManager* manager = + mTextCtrlElement->GetExistingListenerManager(); + if (manager) { + manager->RemoveEventListenerByType(mTextListener, u"keydown"_ns, + TrustedEventsAtSystemGroupBubble()); + manager->RemoveEventListenerByType(mTextListener, u"keypress"_ns, + TrustedEventsAtSystemGroupBubble()); + manager->RemoveEventListenerByType(mTextListener, u"keyup"_ns, + TrustedEventsAtSystemGroupBubble()); + } + + mTextListener = nullptr; + } + + mBoundFrame = nullptr; + + // Now that we don't have a frame any more, store the value in the text + // buffer. The only case where we don't do this is if a value transfer is in + // progress. + if (!mValueTransferInProgress) { + DebugOnly<bool> ok = SetValue(value, ValueSetterOption::ByInternalAPI); + // TODO Find something better to do if this fails... + NS_WARNING_ASSERTION(ok, "SetValue() couldn't allocate memory"); + } +} + +void TextControlState::GetValue(nsAString& aValue, bool aIgnoreWrap, + bool aForDisplay) const { + // While SetValue() is being called and requesting to commit composition to + // IME, GetValue() may be called for appending text or something. Then, we + // need to return the latest aValue of SetValue() since the value hasn't + // been set to the editor yet. + // XXX After implementing "beforeinput" event, this becomes wrong. The + // value should be modified immediately after "beforeinput" event for + // "insertReplacementText". + if (mHandlingState && + mHandlingState->IsHandling(TextControlAction::CommitComposition)) { + aValue = mHandlingState->GetSettingValue(); + MOZ_ASSERT(aValue.FindChar(u'\r') == -1); + return; + } + + if (mTextEditor && mBoundFrame && + (mEditorInitialized || !IsSingleLineTextControl())) { + if (aIgnoreWrap && !mBoundFrame->CachedValue().IsVoid()) { + aValue = mBoundFrame->CachedValue(); + MOZ_ASSERT(aValue.FindChar(u'\r') == -1); + return; + } + + aValue.Truncate(); // initialize out param + + uint32_t flags = (nsIDocumentEncoder::OutputLFLineBreak | + nsIDocumentEncoder::OutputPreformatted | + nsIDocumentEncoder::OutputPersistNBSP | + nsIDocumentEncoder::OutputBodyOnly); + if (!aIgnoreWrap) { + TextControlElement::nsHTMLTextWrap wrapProp; + if (mTextCtrlElement && + TextControlElement::GetWrapPropertyEnum(mTextCtrlElement, wrapProp) && + wrapProp == TextControlElement::eHTMLTextWrap_Hard) { + flags |= nsIDocumentEncoder::OutputWrap; + } + } + + // What follows is a bit of a hack. The problem is that we could be in + // this method because we're being destroyed for whatever reason while + // script is executing. If that happens, editor will run with the + // privileges of the executing script, which means it may not be able to + // access its own DOM nodes! Let's try to deal with that by pushing a null + // JSContext on the JSContext stack to make it clear that we're native + // code. Note that any script that's directly trying to access our value + // has to be going through some scriptable object to do that and that + // already does the relevant security checks. + // XXXbz if we could just get the textContent of our anonymous content (eg + // if plaintext editor didn't create <br> nodes all over), we wouldn't need + // this. + { /* Scope for AutoNoJSAPI. */ + AutoNoJSAPI nojsapi; + + DebugOnly<nsresult> rv = mTextEditor->ComputeTextValue(flags, aValue); + MOZ_ASSERT(aValue.FindChar(u'\r') == -1); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to get value"); + } + // Only when the result doesn't include line breaks caused by hard-wrap, + // mCacheValue should cache the value. + if (!(flags & nsIDocumentEncoder::OutputWrap)) { + mBoundFrame->CacheValue(aValue); + } else { + mBoundFrame->ClearCachedValue(); + } + } else if (!mTextCtrlElement->ValueChanged() || mValue.IsVoid()) { + // Use nsString to avoid copying string buffer at setting aValue. + nsString value; + mTextCtrlElement->GetDefaultValueFromContent(value, aForDisplay); + // TODO: We should make default value not include \r. + nsContentUtils::PlatformToDOMLineBreaks(value); + aValue = std::move(value); + } else { + aValue = mValue; + MOZ_ASSERT(aValue.FindChar(u'\r') == -1); + } +} + +bool TextControlState::ValueEquals(const nsAString& aValue) const { + nsAutoString value; + GetValue(value, true, /* aForDisplay = */ true); + return aValue.Equals(value); +} + +#ifdef DEBUG +// @param aOptions TextControlState::ValueSetterOptions +bool AreFlagsNotDemandingContradictingMovements( + const ValueSetterOptions& aOptions) { + return !aOptions.contains( + {ValueSetterOption::MoveCursorToBeginSetSelectionDirectionForward, + ValueSetterOption::MoveCursorToEndIfValueChanged}); +} +#endif // DEBUG + +bool TextControlState::SetValue(const nsAString& aValue, + const nsAString* aOldValue, + const ValueSetterOptions& aOptions) { + if (mHandlingState && + mHandlingState->IsHandling(TextControlAction::CommitComposition)) { + // GetValue doesn't return current text frame's content during committing. + // So we cannot trust this old value + aOldValue = nullptr; + } + + if (mPasswordMaskData) { + if (mHandlingState && + mHandlingState->Is(TextControlAction::UnbindFromFrame)) { + // If we're called by UnbindFromFrame, we shouldn't reset unmasked range. + } else { + // Otherwise, we should mask the new password, even if it's same value + // since the same value may be one for different web app's. + mPasswordMaskData->Reset(); + } + } + + const bool wasHandlingSetValue = + mHandlingState && mHandlingState->IsHandling(TextControlAction::SetValue); + + ErrorResult error; + AutoTextControlHandlingState handlingSetValue( + *this, TextControlAction::SetValue, aValue, aOldValue, aOptions, error); + if (error.Failed()) { + MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY)); + error.SuppressException(); + return false; + } + + const auto changeKind = [&] { + if (aOptions.contains(ValueSetterOption::ByInternalAPI)) { + return ValueChangeKind::Internal; + } + if (aOptions.contains(ValueSetterOption::BySetUserInputAPI)) { + return ValueChangeKind::UserInteraction; + } + return ValueChangeKind::Script; + }(); + + if (changeKind == ValueChangeKind::Script) { + // This value change will not be interactive. If we're an input that was + // interactively edited, save the last interactive value now before it goes + // away. + if (auto* input = HTMLInputElement::FromNode(mTextCtrlElement)) { + if (input->LastValueChangeWasInteractive()) { + GetValue(mLastInteractiveValue, /* aIgnoreWrap = */ true, + /* aForDisplay = */ true); + } + } + } + + // Note that if this may be called during reframe of the editor. In such + // case, we shouldn't commit composition. Therefore, when this is called + // for internal processing, we shouldn't commit the composition. + // TODO: In strictly speaking, we should move committing composition into + // editor because if "beforeinput" for this setting value is canceled, + // we shouldn't commit composition. However, in Firefox, we never + // call this via `setUserInput` during composition. Therefore, the + // bug must not be reproducible actually. + if (aOptions.contains(ValueSetterOption::BySetUserInputAPI) || + aOptions.contains(ValueSetterOption::ByContentAPI)) { + if (EditorHasComposition()) { + // When this is called recursively, there shouldn't be composition. + if (handlingSetValue.IsHandling(TextControlAction::CommitComposition)) { + // Don't request to commit composition again. But if it occurs, + // we should skip to set the new value to the editor here. It should + // be set later with the newest value. + return true; + } + if (NS_WARN_IF(!mBoundFrame)) { + // We're not sure if this case is possible. + } else { + // If setting value won't change current value, we shouldn't commit + // composition for compatibility with the other browsers. + MOZ_ASSERT(!aOldValue || ValueEquals(*aOldValue)); + bool isSameAsCurrentValue = + aOldValue ? aOldValue->Equals(handlingSetValue.GetSettingValue()) + : ValueEquals(handlingSetValue.GetSettingValue()); + if (isSameAsCurrentValue) { + // Note that in this case, we shouldn't fire any events with setting + // value because event handlers may try to set value recursively but + // we cannot commit composition at that time due to unsafe to run + // script (see below). + return true; + } + } + // If there is composition, need to commit composition first because + // other browsers do that. + // NOTE: We don't need to block nested calls of this because input nor + // other events won't be fired by setting values and script blocker + // is used during setting the value to the editor. IE also allows + // to set the editor value on the input event which is caused by + // forcibly committing composition. + AutoTextControlHandlingState handlingCommitComposition( + *this, TextControlAction::CommitComposition); + if (nsContentUtils::IsSafeToRunScript()) { + // WARNING: During this call, compositionupdate, compositionend, input + // events will be fired. Therefore, everything can occur. E.g., the + // document may be unloaded. + RefPtr<TextEditor> textEditor = mTextEditor; + nsresult rv = textEditor->CommitComposition(); + if (handlingCommitComposition.IsTextControlStateDestroyed()) { + return true; + } + if (NS_FAILED(rv)) { + NS_WARNING("TextControlState failed to commit composition"); + return true; + } + // Note that if a composition event listener sets editor value again, + // we should use the new value here. The new value is stored in + // handlingSetValue right now. + } else { + NS_WARNING( + "SetValue() is called when there is composition but " + "it's not safe to request to commit the composition"); + } + } + } + + if (mTextEditor && mBoundFrame) { + if (!SetValueWithTextEditor(handlingSetValue)) { + return false; + } + } else if (!SetValueWithoutTextEditor(handlingSetValue)) { + return false; + } + + // If we were handling SetValue() before, don't update the DOM state twice, + // just let the outer call do so. + if (!wasHandlingSetValue) { + handlingSetValue.GetTextControlElement()->OnValueChanged( + changeKind, handlingSetValue.GetSettingValue()); + } + return true; +} + +bool TextControlState::SetValueWithTextEditor( + AutoTextControlHandlingState& aHandlingSetValue) { + MOZ_ASSERT(aHandlingSetValue.Is(TextControlAction::SetValue)); + MOZ_ASSERT(mTextEditor); + MOZ_ASSERT(mBoundFrame); + NS_WARNING_ASSERTION(!EditorHasComposition(), + "Failed to commit composition before setting value. " + "Investigate the cause!"); + +#ifdef DEBUG + if (IsSingleLineTextControl()) { + NS_ASSERTION(mEditorInitialized || aHandlingSetValue.IsHandling( + TextControlAction::PrepareEditor), + "We should never try to use the editor if we're not " + "initialized unless we're being initialized"); + } +#endif + + MOZ_ASSERT(!aHandlingSetValue.GetOldValue() || + ValueEquals(*aHandlingSetValue.GetOldValue())); + const bool isSameAsCurrentValue = + aHandlingSetValue.GetOldValue() + ? aHandlingSetValue.GetOldValue()->Equals( + aHandlingSetValue.GetSettingValue()) + : ValueEquals(aHandlingSetValue.GetSettingValue()); + + // this is necessary to avoid infinite recursion + if (isSameAsCurrentValue) { + return true; + } + + RefPtr<TextEditor> textEditor = mTextEditor; + + nsCOMPtr<Document> document = textEditor->GetDocument(); + if (NS_WARN_IF(!document)) { + return true; + } + + // Time to mess with our security context... See comments in GetValue() + // for why this is needed. Note that we have to do this up here, because + // otherwise SelectAll() will fail. + AutoNoJSAPI nojsapi; + + // FYI: It's safe to use raw pointer for selection here because + // SelectionBatcher will grab it with RefPtr. + Selection* selection = mSelCon->GetSelection(SelectionType::eNormal); + SelectionBatcher selectionBatcher(selection, __FUNCTION__); + + // get the flags, remove readonly, disabled and max-length, + // set the value, restore flags + AutoRestoreEditorState restoreState(textEditor); + + aHandlingSetValue.WillSetValueWithTextEditor(); + + if (aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetUserInputAPI)) { + // If the caller inserts text as part of user input, for example, + // autocomplete, we need to replace the text as "insert string" + // because undo should cancel only this operation (i.e., previous + // transactions typed by user shouldn't be merged with this). + // In this case, we need to dispatch "input" event because + // web apps may need to know the user's operation. + // In this case, we need to dispatch "beforeinput" events since + // we're emulating the user's input. Passing nullptr as + // nsIPrincipal means that that may be user's input. So, let's + // do it. + nsresult rv = textEditor->ReplaceTextAsAction( + aHandlingSetValue.GetSettingValue(), nullptr, + StaticPrefs::dom_input_event_allow_to_cancel_set_user_input() + ? TextEditor::AllowBeforeInputEventCancelable::Yes + : TextEditor::AllowBeforeInputEventCancelable::No, + nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "EditorBase::ReplaceTextAsAction() failed"); + return rv != NS_ERROR_OUT_OF_MEMORY; + } + + // Don't dispatch "beforeinput" event nor "input" event for setting value + // by script. + AutoInputEventSuppresser suppressInputEventDispatching(textEditor); + + // On <input> or <textarea>, we shouldn't preserve existing undo + // transactions because other browsers do not preserve them too + // and not preserving transactions makes setting value faster. + // + // (Except if chrome opts into this behavior). + Maybe<AutoDisableUndo> disableUndo; + if (!aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::PreserveUndoHistory)) { + disableUndo.emplace(textEditor); + } + + if (selection) { + // Since we don't use undo transaction, we don't need to store + // selection state. SetText will set selection to tail. + IgnoredErrorResult ignoredError; + MOZ_KnownLive(selection)->RemoveAllRanges(ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "Selection::RemoveAllRanges() failed, but ignored"); + } + + // In this case, we makes the editor stop dispatching "input" + // event so that passing nullptr as nsIPrincipal is safe for now. + nsresult rv = textEditor->SetTextAsAction( + aHandlingSetValue.GetSettingValue(), + aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetUserInputAPI) && + !StaticPrefs::dom_input_event_allow_to_cancel_set_user_input() + ? TextEditor::AllowBeforeInputEventCancelable::No + : TextEditor::AllowBeforeInputEventCancelable::Yes, + nullptr); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "TextEditor::SetTextAsAction() failed"); + + // Call the listener's OnEditActionHandled() callback manually if + // OnEditActionHandled() hasn't been called yet since TextEditor don't use + // the transaction manager in this path and it could be that the editor + // would bypass calling the listener for that reason. + if (!aHandlingSetValue.HasEditActionHandled()) { + nsresult rvOnEditActionHandled = + MOZ_KnownLive(aHandlingSetValue.GetTextInputListener()) + ->OnEditActionHandled(*textEditor); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvOnEditActionHandled), + "TextInputListener::OnEditActionHandled() failed"); + if (rv != NS_ERROR_OUT_OF_MEMORY) { + rv = rvOnEditActionHandled; + } + } + + // When the <textarea> is not dirty, the default value is mirrored into the + // anonymous subtree asynchronously. This may occur during a reframe. + // Therefore, if IMEContentObserver was initialized with our editor but our + // editor is being initialized, it has not been observing the new anonymous + // subtree. In this case, we need to notify IMEContentObserver of the default + // value change. + if (mTextCtrlElement && mTextCtrlElement->IsTextArea() && + !mTextCtrlElement->ValueChanged() && textEditor->IsBeingInitialized() && + !textEditor->Destroyed()) { + IMEContentObserver* observer = GetIMEContentObserver(); + if (observer && observer->WasInitializedWith(*textEditor)) { + nsAutoString currentValue; + textEditor->ComputeTextValue(0, currentValue); + observer->OnTextControlValueChangedWhileNotObservable(currentValue); + } + } + + return rv != NS_ERROR_OUT_OF_MEMORY; +} + +bool TextControlState::SetValueWithoutTextEditor( + AutoTextControlHandlingState& aHandlingSetValue) { + MOZ_ASSERT(aHandlingSetValue.Is(TextControlAction::SetValue)); + MOZ_ASSERT(!mTextEditor || !mBoundFrame); + NS_WARNING_ASSERTION(!EditorHasComposition(), + "Failed to commit composition before setting value. " + "Investigate the cause!"); + + if (mValue.IsVoid()) { + mValue.SetIsVoid(false); + } + + // We can't just early-return here, because OnValueChanged below still need to + // be called. + if (!mValue.Equals(aHandlingSetValue.GetSettingValue()) || + !StaticPrefs::dom_input_skip_cursor_move_for_same_value_set()) { + bool handleSettingValue = true; + // If `SetValue()` call is nested, `GetSettingValue()` result will be + // modified. So, we need to store input event data value before + // dispatching beforeinput event. + nsString inputEventData(aHandlingSetValue.GetSettingValue()); + if (aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetUserInputAPI) && + !aHandlingSetValue.HasBeforeInputEventDispatched()) { + // This probably occurs when session restorer sets the old value with + // `setUserInput`. If so, we need to dispatch "beforeinput" event of + // "insertReplacementText" for conforming to the spec. However, the + // spec does NOT treat the session restoring case. Therefore, if this + // breaks session restorere in a lot of web apps, we should probably + // stop dispatching it or make it non-cancelable. + MOZ_ASSERT(aHandlingSetValue.GetTextControlElement()); + MOZ_ASSERT(!aHandlingSetValue.GetSettingValue().IsVoid()); + aHandlingSetValue.WillDispatchBeforeInputEvent(); + nsEventStatus status = nsEventStatus_eIgnore; + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent( + MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()), + eEditorBeforeInput, EditorInputType::eInsertReplacementText, nullptr, + InputEventOptions( + inputEventData, + StaticPrefs::dom_input_event_allow_to_cancel_set_user_input() + ? InputEventOptions::NeverCancelable::No + : InputEventOptions::NeverCancelable::Yes), + &status); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch beforeinput event"); + if (status == nsEventStatus_eConsumeNoDefault) { + return true; // "beforeinput" event was canceled. + } + // If we were destroyed by "beforeinput" event listeners, probably, we + // don't need to keep handling it. + if (aHandlingSetValue.IsTextControlStateDestroyed()) { + return true; + } + // Even if "beforeinput" event was not canceled, its listeners may do + // something. If it causes creating `TextEditor` and bind this to a + // frame, we need to use the path, but `TextEditor` shouldn't fire + // "beforeinput" event again. Therefore, we need to prevent editor + // to dispatch it. + if (mTextEditor && mBoundFrame) { + AutoInputEventSuppresser suppressInputEvent(mTextEditor); + if (!SetValueWithTextEditor(aHandlingSetValue)) { + return false; + } + // If we were destroyed by "beforeinput" event listeners, probably, we + // don't need to keep handling it. + if (aHandlingSetValue.IsTextControlStateDestroyed()) { + return true; + } + handleSettingValue = false; + } + } + + if (handleSettingValue) { + if (!mValue.Assign(aHandlingSetValue.GetSettingValue(), fallible)) { + return false; + } + + // Since we have no editor we presumably have cached selection state. + if (IsSelectionCached()) { + MOZ_ASSERT(AreFlagsNotDemandingContradictingMovements( + aHandlingSetValue.ValueSetterOptionsRef())); + + SelectionProperties& props = GetSelectionProperties(); + // Setting a max length and thus capping selection range early prevents + // selection change detection in setRangeText. Temporarily disable + // capping here with UINT32_MAX, and set it later in ::SetRangeText(). + props.SetMaxLength(aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetRangeTextAPI) + ? UINT32_MAX + : aHandlingSetValue.GetSettingValue().Length()); + if (aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::MoveCursorToEndIfValueChanged)) { + props.SetStart(aHandlingSetValue.GetSettingValue().Length()); + props.SetEnd(aHandlingSetValue.GetSettingValue().Length()); + props.SetDirection(SelectionDirection::Forward); + } else if (aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption:: + MoveCursorToBeginSetSelectionDirectionForward)) { + props.SetStart(0); + props.SetEnd(0); + props.SetDirection(SelectionDirection::Forward); + } + } + + // Update the frame display if needed + if (mBoundFrame) { + mBoundFrame->UpdateValueDisplay(true); + } + + // If the text control element has focus, IMEContentObserver is not + // observing the content changes due to no bound frame or no TextEditor. + // Therefore, we need to let IMEContentObserver know all values are being + // replaced. + if (IMEContentObserver* observer = GetIMEContentObserver()) { + observer->OnTextControlValueChangedWhileNotObservable(mValue); + } + } + + // If this is called as part of user input, we need to dispatch "input" + // event with "insertReplacementText" since web apps may want to know + // the user operation which changes editor value with a built-in function + // like autocomplete, password manager, session restore, etc. + // XXX Should we stop dispatching `input` event if the text control + // element has already removed from the DOM tree by a `beforeinput` + // event listener? + if (aHandlingSetValue.ValueSetterOptionsRef().contains( + ValueSetterOption::BySetUserInputAPI)) { + MOZ_ASSERT(aHandlingSetValue.GetTextControlElement()); + + // Update validity state before dispatching "input" event for its + // listeners like `EditorBase::NotifyEditorObservers()`. + aHandlingSetValue.GetTextControlElement()->OnValueChanged( + ValueChangeKind::UserInteraction, + aHandlingSetValue.GetSettingValue()); + + ClearLastInteractiveValue(); + + MOZ_ASSERT(!aHandlingSetValue.GetSettingValue().IsVoid()); + DebugOnly<nsresult> rvIgnored = nsContentUtils::DispatchInputEvent( + MOZ_KnownLive(aHandlingSetValue.GetTextControlElement()), + eEditorInput, EditorInputType::eInsertReplacementText, nullptr, + InputEventOptions(inputEventData, + InputEventOptions::NeverCancelable::No)); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvIgnored), + "Failed to dispatch input event"); + } + } else { + // Even if our value is not actually changing, apparently we need to mark + // our SelectionProperties dirty to make accessibility tests happy. + // Probably because they depend on the SetSelectionRange() call we make on + // our frame in RestoreSelectionState, but I have no idea why they do. + if (IsSelectionCached()) { + SelectionProperties& props = GetSelectionProperties(); + props.SetIsDirty(); + } + } + + return true; +} + +void TextControlState::InitializeKeyboardEventListeners() { + // register key listeners + EventListenerManager* manager = + mTextCtrlElement->GetOrCreateListenerManager(); + if (manager) { + manager->AddEventListenerByType(mTextListener, u"keydown"_ns, + TrustedEventsAtSystemGroupBubble()); + manager->AddEventListenerByType(mTextListener, u"keypress"_ns, + TrustedEventsAtSystemGroupBubble()); + manager->AddEventListenerByType(mTextListener, u"keyup"_ns, + TrustedEventsAtSystemGroupBubble()); + } + + mSelCon->SetScrollableFrame(mBoundFrame->GetScrollTargetFrame()); +} + +void TextControlState::SetPreviewText(const nsAString& aValue, bool aNotify) { + // If we don't have a preview div, there's nothing to do. + Element* previewDiv = GetPreviewNode(); + if (!previewDiv) { + return; + } + + nsAutoString previewValue(aValue); + + nsContentUtils::RemoveNewlines(previewValue); + MOZ_ASSERT(previewDiv->GetFirstChild(), "preview div has no child"); + previewDiv->GetFirstChild()->AsText()->SetText(previewValue, aNotify); +} + +void TextControlState::GetPreviewText(nsAString& aValue) { + // If we don't have a preview div, there's nothing to do. + Element* previewDiv = GetPreviewNode(); + if (!previewDiv) { + return; + } + + MOZ_ASSERT(previewDiv->GetFirstChild(), "preview div has no child"); + const nsTextFragment* text = previewDiv->GetFirstChild()->GetText(); + + aValue.Truncate(); + text->AppendTo(aValue); +} + +bool TextControlState::EditorHasComposition() { + return mTextEditor && mTextEditor->IsIMEComposing(); +} + +IMEContentObserver* TextControlState::GetIMEContentObserver() const { + if (NS_WARN_IF(!mTextCtrlElement) || + mTextCtrlElement != IMEStateManager::GetFocusedElement()) { + return nullptr; + } + IMEContentObserver* observer = IMEStateManager::GetActiveContentObserver(); + // The text control element may be an editing host. In this case, the + // observer does not observe the anonymous nodes under mTextCtrlElement. + // So, it means that the observer is not for ours. + return observer && observer->EditorIsTextEditor() ? observer : nullptr; +} + +} // namespace mozilla diff --git a/dom/html/TextControlState.h b/dom/html/TextControlState.h new file mode 100644 index 0000000000..3dba24d255 --- /dev/null +++ b/dom/html/TextControlState.h @@ -0,0 +1,550 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_TextControlState_h +#define mozilla_TextControlState_h + +#include "mozilla/Assertions.h" +#include "mozilla/Attributes.h" +#include "mozilla/EnumSet.h" +#include "mozilla/Maybe.h" +#include "mozilla/TextControlElement.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/WeakPtr.h" +#include "mozilla/dom/HTMLInputElementBinding.h" +#include "mozilla/dom/Nullable.h" +#include "nsCycleCollectionParticipant.h" +#include "nsITextControlFrame.h" +#include "nsITimer.h" + +class nsTextControlFrame; +class nsISelectionController; +class nsFrameSelection; +class nsFrame; + +namespace mozilla { + +class AutoTextControlHandlingState; +class ErrorResult; +class IMEContentObserver; +class TextEditor; +class TextInputListener; +class TextInputSelectionController; + +namespace dom { +class Element; +class HTMLInputElement; +} // namespace dom + +/** + * PasswordMaskData stores making information and necessary timer for + * `TextEditor` instances. + */ +struct PasswordMaskData final { + // Timer to mask unmasked characters automatically. Used only when it's + // a password field. + nsCOMPtr<nsITimer> mTimer; + + // Unmasked character range. Used only when it's a password field. + // If mUnmaskedLength is 0, it means there is no unmasked characters. + uint32_t mUnmaskedStart = UINT32_MAX; + uint32_t mUnmaskedLength = 0; + + // Set to true if all characters are masked or waiting notification from + // `mTimer`. Otherwise, i.e., part of or all of password is unmasked + // without setting `mTimer`, set to false. + bool mIsMaskingPassword = true; + + // Set to true if a manager of the instance wants to disable echoing + // password temporarily. + bool mEchoingPasswordPrevented = false; + + MOZ_ALWAYS_INLINE bool IsAllMasked() const { + return mUnmaskedStart == UINT32_MAX && mUnmaskedLength == 0; + } + MOZ_ALWAYS_INLINE uint32_t UnmaskedEnd() const { + return mUnmaskedStart + mUnmaskedLength; + } + MOZ_ALWAYS_INLINE void MaskAll() { + mUnmaskedStart = UINT32_MAX; + mUnmaskedLength = 0; + } + MOZ_ALWAYS_INLINE void Reset() { + MaskAll(); + mIsMaskingPassword = true; + } + enum class ReleaseTimer { No, Yes }; + MOZ_ALWAYS_INLINE void CancelTimer(ReleaseTimer aReleaseTimer) { + if (mTimer) { + mTimer->Cancel(); + if (aReleaseTimer == ReleaseTimer::Yes) { + mTimer = nullptr; + } + } + if (mIsMaskingPassword) { + MaskAll(); + } + } +}; + +/** + * TextControlState is a class which is responsible for managing the state of + * plaintext controls. This currently includes the following HTML elements: + * <input type=text> + * <input type=search> + * <input type=url> + * <input type=telephone> + * <input type=email> + * <input type=password> + * <textarea> + * + * This class is held as a member of HTMLInputElement and HTMLTextAreaElement. + * The public functions in this class include the public APIs which dom/ + * uses. Layout code uses the TextControlElement interface to invoke + * functions on this class. + * + * The design motivation behind this class is maintaining all of the things + * which collectively are considered the "state" of the text control in a single + * location. This state includes several things: + * + * * The control's value. This value is stored in the mValue member, and is + * only used when there is no frame for the control, or when the editor object + * has not been initialized yet. + * + * * The control's associated frame. This value is stored in the mBoundFrame + * member. A text control might never have an associated frame during its life + * cycle, or might have several different ones, but at any given moment in time + * there is a maximum of 1 bound frame to each text control. + * + * * The control's associated editor. This value is stored in the mTextEditor + * member. An editor is initialized for the control only when necessary (that + * is, when either the user is about to interact with the text control, or when + * some other code needs to access the editor object. Without a frame bound to + * the control, an editor is never initialized. Once initialized, the editor + * might outlive the frame, in which case the same editor will be used if a new + * frame gets bound to the text control. + * + * * The anonymous content associated with the text control's frame, including + * the value div (the DIV element responsible for holding the value of the text + * control) and the placeholder div (the DIV element responsible for holding the + * placeholder value of the text control.) These values are stored in the + * mRootNode and mPlaceholderDiv members, respectively. They will be created + * when a frame is bound to the text control. They will be destroyed when the + * frame is unbound from the object. We could try and hold on to the anonymous + * content between different frames, but unfortunately that is not currently + * possible because they are not unbound from the document in time. + * + * * The frame selection controller. This value is stored in the mSelCon + * member. The frame selection controller is responsible for maintaining the + * selection state on a frame. It is created when a frame is bound to the text + * control element, and will be destroy when the frame is being unbound from the + * text control element. It is created alongside with the frame selection object + * which is stored in the mFrameSel member. + * + * * The editor text listener. This value is stored in the mTextListener + * member. Its job is to listen to selection and keyboard events, and act + * accordingly. It is created when an a frame is first bound to the control, and + * will be destroyed when the frame is unbound from the text control element. + * + * * The editor's cached value. This value is stored in the mCachedValue + * member. It is used to improve the performance of append operations to the + * text control. A mutation observer stored in the mMutationObserver has the + * job of invalidating this cache when the anonymous contect containing the + * value is changed. + * + * * The editor's cached selection properties. These vales are stored in the + * mSelectionProperties member, and include the selection's start, end and + * direction. They are only used when there is no frame available for the + * text field. + * + * + * As a general rule, TextControlState objects own the value of the text + * control, and any attempt to retrieve or set the value must be made through + * those objects. Internally, the value can be represented in several different + * ways, based on the state the control is in. + * + * * When the control is first initialized, its value is equal to the default + * value of the DOM node. For <input> text controls, this default value is the + * value of the value attribute. For <textarea> elements, this default value is + * the value of the text node children of the element. + * + * * If the value has been changed through the DOM node (before the editor for + * the object is initialized), the value is stored as a simple string inside the + * mValue member of the TextControlState object. + * + * * If an editor has been initialized for the control, the value is set and + * retrievd via the nsIEditor interface, and is internally managed by the + * editor as the native anonymous content tree attached to the control's frame. + * + * * If the text control state object is unbound from the control's frame, the + * value is transferred to the mValue member variable, and will be managed there + * until a new frame is bound to the text editor state object. + */ + +class RestoreSelectionState; + +class TextControlState final : public SupportsWeakPtr { + public: + using Element = dom::Element; + using HTMLInputElement = dom::HTMLInputElement; + using SelectionDirection = nsITextControlFrame::SelectionDirection; + + static TextControlState* Construct(TextControlElement* aOwningElement); + + // Note that this does not run script actually because of `sHasShutDown` + // is set to true before calling `DeleteOrCacheForReuse()`. + MOZ_CAN_RUN_SCRIPT_BOUNDARY static void Shutdown(); + + /** + * Destroy() deletes the instance immediately or later. + */ + MOZ_CAN_RUN_SCRIPT void Destroy(); + + TextControlState() = delete; + explicit TextControlState(const TextControlState&) = delete; + TextControlState(TextControlState&&) = delete; + + void operator=(const TextControlState&) = delete; + void operator=(TextControlState&&) = delete; + + void Traverse(nsCycleCollectionTraversalCallback& cb); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void Unlink(); + + bool IsBusy() const { return !!mHandlingState || mValueTransferInProgress; } + + MOZ_CAN_RUN_SCRIPT TextEditor* GetTextEditor(); + TextEditor* GetTextEditorWithoutCreation() const; + nsISelectionController* GetSelectionController() const; + nsFrameSelection* GetConstFrameSelection(); + nsresult BindToFrame(nsTextControlFrame* aFrame); + MOZ_CAN_RUN_SCRIPT void UnbindFromFrame(nsTextControlFrame* aFrame); + MOZ_CAN_RUN_SCRIPT nsresult PrepareEditor(const nsAString* aValue = nullptr); + void InitializeKeyboardEventListeners(); + + /** + * OnEditActionHandled() is called when mTextEditor handles something + * and immediately before dispatching "input" event. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult OnEditActionHandled(); + + enum class ValueSetterOption { + // The call is for setting value to initial one, computed one, etc. + ByInternalAPI, + // The value is changed by a call of setUserInput() API from chrome. + BySetUserInputAPI, + // The value is changed by changing value attribute of the element or + // something like setRangeText(). + ByContentAPI, + // The value is changed by setRangeText(). Intended to prevent silent + // selection range change. + BySetRangeTextAPI, + // Whether SetValueChanged should be called as a result of this value + // change. + SetValueChanged, + // Whether to move the cursor to end of the value (in the case when we have + // cached selection offsets), in the case when the value has changed. If + // this is not set and MoveCursorToBeginSetSelectionDirectionForward + // is not set, the cached selection offsets will simply be clamped to + // be within the length of the new value. In either case, if the value has + // not changed the cursor won't move. + // TODO(mbrodesser): update comment and enumerator identifier to reflect + // that also the direction is set to forward. + MoveCursorToEndIfValueChanged, + + // The value change should preserve undo history. + PreserveUndoHistory, + + // Whether it should be tried to move the cursor to the beginning of the + // text control and set the selection direction to "forward". + // TODO(mbrodesser): As soon as "none" is supported + // (https://bugzilla.mozilla.org/show_bug.cgi?id=1541454), it should be set + // to "none" and only fall back to "forward" if the platform doesn't support + // it. + MoveCursorToBeginSetSelectionDirectionForward, + }; + using ValueSetterOptions = EnumSet<ValueSetterOption, uint32_t>; + + /** + * SetValue() sets the value to aValue with replacing \r\n and \r with \n. + * + * @param aValue The new value. Can contain \r. + * @param aOldValue Optional. If you have already know current value, + * set this to it. However, this must not contain \r + * for the performance. + * @param aOptions See ValueSetterOption. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool SetValue( + const nsAString& aValue, const nsAString* aOldValue, + const ValueSetterOptions& aOptions); + [[nodiscard]] MOZ_CAN_RUN_SCRIPT bool SetValue( + const nsAString& aValue, const ValueSetterOptions& aOptions) { + return SetValue(aValue, nullptr, aOptions); + } + + /** + * GetValue() returns current value either with or without TextEditor. + * The result never includes \r. + */ + void GetValue(nsAString& aValue, bool aIgnoreWrap, bool aForDisplay) const; + + /** + * ValueEquals() is designed for internal use so that aValue shouldn't + * include \r character. It should be handled before calling this with + * nsContentUtils::PlatformToDOMLineBreaks(). + */ + bool ValueEquals(const nsAString& aValue) const; + // The following methods are for textarea element to use whether default + // value or not. + // XXX We might have to add assertion when it is into editable, + // or reconsider fixing bug 597525 to remove these. + void EmptyValue() { + if (!mValue.IsVoid()) { + mValue.Truncate(); + } + } + bool IsEmpty() const { return mValue.IsEmpty(); } + + const nsAString& LastInteractiveValueIfLastChangeWasNonInteractive() const { + return mLastInteractiveValue; + } + // When an interactive value change happens, we clear mLastInteractiveValue + // because it's not needed (mValue is the new interactive value). + void ClearLastInteractiveValue() { mLastInteractiveValue.SetIsVoid(true); } + + Element* GetRootNode(); + Element* GetPreviewNode(); + + bool IsSingleLineTextControl() const { + return mTextCtrlElement->IsSingleLineTextControl(); + } + bool IsTextArea() const { return mTextCtrlElement->IsTextArea(); } + bool IsPasswordTextControl() const { + return mTextCtrlElement->IsPasswordTextControl(); + } + int32_t GetCols() { return mTextCtrlElement->GetCols(); } + int32_t GetWrapCols() { + int32_t wrapCols = mTextCtrlElement->GetWrapCols(); + MOZ_ASSERT(wrapCols >= 0); + return wrapCols; + } + int32_t GetRows() { return mTextCtrlElement->GetRows(); } + + // preview methods + void SetPreviewText(const nsAString& aValue, bool aNotify); + void GetPreviewText(nsAString& aValue); + + struct SelectionProperties { + public: + bool IsDefault() const { + return mStart == 0 && mEnd == 0 && + mDirection == SelectionDirection::Forward; + } + uint32_t GetStart() const { return mStart; } + bool SetStart(uint32_t value) { + uint32_t newValue = std::min(value, *mMaxLength); + bool changed = mStart != newValue; + mStart = newValue; + mIsDirty |= changed; + return changed; + } + uint32_t GetEnd() const { return mEnd; } + bool SetEnd(uint32_t value) { + uint32_t newValue = std::min(value, *mMaxLength); + bool changed = mEnd != newValue; + mEnd = newValue; + mIsDirty |= changed; + return changed; + } + SelectionDirection GetDirection() const { return mDirection; } + bool SetDirection(SelectionDirection value) { + bool changed = mDirection != value; + mDirection = value; + mIsDirty |= changed; + return changed; + } + void SetMaxLength(uint32_t aMax) { + mMaxLength = Some(aMax); + // recompute against the new max length + SetStart(GetStart()); + SetEnd(GetEnd()); + } + bool HasMaxLength() { return mMaxLength.isSome(); } + + // return true only if mStart, mEnd, or mDirection have been modified, + // or if SetIsDirty() was explicitly called. + bool IsDirty() const { return mIsDirty; } + void SetIsDirty() { mIsDirty = true; } + + private: + uint32_t mStart = 0; + uint32_t mEnd = 0; + Maybe<uint32_t> mMaxLength; + bool mIsDirty = false; + SelectionDirection mDirection = SelectionDirection::Forward; + }; + + bool IsSelectionCached() const { return mSelectionCached; } + SelectionProperties& GetSelectionProperties() { return mSelectionProperties; } + MOZ_CAN_RUN_SCRIPT void SetSelectionProperties(SelectionProperties& aProps); + bool HasNeverInitializedBefore() const { return !mEverInited; } + // Sync up our selection properties with our editor prior to being destroyed. + // This will invoke UnbindFromFrame() to ensure that we grab whatever + // selection state may be at the moment. + MOZ_CAN_RUN_SCRIPT void SyncUpSelectionPropertiesBeforeDestruction(); + + // Get the selection range start and end points in our text. + void GetSelectionRange(uint32_t* aSelectionStart, uint32_t* aSelectionEnd, + ErrorResult& aRv); + + // Get the selection direction + nsITextControlFrame::SelectionDirection GetSelectionDirection( + ErrorResult& aRv); + + enum class ScrollAfterSelection { No, Yes }; + + // Set the selection range (start, end, direction). aEnd is allowed to be + // smaller than aStart; in that case aStart will be reset to the same value as + // aEnd. This basically implements + // https://html.spec.whatwg.org/multipage/forms.html#set-the-selection-range + // but with the start/end already coerced to zero if null (and without the + // special infinity value), and the direction already converted to a + // SelectionDirection. + // + // If we have a frame, this method will scroll the selection into view. + MOZ_CAN_RUN_SCRIPT void SetSelectionRange( + uint32_t aStart, uint32_t aEnd, + nsITextControlFrame::SelectionDirection aDirection, ErrorResult& aRv, + ScrollAfterSelection aScroll = ScrollAfterSelection::Yes); + + // Set the selection range, but with an optional string for the direction. + // This will convert aDirection to an nsITextControlFrame::SelectionDirection + // and then call our other SetSelectionRange overload. + MOZ_CAN_RUN_SCRIPT void SetSelectionRange( + uint32_t aSelectionStart, uint32_t aSelectionEnd, + const dom::Optional<nsAString>& aDirection, ErrorResult& aRv, + ScrollAfterSelection aScroll = ScrollAfterSelection::Yes); + + // Set the selection start. This basically implements the + // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-selectionstart + // setter. + MOZ_CAN_RUN_SCRIPT void SetSelectionStart( + const dom::Nullable<uint32_t>& aStart, ErrorResult& aRv); + + // Set the selection end. This basically implements the + // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-selectionend + // setter. + MOZ_CAN_RUN_SCRIPT void SetSelectionEnd(const dom::Nullable<uint32_t>& aEnd, + ErrorResult& aRv); + + // Get the selection direction as a string. This implements the + // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-selectiondirection + // getter. + void GetSelectionDirectionString(nsAString& aDirection, ErrorResult& aRv); + + // Set the selection direction. This basically implements the + // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-selectiondirection + // setter. + MOZ_CAN_RUN_SCRIPT void SetSelectionDirection(const nsAString& aDirection, + ErrorResult& aRv); + + // Set the range text. This basically implements + // https://html.spec.whatwg.org/multipage/forms.html#dom-textarea/input-setrangetext + MOZ_CAN_RUN_SCRIPT void SetRangeText(const nsAString& aReplacement, + ErrorResult& aRv); + // The last two arguments are -1 if we don't know our selection range; + // otherwise they're the start and end of our selection range. + MOZ_CAN_RUN_SCRIPT void SetRangeText( + const nsAString& aReplacement, uint32_t aStart, uint32_t aEnd, + dom::SelectionMode aSelectMode, ErrorResult& aRv, + const Maybe<uint32_t>& aSelectionStart = Nothing(), + const Maybe<uint32_t>& aSelectionEnd = Nothing()); + + private: + explicit TextControlState(TextControlElement* aOwningElement); + MOZ_CAN_RUN_SCRIPT ~TextControlState(); + + /** + * Delete the instance or cache to reuse it if possible. + */ + MOZ_CAN_RUN_SCRIPT void DeleteOrCacheForReuse(); + + MOZ_CAN_RUN_SCRIPT void UnlinkInternal(); + + MOZ_CAN_RUN_SCRIPT void DestroyEditor(); + MOZ_CAN_RUN_SCRIPT void Clear(); + + nsresult InitializeRootNode(); + + void FinishedRestoringSelection(); + + bool EditorHasComposition(); + + /** + * SetValueWithTextEditor() modifies the editor value with mTextEditor. + * This may cause destroying mTextEditor, mBoundFrame, the TextControlState + * itself. Must be called when both mTextEditor and mBoundFrame are not + * nullptr. + * + * @param aHandlingSetValue Must be inner-most handling state for SetValue. + * @return false if fallible allocation failed. Otherwise, + * true. + */ + MOZ_CAN_RUN_SCRIPT bool SetValueWithTextEditor( + AutoTextControlHandlingState& aHandlingSetValue); + + /** + * SetValueWithoutTextEditor() modifies the value without editor. I.e., + * modifying the value in this instance and mBoundFrame. Must be called + * when at least mTextEditor or mBoundFrame is nullptr. + * + * @param aHandlingSetValue Must be inner-most handling state for SetValue. + * @return false if fallible allocation failed. Otherwise, + * true. + */ + MOZ_CAN_RUN_SCRIPT bool SetValueWithoutTextEditor( + AutoTextControlHandlingState& aHandlingSetValue); + + IMEContentObserver* GetIMEContentObserver() const; + + // When this class handles something which may run script, this should be + // set to non-nullptr. If so, this class claims that it's busy and that + // prevents destroying TextControlState instance. + AutoTextControlHandlingState* mHandlingState = nullptr; + + // The text control element owns this object, and ensures that this object + // has a smaller lifetime except the owner releases the instance while it + // does something with this. + TextControlElement* MOZ_NON_OWNING_REF mTextCtrlElement; + RefPtr<TextInputSelectionController> mSelCon; + RefPtr<RestoreSelectionState> mRestoringSelection; + RefPtr<TextEditor> mTextEditor; + nsTextControlFrame* mBoundFrame = nullptr; + RefPtr<TextInputListener> mTextListener; + UniquePtr<PasswordMaskData> mPasswordMaskData; + + nsString mValue{VoidString()}; // Void if there's no value. + + // If our input's last value change was not interactive (as in, the value + // change was caused by a ValueChangeKind::UserInteraction), this is the value + // that the last interaction had. + nsString mLastInteractiveValue{VoidString()}; + + SelectionProperties mSelectionProperties; + + bool mEverInited : 1; // Have we ever been initialized? + bool mEditorInitialized : 1; + bool mValueTransferInProgress : 1; // Whether a value is being transferred to + // the frame + bool mSelectionCached : 1; // Whether mSelectionProperties is valid + + friend class AutoTextControlHandlingState; + friend class PrepareEditorEvent; + friend class RestoreSelectionState; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_TextControlState_h diff --git a/dom/html/TextInputListener.h b/dom/html/TextInputListener.h new file mode 100644 index 0000000000..b8f02e4cfa --- /dev/null +++ b/dom/html/TextInputListener.h @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_TextInputListener_h +#define mozilla_TextInputListener_h + +#include "mozilla/WeakPtr.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIDOMEventListener.h" +#include "nsStringFwd.h" +#include "nsWeakReference.h" + +class nsIFrame; +class nsTextControlFrame; + +namespace mozilla { +class TextControlElement; +class TextControlState; +class TextEditor; + +namespace dom { +class Selection; +} // namespace dom + +class TextInputListener final : public nsIDOMEventListener, + public nsSupportsWeakReference { + public: + explicit TextInputListener(TextControlElement* aTextControlElement); + + void SetFrame(nsIFrame* aTextControlFrame) { mFrame = aTextControlFrame; } + void SettingValue(bool aValue) { mSettingValue = aValue; } + void SetValueChanged(bool aSetValueChanged) { + mSetValueChanged = aSetValueChanged; + } + + /** + * aFrame is an optional pointer to our frame, if not passed the method will + * use mFrame to compute it lazily. + */ + void HandleValueChanged(TextEditor&); + + /** + * OnEditActionHandled() is called when the editor handles each edit action. + */ + [[nodiscard]] MOZ_CAN_RUN_SCRIPT nsresult OnEditActionHandled(TextEditor&); + + /** + * OnSelectionChange() is called when selection is changed in the editor. + */ + MOZ_CAN_RUN_SCRIPT + void OnSelectionChange(dom::Selection& aSelection, int16_t aReason); + + /** + * Start to listen or end listening to selection change in the editor. + */ + void StartToListenToSelectionChange() { mListeningToSelectionChange = true; } + void EndListeningToSelectionChange() { mListeningToSelectionChange = false; } + + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(TextInputListener, + nsIDOMEventListener) + NS_DECL_NSIDOMEVENTLISTENER + + protected: + virtual ~TextInputListener() = default; + + nsresult UpdateTextInputCommands(const nsAString& aCommandsToUpdate); + + protected: + nsIFrame* mFrame; + TextControlElement* const mTxtCtrlElement; + WeakPtr<TextControlState> const mTextControlState; + + bool mSelectionWasCollapsed; + + /** + * Whether we had undo items or not the last time we got EditAction() + * notification (when this state changes we update undo and redo menus) + */ + bool mHadUndoItems; + /** + * Whether we had redo items or not the last time we got EditAction() + * notification (when this state changes we update undo and redo menus) + */ + bool mHadRedoItems; + /** + * Whether we're in the process of a SetValue call, and should therefore + * refrain from calling OnValueChanged. + */ + bool mSettingValue; + /** + * Whether we are in the process of a SetValue call that doesn't want + * |SetValueChanged| to be called. + */ + bool mSetValueChanged; + /** + * Whether we're listening to selection change in the editor. + */ + bool mListeningToSelectionChange; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_TextInputListener_h diff --git a/dom/html/TextTrackManager.cpp b/dom/html/TextTrackManager.cpp new file mode 100644 index 0000000000..7aacb5bf73 --- /dev/null +++ b/dom/html/TextTrackManager.cpp @@ -0,0 +1,874 @@ +/* -*- 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/TextTrackManager.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/Maybe.h" +#include "mozilla/Telemetry.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "mozilla/dom/HTMLTrackElement.h" +#include "mozilla/dom/HTMLVideoElement.h" +#include "mozilla/dom/TextTrack.h" +#include "mozilla/dom/TextTrackCue.h" +#include "nsComponentManagerUtils.h" +#include "nsGlobalWindowInner.h" +#include "nsIFrame.h" +#include "nsIWebVTTParserWrapper.h" +#include "nsVariant.h" +#include "nsVideoFrame.h" + +mozilla::LazyLogModule gTextTrackLog("WebVTT"); + +#define WEBVTT_LOG(msg, ...) \ + MOZ_LOG(gTextTrackLog, LogLevel::Debug, \ + ("TextTrackManager=%p, " msg, this, ##__VA_ARGS__)) +#define WEBVTT_LOGV(msg, ...) \ + MOZ_LOG(gTextTrackLog, LogLevel::Verbose, \ + ("TextTrackManager=%p, " msg, this, ##__VA_ARGS__)) + +namespace mozilla::dom { + +NS_IMPL_ISUPPORTS(TextTrackManager::ShutdownObserverProxy, nsIObserver); + +void TextTrackManager::ShutdownObserverProxy::Unregister() { + nsContentUtils::UnregisterShutdownObserver(this); + mManager = nullptr; +} + +CompareTextTracks::CompareTextTracks(HTMLMediaElement* aMediaElement) { + mMediaElement = aMediaElement; +} + +Maybe<uint32_t> CompareTextTracks::TrackChildPosition( + TextTrack* aTextTrack) const { + MOZ_DIAGNOSTIC_ASSERT(aTextTrack); + HTMLTrackElement* trackElement = aTextTrack->GetTrackElement(); + if (!trackElement) { + return Nothing(); + } + return mMediaElement->ComputeIndexOf(trackElement); +} + +bool CompareTextTracks::Equals(TextTrack* aOne, TextTrack* aTwo) const { + // Two tracks can never be equal. If they have corresponding TrackElements + // they would need to occupy the same tree position (impossible) and in the + // case of tracks coming from AddTextTrack source we put the newest at the + // last position, so they won't be equal as well. + return false; +} + +bool CompareTextTracks::LessThan(TextTrack* aOne, TextTrack* aTwo) const { + // Protect against nullptr TextTrack objects; treat them as + // sorting toward the end. + if (!aOne) { + return false; + } + if (!aTwo) { + return true; + } + TextTrackSource sourceOne = aOne->GetTextTrackSource(); + TextTrackSource sourceTwo = aTwo->GetTextTrackSource(); + if (sourceOne != sourceTwo) { + return sourceOne == TextTrackSource::Track || + (sourceOne == TextTrackSource::AddTextTrack && + sourceTwo == TextTrackSource::MediaResourceSpecific); + } + switch (sourceOne) { + case TextTrackSource::Track: { + Maybe<uint32_t> positionOne = TrackChildPosition(aOne); + Maybe<uint32_t> positionTwo = TrackChildPosition(aTwo); + // If either position one or positiontwo are Nothing then something has + // gone wrong. In this case we should just put them at the back of the + // list. + return positionOne.isSome() && positionTwo.isSome() && + *positionOne < *positionTwo; + } + case TextTrackSource::AddTextTrack: + // For AddTextTrack sources the tracks will already be in the correct + // relative order in the source array. Assume we're called in iteration + // order and can therefore always report aOne < aTwo to maintain the + // original temporal ordering. + return true; + case TextTrackSource::MediaResourceSpecific: + // No rules for Media Resource Specific tracks yet. + break; + } + return true; +} + +NS_IMPL_CYCLE_COLLECTION(TextTrackManager, mMediaElement, mTextTracks, + mPendingTextTracks, mNewCues) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackManager) + NS_INTERFACE_MAP_ENTRY(nsIDOMEventListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(TextTrackManager) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TextTrackManager) + +StaticRefPtr<nsIWebVTTParserWrapper> TextTrackManager::sParserWrapper; + +TextTrackManager::TextTrackManager(HTMLMediaElement* aMediaElement) + : mMediaElement(aMediaElement), + mHasSeeked(false), + mLastTimeMarchesOnCalled(media::TimeUnit::Zero()), + mTimeMarchesOnDispatched(false), + mUpdateCueDisplayDispatched(false), + performedTrackSelection(false), + mShutdown(false) { + nsISupports* parentObject = mMediaElement->OwnerDoc()->GetParentObject(); + + NS_ENSURE_TRUE_VOID(parentObject); + WEBVTT_LOG("Create TextTrackManager"); + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject); + mNewCues = new TextTrackCueList(window); + mTextTracks = new TextTrackList(window, this); + mPendingTextTracks = new TextTrackList(window, this); + + if (!sParserWrapper) { + nsCOMPtr<nsIWebVTTParserWrapper> parserWrapper = + do_CreateInstance(NS_WEBVTTPARSERWRAPPER_CONTRACTID); + MOZ_ASSERT(parserWrapper, "Can't create nsIWebVTTParserWrapper"); + sParserWrapper = parserWrapper; + ClearOnShutdown(&sParserWrapper); + } + mShutdownProxy = new ShutdownObserverProxy(this); +} + +TextTrackManager::~TextTrackManager() { + WEBVTT_LOG("~TextTrackManager"); + mShutdownProxy->Unregister(); +} + +TextTrackList* TextTrackManager::GetTextTracks() const { return mTextTracks; } + +already_AddRefed<TextTrack> TextTrackManager::AddTextTrack( + TextTrackKind aKind, const nsAString& aLabel, const nsAString& aLanguage, + TextTrackMode aMode, TextTrackReadyState aReadyState, + TextTrackSource aTextTrackSource) { + if (!mMediaElement || !mTextTracks) { + return nullptr; + } + RefPtr<TextTrack> track = mTextTracks->AddTextTrack( + aKind, aLabel, aLanguage, aMode, aReadyState, aTextTrackSource, + CompareTextTracks(mMediaElement)); + WEBVTT_LOG("AddTextTrack %p kind %" PRIu32 " Label %s Language %s", + track.get(), static_cast<uint32_t>(aKind), + NS_ConvertUTF16toUTF8(aLabel).get(), + NS_ConvertUTF16toUTF8(aLanguage).get()); + AddCues(track); + + if (aTextTrackSource == TextTrackSource::Track) { + RefPtr<nsIRunnable> task = NewRunnableMethod( + "dom::TextTrackManager::HonorUserPreferencesForTrackSelection", this, + &TextTrackManager::HonorUserPreferencesForTrackSelection); + NS_DispatchToMainThread(task.forget()); + } + + return track.forget(); +} + +void TextTrackManager::AddTextTrack(TextTrack* aTextTrack) { + if (!mMediaElement || !mTextTracks) { + return; + } + WEBVTT_LOG("AddTextTrack TextTrack %p", aTextTrack); + mTextTracks->AddTextTrack(aTextTrack, CompareTextTracks(mMediaElement)); + AddCues(aTextTrack); + + if (aTextTrack->GetTextTrackSource() == TextTrackSource::Track) { + RefPtr<nsIRunnable> task = NewRunnableMethod( + "dom::TextTrackManager::HonorUserPreferencesForTrackSelection", this, + &TextTrackManager::HonorUserPreferencesForTrackSelection); + NS_DispatchToMainThread(task.forget()); + } +} + +void TextTrackManager::AddCues(TextTrack* aTextTrack) { + if (!mNewCues) { + WEBVTT_LOG("AddCues mNewCues is null"); + return; + } + + TextTrackCueList* cueList = aTextTrack->GetCues(); + if (cueList) { + bool dummy; + WEBVTT_LOGV("AddCues, CuesNum=%d", cueList->Length()); + for (uint32_t i = 0; i < cueList->Length(); ++i) { + mNewCues->AddCue(*cueList->IndexedGetter(i, dummy)); + } + MaybeRunTimeMarchesOn(); + } +} + +void TextTrackManager::RemoveTextTrack(TextTrack* aTextTrack, + bool aPendingListOnly) { + if (!mPendingTextTracks || !mTextTracks) { + return; + } + + WEBVTT_LOG("RemoveTextTrack TextTrack %p", aTextTrack); + mPendingTextTracks->RemoveTextTrack(aTextTrack); + if (aPendingListOnly) { + return; + } + + mTextTracks->RemoveTextTrack(aTextTrack); + // Remove the cues in mNewCues belong to aTextTrack. + TextTrackCueList* removeCueList = aTextTrack->GetCues(); + if (removeCueList) { + WEBVTT_LOGV("RemoveTextTrack removeCuesNum=%d", removeCueList->Length()); + for (uint32_t i = 0; i < removeCueList->Length(); ++i) { + mNewCues->RemoveCue(*((*removeCueList)[i])); + } + MaybeRunTimeMarchesOn(); + } +} + +void TextTrackManager::DidSeek() { + WEBVTT_LOG("DidSeek"); + mHasSeeked = true; +} + +void TextTrackManager::UpdateCueDisplay() { + WEBVTT_LOG("UpdateCueDisplay"); + mUpdateCueDisplayDispatched = false; + + if (!mMediaElement || !mTextTracks || IsShutdown()) { + WEBVTT_LOG("Abort UpdateCueDisplay."); + return; + } + + nsIFrame* frame = mMediaElement->GetPrimaryFrame(); + nsVideoFrame* videoFrame = do_QueryFrame(frame); + if (!videoFrame) { + WEBVTT_LOG("Abort UpdateCueDisplay, because of no video frame."); + return; + } + + nsCOMPtr<nsIContent> overlay = videoFrame->GetCaptionOverlay(); + if (!overlay) { + WEBVTT_LOG("Abort UpdateCueDisplay, because of no overlay."); + return; + } + + RefPtr<nsPIDOMWindowInner> window = + mMediaElement->OwnerDoc()->GetInnerWindow(); + if (!window) { + WEBVTT_LOG("Abort UpdateCueDisplay, because of no window."); + } + + nsTArray<RefPtr<TextTrackCue>> showingCues; + mTextTracks->GetShowingCues(showingCues); + + WEBVTT_LOG("UpdateCueDisplay, processCues, showingCuesNum=%zu", + showingCues.Length()); + RefPtr<nsVariantCC> jsCues = new nsVariantCC(); + jsCues->SetAsArray(nsIDataType::VTYPE_INTERFACE, &NS_GET_IID(EventTarget), + showingCues.Length(), + static_cast<void*>(showingCues.Elements())); + nsCOMPtr<nsIContent> controls = videoFrame->GetVideoControls(); + + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "TextTrackManager::UpdateCueDisplay", + [window, jsCues, overlay, controls]() { + if (sParserWrapper) { + sParserWrapper->ProcessCues(window, jsCues, overlay, controls); + } + })); +} + +void TextTrackManager::NotifyCueAdded(TextTrackCue& aCue) { + WEBVTT_LOG("NotifyCueAdded, cue=%p", &aCue); + if (mNewCues) { + mNewCues->AddCue(aCue); + } + MaybeRunTimeMarchesOn(); +} + +void TextTrackManager::NotifyCueRemoved(TextTrackCue& aCue) { + WEBVTT_LOG("NotifyCueRemoved, cue=%p", &aCue); + if (mNewCues) { + mNewCues->RemoveCue(aCue); + } + MaybeRunTimeMarchesOn(); + DispatchUpdateCueDisplay(); +} + +void TextTrackManager::PopulatePendingList() { + if (!mTextTracks || !mPendingTextTracks || !mMediaElement) { + return; + } + uint32_t len = mTextTracks->Length(); + bool dummy; + for (uint32_t index = 0; index < len; ++index) { + TextTrack* ttrack = mTextTracks->IndexedGetter(index, dummy); + if (ttrack && ttrack->Mode() != TextTrackMode::Disabled && + ttrack->ReadyState() == TextTrackReadyState::Loading) { + mPendingTextTracks->AddTextTrack(ttrack, + CompareTextTracks(mMediaElement)); + } + } +} + +void TextTrackManager::AddListeners() { + if (mMediaElement) { + mMediaElement->AddEventListener(u"resizecaption"_ns, this, false, false); + mMediaElement->AddEventListener(u"resizevideocontrols"_ns, this, false, + false); + mMediaElement->AddEventListener(u"seeked"_ns, this, false, false); + mMediaElement->AddEventListener(u"controlbarchange"_ns, this, false, true); + } +} + +void TextTrackManager::HonorUserPreferencesForTrackSelection() { + if (performedTrackSelection || !mTextTracks) { + return; + } + WEBVTT_LOG("HonorUserPreferencesForTrackSelection"); + TextTrackKind ttKinds[] = {TextTrackKind::Captions, TextTrackKind::Subtitles}; + + // Steps 1 - 3: Perform automatic track selection for different TextTrack + // Kinds. + PerformTrackSelection(ttKinds, ArrayLength(ttKinds)); + PerformTrackSelection(TextTrackKind::Descriptions); + PerformTrackSelection(TextTrackKind::Chapters); + + // Step 4: Set all TextTracks with a kind of metadata that are disabled + // to hidden. + for (uint32_t i = 0; i < mTextTracks->Length(); i++) { + TextTrack* track = (*mTextTracks)[i]; + if (track->Kind() == TextTrackKind::Metadata && TrackIsDefault(track) && + track->Mode() == TextTrackMode::Disabled) { + track->SetMode(TextTrackMode::Hidden); + } + } + + performedTrackSelection = true; +} + +bool TextTrackManager::TrackIsDefault(TextTrack* aTextTrack) { + HTMLTrackElement* trackElement = aTextTrack->GetTrackElement(); + if (!trackElement) { + return false; + } + return trackElement->Default(); +} + +void TextTrackManager::PerformTrackSelection(TextTrackKind aTextTrackKind) { + TextTrackKind ttKinds[] = {aTextTrackKind}; + PerformTrackSelection(ttKinds, ArrayLength(ttKinds)); +} + +void TextTrackManager::PerformTrackSelection(TextTrackKind aTextTrackKinds[], + uint32_t size) { + nsTArray<TextTrack*> candidates; + GetTextTracksOfKinds(aTextTrackKinds, size, candidates); + + // Step 3: If any TextTracks in candidates are showing then abort these steps. + for (uint32_t i = 0; i < candidates.Length(); i++) { + if (candidates[i]->Mode() == TextTrackMode::Showing) { + WEBVTT_LOGV("PerformTrackSelection Showing return kind %d", + static_cast<int>(candidates[i]->Kind())); + return; + } + } + + // Step 4: Honor user preferences for track selection, otherwise, set the + // first TextTrack in candidates with a default attribute to showing. + // TODO: Bug 981691 - Honor user preferences for text track selection. + for (uint32_t i = 0; i < candidates.Length(); i++) { + if (TrackIsDefault(candidates[i]) && + candidates[i]->Mode() == TextTrackMode::Disabled) { + candidates[i]->SetMode(TextTrackMode::Showing); + WEBVTT_LOGV("PerformTrackSelection set Showing kind %d", + static_cast<int>(candidates[i]->Kind())); + return; + } + } +} + +void TextTrackManager::GetTextTracksOfKinds(TextTrackKind aTextTrackKinds[], + uint32_t size, + nsTArray<TextTrack*>& aTextTracks) { + for (uint32_t i = 0; i < size; i++) { + GetTextTracksOfKind(aTextTrackKinds[i], aTextTracks); + } +} + +void TextTrackManager::GetTextTracksOfKind(TextTrackKind aTextTrackKind, + nsTArray<TextTrack*>& aTextTracks) { + if (!mTextTracks) { + return; + } + for (uint32_t i = 0; i < mTextTracks->Length(); i++) { + TextTrack* textTrack = (*mTextTracks)[i]; + if (textTrack->Kind() == aTextTrackKind) { + aTextTracks.AppendElement(textTrack); + } + } +} + +NS_IMETHODIMP +TextTrackManager::HandleEvent(Event* aEvent) { + if (!mTextTracks) { + return NS_OK; + } + + nsAutoString type; + aEvent->GetType(type); + WEBVTT_LOG("Handle event %s", NS_ConvertUTF16toUTF8(type).get()); + + const bool setDirty = type.EqualsLiteral("seeked") || + type.EqualsLiteral("resizecaption") || + type.EqualsLiteral("resizevideocontrols"); + const bool updateDisplay = type.EqualsLiteral("controlbarchange") || + type.EqualsLiteral("resizecaption"); + + if (setDirty) { + for (uint32_t i = 0; i < mTextTracks->Length(); i++) { + ((*mTextTracks)[i])->SetCuesDirty(); + } + } + if (updateDisplay) { + UpdateCueDisplay(); + } + + return NS_OK; +} + +class SimpleTextTrackEvent : public Runnable { + public: + friend class CompareSimpleTextTrackEvents; + SimpleTextTrackEvent(const nsAString& aEventName, double aTime, + TextTrack* aTrack, TextTrackCue* aCue) + : Runnable("dom::SimpleTextTrackEvent"), + mName(aEventName), + mTime(aTime), + mTrack(aTrack), + mCue(aCue) {} + + NS_IMETHOD Run() override { + WEBVTT_LOGV("SimpleTextTrackEvent cue %p mName %s mTime %lf", mCue.get(), + NS_ConvertUTF16toUTF8(mName).get(), mTime); + mCue->DispatchTrustedEvent(mName); + return NS_OK; + } + + void Dispatch() { + if (nsCOMPtr<nsIGlobalObject> global = mCue->GetOwnerGlobal()) { + global->Dispatch(do_AddRef(this)); + } else { + NS_DispatchToMainThread(do_AddRef(this)); + } + } + + private: + nsString mName; + double mTime; + TextTrack* mTrack; + RefPtr<TextTrackCue> mCue; +}; + +class CompareSimpleTextTrackEvents { + private: + Maybe<uint32_t> TrackChildPosition(SimpleTextTrackEvent* aEvent) const { + if (aEvent->mTrack) { + HTMLTrackElement* trackElement = aEvent->mTrack->GetTrackElement(); + if (trackElement) { + return mMediaElement->ComputeIndexOf(trackElement); + } + } + return Nothing(); + } + HTMLMediaElement* mMediaElement; + + public: + explicit CompareSimpleTextTrackEvents(HTMLMediaElement* aMediaElement) { + mMediaElement = aMediaElement; + } + + bool Equals(SimpleTextTrackEvent* aOne, SimpleTextTrackEvent* aTwo) const { + return false; + } + + bool LessThan(SimpleTextTrackEvent* aOne, SimpleTextTrackEvent* aTwo) const { + // TimeMarchesOn step 13.1. + if (aOne->mTime < aTwo->mTime) { + return true; + } + if (aOne->mTime > aTwo->mTime) { + return false; + } + + // TimeMarchesOn step 13.2 text track cue order. + // TextTrack position in TextTrackList + TextTrack* t1 = aOne->mTrack; + TextTrack* t2 = aTwo->mTrack; + MOZ_ASSERT(t1, "CompareSimpleTextTrackEvents t1 is null"); + MOZ_ASSERT(t2, "CompareSimpleTextTrackEvents t2 is null"); + if (t1 != t2) { + TextTrackList* tList = t1->GetTextTrackList(); + MOZ_ASSERT(tList, "CompareSimpleTextTrackEvents tList is null"); + nsTArray<RefPtr<TextTrack>>& textTracks = tList->GetTextTrackArray(); + auto index1 = textTracks.IndexOf(t1); + auto index2 = textTracks.IndexOf(t2); + if (index1 < index2) { + return true; + } + if (index1 > index2) { + return false; + } + } + + MOZ_ASSERT(t1 == t2, "CompareSimpleTextTrackEvents t1 != t2"); + // c1 and c2 are both belongs to t1. + TextTrackCue* c1 = aOne->mCue; + TextTrackCue* c2 = aTwo->mCue; + if (c1 != c2) { + if (c1->StartTime() < c2->StartTime()) { + return true; + } + if (c1->StartTime() > c2->StartTime()) { + return false; + } + if (c1->EndTime() < c2->EndTime()) { + return true; + } + if (c1->EndTime() > c2->EndTime()) { + return false; + } + + TextTrackCueList* cueList = t1->GetCues(); + MOZ_ASSERT(cueList); + nsTArray<RefPtr<TextTrackCue>>& cues = cueList->GetCuesArray(); + auto index1 = cues.IndexOf(c1); + auto index2 = cues.IndexOf(c2); + if (index1 < index2) { + return true; + } + if (index1 > index2) { + return false; + } + } + + // TimeMarchesOn step 13.3. + if (aOne->mName.EqualsLiteral("enter") || + aTwo->mName.EqualsLiteral("exit")) { + return true; + } + return false; + } +}; + +class TextTrackListInternal { + public: + void AddTextTrack(TextTrack* aTextTrack, + const CompareTextTracks& aCompareTT) { + if (!mTextTracks.Contains(aTextTrack)) { + mTextTracks.InsertElementSorted(aTextTrack, aCompareTT); + } + } + uint32_t Length() const { return mTextTracks.Length(); } + TextTrack* operator[](uint32_t aIndex) { + return mTextTracks.SafeElementAt(aIndex, nullptr); + } + + private: + nsTArray<RefPtr<TextTrack>> mTextTracks; +}; + +void TextTrackManager::DispatchUpdateCueDisplay() { + if (!mUpdateCueDisplayDispatched && !IsShutdown()) { + WEBVTT_LOG("DispatchUpdateCueDisplay"); + if (nsPIDOMWindowInner* win = mMediaElement->OwnerDoc()->GetInnerWindow()) { + nsGlobalWindowInner::Cast(win)->Dispatch( + NewRunnableMethod("dom::TextTrackManager::UpdateCueDisplay", this, + &TextTrackManager::UpdateCueDisplay)); + mUpdateCueDisplayDispatched = true; + } + } +} + +void TextTrackManager::DispatchTimeMarchesOn() { + // Run the algorithm if no previous instance is still running, otherwise + // enqueue the current playback position and whether only that changed + // through its usual monotonic increase during normal playback; current + // executing call upon completion will check queue for further 'work'. + if (!mTimeMarchesOnDispatched && !IsShutdown()) { + WEBVTT_LOG("DispatchTimeMarchesOn"); + if (nsPIDOMWindowInner* win = mMediaElement->OwnerDoc()->GetInnerWindow()) { + nsGlobalWindowInner::Cast(win)->Dispatch( + NewRunnableMethod("dom::TextTrackManager::TimeMarchesOn", this, + &TextTrackManager::TimeMarchesOn)); + mTimeMarchesOnDispatched = true; + } + } +} + +// https://html.spec.whatwg.org/multipage/embedded-content.html#time-marches-on +void TextTrackManager::TimeMarchesOn() { + NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); + mTimeMarchesOnDispatched = false; + + CycleCollectedJSContext* context = CycleCollectedJSContext::Get(); + if (context && context->IsInStableOrMetaStableState()) { + // FireTimeUpdate can be called while at stable state following a + // current position change which triggered a state watcher in MediaDecoder + // (see bug 1443429). + // TimeMarchesOn() will modify JS attributes which is forbidden while in + // stable state. So we dispatch a task to perform such operation later + // instead. + DispatchTimeMarchesOn(); + return; + } + WEBVTT_LOG("TimeMarchesOn"); + + // Early return if we don't have any TextTracks or shutting down. + if (!mTextTracks || mTextTracks->Length() == 0 || IsShutdown() || + !mMediaElement) { + return; + } + + if (mMediaElement->ReadyState() == HTMLMediaElement_Binding::HAVE_NOTHING) { + WEBVTT_LOG( + "TimeMarchesOn return because media doesn't contain any data yet"); + return; + } + + if (mMediaElement->Seeking()) { + WEBVTT_LOG("TimeMarchesOn return during seeking"); + return; + } + + // Step 1, 2. + nsISupports* parentObject = mMediaElement->OwnerDoc()->GetParentObject(); + if (NS_WARN_IF(!parentObject)) { + return; + } + nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(parentObject); + RefPtr<TextTrackCueList> currentCues = new TextTrackCueList(window); + RefPtr<TextTrackCueList> otherCues = new TextTrackCueList(window); + + // Step 3. + auto currentPlaybackTime = + media::TimeUnit::FromSeconds(mMediaElement->CurrentTime()); + bool hasNormalPlayback = !mHasSeeked; + mHasSeeked = false; + WEBVTT_LOG( + "TimeMarchesOn mLastTimeMarchesOnCalled %lf currentPlaybackTime %lf " + "hasNormalPlayback %d", + mLastTimeMarchesOnCalled.ToSeconds(), currentPlaybackTime.ToSeconds(), + hasNormalPlayback); + + // The reason we collect other cues is (1) to change active cues to inactive, + // (2) find missing cues, so we actually no need to process all cues. We just + // need to handle cues which are in the time interval [lastTime:currentTime] + // or [currentTime:lastTime] (seeking forward). That can help us to reduce the + // size of other cues, which can improve execution time. + auto start = std::min(mLastTimeMarchesOnCalled, currentPlaybackTime); + auto end = std::max(mLastTimeMarchesOnCalled, currentPlaybackTime); + media::TimeInterval interval(start, end); + WEBVTT_LOGV("TimeMarchesOn Time interval [%f:%f]", start.ToSeconds(), + end.ToSeconds()); + for (uint32_t idx = 0; idx < mTextTracks->Length(); ++idx) { + TextTrack* track = (*mTextTracks)[idx]; + if (track) { + track->GetCurrentCuesAndOtherCues(currentCues, otherCues, interval); + } + } + + // Step 4. + RefPtr<TextTrackCueList> missedCues = new TextTrackCueList(window); + if (hasNormalPlayback) { + for (uint32_t i = 0; i < otherCues->Length(); ++i) { + TextTrackCue* cue = (*otherCues)[i]; + if (cue->StartTime() >= mLastTimeMarchesOnCalled.ToSeconds() && + cue->EndTime() <= currentPlaybackTime.ToSeconds()) { + missedCues->AddCue(*cue); + } + } + } + + WEBVTT_LOGV("TimeMarchesOn currentCues %d", currentCues->Length()); + WEBVTT_LOGV("TimeMarchesOn otherCues %d", otherCues->Length()); + WEBVTT_LOGV("TimeMarchesOn missedCues %d", missedCues->Length()); + // Step 5. Empty now. + // TODO: Step 6: fire timeupdate? + + // Step 7. Abort steps if condition 1, 2, 3 are satisfied. + // 1. All of the cues in current cues have their active flag set. + // 2. None of the cues in other cues have their active flag set. + // 3. Missed cues is empty. + bool c1 = true; + for (uint32_t i = 0; i < currentCues->Length(); ++i) { + if (!(*currentCues)[i]->GetActive()) { + c1 = false; + break; + } + } + bool c2 = true; + for (uint32_t i = 0; i < otherCues->Length(); ++i) { + if ((*otherCues)[i]->GetActive()) { + c2 = false; + break; + } + } + bool c3 = (missedCues->Length() == 0); + if (c1 && c2 && c3) { + mLastTimeMarchesOnCalled = currentPlaybackTime; + WEBVTT_LOG("TimeMarchesOn step 7 return, mLastTimeMarchesOnCalled %lf", + mLastTimeMarchesOnCalled.ToSeconds()); + return; + } + + // Step 8. Respect PauseOnExit flag if not seek. + if (hasNormalPlayback) { + for (uint32_t i = 0; i < otherCues->Length(); ++i) { + TextTrackCue* cue = (*otherCues)[i]; + if (cue && cue->PauseOnExit() && cue->GetActive()) { + WEBVTT_LOG("TimeMarchesOn pause the MediaElement"); + mMediaElement->Pause(); + break; + } + } + for (uint32_t i = 0; i < missedCues->Length(); ++i) { + TextTrackCue* cue = (*missedCues)[i]; + if (cue && cue->PauseOnExit()) { + WEBVTT_LOG("TimeMarchesOn pause the MediaElement"); + mMediaElement->Pause(); + break; + } + } + } + + // Step 15. + // Sort text tracks in the same order as the text tracks appear + // in the media element's list of text tracks, and remove + // duplicates. + TextTrackListInternal affectedTracks; + // Step 13, 14. + nsTArray<RefPtr<SimpleTextTrackEvent>> eventList; + // Step 9, 10. + // For each text track cue in missed cues, prepare an event named + // enter for the TextTrackCue object with the cue start time. + for (uint32_t i = 0; i < missedCues->Length(); ++i) { + TextTrackCue* cue = (*missedCues)[i]; + if (cue) { + WEBVTT_LOG("Prepare 'enter' event for cue %p [%f, %f] in missing cues", + cue, cue->StartTime(), cue->EndTime()); + SimpleTextTrackEvent* event = new SimpleTextTrackEvent( + u"enter"_ns, cue->StartTime(), cue->GetTrack(), cue); + eventList.InsertElementSorted( + event, CompareSimpleTextTrackEvents(mMediaElement)); + affectedTracks.AddTextTrack(cue->GetTrack(), + CompareTextTracks(mMediaElement)); + } + } + + // Step 11, 17. + for (uint32_t i = 0; i < otherCues->Length(); ++i) { + TextTrackCue* cue = (*otherCues)[i]; + if (cue->GetActive() || missedCues->IsCueExist(cue)) { + double time = + cue->StartTime() > cue->EndTime() ? cue->StartTime() : cue->EndTime(); + WEBVTT_LOG("Prepare 'exit' event for cue %p [%f, %f] in other cues", cue, + cue->StartTime(), cue->EndTime()); + SimpleTextTrackEvent* event = + new SimpleTextTrackEvent(u"exit"_ns, time, cue->GetTrack(), cue); + eventList.InsertElementSorted( + event, CompareSimpleTextTrackEvents(mMediaElement)); + affectedTracks.AddTextTrack(cue->GetTrack(), + CompareTextTracks(mMediaElement)); + } + cue->SetActive(false); + } + + // Step 12, 17. + for (uint32_t i = 0; i < currentCues->Length(); ++i) { + TextTrackCue* cue = (*currentCues)[i]; + if (!cue->GetActive()) { + WEBVTT_LOG("Prepare 'enter' event for cue %p [%f, %f] in current cues", + cue, cue->StartTime(), cue->EndTime()); + SimpleTextTrackEvent* event = new SimpleTextTrackEvent( + u"enter"_ns, cue->StartTime(), cue->GetTrack(), cue); + eventList.InsertElementSorted( + event, CompareSimpleTextTrackEvents(mMediaElement)); + affectedTracks.AddTextTrack(cue->GetTrack(), + CompareTextTracks(mMediaElement)); + } + cue->SetActive(true); + } + + // Fire the eventList + for (uint32_t i = 0; i < eventList.Length(); ++i) { + eventList[i]->Dispatch(); + } + + // Step 16. + for (uint32_t i = 0; i < affectedTracks.Length(); ++i) { + TextTrack* ttrack = affectedTracks[i]; + if (ttrack) { + ttrack->DispatchAsyncTrustedEvent(u"cuechange"_ns); + HTMLTrackElement* trackElement = ttrack->GetTrackElement(); + if (trackElement) { + trackElement->DispatchTrackRunnable(u"cuechange"_ns); + } + } + } + + mLastTimeMarchesOnCalled = currentPlaybackTime; + + // Step 18. + UpdateCueDisplay(); +} + +void TextTrackManager::NotifyCueUpdated(TextTrackCue* aCue) { + // TODO: Add/Reorder the cue to mNewCues if we have some optimization? + WEBVTT_LOG("NotifyCueUpdated, cue=%p", aCue); + MaybeRunTimeMarchesOn(); + // For the case "Texttrack.mode = hidden/showing", if the mode + // changing between showing and hidden, TimeMarchesOn + // doesn't render the cue. Call DispatchUpdateCueDisplay() explicitly. + DispatchUpdateCueDisplay(); +} + +void TextTrackManager::NotifyReset() { + // https://html.spec.whatwg.org/multipage/media.html#text-track-cue-active-flag + // This will unset all cues' active flag and update the cue display. + WEBVTT_LOG("NotifyReset"); + mLastTimeMarchesOnCalled = media::TimeUnit::Zero(); + for (uint32_t idx = 0; idx < mTextTracks->Length(); ++idx) { + (*mTextTracks)[idx]->SetCuesInactive(); + } + UpdateCueDisplay(); +} + +bool TextTrackManager::IsLoaded() { + return mTextTracks ? mTextTracks->AreTextTracksLoaded() : true; +} + +bool TextTrackManager::IsShutdown() const { + return (mShutdown || !sParserWrapper); +} + +void TextTrackManager::MaybeRunTimeMarchesOn() { + MOZ_ASSERT(mMediaElement); + // According to spec, we should check media element's show poster flag before + // running `TimeMarchesOn` in following situations, (1) add cue (2) remove cue + // (3) cue's start time changes (4) cues's end time changes + // https://html.spec.whatwg.org/multipage/media.html#playing-the-media-resource:time-marches-on + // https://html.spec.whatwg.org/multipage/media.html#text-track-api:time-marches-on + if (mMediaElement->GetShowPosterFlag()) { + return; + } + TimeMarchesOn(); +} + +} // namespace mozilla::dom diff --git a/dom/html/TextTrackManager.h b/dom/html/TextTrackManager.h new file mode 100644 index 0000000000..6ce19963d5 --- /dev/null +++ b/dom/html/TextTrackManager.h @@ -0,0 +1,194 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_TextTrackManager_h +#define mozilla_dom_TextTrackManager_h + +#include "mozilla/dom/TextTrack.h" +#include "mozilla/dom/TextTrackList.h" +#include "mozilla/dom/TextTrackCueList.h" +#include "mozilla/StaticPtr.h" +#include "nsContentUtils.h" +#include "nsIDOMEventListener.h" +#include "TimeUnits.h" + +class nsIWebVTTParserWrapper; + +namespace mozilla { +template <typename T> +class Maybe; +namespace dom { + +class HTMLMediaElement; + +class CompareTextTracks { + private: + HTMLMediaElement* mMediaElement; + Maybe<uint32_t> TrackChildPosition(TextTrack* aTrack) const; + + public: + explicit CompareTextTracks(HTMLMediaElement* aMediaElement); + bool Equals(TextTrack* aOne, TextTrack* aTwo) const; + bool LessThan(TextTrack* aOne, TextTrack* aTwo) const; +}; + +class TextTrack; +class TextTrackCue; + +class TextTrackManager final : public nsIDOMEventListener { + ~TextTrackManager(); + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS(TextTrackManager) + + NS_DECL_NSIDOMEVENTLISTENER + + explicit TextTrackManager(HTMLMediaElement* aMediaElement); + + TextTrackList* GetTextTracks() const; + already_AddRefed<TextTrack> AddTextTrack(TextTrackKind aKind, + const nsAString& aLabel, + const nsAString& aLanguage, + TextTrackMode aMode, + TextTrackReadyState aReadyState, + TextTrackSource aTextTrackSource); + void AddTextTrack(TextTrack* aTextTrack); + void RemoveTextTrack(TextTrack* aTextTrack, bool aPendingListOnly); + void DidSeek(); + + void NotifyCueAdded(TextTrackCue& aCue); + void AddCues(TextTrack* aTextTrack); + void NotifyCueRemoved(TextTrackCue& aCue); + /** + * Overview of WebVTT cuetext and anonymous content setup. + * + * WebVTT nodes are the parsed version of WebVTT cuetext. WebVTT cuetext is + * the portion of a WebVTT cue that specifies what the caption will actually + * show up as on screen. + * + * WebVTT cuetext can contain markup that loosely relates to HTML markup. It + * can contain tags like <b>, <u>, <i>, <c>, <v>, <ruby>, <rt>, <lang>, + * including timestamp tags. + * + * When the caption is ready to be displayed the WebVTT nodes are converted + * over to anonymous DOM content. <i>, <u>, <b>, <ruby>, and <rt> all become + * HTMLElements of their corresponding HTML markup tags. <c> and <v> are + * converted to <span> tags. Timestamp tags are converted to XML processing + * instructions. Additionally, all cuetext tags support specifying of classes. + * This takes the form of <foo.class.subclass>. These classes are then parsed + * and set as the anonymous content's class attribute. + * + * Rules on constructing DOM objects from WebVTT nodes can be found here + * http://dev.w3.org/html5/webvtt/#webvtt-cue-text-dom-construction-rules. + * Current rules are taken from revision on April 15, 2013. + */ + + void PopulatePendingList(); + + void AddListeners(); + + // The HTMLMediaElement that this TextTrackManager manages the TextTracks of. + RefPtr<HTMLMediaElement> mMediaElement; + + void DispatchTimeMarchesOn(); + void TimeMarchesOn(); + void DispatchUpdateCueDisplay(); + + void NotifyShutdown() { mShutdown = true; } + + void NotifyCueUpdated(TextTrackCue* aCue); + + void NotifyReset(); + + bool IsLoaded(); + + private: + /** + * Converts the TextTrackCue's cuetext into a tree of DOM objects + * and attaches it to a div on its owning TrackElement's + * MediaElement's caption overlay. + */ + void UpdateCueDisplay(); + + // List of the TextTrackManager's owning HTMLMediaElement's TextTracks. + RefPtr<TextTrackList> mTextTracks; + // List of text track objects awaiting loading. + RefPtr<TextTrackList> mPendingTextTracks; + // List of newly introduced Text Track cues. + + // Contain all cues for a MediaElement. Not sorted. + RefPtr<TextTrackCueList> mNewCues; + + // True if the media player playback changed due to seeking prior to and + // during running the "Time Marches On" algorithm. + bool mHasSeeked; + // Playback position at the time of last "Time Marches On" call + media::TimeUnit mLastTimeMarchesOnCalled; + + bool mTimeMarchesOnDispatched; + bool mUpdateCueDisplayDispatched; + + static StaticRefPtr<nsIWebVTTParserWrapper> sParserWrapper; + + bool performedTrackSelection; + + // Runs the algorithm for performing automatic track selection. + void HonorUserPreferencesForTrackSelection(); + // Performs track selection for a single TextTrackKind. + void PerformTrackSelection(TextTrackKind aTextTrackKind); + // Performs track selection for a set of TextTrackKinds, for example, + // 'subtitles' and 'captions' should be selected together. + void PerformTrackSelection(TextTrackKind aTextTrackKinds[], uint32_t size); + void GetTextTracksOfKinds(TextTrackKind aTextTrackKinds[], uint32_t size, + nsTArray<TextTrack*>& aTextTracks); + void GetTextTracksOfKind(TextTrackKind aTextTrackKind, + nsTArray<TextTrack*>& aTextTracks); + bool TrackIsDefault(TextTrack* aTextTrack); + + bool IsShutdown() const; + + // This function will check media element's show poster flag to decide whether + // we need to run `TimeMarchesOn`. + void MaybeRunTimeMarchesOn(); + + class ShutdownObserverProxy final : public nsIObserver { + NS_DECL_ISUPPORTS + + public: + explicit ShutdownObserverProxy(TextTrackManager* aManager) + : mManager(aManager) { + nsContentUtils::RegisterShutdownObserver(this); + } + + NS_IMETHODIMP Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) override { + MOZ_ASSERT(NS_IsMainThread()); + if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { + if (mManager) { + mManager->NotifyShutdown(); + } + Unregister(); + } + return NS_OK; + } + + void Unregister(); + + private: + ~ShutdownObserverProxy() = default; + + TextTrackManager* mManager; + }; + + RefPtr<ShutdownObserverProxy> mShutdownProxy; + bool mShutdown; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TextTrackManager_h diff --git a/dom/html/TimeRanges.cpp b/dom/html/TimeRanges.cpp new file mode 100644 index 0000000000..21f1f56baa --- /dev/null +++ b/dom/html/TimeRanges.cpp @@ -0,0 +1,183 @@ +/* -*- 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/TimeRanges.h" +#include "mozilla/dom/TimeRangesBinding.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "TimeUnits.h" +#include "nsError.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TimeRanges, mParent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(TimeRanges) +NS_IMPL_CYCLE_COLLECTING_RELEASE(TimeRanges) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TimeRanges) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +TimeRanges::TimeRanges() : mParent(nullptr) {} + +TimeRanges::TimeRanges(nsISupports* aParent) : mParent(aParent) {} + +TimeRanges::TimeRanges(nsISupports* aParent, + const media::TimeIntervals& aTimeIntervals) + : TimeRanges(aParent) { + if (aTimeIntervals.IsInvalid()) { + return; + } + for (const media::TimeInterval& interval : aTimeIntervals) { + Add(interval.mStart.ToSeconds(), interval.mEnd.ToSeconds()); + } +} + +TimeRanges::TimeRanges(nsISupports* aParent, + const media::TimeRanges& aTimeRanges) + : TimeRanges(aParent) { + if (aTimeRanges.IsInvalid()) { + return; + } + for (const media::TimeRange& interval : aTimeRanges) { + Add(interval.mStart, interval.mEnd); + } +} + +TimeRanges::TimeRanges(const media::TimeIntervals& aTimeIntervals) + : TimeRanges(nullptr, aTimeIntervals) {} + +TimeRanges::TimeRanges(const media::TimeRanges& aTimeRanges) + : TimeRanges(nullptr, aTimeRanges) {} + +media::TimeIntervals TimeRanges::ToTimeIntervals() const { + media::TimeIntervals t; + for (uint32_t i = 0; i < Length(); i++) { + t += media::TimeInterval(media::TimeUnit::FromSeconds(Start(i)), + media::TimeUnit::FromSeconds(End(i))); + } + return t; +} + +TimeRanges::~TimeRanges() = default; + +double TimeRanges::Start(uint32_t aIndex, ErrorResult& aRv) const { + if (aIndex >= mRanges.Length()) { + aRv = NS_ERROR_DOM_INDEX_SIZE_ERR; + return 0; + } + + return Start(aIndex); +} + +double TimeRanges::End(uint32_t aIndex, ErrorResult& aRv) const { + if (aIndex >= mRanges.Length()) { + aRv = NS_ERROR_DOM_INDEX_SIZE_ERR; + return 0; + } + + return End(aIndex); +} + +void TimeRanges::Add(double aStart, double aEnd) { + if (aStart > aEnd) { + NS_WARNING("Can't add a range if the end is older that the start."); + return; + } + mRanges.AppendElement(TimeRange(aStart, aEnd)); +} + +double TimeRanges::GetStartTime() { + if (mRanges.IsEmpty()) { + return -1.0; + } + return mRanges[0].mStart; +} + +double TimeRanges::GetEndTime() { + if (mRanges.IsEmpty()) { + return -1.0; + } + return mRanges[mRanges.Length() - 1].mEnd; +} + +void TimeRanges::Normalize(double aTolerance) { + if (mRanges.Length() >= 2) { + AutoTArray<TimeRange, 4> normalized; + + mRanges.Sort(CompareTimeRanges()); + + // This merges the intervals. + TimeRange current(mRanges[0]); + for (uint32_t i = 1; i < mRanges.Length(); i++) { + if (current.mStart <= mRanges[i].mStart && + current.mEnd >= mRanges[i].mEnd) { + continue; + } + if (current.mEnd + aTolerance >= mRanges[i].mStart) { + current.mEnd = mRanges[i].mEnd; + } else { + normalized.AppendElement(current); + current = mRanges[i]; + } + } + + normalized.AppendElement(current); + + mRanges = std::move(normalized); + } +} + +void TimeRanges::Union(const TimeRanges* aOtherRanges, double aTolerance) { + mRanges.AppendElements(aOtherRanges->mRanges); + Normalize(aTolerance); +} + +void TimeRanges::Intersection(const TimeRanges* aOtherRanges) { + AutoTArray<TimeRange, 4> intersection; + + const nsTArray<TimeRange>& otherRanges = aOtherRanges->mRanges; + for (index_type i = 0, j = 0; + i < mRanges.Length() && j < otherRanges.Length();) { + double start = std::max(mRanges[i].mStart, otherRanges[j].mStart); + double end = std::min(mRanges[i].mEnd, otherRanges[j].mEnd); + if (start < end) { + intersection.AppendElement(TimeRange(start, end)); + } + if (mRanges[i].mEnd < otherRanges[j].mEnd) { + i += 1; + } else { + j += 1; + } + } + + mRanges = std::move(intersection); +} + +TimeRanges::index_type TimeRanges::Find(double aTime, + double aTolerance /* = 0 */) { + for (index_type i = 0; i < mRanges.Length(); ++i) { + if (aTime < mRanges[i].mEnd && (aTime + aTolerance) >= mRanges[i].mStart) { + return i; + } + } + return NoIndex; +} + +JSObject* TimeRanges::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return TimeRanges_Binding::Wrap(aCx, this, aGivenProto); +} + +nsISupports* TimeRanges::GetParentObject() const { return mParent; } + +void TimeRanges::Shift(double aOffset) { + for (index_type i = 0; i < mRanges.Length(); ++i) { + mRanges[i].mStart += aOffset; + mRanges[i].mEnd += aOffset; + } +} + +} // namespace mozilla::dom diff --git a/dom/html/TimeRanges.h b/dom/html/TimeRanges.h new file mode 100644 index 0000000000..b96e747c01 --- /dev/null +++ b/dom/html/TimeRanges.h @@ -0,0 +1,119 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_TimeRanges_h_ +#define mozilla_dom_TimeRanges_h_ + +#include "nsCOMPtr.h" +#include "nsISupports.h" +#include "nsTArray.h" +#include "nsWrapperCache.h" +#include "TimeUnits.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { +class TimeRanges; +} // namespace dom + +namespace dom { + +// Implements media TimeRanges: +// http://www.whatwg.org/specs/web-apps/current-work/multipage/video.html#timeranges +class TimeRanges final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TimeRanges) + + TimeRanges(); + explicit TimeRanges(nsISupports* aParent); + explicit TimeRanges(const media::TimeIntervals& aTimeIntervals); + explicit TimeRanges(const media::TimeRanges& aTimeRanges); + TimeRanges(nsISupports* aParent, const media::TimeIntervals& aTimeIntervals); + TimeRanges(nsISupports* aParent, const media::TimeRanges& aTimeRanges); + + media::TimeIntervals ToTimeIntervals() const; + + void Add(double aStart, double aEnd); + + // Returns the start time of the first range, or -1 if no ranges exist. + double GetStartTime(); + + // Returns the end time of the last range, or -1 if no ranges exist. + double GetEndTime(); + + // See http://www.whatwg.org/html/#normalized-timeranges-object + void Normalize(double aTolerance = 0.0); + + // Mutate this TimeRange to be the union of this and aOtherRanges. + void Union(const TimeRanges* aOtherRanges, double aTolerance); + + // Mutate this TimeRange to be the intersection of this and aOtherRanges. + void Intersection(const TimeRanges* aOtherRanges); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + nsISupports* GetParentObject() const; + + uint32_t Length() const { return mRanges.Length(); } + + double Start(uint32_t aIndex, ErrorResult& aRv) const; + + double End(uint32_t aIndex, ErrorResult& aRv) const; + + double Start(uint32_t aIndex) const { return mRanges[aIndex].mStart; } + + double End(uint32_t aIndex) const { return mRanges[aIndex].mEnd; } + + // Shift all values by aOffset seconds. + void Shift(double aOffset); + + private: + ~TimeRanges(); + + // Comparator which orders TimeRanges by start time. Used by Normalize(). + struct TimeRange { + TimeRange(double aStart, double aEnd) : mStart(aStart), mEnd(aEnd) {} + double mStart; + double mEnd; + }; + + struct CompareTimeRanges { + bool Equals(const TimeRange& aTr1, const TimeRange& aTr2) const { + return aTr1.mStart == aTr2.mStart && aTr1.mEnd == aTr2.mEnd; + } + + bool LessThan(const TimeRange& aTr1, const TimeRange& aTr2) const { + return aTr1.mStart < aTr2.mStart; + } + }; + + AutoTArray<TimeRange, 4> mRanges; + + nsCOMPtr<nsISupports> mParent; + + public: + typedef nsTArray<TimeRange>::index_type index_type; + static const index_type NoIndex = index_type(-1); + + index_type Find(double aTime, double aTolerance = 0); + + bool Contains(double aStart, double aEnd) { + index_type target = Find(aStart); + if (target == NoIndex) { + return false; + } + + return mRanges[target].mEnd >= aEnd; + } +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_TimeRanges_h_ diff --git a/dom/html/ValidityState.cpp b/dom/html/ValidityState.cpp new file mode 100644 index 0000000000..0c78cf9385 --- /dev/null +++ b/dom/html/ValidityState.cpp @@ -0,0 +1,31 @@ +/* -*- 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/ValidityState.h" +#include "mozilla/dom/ValidityStateBinding.h" + +#include "nsCycleCollectionParticipant.h" + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(ValidityState, mConstraintValidation) +NS_IMPL_CYCLE_COLLECTING_ADDREF(ValidityState) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ValidityState) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ValidityState) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +ValidityState::ValidityState(nsIConstraintValidation* aConstraintValidation) + : mConstraintValidation(aConstraintValidation) {} + +JSObject* ValidityState::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return ValidityState_Binding::Wrap(aCx, this, aGivenProto); +} + +} // namespace mozilla::dom diff --git a/dom/html/ValidityState.h b/dom/html/ValidityState.h new file mode 100644 index 0000000000..b9cf7cf464 --- /dev/null +++ b/dom/html/ValidityState.h @@ -0,0 +1,93 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ValidityState_h +#define mozilla_dom_ValidityState_h + +#include "nsCOMPtr.h" +#include "nsIConstraintValidation.h" +#include "nsWrapperCache.h" +#include "js/TypeDecls.h" + +namespace mozilla::dom { + +class ValidityState final : public nsISupports, public nsWrapperCache { + ~ValidityState() = default; + + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(ValidityState) + + friend class ::nsIConstraintValidation; + + nsIConstraintValidation* GetParentObject() const { + return mConstraintValidation; + } + + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + + // Web IDL methods + bool ValueMissing() const { + return GetValidityState( + nsIConstraintValidation::VALIDITY_STATE_VALUE_MISSING); + } + bool TypeMismatch() const { + return GetValidityState( + nsIConstraintValidation::VALIDITY_STATE_TYPE_MISMATCH); + } + bool PatternMismatch() const { + return GetValidityState( + nsIConstraintValidation::VALIDITY_STATE_PATTERN_MISMATCH); + } + bool TooLong() const { + return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_TOO_LONG); + } + bool TooShort() const { + return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_TOO_SHORT); + } + bool RangeUnderflow() const { + return GetValidityState( + nsIConstraintValidation::VALIDITY_STATE_RANGE_UNDERFLOW); + } + bool RangeOverflow() const { + return GetValidityState( + nsIConstraintValidation::VALIDITY_STATE_RANGE_OVERFLOW); + } + bool StepMismatch() const { + return GetValidityState( + nsIConstraintValidation::VALIDITY_STATE_STEP_MISMATCH); + } + bool BadInput() const { + return GetValidityState(nsIConstraintValidation::VALIDITY_STATE_BAD_INPUT); + } + bool CustomError() const { + return GetValidityState( + nsIConstraintValidation::VALIDITY_STATE_CUSTOM_ERROR); + } + bool Valid() const { + return !mConstraintValidation || mConstraintValidation->IsValid(); + } + + protected: + explicit ValidityState(nsIConstraintValidation* aConstraintValidation); + + /** + * Helper function to get a validity state from constraint validation + * instance. + */ + inline bool GetValidityState( + nsIConstraintValidation::ValidityStateType aState) const { + return mConstraintValidation && + mConstraintValidation->GetValidityState(aState); + } + + nsCOMPtr<nsIConstraintValidation> mConstraintValidation; +}; + +} // namespace mozilla::dom + +#endif // mozilla_dom_ValidityState_h diff --git a/dom/html/VideoDocument.cpp b/dom/html/VideoDocument.cpp new file mode 100644 index 0000000000..00b5bf5308 --- /dev/null +++ b/dom/html/VideoDocument.cpp @@ -0,0 +1,158 @@ +/* -*- 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 "MediaDocument.h" +#include "nsGkAtoms.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "mozilla/dom/HTMLMediaElement.h" +#include "DocumentInlines.h" +#include "nsContentUtils.h" +#include "mozilla/dom/Element.h" + +namespace mozilla::dom { + +class VideoDocument final : public MediaDocument { + public: + enum MediaDocumentKind MediaDocumentKind() const override { + return MediaDocumentKind::Video; + } + + virtual nsresult StartDocumentLoad(const char* aCommand, nsIChannel* aChannel, + nsILoadGroup* aLoadGroup, + nsISupports* aContainer, + nsIStreamListener** aDocListener, + bool aReset = true) override; + virtual void SetScriptGlobalObject( + nsIScriptGlobalObject* aScriptGlobalObject) override; + + virtual void Destroy() override { + if (mStreamListener) { + mStreamListener->DropDocumentRef(); + } + MediaDocument::Destroy(); + } + + nsresult StartLayout() override; + + protected: + nsresult CreateVideoElement(); + // Sets document <title> to reflect the file name and description. + void UpdateTitle(nsIChannel* aChannel); + + RefPtr<MediaDocumentStreamListener> mStreamListener; +}; + +nsresult VideoDocument::StartDocumentLoad( + const char* aCommand, nsIChannel* aChannel, nsILoadGroup* aLoadGroup, + nsISupports* aContainer, nsIStreamListener** aDocListener, bool aReset) { + nsresult rv = MediaDocument::StartDocumentLoad( + aCommand, aChannel, aLoadGroup, aContainer, aDocListener, aReset); + NS_ENSURE_SUCCESS(rv, rv); + + mStreamListener = new MediaDocumentStreamListener(this); + NS_ADDREF(*aDocListener = mStreamListener); + return rv; +} + +nsresult VideoDocument::StartLayout() { + // Create video element, and begin loading the media resource. Note we + // delay creating the video element until now (we're called from + // MediaDocumentStreamListener::OnStartRequest) as the PresShell is likely + // to have been created by now, so the MediaDecoder will be able to tell + // what kind of compositor we have, so the video element knows whether + // it can create a hardware accelerated video decoder or not. + nsresult rv = CreateVideoElement(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = MediaDocument::StartLayout(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void VideoDocument::SetScriptGlobalObject( + nsIScriptGlobalObject* aScriptGlobalObject) { + // Set the script global object on the superclass before doing + // anything that might require it.... + MediaDocument::SetScriptGlobalObject(aScriptGlobalObject); + + if (aScriptGlobalObject && !InitialSetupHasBeenDone()) { + DebugOnly<nsresult> rv = CreateSyntheticDocument(); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create synthetic video document"); + + if (!nsContentUtils::IsChildOfSameType(this)) { + LinkStylesheet(nsLiteralString( + u"resource://content-accessible/TopLevelVideoDocument.css")); + LinkScript(u"chrome://global/content/TopLevelVideoDocument.js"_ns); + } + InitialSetupDone(); + } +} + +nsresult VideoDocument::CreateVideoElement() { + RefPtr<Element> body = GetBodyElement(); + if (!body) { + NS_WARNING("no body on video document!"); + return NS_ERROR_FAILURE; + } + + // make content + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::video, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + RefPtr<HTMLMediaElement> element = static_cast<HTMLMediaElement*>( + NS_NewHTMLVideoElement(nodeInfo.forget(), NOT_FROM_PARSER)); + if (!element) return NS_ERROR_OUT_OF_MEMORY; + element->SetAutoplay(true, IgnoreErrors()); + element->SetControls(true, IgnoreErrors()); + element->LoadWithChannel(mChannel, + getter_AddRefs(mStreamListener->mNextStream)); + UpdateTitle(mChannel); + + if (nsContentUtils::IsChildOfSameType(this)) { + // Video documents that aren't toplevel should fill their frames and + // not have margins + element->SetAttr( + kNameSpaceID_None, nsGkAtoms::style, + nsLiteralString( + u"position:absolute; top:0; left:0; width:100%; height:100%"), + true); + } + + ErrorResult rv; + body->AppendChildTo(element, false, rv); + return rv.StealNSResult(); +} + +void VideoDocument::UpdateTitle(nsIChannel* aChannel) { + if (!aChannel) return; + + nsAutoString fileName; + GetFileName(fileName, aChannel); + IgnoredErrorResult ignored; + SetTitle(fileName, ignored); +} + +} // namespace mozilla::dom + +nsresult NS_NewVideoDocument(mozilla::dom::Document** aResult, + nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) { + auto* doc = new mozilla::dom::VideoDocument(); + + NS_ADDREF(doc); + nsresult rv = doc->Init(aPrincipal, aPartitionedPrincipal); + + if (NS_FAILED(rv)) { + NS_RELEASE(doc); + } + + *aResult = doc; + + return rv; +} diff --git a/dom/html/crashtests/1032654.html b/dom/html/crashtests/1032654.html new file mode 100644 index 0000000000..3067e10770 --- /dev/null +++ b/dom/html/crashtests/1032654.html @@ -0,0 +1 @@ +<template>
# diff --git a/dom/html/crashtests/1141260.html b/dom/html/crashtests/1141260.html new file mode 100644 index 0000000000..c5ced3a447 --- /dev/null +++ b/dom/html/crashtests/1141260.html @@ -0,0 +1,4 @@ +<img src="foo" srcset="bar"> +<script> + document.querySelector("img").removeAttribute('src'); +</script> diff --git a/dom/html/crashtests/1228876.html b/dom/html/crashtests/1228876.html new file mode 100644 index 0000000000..b7beb645cf --- /dev/null +++ b/dom/html/crashtests/1228876.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var a = document.createElement("select"); + var f = document.createElement("optgroup"); + var g = document.createElement("optgroup"); + a.appendChild(f); + g.appendChild(document.createElement("option")); + f.appendChild(g); + a.appendChild(document.createElement("option")); + document.body.appendChild(a); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/html/crashtests/1230110.html b/dom/html/crashtests/1230110.html new file mode 100644 index 0000000000..4654641874 --- /dev/null +++ b/dom/html/crashtests/1230110.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> +// This test case should not leak. +function leak() +{ + var img = document.createElement("img"); + var iframe = document.createElement("iframe"); + img.appendChild(iframe); + document.body.appendChild(img); + + document.addEventListener('Foo', function(){}); +} +</script> +</head> +<body onload="leak();"></body> +</html> diff --git a/dom/html/crashtests/1237633.html b/dom/html/crashtests/1237633.html new file mode 100644 index 0000000000..c235f03158 --- /dev/null +++ b/dom/html/crashtests/1237633.html @@ -0,0 +1 @@ +<img srcset="data:,a 2400w" sizes="(min-width: 1px) calc(300px - 100vw)"> diff --git a/dom/html/crashtests/1281972-1.html b/dom/html/crashtests/1281972-1.html new file mode 100644 index 0000000000..6fa5bb250b --- /dev/null +++ b/dom/html/crashtests/1281972-1.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<meta charset="UTF-8"> +<body onload="window[0].document.forms[0].submit();"> +<iframe src="data:text/html;charset=UTF-8,<form accept-charset=HZ-GB-2312>"></iframe> +</body> diff --git a/dom/html/crashtests/1282894.html b/dom/html/crashtests/1282894.html new file mode 100644 index 0000000000..456a4541fd --- /dev/null +++ b/dom/html/crashtests/1282894.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> + +function boom() { + var table = document.createElement("table"); + var cap = document.createElement("caption"); + cap.appendChild(table) + table.caption = cap; +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/html/crashtests/1290904.html b/dom/html/crashtests/1290904.html new file mode 100644 index 0000000000..5ea23ba3b6 --- /dev/null +++ b/dom/html/crashtests/1290904.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> + <body> + <fieldset id="outer"> + <fieldset id="inner"> + </fieldset> + </fieldset> + </body> +</html> +<script> +function appendTextareaToFieldset(fieldset) { + var textarea = document.createElement("textarea"); + textarea.setAttribute("required", ""); + fieldset.appendChild(textarea); +} + +var innerFieldset = document.getElementById('inner'); +var outerFieldset = document.getElementById('outer'); + +var fieldset = document.createElement('fieldset'); +appendTextareaToFieldset(fieldset); +appendTextareaToFieldset(fieldset); +appendTextareaToFieldset(fieldset); +appendTextareaToFieldset(fieldset); + +// Adding a fieldset to a nested fieldset. +innerFieldset.appendChild(fieldset); +appendTextareaToFieldset(fieldset); +appendTextareaToFieldset(fieldset); +// This triggers mInvalidElementsCount checks in outer fieldset. +appendTextareaToFieldset(outerFieldset); + +// Removing a fieldset from a nested fieldset. +innerFieldset.removeChild(fieldset); +// This triggers mInvalidElementsCount checks in outer fieldset. +appendTextareaToFieldset(outerFieldset); +</script> diff --git a/dom/html/crashtests/1343886-1.html b/dom/html/crashtests/1343886-1.html new file mode 100644 index 0000000000..fe84959ac2 --- /dev/null +++ b/dom/html/crashtests/1343886-1.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <script> + document.documentElement.scrollTop = "500"; + o1 = document.createRange(); + o2 = document.createElement('input'); + o1.selectNode(document.documentElement); + o1.surroundContents(o2); + o2.selectionStart; + </script> + </head> + <body></body> +</html>
\ No newline at end of file diff --git a/dom/html/crashtests/1343886-2.xml b/dom/html/crashtests/1343886-2.xml new file mode 100644 index 0000000000..91ddae103d --- /dev/null +++ b/dom/html/crashtests/1343886-2.xml @@ -0,0 +1,3 @@ +<input xmlns="http://www.w3.org/1999/xhtml"> + <script>document.documentElement.selectionStart</script> +</input> diff --git a/dom/html/crashtests/1343886-3.xml b/dom/html/crashtests/1343886-3.xml new file mode 100644 index 0000000000..a579a5e074 --- /dev/null +++ b/dom/html/crashtests/1343886-3.xml @@ -0,0 +1,3 @@ +<textarea xmlns="http://www.w3.org/1999/xhtml"> + <script>document.documentElement.selectionStart</script> +</textarea> diff --git a/dom/html/crashtests/1350972.html b/dom/html/crashtests/1350972.html new file mode 100644 index 0000000000..7af7f9e174 --- /dev/null +++ b/dom/html/crashtests/1350972.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<script> + try { o1 = document.createElement('tr'); } catch(e) {}; + try { o2 = document.createElement('div'); } catch(e) {}; + try { o3 = document.createElement('hr'); } catch(e) {}; + try { o4 = document.createElement('textarea'); } catch(e) {}; + try { o5 = document.getSelection(); } catch(e) {}; + try { o6 = document.createRange(); } catch(e) {}; + try { document.documentElement.appendChild(o2); } catch(e) {}; + try { document.documentElement.appendChild(o3); } catch(e) {}; + try { o2.appendChild(o4); } catch(e) {}; + try { o3.outerHTML = "<noscript contenteditable='true'>"; } catch(e) {}; + try { o4.select(); } catch(e) {}; + try { o5.addRange(o6); } catch(e) {}; + try { document.documentElement.appendChild(o1); } catch(e) {}; + try { o5.selectAllChildren(o1); } catch(e) {}; + try { o6.selectNode(o1); } catch(e) {}; +</script> +</head> +</html>
\ No newline at end of file diff --git a/dom/html/crashtests/1386905.html b/dom/html/crashtests/1386905.html new file mode 100644 index 0000000000..6ecc59e23b --- /dev/null +++ b/dom/html/crashtests/1386905.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> +<script> +document.documentElement.getBoundingClientRect() +document.documentElement.innerHTML = "<input placeholder=e type=number readonly>" +document.designMode = "on" +document.execCommand("inserttext", false, "") +document.designMode = "off" +document.documentElement.style.display = 'none' +</script> +</head> +</html> diff --git a/dom/html/crashtests/1401726.html b/dom/html/crashtests/1401726.html new file mode 100644 index 0000000000..bf4b4918ab --- /dev/null +++ b/dom/html/crashtests/1401726.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> +<script> + try { o1 = document.createElement('button') } catch(e) { } + try { o2 = document.createElement('p') } catch(e) { } + try { o3 = o1.labels } catch(e) { } + try { o4 = document.createNSResolver(document.documentElement) } catch(e) { } + try { o5 = document.createRange(); } catch(e) { } + try { document.documentElement.appendChild(o1) } catch(e) { } + try { o5.selectNode(o4); } catch(e) { } + try { o5.surroundContents(o2) } catch(e) { } + try { o5.surroundContents(o2) } catch(e) { } + try { o1.labels.length } catch(e) { } +</script> +</head> +</html> diff --git a/dom/html/crashtests/1412173.html b/dom/html/crashtests/1412173.html new file mode 100644 index 0000000000..6989260cc7 --- /dev/null +++ b/dom/html/crashtests/1412173.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> +let f = document.createElement('frame'); +f.onload = function() { + let frameDocument = f.contentDocument; + frameDocument.body.onbeforeunload = function () { frameDocument.write('<p>beforeUnload</p>') }; + frameDocument.write('<p>trigger unload</p>') + window.stop(); + document.documentElement.className = ''; +}; +document.documentElement.appendChild(f); +</script> +</head> +<body> +</body> +</html> diff --git a/dom/html/crashtests/1429783.html b/dom/html/crashtests/1429783.html new file mode 100644 index 0000000000..211f9e7ee2 --- /dev/null +++ b/dom/html/crashtests/1429783.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> + <head> + <script> + try { o2 = document.createElement('textarea') } catch(e) { } + try { o3 = document.getSelection() } catch(e) { } + try { document.documentElement.appendChild(o2) } catch(e) { } + try { o2.select() } catch(e) { } + try { o4 = o3.getRangeAt(0) } catch(e) { } + try { o5 = o4.commonAncestorContainer } catch(e) { } + try { o6 = o4.commonAncestorContainer } catch(e) { } + try { document.documentElement.after("", o5, "") } catch(e) { } + try { o4 = document.createElement('t') } catch(e) { } + try { document.appendChild(o4); } catch(e) { } + try { document.write(atob("PHNjcmlwdCBzcmM9Jyc+PC9zY3JpcHQ+Cg==")) } catch(e) { } + try { document.replaceChild(o6, document.documentElement); } catch(e) { } + </script> + </head> +</html> diff --git a/dom/html/crashtests/1440523.html b/dom/html/crashtests/1440523.html new file mode 100644 index 0000000000..11ce699781 --- /dev/null +++ b/dom/html/crashtests/1440523.html @@ -0,0 +1,13 @@ +<html> + <head> + <script> + try { frame = document.createElement('frame') } catch(e) { } + try { document.documentElement.appendChild(frame) } catch(e) { } + try { contentDocument = frame.contentDocument } catch(e) { } + try { contentDocument.writeln("<p contenteditable='true'>") } catch(e) { } + try { anotherDocument = document.implementation.createHTMLDocument(); } catch(e) { } + try { rootOfAnotherDocument = anotherDocument.documentElement; } catch(e) { } + try { document.replaceChild(rootOfAnotherDocument, document.documentElement); } catch(e) { } + </script> + </head> +</html> diff --git a/dom/html/crashtests/1440827.html b/dom/html/crashtests/1440827.html new file mode 100644 index 0000000000..ee204212ff --- /dev/null +++ b/dom/html/crashtests/1440827.html @@ -0,0 +1,6 @@ +<html> +<head> +<meta charset="UTF-8"> +> +<meta content='referrer no-referrer;manifest-src self http:' http-equiv='Content-Security-Policy'> +<script src='http://zzzzzz.z'></script> diff --git a/dom/html/crashtests/1547057.html b/dom/html/crashtests/1547057.html new file mode 100644 index 0000000000..89f48b5344 --- /dev/null +++ b/dom/html/crashtests/1547057.html @@ -0,0 +1,11 @@ +<html> +<head> + <script> + function start() { + const iframe = document.createElement('iframe') + iframe.policy.allowedFeatures() + } + window.addEventListener('load', start) + </script> +</head> +</html> diff --git a/dom/html/crashtests/1550524.html b/dom/html/crashtests/1550524.html new file mode 100644 index 0000000000..1b41f527dc --- /dev/null +++ b/dom/html/crashtests/1550524.html @@ -0,0 +1,7 @@ +<iframe></iframe> +<script> + var doc = frames[0].document; + doc.open(); + doc.open(); + doc.close(); +</script> diff --git a/dom/html/crashtests/1550881-1.html b/dom/html/crashtests/1550881-1.html new file mode 100644 index 0000000000..b9d73df957 --- /dev/null +++ b/dom/html/crashtests/1550881-1.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<iframe srcdoc="<iframe></iframe>"></iframe> +<script> + onload = function() { + parent = frames[0]; + child = parent[0]; + child.onunload = function() { + parent.document.open(); + } + parent.document.open(); + parent.document.write("Hello"); + } +</script> diff --git a/dom/html/crashtests/1550881-2.html b/dom/html/crashtests/1550881-2.html new file mode 100644 index 0000000000..97bf5ef350 --- /dev/null +++ b/dom/html/crashtests/1550881-2.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<iframe srcdoc="<iframe></iframe>"></iframe> +<script> + onload = function() { + parent = frames[0]; + child = parent[0]; + child.onunload = function() { + parent.document.open(); + parent.document.write("Nested"); + } + parent.document.open(); + parent.document.write("Hello"); + } +</script> diff --git a/dom/html/crashtests/1667493.html b/dom/html/crashtests/1667493.html new file mode 100644 index 0000000000..d7cf6c6174 --- /dev/null +++ b/dom/html/crashtests/1667493.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<script> +function runTest() { + let win = window.open("1667493_1.html"); + win.finish = function() { + document.documentElement.removeAttribute("class"); + }; +} +</script> +<body onload="runTest()"> +</body> +</html> diff --git a/dom/html/crashtests/1667493_1.html b/dom/html/crashtests/1667493_1.html new file mode 100644 index 0000000000..9b1a5fc047 --- /dev/null +++ b/dom/html/crashtests/1667493_1.html @@ -0,0 +1,7 @@ +<script> +window.requestIdleCallback(() => { + window.close(); + finish(); +}); +</script> +<input required="" value="r" type="number"> diff --git a/dom/html/crashtests/1680418.html b/dom/html/crashtests/1680418.html new file mode 100644 index 0000000000..ccebe0ed33 --- /dev/null +++ b/dom/html/crashtests/1680418.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<script> +window.addEventListener("hashchange", function() { + document.documentElement.removeAttribute("class"); +}); + +async function runTest() { + document.getElementById("a").click(); + await new Promise(resolve => setTimeout(resolve, 0)); + document.location.hash = "#foo"; +} +</script> +<body onload="runTest()"> +<a id='a' type='text/html' href='telnet://'> +</body> +</html> diff --git a/dom/html/crashtests/1704660.html b/dom/html/crashtests/1704660.html new file mode 100644 index 0000000000..01497975df --- /dev/null +++ b/dom/html/crashtests/1704660.html @@ -0,0 +1,17 @@ +<html> + <head> + <script> + function test() { + var ifr = document.getElementsByTagName("iframe")[0]; + var form = ifr.contentDocument.getElementsByTagName("form")[0]; + form.onsubmit = function() { + ifr.remove() + } + form.lastChild.click(); + } + </script> + </head> + <body onload="test()"> + <iframe srcdoc="<form><input name=f type=file><button></button></form>"></iframe> + </body> +</html> diff --git a/dom/html/crashtests/1724816.html b/dom/html/crashtests/1724816.html new file mode 100644 index 0000000000..9264a5225f --- /dev/null +++ b/dom/html/crashtests/1724816.html @@ -0,0 +1,14 @@ +<script> +window.onload = () => { + window[0].focus() + a.close() + a.showModal() + document.execCommand("selectAll", false) +} +function go() { + document.onselectstart = document.createElement("frameset").onload +} +</script> +<details open="true" ontoggle="go()">a</details> +<iframe></iframe> +<dialog id="a" tabindex="3">a</dialog> diff --git a/dom/html/crashtests/1785933-inner.html b/dom/html/crashtests/1785933-inner.html new file mode 100644 index 0000000000..8c97da8a8e --- /dev/null +++ b/dom/html/crashtests/1785933-inner.html @@ -0,0 +1,28 @@ +<style> +* { + image-rendering: pixelated; +} +</style> +<script> +window.requestIdleCallback(() => { + a.setSelectionRange(-1, 1) + document.head.innerHTML = undefined +}) +</script> +<embed src="data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs="></embed> +<textarea id="a">aaaaaaaaaaaaaaaa</textarea> +<script> + const embed = document.querySelector("embed"); + embed.onload = function() { + var countdown = 20; + if (location.search) { + countdown = parseInt(location.search.slice(1)) - 1; + } + + if (countdown > 0) { + location.search = countdown; + } else { + window.parent.postMessage("done", "*"); + } + } +</script> diff --git a/dom/html/crashtests/1785933.html b/dom/html/crashtests/1785933.html new file mode 100644 index 0000000000..e73395faea --- /dev/null +++ b/dom/html/crashtests/1785933.html @@ -0,0 +1,10 @@ +<html class="reftest-wait"> +<body> + <iframe src="./1785933-inner.html"></iframe> + <script> + window.onmessage = function() { + document.documentElement.removeAttribute("class"); + } + </script> +</body> +</html> diff --git a/dom/html/crashtests/1787671.html b/dom/html/crashtests/1787671.html new file mode 100644 index 0000000000..1b4106435e --- /dev/null +++ b/dom/html/crashtests/1787671.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<script> +let pp; +window.addEventListener("MozReftestInvalidate", finish); +document.addEventListener('DOMContentLoaded', () => { + pp = SpecialPowers.wrap(self).printPreview(); + pp?.print(); + setTimeout(window.close, 250); +}); +function finish() { + setTimeout(function() { + pp.close(); + document.documentElement.className = ""; + }, 0); +} +</script> +<img srcset="#"></img> +</html> diff --git a/dom/html/crashtests/1789475.html b/dom/html/crashtests/1789475.html new file mode 100644 index 0000000000..f12924a742 --- /dev/null +++ b/dom/html/crashtests/1789475.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script> +window.addEventListener('load', () => { + let a = document.createElement("img"); + a.srcset = "1"; + let b = new DOMParser() + let c = b.parseFromString("<div></div>", "text/html") + c.adoptNode(a) +}) +</script> diff --git a/dom/html/crashtests/1801380.html b/dom/html/crashtests/1801380.html new file mode 100644 index 0000000000..f4ce3e109f --- /dev/null +++ b/dom/html/crashtests/1801380.html @@ -0,0 +1 @@ +<input dir="auto" type="tel"> diff --git a/dom/html/crashtests/1840088.html b/dom/html/crashtests/1840088.html new file mode 100644 index 0000000000..826b465b01 --- /dev/null +++ b/dom/html/crashtests/1840088.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<table cellpadding="-129"> diff --git a/dom/html/crashtests/257818-1.html b/dom/html/crashtests/257818-1.html new file mode 100644 index 0000000000..27929fd793 --- /dev/null +++ b/dom/html/crashtests/257818-1.html @@ -0,0 +1,82 @@ +<html><head> +<script type="text/javascript"> +function cE (v) { + return document.createElement(v) +} +function cTN (v) { + return document.createTextNode(v) +} + +function OSXBarIcon(elt) { + this.element = elt; + this.labelNode = this.element.firstChild; + this.labelNodeParent = this.element; + this.labelNodeParent.removeChild(this.labelNode); + + this.contents = []; + var kids = this.element.childNodes; + for(var i=0; i<kids.length; i++) this.contents[this.contents.length] = this.element.removeChild(kids[i]); + this.popupSubmenu = new OSXBarSubmenu(this); +} + +function OSXBarSubmenu(icon) { + this.parentIcon = icon; + this.create(); + this.addContent(); +} +OSXBarSubmenu.prototype = { + create : function() { + var p = this.popupNode = document.createElement("div"); + var b = document.getElementsByTagName("body").item(0); + if(b) b.appendChild(p); + this.popupNode.style.display = "none"; + // Uncomment next line to fix the problem +// var v = document.body.offsetWidth; + } +}; +OSXBarSubmenu.prototype.addContent = function() { + + // add popup label: + var label = document.createElement("div"); + label.appendChild(document.createTextNode(this.parentIcon.label)); + this.popupNode.appendChild(label); + + // add <li> children to the popup: + var contents = this.parentIcon.contents; + for(var i=0; i<contents.length; i++) { + this.popupNode.appendChild(contents[i]); + + } +}; + +</script> + +<script type="text/javascript"> +function createControlPanel() { + var bar = document.getElementById("navigation"); + var item = cE("li"); + item.appendChild(cTN("aaa")); + var textfield = cE("input"); + textfield.value = 0; + item.appendChild(textfield); + bar.insertBefore(item, bar.firstChild); +} + +window.addEventListener("load", createControlPanel); +</script> + +<script type="text/javascript"> +function ssload() { + new OSXBarIcon(document.getElementById("navigation").childNodes[0]); +} +window.addEventListener("load",ssload); + + +</script> +</head> + +<body> +<ul id="navigation"></ul> +</body></html> + + diff --git a/dom/html/crashtests/285166-1.html b/dom/html/crashtests/285166-1.html new file mode 100644 index 0000000000..8a73d7d74e --- /dev/null +++ b/dom/html/crashtests/285166-1.html @@ -0,0 +1,3 @@ +<script> +document.createElement("#text"); +</script> diff --git a/dom/html/crashtests/294235-1.html b/dom/html/crashtests/294235-1.html new file mode 100644 index 0000000000..00eed38f4e --- /dev/null +++ b/dom/html/crashtests/294235-1.html @@ -0,0 +1,14 @@ +<html> + <head> + <title>bug 294235</title> + <script> + function countdown(){ + count2.innerHTML="foo"; + } + document.links.length; + </script> + </head> + <body> + <strong><div id="count2"><script>countdown();</script><noscript>bar</noscript></div></strong> + </body> +</html> diff --git a/dom/html/crashtests/307616-1.html b/dom/html/crashtests/307616-1.html new file mode 100644 index 0000000000..8f28ddd6e8 --- /dev/null +++ b/dom/html/crashtests/307616-1.html @@ -0,0 +1,8 @@ +<html> +<head><title>Testcase for assertion</title></head> +<body> +
+<input type="image" src="./nosuch.gif" alt="Search"> + +</body> +</html>
\ No newline at end of file diff --git a/dom/html/crashtests/324918-1.xhtml b/dom/html/crashtests/324918-1.xhtml new file mode 100644 index 0000000000..921714cff3 --- /dev/null +++ b/dom/html/crashtests/324918-1.xhtml @@ -0,0 +1,26 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script> + + +function init() +{ + var sel = document.getElementById("sel"); + var div = document.getElementById("div"); + var newo = document.createElementNS("http://www.w3.org/1999/xhtml", "option"); + + sel.appendChild(div); + div.appendChild(newo); + sel.removeChild(div); +} + +</script> +</head> +<body onload="init();"> + +<div id="div"><option></option></div> + +<select id="sel"></select> + +</body> +</html> diff --git a/dom/html/crashtests/338649-1.xhtml b/dom/html/crashtests/338649-1.xhtml new file mode 100644 index 0000000000..b0bf3186fc --- /dev/null +++ b/dom/html/crashtests/338649-1.xhtml @@ -0,0 +1,22 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<title>ASSERTION: Options collection broken</title> +</head> + +<body> + +<div> + +<select> + <option id="n29">B + <optgroup label="middle" id="n20"> + <option>N</option> + </optgroup> + </option> + <option>M</option> +</select> + +</div> + +</body> +</html>
\ No newline at end of file diff --git a/dom/html/crashtests/339501-1.xhtml b/dom/html/crashtests/339501-1.xhtml new file mode 100644 index 0000000000..1a231ee645 --- /dev/null +++ b/dom/html/crashtests/339501-1.xhtml @@ -0,0 +1,33 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + +<script> + +function boom() +{ + document.addEventListener("DOMNodeRemoved", foo, false); + remove(document.getElementById("A")); + document.removeEventListener("DOMNodeRemoved", foo, false); + + function foo() + { + document.removeEventListener("DOMNodeRemoved", foo, false); + remove(document.getElementById("B")); + } +} + + +window.addEventListener("load", boom, false); + +function remove(q1) { q1.parentNode.removeChild(q1); } + +</script> + +</head> + +<body> + +<select><option id="A">A</option><option id="B">B</option></select> + +</body> +</html> diff --git a/dom/html/crashtests/339501-2.xhtml b/dom/html/crashtests/339501-2.xhtml new file mode 100644 index 0000000000..6a1835fb71 --- /dev/null +++ b/dom/html/crashtests/339501-2.xhtml @@ -0,0 +1,33 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + +<script> + +function boom() +{ + document.addEventListener("DOMNodeRemoved", foo, false); + remove(document.getElementById("B")); + document.removeEventListener("DOMNodeRemoved", foo, false); + + function foo() + { + document.removeEventListener("DOMNodeRemoved", foo, false); + remove(document.getElementById("A")); + } +} + + +window.addEventListener("load", boom, false); + +function remove(q1) { q1.parentNode.removeChild(q1); } + +</script> + +</head> + +<body> + +<select><option id="A">A</option><option id="B">B</option></select> + +</body> +</html> diff --git a/dom/html/crashtests/378993-1.xhtml b/dom/html/crashtests/378993-1.xhtml new file mode 100644 index 0000000000..99cbb01dae --- /dev/null +++ b/dom/html/crashtests/378993-1.xhtml @@ -0,0 +1,7 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<body> +<script> +document.createElement('AREA'); +</script> +</body> +</html> diff --git a/dom/html/crashtests/382568-1-inner.xhtml b/dom/html/crashtests/382568-1-inner.xhtml new file mode 100644 index 0000000000..67d7427582 --- /dev/null +++ b/dom/html/crashtests/382568-1-inner.xhtml @@ -0,0 +1,52 @@ +<?xml version="1.0"?> +<!DOCTYPE html +PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" +"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> <head> +<title>Test</title> +<script type="text/javascript"><![CDATA[ + +function onAttrModified(evt) { +// window.alert("Mutation event fired within the frame code."); +// evt.target.focus(); +// evt.target.blur(); + evt.target.style.background = 'green'; + bounce(evt.target); +// evt.target.normalize(); +// bounce(evt.target.parentNode); +} +function die(n) { + p = n.parentNode; + p.removeChild(n); +} + +function bounce(n) { + p = n.parentNode; + p.removeChild(n); + p.appendChild(n); +} + + +function test_AttrModified() { + var x = document.getElementById("x"); + x.addEventListener("DOMAttrModified", onAttrModified); + bounce(x); +} + +function test() { + setTimeout(test_AttrModified, 3000); +} +]]></script> +</head> + +<body onload="test()"> +<h1>TestCase for unsafe mutable events from textarea</h1> +<p>Please wait for 3 seconds after document was loaded, +if your browser is vulnerable, it may stop responding +to keyboard and mouse event +and most likely it will eventually crash (may take a +while for debug builds).</p> +<p> +<textarea id="x"></textarea> +</p> +</body> </html> diff --git a/dom/html/crashtests/382568-1.html b/dom/html/crashtests/382568-1.html new file mode 100644 index 0000000000..c10c71fd14 --- /dev/null +++ b/dom/html/crashtests/382568-1.html @@ -0,0 +1,9 @@ +<html class="reftest-wait"> +<head> +<script> +setTimeout('document.documentElement.className = ""', 3500); +</script> +<body> +<iframe src="382568-1-inner.xhtml"></iframe> +</body> +</html> diff --git a/dom/html/crashtests/383137.xhtml b/dom/html/crashtests/383137.xhtml new file mode 100644 index 0000000000..87d853c035 --- /dev/null +++ b/dom/html/crashtests/383137.xhtml @@ -0,0 +1,13 @@ +<html xmlns="http://www.w3.org/1999/xhtml">
+<script id="script" xmlns="http://www.w3.org/1999/xhtml">//<![CDATA[
+function doe(){
+document.documentElement.setAttribute('style', '');
+}
+
+setTimeout(doe,100);
+//]]></script>
+
+<style style="overflow: scroll; display: block;">
+style::before {content: counter(section); }
+</style>
+</html>
\ No newline at end of file diff --git a/dom/html/crashtests/388183-1.html b/dom/html/crashtests/388183-1.html new file mode 100644 index 0000000000..fcca410e93 --- /dev/null +++ b/dom/html/crashtests/388183-1.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html contenteditable="true"> +<head> +</head> +<body> +<div contenteditable="true"></div> +</body> +</html> diff --git a/dom/html/crashtests/395340-1.html b/dom/html/crashtests/395340-1.html new file mode 100644 index 0000000000..ddbccfe968 --- /dev/null +++ b/dom/html/crashtests/395340-1.html @@ -0,0 +1,28 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script type="text/javascript"> + +function boom() +{ + var div1 = document.createElement("div"); + div1.style.counterIncrement = "chicken"; + div1.contentEditable = "true"; + + var div2 = document.createElement("div"); + div2.style.counterIncrement = "chicken"; + + document.body.style.content = "'A'"; + + div1.appendChild(div2); + document.body.appendChild(div1); + div1.removeChild(div2); +} + +</script> + +</head> + +<body onload="boom();"> + +</body> +</html> diff --git a/dom/html/crashtests/399694-1.html b/dom/html/crashtests/399694-1.html new file mode 100644 index 0000000000..e6db2342b5 --- /dev/null +++ b/dom/html/crashtests/399694-1.html @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> +<head> +<script> +function boom() +{ + var legend = document.createElementNS("http://www.w3.org/1999/xhtml", "legend") + var input = document.getElementById("input"); + input.appendChild(legend); + legend.focus(); + + document.documentElement.removeAttribute("class"); +} +</script> +</head> +<body onload="setTimeout(boom, 3);" contenteditable="true"> + +<input contenteditable="false"><input id="input"><q><div></div></q> + +</body> +</html> diff --git a/dom/html/crashtests/407053.html b/dom/html/crashtests/407053.html new file mode 100644 index 0000000000..b80a231722 --- /dev/null +++ b/dom/html/crashtests/407053.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body onload="document.execCommand('copy', false, '');"> +<div contenteditable="true"></div> +</body> +</html> diff --git a/dom/html/crashtests/423371-1.html b/dom/html/crashtests/423371-1.html new file mode 100644 index 0000000000..0069cf95db --- /dev/null +++ b/dom/html/crashtests/423371-1.html @@ -0,0 +1,9 @@ +<!DOCTYPE HTML> +<html> +<body> +<div style="position:fixed; top:100px;" id="d">hello</div> +<script> +document.write(document.getElementById("d").offsetTop); +</script> +</body> +</html> diff --git a/dom/html/crashtests/448564.html b/dom/html/crashtests/448564.html new file mode 100644 index 0000000000..f31830afa1 --- /dev/null +++ b/dom/html/crashtests/448564.html @@ -0,0 +1,7 @@ +<s> +<form>a</form> +<iframe></iframe> +<script src=a></script> +<form></form> +<table> +<optgroup> diff --git a/dom/html/crashtests/451123-1.html b/dom/html/crashtests/451123-1.html new file mode 100644 index 0000000000..abf23c1e73 --- /dev/null +++ b/dom/html/crashtests/451123-1.html @@ -0,0 +1,7 @@ +<html> +<head> +</head> +<body> +<input type="file" type="file"> +</body> +</html> diff --git a/dom/html/crashtests/453406-1.html b/dom/html/crashtests/453406-1.html new file mode 100644 index 0000000000..bf75686553 --- /dev/null +++ b/dom/html/crashtests/453406-1.html @@ -0,0 +1,34 @@ +<html> +<head> +<script type="text/javascript"> +function boom() +{ + var r = document.documentElement; + while (r.firstChild) + r.firstChild.remove(); + + var b = document.createElement("BODY"); + var s = document.createElement("SCRIPT"); + var f1 = document.createElement("FORM"); + var i = document.createElement("INPUT"); + var br = document.createElement("BR"); + var f2 = document.createElement("FORM"); + var span = document.createElement("SPAN"); + f1.appendChild(i); + f1.appendChild(br); + s.appendChild(f1); + b.appendChild(s); + f2.appendChild(span); + b.appendChild(f2) + document.documentElement.appendChild(b); + b.contentEditable = "true"; + document.execCommand("indent", false, null); + b.contentEditable = "false"; + span.appendChild(i); + i.remove(); +} +</script> +</head> +<body onload="boom();"> +</body> +</html> diff --git a/dom/html/crashtests/464197-1.html b/dom/html/crashtests/464197-1.html new file mode 100644 index 0000000000..785686023e --- /dev/null +++ b/dom/html/crashtests/464197-1.html @@ -0,0 +1,23 @@ +<html class="reftest-wait"> +<head> +<script type="text/javascript"> + +var i = 0; + +function boom() +{ + var s = document.createElement("span"); + s.innerHTML = "<audio src='javascript:5'><\/audio>"; + s.innerHTML = ""; + + if (++i < 10) + setTimeout(boom, 0); + else + document.documentElement.removeAttribute("class"); +} + +</script> +</head> +<body onload="boom();"> +</body> +</html> diff --git a/dom/html/crashtests/468562-1.html b/dom/html/crashtests/468562-1.html new file mode 100644 index 0000000000..81123fe2ea --- /dev/null +++ b/dom/html/crashtests/468562-1.html @@ -0,0 +1,6 @@ +<html> +<head></head> +<body> +<table><script>document.write("Q");</script><link> +</body> +</html> diff --git a/dom/html/crashtests/468562-2.html b/dom/html/crashtests/468562-2.html new file mode 100644 index 0000000000..e0db5cd974 --- /dev/null +++ b/dom/html/crashtests/468562-2.html @@ -0,0 +1,6 @@ +<html> +<head></head> +<body> +<table><script>document.write("Q");</script><link></table> +</body> +</html> diff --git a/dom/html/crashtests/494225.html b/dom/html/crashtests/494225.html new file mode 100644 index 0000000000..a78051d2a1 --- /dev/null +++ b/dom/html/crashtests/494225.html @@ -0,0 +1,10 @@ +<!doctype html> +<html><head><script> + var x = "f: '" + document.fgColor + + "' b: '" + document.bgColor + + "' l: '" + document.linkColor + + "' a: '" + document.alinkColor + + "' v: '" + document.vlinkColor + "'"; +</script></head><body +onload="document.body.appendChild(document.createTextNode(x))" +></body></html> diff --git a/dom/html/crashtests/495543.svg b/dom/html/crashtests/495543.svg new file mode 100644 index 0000000000..b795796ad4 --- /dev/null +++ b/dom/html/crashtests/495543.svg @@ -0,0 +1,16 @@ +<svg xmlns="http://www.w3.org/2000/svg"> +<script type="text/javascript"> + +function boom() +{ + var htmlBody = document.createElementNS("http://www.w3.org/1999/xhtml", "body"); + document.documentElement.appendChild(htmlBody); + htmlBody.setAttribute('vlink', "transparent"); + document.height; + document.vlinkColor; +} + +window.addEventListener("load", boom, false); +</script> + +</svg> diff --git a/dom/html/crashtests/495546-1.html b/dom/html/crashtests/495546-1.html new file mode 100644 index 0000000000..0547b98674 --- /dev/null +++ b/dom/html/crashtests/495546-1.html @@ -0,0 +1,19 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<script type="text/javascript"> + +function boom() +{ + var video = document.createElement("video"); + var source = document.createElement("source"); + source.setAttributeNS(null, "src", "http://127.0.0.1/"); + video.appendChild(source); + document.body.appendChild(video); + setTimeout(function() { document.body.removeChild(video); }, 20); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/dom/html/crashtests/504183-1.html b/dom/html/crashtests/504183-1.html new file mode 100644 index 0000000000..e44db41520 --- /dev/null +++ b/dom/html/crashtests/504183-1.html @@ -0,0 +1,12 @@ +<html class="reftest-wait"> +<input id="a" type="file"> +<script> +function doe() { +var elem = document.getElementById('a'); +elem.style.display = "none"; +elem.focus(); +document.documentElement.className = ""; +} +setTimeout(doe, 0); +</script> +</html> diff --git a/dom/html/crashtests/515829-1.html b/dom/html/crashtests/515829-1.html new file mode 100644 index 0000000000..e2fc655c01 --- /dev/null +++ b/dom/html/crashtests/515829-1.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<head></head> +<body onload="document.getElementById('x').innerHTML = '<button></button>';"> +<form><div id="x"><button></button></div><button></button></form> +</body> +</html> diff --git a/dom/html/crashtests/515829-2.html b/dom/html/crashtests/515829-2.html new file mode 100644 index 0000000000..6de6d5986c --- /dev/null +++ b/dom/html/crashtests/515829-2.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<head></head> +<body onload="document.getElementById('x').innerHTML = '<button></button>';"> +<form><div id="x"><button></button></div><button></button><input type="image"></form> +</body> +</html> diff --git a/dom/html/crashtests/570566-1.html b/dom/html/crashtests/570566-1.html new file mode 100644 index 0000000000..70ec003a68 --- /dev/null +++ b/dom/html/crashtests/570566-1.html @@ -0,0 +1,2 @@ +<body onload="document.getElementById('i').removeAttribute('type')"><input id=i type=image></body> + diff --git a/dom/html/crashtests/571428-1.html b/dom/html/crashtests/571428-1.html new file mode 100644 index 0000000000..ae2b960cac --- /dev/null +++ b/dom/html/crashtests/571428-1.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<script> +function boom() +{ + var i = document.getElementById("i"); + i.setAttribute("type", "button"); + i.removeAttribute("type"); +} +</script> +</head> +<body onload="boom();"><input id="i"></body> +</html> diff --git a/dom/html/crashtests/580507-1.xhtml b/dom/html/crashtests/580507-1.xhtml new file mode 100644 index 0000000000..eff3fb255d --- /dev/null +++ b/dom/html/crashtests/580507-1.xhtml @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var input = document.getElementById("i"); + input.setAttribute('type', "image"); + input.removeAttribute('type'); + input.placeholder = "Y"; +} + +</script> +</head> + +<body onload="boom();"><input id="i" /></body> +</html> diff --git a/dom/html/crashtests/590387.html b/dom/html/crashtests/590387.html new file mode 100644 index 0000000000..50cd05ac9e --- /dev/null +++ b/dom/html/crashtests/590387.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> +<head> +<script> +document.createElement("div").innerHTML = "<output form=x>"; +</script> +</head> +</html> diff --git a/dom/html/crashtests/596785-1.html b/dom/html/crashtests/596785-1.html new file mode 100644 index 0000000000..5d830628b0 --- /dev/null +++ b/dom/html/crashtests/596785-1.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <body onload="document.getElementById('i').type = 'image'; + document.documentElement.className = '';"> + <form> + <input id='i' required> + </form> + </body> +</html> diff --git a/dom/html/crashtests/596785-2.html b/dom/html/crashtests/596785-2.html new file mode 100644 index 0000000000..18cfe2788d --- /dev/null +++ b/dom/html/crashtests/596785-2.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <body onload="document.getElementById('i').type = 'text'; + document.documentElement.className = '';"> + <form> + <input id='i' type='image' required> + </form> + </body> +</html> diff --git a/dom/html/crashtests/602117.html b/dom/html/crashtests/602117.html new file mode 100644 index 0000000000..0d1818b2a9 --- /dev/null +++ b/dom/html/crashtests/602117.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> + <script> + var isindex = document.createElement("isindex"); + isindex.setAttribute("form", "f"); + isindex.form; + </script> +</html> diff --git a/dom/html/crashtests/604807.html b/dom/html/crashtests/604807.html new file mode 100644 index 0000000000..bec4012e5c --- /dev/null +++ b/dom/html/crashtests/604807.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<script> +try { + var selectElem = document.createElementNS("http://www.w3.org/1999/xhtml", "select"); + selectElem.QueryInterface(Ci.nsISelectElement); + selectElem.getOptionIndex(null, 0, true); +} catch (e) { +} +</script> diff --git a/dom/html/crashtests/605264.html b/dom/html/crashtests/605264.html new file mode 100644 index 0000000000..782720a9d6 --- /dev/null +++ b/dom/html/crashtests/605264.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<script> + +var v = document.createElementNS("http://www.w3.org/1999/xhtml", "video"); +v.QueryInterface(Ci.nsIObserver); +v.observe(null, null, null); + +</script> diff --git a/dom/html/crashtests/606430-1.html b/dom/html/crashtests/606430-1.html new file mode 100644 index 0000000000..c347e3c9f1 --- /dev/null +++ b/dom/html/crashtests/606430-1.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var r = document.documentElement; + var i = document.getElementById("i"); + + document.removeChild(r); + document.appendChild(r); + w("dump('A\\n')"); + document.removeChild(r); + w("dump('B\\n')"); + document.appendChild(r); + + function w(s) + { + var ns = document.createElement("script"); + var nt = document.createTextNode(s); + ns.appendChild(nt); + i.appendChild(ns); + } +} + +</script> +</head> + +<body onload="boom();"><input id="i"></body> +</html> diff --git a/dom/html/crashtests/613027.html b/dom/html/crashtests/613027.html new file mode 100644 index 0000000000..a866860f10 --- /dev/null +++ b/dom/html/crashtests/613027.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var a = document.createElementNS("http://www.w3.org/1999/xhtml", "fieldset"); + var b = document.createElementNS("http://www.w3.org/1999/xhtml", "legend"); + var c = document.createElementNS("http://www.w3.org/1999/xhtml", "input"); + + a.appendChild(b); + a.appendChild(c); + a.removeChild(b); + c.expandoQ = a; +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/html/crashtests/614279.html b/dom/html/crashtests/614279.html new file mode 100644 index 0000000000..ff1c505167 --- /dev/null +++ b/dom/html/crashtests/614279.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var a = document.createElementNS("http://www.w3.org/1999/xhtml", "datalist"); + var b = document.createElementNS("http://www.w3.org/1999/xhtml", "option"); + + a.appendChild(b); + b.expando = a.options; +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/html/crashtests/614988-1.html b/dom/html/crashtests/614988-1.html new file mode 100644 index 0000000000..931f83ac73 --- /dev/null +++ b/dom/html/crashtests/614988-1.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<html> +<head><script>window.addEventListener("load", function() { var t = document.getElementById("t"); t.appendChild(t.previousSibling); }, false);</script></head> +<body><fieldset><legend></legend><textarea id="t"></textarea></fieldset></body> +</html> diff --git a/dom/html/crashtests/616401.html b/dom/html/crashtests/616401.html new file mode 100644 index 0000000000..eb5a0bcf4c --- /dev/null +++ b/dom/html/crashtests/616401.html @@ -0,0 +1,8 @@ +<!doctype html> +<script> +var c = document.createElement("canvas"); +c.getContext("experimental-webgl", { + get a() { throw 7; }, + get b() { throw 8; } +}); +</script> diff --git a/dom/html/crashtests/620078-1.html b/dom/html/crashtests/620078-1.html new file mode 100644 index 0000000000..39462706c1 --- /dev/null +++ b/dom/html/crashtests/620078-1.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var frame = document.getElementById("f"); + var frameRoot = frame.contentDocument.documentElement; + document.body.removeChild(frame); + var i = document.createElementNS("http://www.w3.org/1999/xhtml", "input"); + i.setAttribute("autofocus", "true"); + frameRoot.appendChild(i); +} + +</script> +</head> + +<body onload="boom();"><iframe id="f" src="data:text/html,"></iframe></body> +</html> diff --git a/dom/html/crashtests/620078-2.html b/dom/html/crashtests/620078-2.html new file mode 100644 index 0000000000..1be23fa1f2 --- /dev/null +++ b/dom/html/crashtests/620078-2.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> + <body onload='document.cloneNode(1)'> + <button autofocus></button> + </body> +</html> diff --git a/dom/html/crashtests/631421.html b/dom/html/crashtests/631421.html new file mode 100644 index 0000000000..e4a7b9192b --- /dev/null +++ b/dom/html/crashtests/631421.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> +"use strict"; + +var f2; + +function newIframe() +{ + var f = document.createElementNS("http://www.w3.org/1999/xhtml", "iframe"); + f.setAttributeNS(null, "src", "631421.png"); + document.body.appendChild(f); + return f; +} + +function b1() +{ + void newIframe(); + f2 = newIframe(); + setTimeout(b2, 0); +} + +function b2() +{ + document.body.removeChild(f2); + document.documentElement.removeAttribute("class"); +} + +</script> +</head> + +<body onload="b1();"></body> +</html> diff --git a/dom/html/crashtests/631421.png b/dom/html/crashtests/631421.png Binary files differnew file mode 100644 index 0000000000..ef350c4678 --- /dev/null +++ b/dom/html/crashtests/631421.png diff --git a/dom/html/crashtests/673853.html b/dom/html/crashtests/673853.html new file mode 100644 index 0000000000..1325fa9ed6 --- /dev/null +++ b/dom/html/crashtests/673853.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + var otherDoc = document.implementation.createDocument('', '', null); + var input = otherDoc.createElementNS("http://www.w3.org/1999/xhtml", "input"); + var form = otherDoc.createElementNS("http://www.w3.org/1999/xhtml", "form"); + input.setAttributeNS(null, "form", "x"); + form.setAttributeNS(null, "id", "x"); + input.appendChild(form); + otherDoc.appendChild(input); +} + +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/dom/html/crashtests/682058.xhtml b/dom/html/crashtests/682058.xhtml new file mode 100644 index 0000000000..95e7da98fc --- /dev/null +++ b/dom/html/crashtests/682058.xhtml @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<html xmlns="http://www.w3.org/1999/xhtml"> + <body onload="test()"> + <script> + function test() { + document.body.innerHTML = "Foobar"; + } + document.addEventListener("DOMNodeRemoved", function() {}); + </script> + </body> +</html> diff --git a/dom/html/crashtests/682460.html b/dom/html/crashtests/682460.html new file mode 100644 index 0000000000..91306be71d --- /dev/null +++ b/dom/html/crashtests/682460.html @@ -0,0 +1,21 @@ +<html> +<head> +<script> + +function boom() +{ + var f = function() { + document.documentElement.offsetHeight; + }; + window.addEventListener("DOMSubtreeModified", f, true); + + document.getElementsByTagName("table")[0].setAttribute("cellpadding", "2"); +} + +</script> +</head> + +<body onload="boom();"> +<table><tr><td></td></tr></table> +</body> +</html> diff --git a/dom/html/crashtests/68912-1.html b/dom/html/crashtests/68912-1.html new file mode 100644 index 0000000000..bdd2ab4614 --- /dev/null +++ b/dom/html/crashtests/68912-1.html @@ -0,0 +1,24 @@ +<html>
+<head>
+<title>Crash TR.cells = null</title>
+<script language="javascript">
+function crashme()
+{
+ var elm = document.createElement('tr');
+
+ elm.cells = null;
+}
+
+</script>
+</head>
+<body onload="crashme()">
+
+<p>
+This test case creates a TR element then tries to assign to the cells property
+</p>
+<p>
+Crash
+</p>
+
+</body>
+</html>
diff --git a/dom/html/crashtests/738744.xhtml b/dom/html/crashtests/738744.xhtml new file mode 100644 index 0000000000..7f91d0149b --- /dev/null +++ b/dom/html/crashtests/738744.xhtml @@ -0,0 +1,4 @@ +<input xmlns="http://www.w3.org/1999/xhtml" form="f"> + <form id="f" /> + <input form="f" /> +</input> diff --git a/dom/html/crashtests/741218.json b/dom/html/crashtests/741218.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/dom/html/crashtests/741218.json @@ -0,0 +1 @@ +{} diff --git a/dom/html/crashtests/741218.json^headers^ b/dom/html/crashtests/741218.json^headers^ new file mode 100644 index 0000000000..7b5e82d4b7 --- /dev/null +++ b/dom/html/crashtests/741218.json^headers^ @@ -0,0 +1 @@ +Content-Type: application/json diff --git a/dom/html/crashtests/741250.xhtml b/dom/html/crashtests/741250.xhtml new file mode 100644 index 0000000000..e18a9409fe --- /dev/null +++ b/dom/html/crashtests/741250.xhtml @@ -0,0 +1,9 @@ +<input form="f" id="x" xmlns="http://www.w3.org/1999/xhtml"> +<form id="f"></form> +<script> +window.addEventListener("load", function() { + var x = document.getElementById("x"); + x.appendChild(x.cloneNode(true)); +}, false); +</script> +</input> diff --git a/dom/html/crashtests/768344.html b/dom/html/crashtests/768344.html new file mode 100644 index 0000000000..4830e6674d --- /dev/null +++ b/dom/html/crashtests/768344.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + function f() { + document.removeEventListener("DOMSubtreeModified", f, true); + document.documentElement.setAttributeNS(null, "contenteditable", "false"); + } + + document.addEventListener("DOMSubtreeModified", f, true); + + document.documentElement.contentEditable = "true"; +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/dom/html/crashtests/795221-1.html b/dom/html/crashtests/795221-1.html new file mode 100644 index 0000000000..70e400bed9 --- /dev/null +++ b/dom/html/crashtests/795221-1.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<style> + div { } +</style> +<script> + document.styleSheets[0].cssRules[0].style.foo = document; +</script> diff --git a/dom/html/crashtests/795221-2.html b/dom/html/crashtests/795221-2.html new file mode 100644 index 0000000000..fc2aa7d506 --- /dev/null +++ b/dom/html/crashtests/795221-2.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<style> + @media all { + div { } + } +</style> +<script> + document.styleSheets[0].cssRules[0].cssRules[0].style.foo = document; +</script> diff --git a/dom/html/crashtests/795221-3.html b/dom/html/crashtests/795221-3.html new file mode 100644 index 0000000000..93348af0bd --- /dev/null +++ b/dom/html/crashtests/795221-3.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<body> +<script> + // Create the stylesheet via script, so that the parser's preloading doesn't + // make the CSS loader hold on to the sheet we're _not_ trying to form a + // cycle through, thus accidentally avoiding the cycle + var link = document.createElement("link"); + link.setAttribute("rel", "stylesheet"); + link.setAttribute("href", "data:text/css,div { }"); + link.onload = function () { + document.styleSheets[0].cssRules[0].style.foo = document; + } + document.body.appendChild(link); +</script> diff --git a/dom/html/crashtests/795221-4.html b/dom/html/crashtests/795221-4.html new file mode 100644 index 0000000000..476dd34672 --- /dev/null +++ b/dom/html/crashtests/795221-4.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<svg> + <style> + div { } + </style> +</svg> +<script> + document.styleSheets[0].cssRules[0].style.foo = document; +</script> diff --git a/dom/html/crashtests/795221-5.xml b/dom/html/crashtests/795221-5.xml new file mode 100644 index 0000000000..286dcff0d4 --- /dev/null +++ b/dom/html/crashtests/795221-5.xml @@ -0,0 +1,6 @@ +<?xml-stylesheet href="data:text/css,div {}"?> +<html xmlns="http://www.w3.org/1999/xhtml"> + <script> + document.styleSheets[0].cssRules[0].style.foo = document; + </script> +</html> diff --git a/dom/html/crashtests/798802-1.html b/dom/html/crashtests/798802-1.html new file mode 100644 index 0000000000..92ab50fd87 --- /dev/null +++ b/dom/html/crashtests/798802-1.html @@ -0,0 +1,18 @@ +<html> + <head> + <script> + onload = function() { + var canvas2d = document.createElement('canvas') + canvas2d.setAttribute('width', 0) + document.body.appendChild(canvas2d) + var ctx2d = canvas2d.getContext('2d') + ctx2d.fillStyle = 'black' + var gl = document.createElement('canvas').getContext('experimental-webgl') + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, canvas2d) + ctx2d.fillRect(0, 0, 1, 1) + } + </script> + </head> + <body> + </body> +</html> diff --git a/dom/html/crashtests/811226.html b/dom/html/crashtests/811226.html new file mode 100644 index 0000000000..990ef9af53 --- /dev/null +++ b/dom/html/crashtests/811226.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body onload="document.getElementById('s').onerror;"> +<span id="s" onerror="0;"></span> +</body> +</html> diff --git a/dom/html/crashtests/819745.html b/dom/html/crashtests/819745.html new file mode 100644 index 0000000000..389e6b8c13 --- /dev/null +++ b/dom/html/crashtests/819745.html @@ -0,0 +1,5 @@ +<!doctype="html"> +<body> +<script> + document.body.onerror = null; +</script> diff --git a/dom/html/crashtests/828180.html b/dom/html/crashtests/828180.html new file mode 100644 index 0000000000..055c8a34ba --- /dev/null +++ b/dom/html/crashtests/828180.html @@ -0,0 +1,5 @@ +<script> +var table = document.createElement("table"); +table.tHead = null; +table.tFoot = null; +</script> diff --git a/dom/html/crashtests/828472.html b/dom/html/crashtests/828472.html new file mode 100644 index 0000000000..59ce4d280b --- /dev/null +++ b/dom/html/crashtests/828472.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> +<input type='date' value='2013-01-01'> +</body> +</html> diff --git a/dom/html/crashtests/837033.html b/dom/html/crashtests/837033.html new file mode 100644 index 0000000000..772d24014c --- /dev/null +++ b/dom/html/crashtests/837033.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<html> +<body onload="document.createElement('button').validity.x = null;"></body> +</html> diff --git a/dom/html/crashtests/838256-1.html b/dom/html/crashtests/838256-1.html new file mode 100644 index 0000000000..e1cdc588e8 --- /dev/null +++ b/dom/html/crashtests/838256-1.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<style> + +::-moz-range-track { + display: none ! important; +} + +::-moz-range-thumb { + display: none ! important; +} + +</style> +</head> +<body> +<input type="range"> +</body> +</html> diff --git a/dom/html/crashtests/862084.html b/dom/html/crashtests/862084.html new file mode 100644 index 0000000000..d6d04f74de --- /dev/null +++ b/dom/html/crashtests/862084.html @@ -0,0 +1,9 @@ +<!doctype html> +<select></select> +<script> +var select = document.getElementsByTagName("select"); +select.item(0); +select[0]; +select.namedItem("x") +select["x"] +</script> diff --git a/dom/html/crashtests/865147.html b/dom/html/crashtests/865147.html new file mode 100644 index 0000000000..841cf8b6d8 --- /dev/null +++ b/dom/html/crashtests/865147.html @@ -0,0 +1,7 @@ +<!doctype html> +<select></select> +<script> +var select = document.getElementsByTagName("select")[0]; +var newOpt = document.createElement("option"); +select.add(newOpt, newOpt); +</script> diff --git a/dom/html/crashtests/877910.html b/dom/html/crashtests/877910.html new file mode 100644 index 0000000000..d454c4478b --- /dev/null +++ b/dom/html/crashtests/877910.html @@ -0,0 +1 @@ +<select><option id="foo"><script>document.querySelector("select").namedItem("foo")</script> diff --git a/dom/html/crashtests/903106.html b/dom/html/crashtests/903106.html new file mode 100644 index 0000000000..fceaf4761a --- /dev/null +++ b/dom/html/crashtests/903106.html @@ -0,0 +1,3 @@ +<script> + document.createElement("tr").sectionRowIndex; +</script> diff --git a/dom/html/crashtests/916322-1.html b/dom/html/crashtests/916322-1.html new file mode 100644 index 0000000000..56b43d6394 --- /dev/null +++ b/dom/html/crashtests/916322-1.html @@ -0,0 +1,10 @@ +<canvas height=16 id=gl> +<script> +// Without the fix, this would trigger an assertion in the debug build +document.addEventListener("DOMContentLoaded", DifferentSizes); +function DifferentSizes() { + gl.getContext("2d"); + gl.removeAttribute('height'); + gl.toBlob(function() { }, false) +} +</script> diff --git a/dom/html/crashtests/916322-2.html b/dom/html/crashtests/916322-2.html new file mode 100644 index 0000000000..c2e5c29205 --- /dev/null +++ b/dom/html/crashtests/916322-2.html @@ -0,0 +1,10 @@ +<canvas height=200 id=gl> +<script> +// Without the fix, this would trigger an assertion in the debug build +document.addEventListener("DOMContentLoaded", DifferentSizes); +function DifferentSizes() { + gl.getContext("2d"); + gl.removeAttribute('height'); + gl.toBlob(function() { }, false) +} +</script> diff --git a/dom/html/crashtests/978644.xhtml b/dom/html/crashtests/978644.xhtml new file mode 100644 index 0000000000..0fd9043a28 --- /dev/null +++ b/dom/html/crashtests/978644.xhtml @@ -0,0 +1,11 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + +<body onload="document.getElementById('b').appendChild(document.getElementById('v'));"> + +<fieldset><fieldset id="b"></fieldset></fieldset> + +<div id="v"><fieldset><input required="" /><input required="" /></fieldset></div> + +</body> + +</html> diff --git a/dom/html/crashtests/crashtests.list b/dom/html/crashtests/crashtests.list new file mode 100644 index 0000000000..be51d248f0 --- /dev/null +++ b/dom/html/crashtests/crashtests.list @@ -0,0 +1,99 @@ +load 68912-1.html +load 257818-1.html +load 285166-1.html +load 294235-1.html +load 307616-1.html +load 324918-1.xhtml +load 338649-1.xhtml +load 339501-1.xhtml +load 339501-2.xhtml +load 378993-1.xhtml +load 382568-1.html +load 383137.xhtml +load 388183-1.html +load 395340-1.html +load 399694-1.html +load 407053.html +load 423371-1.html +load 448564.html +load 451123-1.html +load 453406-1.html +load 464197-1.html +load 468562-1.html +load 468562-2.html +load 494225.html +load 495543.svg +load 495546-1.html +load 504183-1.html +load 515829-1.html +load 515829-2.html +load 570566-1.html +load 571428-1.html +load 580507-1.xhtml +load 590387.html +load 596785-1.html +load 596785-2.html +load 602117.html +load 604807.html +load 605264.html +load 606430-1.html +load 613027.html +load 614279.html +load 614988-1.html +load 620078-1.html +load 620078-2.html +load 631421.html +load 673853.html +load 682058.xhtml +load 682460.html +load 738744.xhtml +load 741218.json +load 741250.xhtml +load 768344.html +load 795221-1.html +load 795221-2.html +load 795221-3.html +load 795221-4.html +load 795221-5.xml +load 811226.html +load 819745.html +load 828180.html +load 828472.html +load 837033.html +load 838256-1.html +load 862084.html +load 865147.html +load 877910.html +load 903106.html +load 916322-1.html +load 916322-2.html +load 978644.xhtml +load 1032654.html +load 1141260.html +load 1228876.html +load 1230110.html +load 1237633.html +load 1281972-1.html +load 1282894.html +load 1290904.html +load 1343886-1.html +load 1343886-2.xml +load 1343886-3.xml +load 1350972.html +load 1386905.html +asserts(0-4) load 1401726.html +load 1412173.html +load 1440523.html +load 1547057.html +load 1550524.html +load 1550881-1.html +load 1550881-2.html +skip-if(Android) pref(dom.disable_open_during_load,false) load 1667493.html +load 1680418.html +load 1704660.html +load 1724816.html +load 1785933.html +skip-if(Android) load 1787671.html # printPreview doesn't work on android +load 1789475.html +load 1801380.html +load 1840088.html diff --git a/dom/html/input/ButtonInputTypes.h b/dom/html/input/ButtonInputTypes.h new file mode 100644 index 0000000000..e891db00e0 --- /dev/null +++ b/dom/html/input/ButtonInputTypes.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ButtonInputTypes_h__ +#define mozilla_dom_ButtonInputTypes_h__ + +#include "mozilla/dom/InputType.h" + +namespace mozilla::dom { + +class ButtonInputTypeBase : public InputType { + public: + ~ButtonInputTypeBase() override = default; + + protected: + explicit ButtonInputTypeBase(HTMLInputElement* aInputElement) + : InputType(aInputElement) {} +}; + +// input type=button +class ButtonInputType : public ButtonInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) ButtonInputType(aInputElement); + } + + private: + explicit ButtonInputType(HTMLInputElement* aInputElement) + : ButtonInputTypeBase(aInputElement) {} +}; + +// input type=image +class ImageInputType : public ButtonInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) ImageInputType(aInputElement); + } + + private: + explicit ImageInputType(HTMLInputElement* aInputElement) + : ButtonInputTypeBase(aInputElement) {} +}; + +// input type=reset +class ResetInputType : public ButtonInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) ResetInputType(aInputElement); + } + + private: + explicit ResetInputType(HTMLInputElement* aInputElement) + : ButtonInputTypeBase(aInputElement) {} +}; + +// input type=submit +class SubmitInputType : public ButtonInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) SubmitInputType(aInputElement); + } + + private: + explicit SubmitInputType(HTMLInputElement* aInputElement) + : ButtonInputTypeBase(aInputElement) {} +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_ButtonInputTypes_h__ */ diff --git a/dom/html/input/CheckableInputTypes.cpp b/dom/html/input/CheckableInputTypes.cpp new file mode 100644 index 0000000000..f9d4d69d19 --- /dev/null +++ b/dom/html/input/CheckableInputTypes.cpp @@ -0,0 +1,36 @@ +/* -*- 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/CheckableInputTypes.h" + +#include "mozilla/dom/HTMLInputElement.h" + +using namespace mozilla; +using namespace mozilla::dom; + +/* input type=checkbox */ + +bool CheckboxInputType::IsValueMissing() const { + if (!mInputElement->IsRequired()) { + return false; + } + + return !mInputElement->Checked(); +} + +nsresult CheckboxInputType::GetValueMissingMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationCheckboxMissing", + mInputElement->OwnerDoc(), aMessage); +} + +/* input type=radio */ + +nsresult RadioInputType::GetValueMissingMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationRadioMissing", + mInputElement->OwnerDoc(), aMessage); +} diff --git a/dom/html/input/CheckableInputTypes.h b/dom/html/input/CheckableInputTypes.h new file mode 100644 index 0000000000..98c673685b --- /dev/null +++ b/dom/html/input/CheckableInputTypes.h @@ -0,0 +1,55 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_CheckableInputTypes_h__ +#define mozilla_dom_CheckableInputTypes_h__ + +#include "mozilla/dom/InputType.h" + +namespace mozilla::dom { + +class CheckableInputTypeBase : public InputType { + public: + ~CheckableInputTypeBase() override = default; + + protected: + explicit CheckableInputTypeBase(HTMLInputElement* aInputElement) + : InputType(aInputElement) {} +}; + +// input type=checkbox +class CheckboxInputType : public CheckableInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) CheckboxInputType(aInputElement); + } + + bool IsValueMissing() const override; + + nsresult GetValueMissingMessage(nsAString& aMessage) override; + + private: + explicit CheckboxInputType(HTMLInputElement* aInputElement) + : CheckableInputTypeBase(aInputElement) {} +}; + +// input type=radio +class RadioInputType : public CheckableInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) RadioInputType(aInputElement); + } + + nsresult GetValueMissingMessage(nsAString& aMessage) override; + + private: + explicit RadioInputType(HTMLInputElement* aInputElement) + : CheckableInputTypeBase(aInputElement) {} +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_CheckableInputTypes_h__ */ diff --git a/dom/html/input/ColorInputType.h b/dom/html/input/ColorInputType.h new file mode 100644 index 0000000000..b6749849b7 --- /dev/null +++ b/dom/html/input/ColorInputType.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_ColorInputType_h__ +#define mozilla_dom_ColorInputType_h__ + +#include "mozilla/dom/InputType.h" + +namespace mozilla::dom { + +// input type=color +class ColorInputType : public InputType { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) ColorInputType(aInputElement); + } + + private: + explicit ColorInputType(HTMLInputElement* aInputElement) + : InputType(aInputElement) {} +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_ColorInputType_h__ */ diff --git a/dom/html/input/DateTimeInputTypes.cpp b/dom/html/input/DateTimeInputTypes.cpp new file mode 100644 index 0000000000..56d6c57c49 --- /dev/null +++ b/dom/html/input/DateTimeInputTypes.cpp @@ -0,0 +1,501 @@ +/* -*- 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/DateTimeInputTypes.h" + +#include "js/Date.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/ShadowRoot.h" +#include "nsDOMTokenList.h" + +namespace mozilla::dom { + +const double DateTimeInputTypeBase::kMinimumYear = 1; +const double DateTimeInputTypeBase::kMaximumYear = 275760; +const double DateTimeInputTypeBase::kMaximumMonthInMaximumYear = 9; +const double DateTimeInputTypeBase::kMaximumWeekInMaximumYear = 37; +const double DateTimeInputTypeBase::kMsPerDay = 24 * 60 * 60 * 1000; + +bool DateTimeInputTypeBase::IsMutable() const { + return !mInputElement->IsDisabledOrReadOnly(); +} + +bool DateTimeInputTypeBase::IsValueMissing() const { + if (!mInputElement->IsRequired()) { + return false; + } + + if (!IsMutable()) { + return false; + } + + return IsValueEmpty(); +} + +bool DateTimeInputTypeBase::IsRangeOverflow() const { + Decimal maximum = mInputElement->GetMaximum(); + if (maximum.isNaN()) { + return false; + } + + Decimal value = mInputElement->GetValueAsDecimal(); + if (value.isNaN()) { + return false; + } + + return value > maximum; +} + +bool DateTimeInputTypeBase::IsRangeUnderflow() const { + Decimal minimum = mInputElement->GetMinimum(); + if (minimum.isNaN()) { + return false; + } + + Decimal value = mInputElement->GetValueAsDecimal(); + if (value.isNaN()) { + return false; + } + + return value < minimum; +} + +bool DateTimeInputTypeBase::HasStepMismatch() const { + Decimal value = mInputElement->GetValueAsDecimal(); + return mInputElement->ValueIsStepMismatch(value); +} + +bool DateTimeInputTypeBase::HasBadInput() const { + ShadowRoot* shadow = mInputElement->GetShadowRoot(); + if (!shadow) { + return false; + } + + Element* editWrapperElement = shadow->GetElementById(u"edit-wrapper"_ns); + if (!editWrapperElement) { + return false; + } + + bool allEmpty = true; + // Empty field does not imply bad input, but incomplete field does. + for (Element* child = editWrapperElement->GetFirstElementChild(); child; + child = child->GetNextElementSibling()) { + if (!child->ClassList()->Contains(u"datetime-edit-field"_ns)) { + continue; + } + nsAutoString value; + child->GetAttr(nsGkAtoms::value, value); + if (!value.IsEmpty()) { + allEmpty = false; + break; + } + } + + // If some fields are available but input element's value is empty implies it + // has been sanitized. + return !allEmpty && IsValueEmpty(); +} + +nsresult DateTimeInputTypeBase::GetRangeOverflowMessage(nsAString& aMessage) { + nsAutoString maxStr; + mInputElement->GetAttr(nsGkAtoms::max, maxStr); + + return nsContentUtils::FormatMaybeLocalizedString( + aMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationDateTimeRangeOverflow", mInputElement->OwnerDoc(), maxStr); +} + +nsresult DateTimeInputTypeBase::GetRangeUnderflowMessage(nsAString& aMessage) { + nsAutoString minStr; + mInputElement->GetAttr(nsGkAtoms::min, minStr); + + return nsContentUtils::FormatMaybeLocalizedString( + aMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationDateTimeRangeUnderflow", mInputElement->OwnerDoc(), + minStr); +} + +void DateTimeInputTypeBase::MinMaxStepAttrChanged() { + if (Element* dateTimeBoxElement = mInputElement->GetDateTimeBoxElement()) { + AsyncEventDispatcher::RunDOMEventWhenSafe( + *dateTimeBoxElement, u"MozNotifyMinMaxStepAttrChanged"_ns, + CanBubble::eNo, ChromeOnlyDispatch::eNo); + } +} + +bool DateTimeInputTypeBase::GetTimeFromMs(double aValue, uint16_t* aHours, + uint16_t* aMinutes, + uint16_t* aSeconds, + uint16_t* aMilliseconds) const { + MOZ_ASSERT(aValue >= 0 && aValue < kMsPerDay, + "aValue must be milliseconds within a day!"); + + uint32_t value = floor(aValue); + + *aMilliseconds = value % 1000; + value /= 1000; + + *aSeconds = value % 60; + value /= 60; + + *aMinutes = value % 60; + value /= 60; + + *aHours = value; + + return true; +} + +// input type=date + +nsresult DateInputType::GetBadInputMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidDate", + mInputElement->OwnerDoc(), aMessage); +} + +auto DateInputType::ConvertStringToNumber(const nsAString& aValue) const + -> StringToNumberResult { + uint32_t year, month, day; + if (!ParseDate(aValue, &year, &month, &day)) { + return {}; + } + JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day)); + if (!time.isValid()) { + return {}; + } + return {Decimal::fromDouble(time.toDouble())}; +} + +bool DateInputType::ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const { + MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); + + aResultString.Truncate(); + + // The specs (and our JS APIs) require |aValue| to be truncated. + aValue = aValue.floor(); + + double year = JS::YearFromTime(aValue.toDouble()); + double month = JS::MonthFromTime(aValue.toDouble()); + double day = JS::DayFromTime(aValue.toDouble()); + + if (std::isnan(year) || std::isnan(month) || std::isnan(day)) { + return false; + } + + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0f", year, month + 1, day); + return true; +} + +// input type=time + +nsresult TimeInputType::GetBadInputMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidTime", + mInputElement->OwnerDoc(), aMessage); +} + +auto TimeInputType::ConvertStringToNumber(const nsAString& aValue) const + -> StringToNumberResult { + uint32_t milliseconds; + if (!ParseTime(aValue, &milliseconds)) { + return {}; + } + return {Decimal(int32_t(milliseconds))}; +} + +bool TimeInputType::ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const { + MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); + + aResultString.Truncate(); + + aValue = aValue.floor(); + // Per spec, we need to truncate |aValue| and we should only represent + // times inside a day [00:00, 24:00[, which means that we should do a + // modulo on |aValue| using the number of milliseconds in a day (86400000). + uint32_t value = + NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); + + uint16_t milliseconds, seconds, minutes, hours; + if (!GetTimeFromMs(value, &hours, &minutes, &seconds, &milliseconds)) { + return false; + } + + if (milliseconds != 0) { + aResultString.AppendPrintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, + milliseconds); + } else if (seconds != 0) { + aResultString.AppendPrintf("%02d:%02d:%02d", hours, minutes, seconds); + } else { + aResultString.AppendPrintf("%02d:%02d", hours, minutes); + } + + return true; +} + +bool TimeInputType::HasReversedRange() const { + mozilla::Decimal maximum = mInputElement->GetMaximum(); + if (maximum.isNaN()) { + return false; + } + + mozilla::Decimal minimum = mInputElement->GetMinimum(); + if (minimum.isNaN()) { + return false; + } + + return maximum < minimum; +} + +bool TimeInputType::IsReversedRangeUnderflowAndOverflow() const { + mozilla::Decimal maximum = mInputElement->GetMaximum(); + mozilla::Decimal minimum = mInputElement->GetMinimum(); + mozilla::Decimal value = mInputElement->GetValueAsDecimal(); + + MOZ_ASSERT(HasReversedRange(), "Must have reserved range."); + + if (value.isNaN()) { + return false; + } + + // When an element has a reversed range, and the value is more than the + // maximum and less than the minimum the element is simultaneously suffering + // from an underflow and suffering from an overflow. + return value > maximum && value < minimum; +} + +bool TimeInputType::IsRangeOverflow() const { + return HasReversedRange() ? IsReversedRangeUnderflowAndOverflow() + : DateTimeInputTypeBase::IsRangeOverflow(); +} + +bool TimeInputType::IsRangeUnderflow() const { + return HasReversedRange() ? IsReversedRangeUnderflowAndOverflow() + : DateTimeInputTypeBase::IsRangeUnderflow(); +} + +nsresult TimeInputType::GetReversedRangeUnderflowAndOverflowMessage( + nsAString& aMessage) { + nsAutoString maxStr; + mInputElement->GetAttr(nsGkAtoms::max, maxStr); + + nsAutoString minStr; + mInputElement->GetAttr(nsGkAtoms::min, minStr); + + return nsContentUtils::FormatMaybeLocalizedString( + aMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationTimeReversedRangeUnderflowAndOverflow", + mInputElement->OwnerDoc(), minStr, maxStr); +} + +nsresult TimeInputType::GetRangeOverflowMessage(nsAString& aMessage) { + return HasReversedRange() + ? GetReversedRangeUnderflowAndOverflowMessage(aMessage) + : DateTimeInputTypeBase::GetRangeOverflowMessage(aMessage); +} + +nsresult TimeInputType::GetRangeUnderflowMessage(nsAString& aMessage) { + return HasReversedRange() + ? GetReversedRangeUnderflowAndOverflowMessage(aMessage) + : DateTimeInputTypeBase::GetRangeUnderflowMessage(aMessage); +} + +// input type=week + +nsresult WeekInputType::GetBadInputMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidWeek", + mInputElement->OwnerDoc(), aMessage); +} + +auto WeekInputType::ConvertStringToNumber(const nsAString& aValue) const + -> StringToNumberResult { + uint32_t year, week; + if (!ParseWeek(aValue, &year, &week)) { + return {}; + } + if (year < kMinimumYear || year > kMaximumYear) { + return {}; + } + // Maximum week is 275760-W37, the week of 275760-09-13. + if (year == kMaximumYear && week > kMaximumWeekInMaximumYear) { + return {}; + } + double days = DaysSinceEpochFromWeek(year, week); + return {Decimal::fromDouble(days * kMsPerDay)}; +} + +bool WeekInputType::ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const { + MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); + + aResultString.Truncate(); + + aValue = aValue.floor(); + + // Based on ISO 8601 date. + double year = JS::YearFromTime(aValue.toDouble()); + double month = JS::MonthFromTime(aValue.toDouble()); + double day = JS::DayFromTime(aValue.toDouble()); + // Adding 1 since day starts from 0. + double dayInYear = JS::DayWithinYear(aValue.toDouble(), year) + 1; + + // Return if aValue is outside the valid JS date-time range. + if (std::isnan(year) || std::isnan(month) || std::isnan(day) || + std::isnan(dayInYear)) { + return false; + } + + // DayOfWeek requires the year to be non-negative. + if (year < 0) { + return false; + } + + // Adding 1 since month starts from 0. + uint32_t isoWeekday = DayOfWeek(year, month + 1, day, true); + // Target on Wednesday since ISO 8601 states that week 1 is the week + // with the first Thursday of that year. + uint32_t week = (dayInYear - isoWeekday + 10) / 7; + + if (week < 1) { + year--; + if (year < 1) { + return false; + } + week = MaximumWeekInYear(year); + } else if (week > MaximumWeekInYear(year)) { + year++; + if (year > kMaximumYear || + (year == kMaximumYear && week > kMaximumWeekInMaximumYear)) { + return false; + } + week = 1; + } + + aResultString.AppendPrintf("%04.0f-W%02d", year, week); + return true; +} + +// input type=month + +nsresult MonthInputType::GetBadInputMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidMonth", + mInputElement->OwnerDoc(), aMessage); +} + +auto MonthInputType::ConvertStringToNumber(const nsAString& aValue) const + -> StringToNumberResult { + uint32_t year, month; + if (!ParseMonth(aValue, &year, &month)) { + return {}; + } + + if (year < kMinimumYear || year > kMaximumYear) { + return {}; + } + + // Maximum valid month is 275760-09. + if (year == kMaximumYear && month > kMaximumMonthInMaximumYear) { + return {}; + } + + int32_t months = MonthsSinceJan1970(year, month); + return {Decimal(int32_t(months))}; +} + +bool MonthInputType::ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const { + MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); + + aResultString.Truncate(); + + aValue = aValue.floor(); + + double month = NS_floorModulo(aValue, Decimal(12)).toDouble(); + month = (month < 0 ? month + 12 : month); + + double year = 1970 + (aValue.toDouble() - month) / 12; + + // Maximum valid month is 275760-09. + if (year < kMinimumYear || year > kMaximumYear) { + return false; + } + + if (year == kMaximumYear && month > 8) { + return false; + } + + aResultString.AppendPrintf("%04.0f-%02.0f", year, month + 1); + return true; +} + +// input type=datetime-local + +nsresult DateTimeLocalInputType::GetBadInputMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidDateTime", + mInputElement->OwnerDoc(), aMessage); +} + +auto DateTimeLocalInputType::ConvertStringToNumber( + const nsAString& aValue) const -> StringToNumberResult { + uint32_t year, month, day, timeInMs; + if (!ParseDateTimeLocal(aValue, &year, &month, &day, &timeInMs)) { + return {}; + } + JS::ClippedTime time = + JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs)); + if (!time.isValid()) { + return {}; + } + return {Decimal::fromDouble(time.toDouble())}; +} + +bool DateTimeLocalInputType::ConvertNumberToString( + Decimal aValue, nsAString& aResultString) const { + MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); + + aResultString.Truncate(); + + aValue = aValue.floor(); + + uint32_t timeValue = + NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); + + uint16_t milliseconds, seconds, minutes, hours; + if (!GetTimeFromMs(timeValue, &hours, &minutes, &seconds, &milliseconds)) { + return false; + } + + double year = JS::YearFromTime(aValue.toDouble()); + double month = JS::MonthFromTime(aValue.toDouble()); + double day = JS::DayFromTime(aValue.toDouble()); + + if (std::isnan(year) || std::isnan(month) || std::isnan(day)) { + return false; + } + + if (milliseconds != 0) { + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d.%03d", year, + month + 1, day, hours, minutes, seconds, + milliseconds); + } else if (seconds != 0) { + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d", year, + month + 1, day, hours, minutes, seconds); + } else { + aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d", year, + month + 1, day, hours, minutes); + } + + return true; +} + +} // namespace mozilla::dom diff --git a/dom/html/input/DateTimeInputTypes.h b/dom/html/input/DateTimeInputTypes.h new file mode 100644 index 0000000000..fa8805f67e --- /dev/null +++ b/dom/html/input/DateTimeInputTypes.h @@ -0,0 +1,154 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_DateTimeInputTypes_h__ +#define mozilla_dom_DateTimeInputTypes_h__ + +#include "mozilla/dom/InputType.h" + +namespace mozilla::dom { + +class DateTimeInputTypeBase : public InputType { + public: + ~DateTimeInputTypeBase() override = default; + + bool IsValueMissing() const override; + bool IsRangeOverflow() const override; + bool IsRangeUnderflow() const override; + bool HasStepMismatch() const override; + bool HasBadInput() const override; + + nsresult GetRangeOverflowMessage(nsAString& aMessage) override; + nsresult GetRangeUnderflowMessage(nsAString& aMessage) override; + + void MinMaxStepAttrChanged() override; + + protected: + explicit DateTimeInputTypeBase(HTMLInputElement* aInputElement) + : InputType(aInputElement) {} + + bool IsMutable() const override; + + nsresult GetBadInputMessage(nsAString& aMessage) override = 0; + + /** + * This method converts aValue (milliseconds within a day) to hours, minutes, + * seconds and milliseconds. + */ + bool GetTimeFromMs(double aValue, uint16_t* aHours, uint16_t* aMinutes, + uint16_t* aSeconds, uint16_t* aMilliseconds) const; + + // Minimum year limited by HTML standard, year >= 1. + static const double kMinimumYear; + // Maximum year limited by ECMAScript date object range, year <= 275760. + static const double kMaximumYear; + // Maximum valid month is 275760-09. + static const double kMaximumMonthInMaximumYear; + // Maximum valid week is 275760-W37. + static const double kMaximumWeekInMaximumYear; + // Milliseconds in a day. + static const double kMsPerDay; +}; + +// input type=date +class DateInputType : public DateTimeInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) DateInputType(aInputElement); + } + + nsresult GetBadInputMessage(nsAString& aMessage) override; + + StringToNumberResult ConvertStringToNumber(const nsAString&) const override; + bool ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const override; + + private: + explicit DateInputType(HTMLInputElement* aInputElement) + : DateTimeInputTypeBase(aInputElement) {} +}; + +// input type=time +class TimeInputType : public DateTimeInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) TimeInputType(aInputElement); + } + + nsresult GetBadInputMessage(nsAString& aMessage) override; + + StringToNumberResult ConvertStringToNumber(const nsAString&) const override; + bool ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const override; + bool IsRangeOverflow() const override; + bool IsRangeUnderflow() const override; + nsresult GetRangeOverflowMessage(nsAString& aMessage) override; + nsresult GetRangeUnderflowMessage(nsAString& aMessage) override; + + private: + explicit TimeInputType(HTMLInputElement* aInputElement) + : DateTimeInputTypeBase(aInputElement) {} + + // https://html.spec.whatwg.org/multipage/input.html#has-a-reversed-range + bool HasReversedRange() const; + bool IsReversedRangeUnderflowAndOverflow() const; + nsresult GetReversedRangeUnderflowAndOverflowMessage(nsAString& aMessage); +}; + +// input type=week +class WeekInputType : public DateTimeInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) WeekInputType(aInputElement); + } + + nsresult GetBadInputMessage(nsAString& aMessage) override; + StringToNumberResult ConvertStringToNumber(const nsAString&) const override; + bool ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const override; + + private: + explicit WeekInputType(HTMLInputElement* aInputElement) + : DateTimeInputTypeBase(aInputElement) {} +}; + +// input type=month +class MonthInputType : public DateTimeInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) MonthInputType(aInputElement); + } + + nsresult GetBadInputMessage(nsAString& aMessage) override; + StringToNumberResult ConvertStringToNumber(const nsAString&) const override; + bool ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const override; + + private: + explicit MonthInputType(HTMLInputElement* aInputElement) + : DateTimeInputTypeBase(aInputElement) {} +}; + +// input type=datetime-local +class DateTimeLocalInputType : public DateTimeInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) DateTimeLocalInputType(aInputElement); + } + + nsresult GetBadInputMessage(nsAString& aMessage) override; + StringToNumberResult ConvertStringToNumber(const nsAString&) const override; + bool ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const override; + + private: + explicit DateTimeLocalInputType(HTMLInputElement* aInputElement) + : DateTimeInputTypeBase(aInputElement) {} +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_DateTimeInputTypes_h__ */ diff --git a/dom/html/input/FileInputType.cpp b/dom/html/input/FileInputType.cpp new file mode 100644 index 0000000000..ed14aaa48d --- /dev/null +++ b/dom/html/input/FileInputType.cpp @@ -0,0 +1,26 @@ +/* -*- 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/FileInputType.h" + +#include "mozilla/dom/HTMLInputElement.h" + +using namespace mozilla; +using namespace mozilla::dom; + +bool FileInputType::IsValueMissing() const { + if (!mInputElement->IsRequired()) { + return false; + } + + return mInputElement->GetFilesOrDirectoriesInternal().IsEmpty(); +} + +nsresult FileInputType::GetValueMissingMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationFileMissing", + mInputElement->OwnerDoc(), aMessage); +} diff --git a/dom/html/input/FileInputType.h b/dom/html/input/FileInputType.h new file mode 100644 index 0000000000..870ef93136 --- /dev/null +++ b/dom/html/input/FileInputType.h @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_FileInputType_h__ +#define mozilla_dom_FileInputType_h__ + +#include "mozilla/dom/InputType.h" + +namespace mozilla::dom { + +// input type=file +class FileInputType : public InputType { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) FileInputType(aInputElement); + } + + bool IsValueMissing() const override; + + nsresult GetValueMissingMessage(nsAString& aMessage) override; + + private: + explicit FileInputType(HTMLInputElement* aInputElement) + : InputType(aInputElement) {} +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_FileInputType_h__ */ diff --git a/dom/html/input/HiddenInputType.h b/dom/html/input/HiddenInputType.h new file mode 100644 index 0000000000..ac7c9c571a --- /dev/null +++ b/dom/html/input/HiddenInputType.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_HiddenInputType_h__ +#define mozilla_dom_HiddenInputType_h__ + +#include "mozilla/dom/InputType.h" + +namespace mozilla::dom { + +// input type=hidden +class HiddenInputType : public InputType { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) HiddenInputType(aInputElement); + } + + private: + explicit HiddenInputType(HTMLInputElement* aInputElement) + : InputType(aInputElement) {} +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_HiddenInputType_h__ */ diff --git a/dom/html/input/InputType.cpp b/dom/html/input/InputType.cpp new file mode 100644 index 0000000000..1d6254c2e2 --- /dev/null +++ b/dom/html/input/InputType.cpp @@ -0,0 +1,354 @@ +/* -*- 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/InputType.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Likely.h" +#include "nsIFormControl.h" +#include "mozilla/dom/ButtonInputTypes.h" +#include "mozilla/dom/CheckableInputTypes.h" +#include "mozilla/dom/ColorInputType.h" +#include "mozilla/dom/DateTimeInputTypes.h" +#include "mozilla/dom/FileInputType.h" +#include "mozilla/dom/HiddenInputType.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/NumericInputTypes.h" +#include "mozilla/dom/SingleLineTextInputTypes.h" + +#include "nsContentUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +constexpr Decimal InputType::kStepAny; + +/* static */ UniquePtr<InputType, InputType::DoNotDelete> InputType::Create( + HTMLInputElement* aInputElement, FormControlType aType, void* aMemory) { + UniquePtr<InputType, InputType::DoNotDelete> inputType; + switch (aType) { + // Single line text + case FormControlType::InputText: + inputType.reset(TextInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputTel: + inputType.reset(TelInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputEmail: + inputType.reset(EmailInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputSearch: + inputType.reset(SearchInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputPassword: + inputType.reset(PasswordInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputUrl: + inputType.reset(URLInputType::Create(aInputElement, aMemory)); + break; + // Button + case FormControlType::InputButton: + inputType.reset(ButtonInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputSubmit: + inputType.reset(SubmitInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputImage: + inputType.reset(ImageInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputReset: + inputType.reset(ResetInputType::Create(aInputElement, aMemory)); + break; + // Checkable + case FormControlType::InputCheckbox: + inputType.reset(CheckboxInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputRadio: + inputType.reset(RadioInputType::Create(aInputElement, aMemory)); + break; + // Numeric + case FormControlType::InputNumber: + inputType.reset(NumberInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputRange: + inputType.reset(RangeInputType::Create(aInputElement, aMemory)); + break; + // DateTime + case FormControlType::InputDate: + inputType.reset(DateInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputTime: + inputType.reset(TimeInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputMonth: + inputType.reset(MonthInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputWeek: + inputType.reset(WeekInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputDatetimeLocal: + inputType.reset(DateTimeLocalInputType::Create(aInputElement, aMemory)); + break; + // Others + case FormControlType::InputColor: + inputType.reset(ColorInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputFile: + inputType.reset(FileInputType::Create(aInputElement, aMemory)); + break; + case FormControlType::InputHidden: + inputType.reset(HiddenInputType::Create(aInputElement, aMemory)); + break; + default: + inputType.reset(TextInputType::Create(aInputElement, aMemory)); + } + + return inputType; +} + +bool InputType::IsMutable() const { return !mInputElement->IsDisabled(); } + +bool InputType::IsValueEmpty() const { return mInputElement->IsValueEmpty(); } + +void InputType::GetNonFileValueInternal(nsAString& aValue) const { + return mInputElement->GetNonFileValueInternal(aValue); +} + +nsresult InputType::SetValueInternal(const nsAString& aValue, + const ValueSetterOptions& aOptions) { + RefPtr<HTMLInputElement> inputElement(mInputElement); + return inputElement->SetValueInternal(aValue, aOptions); +} + +nsIFrame* InputType::GetPrimaryFrame() const { + return mInputElement->GetPrimaryFrame(); +} + +void InputType::DropReference() { + // Drop our (non ref-counted) reference. + mInputElement = nullptr; +} + +bool InputType::IsTooLong() const { return false; } + +bool InputType::IsTooShort() const { return false; } + +bool InputType::IsValueMissing() const { return false; } + +bool InputType::HasTypeMismatch() const { return false; } + +Maybe<bool> InputType::HasPatternMismatch() const { return Some(false); } + +bool InputType::IsRangeOverflow() const { return false; } + +bool InputType::IsRangeUnderflow() const { return false; } + +bool InputType::HasStepMismatch() const { return false; } + +bool InputType::HasBadInput() const { return false; } + +nsresult InputType::GetValidationMessage( + nsAString& aValidationMessage, + nsIConstraintValidation::ValidityStateType aType) { + aValidationMessage.Truncate(); + + switch (aType) { + case nsIConstraintValidation::VALIDITY_STATE_TOO_LONG: { + int32_t maxLength = mInputElement->MaxLength(); + int32_t textLength = mInputElement->InputTextLength(CallerType::System); + nsAutoString strMaxLength; + nsAutoString strTextLength; + + strMaxLength.AppendInt(maxLength); + strTextLength.AppendInt(textLength); + + return nsContentUtils::FormatMaybeLocalizedString( + aValidationMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationTextTooLong", mInputElement->OwnerDoc(), strMaxLength, + strTextLength); + } + case nsIConstraintValidation::VALIDITY_STATE_TOO_SHORT: { + int32_t minLength = mInputElement->MinLength(); + int32_t textLength = mInputElement->InputTextLength(CallerType::System); + nsAutoString strMinLength; + nsAutoString strTextLength; + + strMinLength.AppendInt(minLength); + strTextLength.AppendInt(textLength); + + return nsContentUtils::FormatMaybeLocalizedString( + aValidationMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationTextTooShort", mInputElement->OwnerDoc(), strMinLength, + strTextLength); + } + case nsIConstraintValidation::VALIDITY_STATE_VALUE_MISSING: + return GetValueMissingMessage(aValidationMessage); + case nsIConstraintValidation::VALIDITY_STATE_TYPE_MISMATCH: { + return GetTypeMismatchMessage(aValidationMessage); + } + case nsIConstraintValidation::VALIDITY_STATE_PATTERN_MISMATCH: { + nsAutoString title; + mInputElement->GetAttr(nsGkAtoms::title, title); + + if (title.IsEmpty()) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationPatternMismatch", + mInputElement->OwnerDoc(), aValidationMessage); + } + + if (title.Length() > + nsIConstraintValidation::sContentSpecifiedMaxLengthMessage) { + title.Truncate( + nsIConstraintValidation::sContentSpecifiedMaxLengthMessage); + } + return nsContentUtils::FormatMaybeLocalizedString( + aValidationMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationPatternMismatchWithTitle", mInputElement->OwnerDoc(), + title); + } + case nsIConstraintValidation::VALIDITY_STATE_RANGE_OVERFLOW: + return GetRangeOverflowMessage(aValidationMessage); + case nsIConstraintValidation::VALIDITY_STATE_RANGE_UNDERFLOW: + return GetRangeUnderflowMessage(aValidationMessage); + case nsIConstraintValidation::VALIDITY_STATE_STEP_MISMATCH: { + Decimal value = mInputElement->GetValueAsDecimal(); + if (MOZ_UNLIKELY(NS_WARN_IF(value.isNaN()))) { + // TODO(bug 1651070): This should ideally never happen, but we don't + // deal with lang changes correctly, so it could. + return GetBadInputMessage(aValidationMessage); + } + + Decimal step = mInputElement->GetStep(); + MOZ_ASSERT(step != kStepAny && step > Decimal(0)); + + Decimal stepBase = mInputElement->GetStepBase(); + + Decimal valueLow = value - NS_floorModulo(value - stepBase, step); + Decimal valueHigh = value + step - NS_floorModulo(value - stepBase, step); + + Decimal maximum = mInputElement->GetMaximum(); + + if (maximum.isNaN() || valueHigh <= maximum) { + nsAutoString valueLowStr, valueHighStr; + ConvertNumberToString(valueLow, valueLowStr); + ConvertNumberToString(valueHigh, valueHighStr); + + if (valueLowStr.Equals(valueHighStr)) { + return nsContentUtils::FormatMaybeLocalizedString( + aValidationMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationStepMismatchOneValue", mInputElement->OwnerDoc(), + valueLowStr); + } + return nsContentUtils::FormatMaybeLocalizedString( + aValidationMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationStepMismatch", mInputElement->OwnerDoc(), + valueLowStr, valueHighStr); + } + + nsAutoString valueLowStr; + ConvertNumberToString(valueLow, valueLowStr); + + return nsContentUtils::FormatMaybeLocalizedString( + aValidationMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationStepMismatchOneValue", mInputElement->OwnerDoc(), + valueLowStr); + } + case nsIConstraintValidation::VALIDITY_STATE_BAD_INPUT: + return GetBadInputMessage(aValidationMessage); + default: + MOZ_ASSERT_UNREACHABLE("Unknown validity state"); + return NS_ERROR_UNEXPECTED; + } +} + +nsresult InputType::GetValueMissingMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationValueMissing", + mInputElement->OwnerDoc(), aMessage); +} + +nsresult InputType::GetTypeMismatchMessage(nsAString& aMessage) { + return NS_ERROR_UNEXPECTED; +} + +nsresult InputType::GetRangeOverflowMessage(nsAString& aMessage) { + return NS_ERROR_UNEXPECTED; +} + +nsresult InputType::GetRangeUnderflowMessage(nsAString& aMessage) { + return NS_ERROR_UNEXPECTED; +} + +nsresult InputType::GetBadInputMessage(nsAString& aMessage) { + return NS_ERROR_UNEXPECTED; +} + +auto InputType::ConvertStringToNumber(const nsAString& aValue) const + -> StringToNumberResult { + NS_WARNING("InputType::ConvertStringToNumber called"); + return {}; +} + +bool InputType::ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const { + NS_WARNING("InputType::ConvertNumberToString called"); + + return false; +} + +bool InputType::ParseDate(const nsAString& aValue, uint32_t* aYear, + uint32_t* aMonth, uint32_t* aDay) const { + // TODO: move this function and implementation to DateTimeInpuTypeBase when + // refactoring is completed. Now we can only call HTMLInputElement::ParseDate + // from here, since the method is protected and only InputType is a friend + // class. + return mInputElement->ParseDate(aValue, aYear, aMonth, aDay); +} + +bool InputType::ParseTime(const nsAString& aValue, uint32_t* aResult) const { + // see comment in InputType::ParseDate(). + return HTMLInputElement::ParseTime(aValue, aResult); +} + +bool InputType::ParseMonth(const nsAString& aValue, uint32_t* aYear, + uint32_t* aMonth) const { + // see comment in InputType::ParseDate(). + return mInputElement->ParseMonth(aValue, aYear, aMonth); +} + +bool InputType::ParseWeek(const nsAString& aValue, uint32_t* aYear, + uint32_t* aWeek) const { + // see comment in InputType::ParseDate(). + return mInputElement->ParseWeek(aValue, aYear, aWeek); +} + +bool InputType::ParseDateTimeLocal(const nsAString& aValue, uint32_t* aYear, + uint32_t* aMonth, uint32_t* aDay, + uint32_t* aTime) const { + // see comment in InputType::ParseDate(). + return mInputElement->ParseDateTimeLocal(aValue, aYear, aMonth, aDay, aTime); +} + +int32_t InputType::MonthsSinceJan1970(uint32_t aYear, uint32_t aMonth) const { + // see comment in InputType::ParseDate(). + return mInputElement->MonthsSinceJan1970(aYear, aMonth); +} + +double InputType::DaysSinceEpochFromWeek(uint32_t aYear, uint32_t aWeek) const { + // see comment in InputType::ParseDate(). + return mInputElement->DaysSinceEpochFromWeek(aYear, aWeek); +} + +uint32_t InputType::DayOfWeek(uint32_t aYear, uint32_t aMonth, uint32_t aDay, + bool isoWeek) const { + // see comment in InputType::ParseDate(). + return mInputElement->DayOfWeek(aYear, aMonth, aDay, isoWeek); +} + +uint32_t InputType::MaximumWeekInYear(uint32_t aYear) const { + // see comment in InputType::ParseDate(). + return mInputElement->MaximumWeekInYear(aYear); +} diff --git a/dom/html/input/InputType.h b/dom/html/input/InputType.h new file mode 100644 index 0000000000..a3599feadd --- /dev/null +++ b/dom/html/input/InputType.h @@ -0,0 +1,240 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_InputType_h__ +#define mozilla_dom_InputType_h__ + +#include <stdint.h> +#include "mozilla/Decimal.h" +#include "mozilla/Maybe.h" +#include "mozilla/TextControlState.h" +#include "mozilla/UniquePtr.h" +#include "nsIConstraintValidation.h" +#include "nsString.h" +#include "nsError.h" + +// This must come outside of any namespace, or else it won't overload with the +// double based version in nsMathUtils.h +inline mozilla::Decimal NS_floorModulo(mozilla::Decimal x, mozilla::Decimal y) { + return (x - y * (x / y).floor()); +} + +class nsIFrame; + +namespace mozilla::dom { +class HTMLInputElement; + +/** + * A common superclass for different types of a HTMLInputElement. + */ +class InputType { + public: + using ValueSetterOption = TextControlState::ValueSetterOption; + using ValueSetterOptions = TextControlState::ValueSetterOptions; + + // Custom deleter for UniquePtr<InputType> to avoid freeing memory + // pre-allocated for InputType, but we still need to call the destructor + // explictly. + struct DoNotDelete { + void operator()(InputType* p) { p->~InputType(); } + }; + + static UniquePtr<InputType, DoNotDelete> Create( + HTMLInputElement* aInputElement, FormControlType, void* aMemory); + + virtual ~InputType() = default; + + // Float value returned by GetStep() when the step attribute is set to 'any'. + static constexpr Decimal kStepAny = Decimal(0_d); + + /** + * Drop the reference to the input element. + */ + void DropReference(); + + virtual bool MinAndMaxLengthApply() const { return false; } + virtual bool IsTooLong() const; + virtual bool IsTooShort() const; + virtual bool IsValueMissing() const; + virtual bool HasTypeMismatch() const; + // May return Nothing() if the JS engine failed to evaluate the regex. + virtual Maybe<bool> HasPatternMismatch() const; + virtual bool IsRangeOverflow() const; + virtual bool IsRangeUnderflow() const; + virtual bool HasStepMismatch() const; + virtual bool HasBadInput() const; + + nsresult GetValidationMessage( + nsAString& aValidationMessage, + nsIConstraintValidation::ValidityStateType aType); + virtual nsresult GetValueMissingMessage(nsAString& aMessage); + virtual nsresult GetTypeMismatchMessage(nsAString& aMessage); + virtual nsresult GetRangeOverflowMessage(nsAString& aMessage); + virtual nsresult GetRangeUnderflowMessage(nsAString& aMessage); + virtual nsresult GetBadInputMessage(nsAString& aMessage); + + MOZ_CAN_RUN_SCRIPT virtual void MinMaxStepAttrChanged() {} + + /** + * Convert a string to a Decimal number in a type specific way, + * http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#concept-input-value-string-number + * ie parse a date string to a timestamp if type=date, + * or parse a number string to its value if type=number. + * @param aValue the string to be parsed. + */ + struct StringToNumberResult { + // The result decimal. Successfully parsed if it's finite. + Decimal mResult = Decimal::nan(); + // Whether the result required reading locale-dependent data (for input + // type=number), or the value parses using the regular HTML rules. + bool mLocalized = false; + }; + virtual StringToNumberResult ConvertStringToNumber( + const nsAString& aValue) const; + + /** + * Convert a Decimal to a string in a type specific way, ie convert a + * timestamp to a date string if type=date or append the number string + * representing the value if type=number. + * + * @param aValue the Decimal to be converted + * @param aResultString [out] the string representing the Decimal + * @return whether the function succeeded, it will fail if the current input's + * type is not supported or the number can't be converted to a string + * as expected by the type. + */ + virtual bool ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const; + + protected: + explicit InputType(HTMLInputElement* aInputElement) + : mInputElement(aInputElement) {} + + /** + * Get the mutable state of the element. + * When the element isn't mutable (immutable), the value or checkedness + * should not be changed by the user. + * + * See: + * https://html.spec.whatwg.org/multipage/forms.html#the-input-element:concept-fe-mutable + */ + virtual bool IsMutable() const; + + /** + * Returns whether the input element's current value is the empty string. + * This only makes sense for some input types; does NOT make sense for file + * inputs. + * + * @return whether the input element's current value is the empty string. + */ + bool IsValueEmpty() const; + + // A getter for callers that know we're not dealing with a file input, so they + // don't have to think about the caller type. + void GetNonFileValueInternal(nsAString& aValue) const; + + /** + * Setting the input element's value. + * + * @param aValue String to set. + * @param aOptions See TextControlState::ValueSetterOption. + */ + MOZ_CAN_RUN_SCRIPT nsresult + SetValueInternal(const nsAString& aValue, const ValueSetterOptions& aOptions); + + /** + * Get the primary frame for the input element. + */ + nsIFrame* GetPrimaryFrame() const; + + /** + * Parse a date string of the form yyyy-mm-dd + * + * @param aValue the string to be parsed. + * @return the date in aYear, aMonth, aDay. + * @return whether the parsing was successful. + */ + bool ParseDate(const nsAString& aValue, uint32_t* aYear, uint32_t* aMonth, + uint32_t* aDay) const; + + /** + * Returns the time expressed in milliseconds of |aValue| being parsed as a + * time following the HTML specifications: + * https://html.spec.whatwg.org/multipage/infrastructure.html#parse-a-time-string + * + * Note: |aResult| can be null. + * + * @param aValue the string to be parsed. + * @param aResult the time expressed in milliseconds representing the time + * [out] + * @return whether the parsing was successful. + */ + bool ParseTime(const nsAString& aValue, uint32_t* aResult) const; + + /** + * Parse a month string of the form yyyy-mm + * + * @param the string to be parsed. + * @return the year and month in aYear and aMonth. + * @return whether the parsing was successful. + */ + bool ParseMonth(const nsAString& aValue, uint32_t* aYear, + uint32_t* aMonth) const; + + /** + * Parse a week string of the form yyyy-Www + * + * @param the string to be parsed. + * @return the year and week in aYear and aWeek. + * @return whether the parsing was successful. + */ + bool ParseWeek(const nsAString& aValue, uint32_t* aYear, + uint32_t* aWeek) const; + + /** + * Parse a datetime-local string of the form yyyy-mm-ddThh:mm[:ss.s] or + * yyyy-mm-dd hh:mm[:ss.s], where fractions of seconds can be 1 to 3 digits. + * + * @param the string to be parsed. + * @return the date in aYear, aMonth, aDay and time expressed in milliseconds + * in aTime. + * @return whether the parsing was successful. + */ + bool ParseDateTimeLocal(const nsAString& aValue, uint32_t* aYear, + uint32_t* aMonth, uint32_t* aDay, + uint32_t* aTime) const; + + /** + * This methods returns the number of months between January 1970 and the + * given year and month. + */ + int32_t MonthsSinceJan1970(uint32_t aYear, uint32_t aMonth) const; + + /** + * This methods returns the number of days since epoch for a given year and + * week. + */ + double DaysSinceEpochFromWeek(uint32_t aYear, uint32_t aWeek) const; + + /** + * This methods returns the day of the week given a date. If @isoWeek is true, + * 7=Sunday, otherwise, 0=Sunday. + */ + uint32_t DayOfWeek(uint32_t aYear, uint32_t aMonth, uint32_t aDay, + bool isoWeek) const; + + /** + * This methods returns the maximum number of week in a given year, the + * result is either 52 or 53. + */ + uint32_t MaximumWeekInYear(uint32_t aYear) const; + + HTMLInputElement* mInputElement; +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_InputType_h__ */ diff --git a/dom/html/input/NumericInputTypes.cpp b/dom/html/input/NumericInputTypes.cpp new file mode 100644 index 0000000000..93941b30f7 --- /dev/null +++ b/dom/html/input/NumericInputTypes.cpp @@ -0,0 +1,166 @@ +/* -*- 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/NumericInputTypes.h" + +#include "mozilla/TextControlState.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "ICUUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; + +bool NumericInputTypeBase::IsRangeOverflow() const { + Decimal maximum = mInputElement->GetMaximum(); + if (maximum.isNaN()) { + return false; + } + + Decimal value = mInputElement->GetValueAsDecimal(); + if (value.isNaN()) { + return false; + } + + return value > maximum; +} + +bool NumericInputTypeBase::IsRangeUnderflow() const { + Decimal minimum = mInputElement->GetMinimum(); + if (minimum.isNaN()) { + return false; + } + + Decimal value = mInputElement->GetValueAsDecimal(); + if (value.isNaN()) { + return false; + } + + return value < minimum; +} + +bool NumericInputTypeBase::HasStepMismatch() const { + Decimal value = mInputElement->GetValueAsDecimal(); + return mInputElement->ValueIsStepMismatch(value); +} + +nsresult NumericInputTypeBase::GetRangeOverflowMessage(nsAString& aMessage) { + // We want to show the value as parsed when it's a number + Decimal maximum = mInputElement->GetMaximum(); + MOZ_ASSERT(!maximum.isNaN()); + + nsAutoString maxStr; + ConvertNumberToString(maximum, maxStr); + return nsContentUtils::FormatMaybeLocalizedString( + aMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationNumberRangeOverflow", mInputElement->OwnerDoc(), maxStr); +} + +nsresult NumericInputTypeBase::GetRangeUnderflowMessage(nsAString& aMessage) { + Decimal minimum = mInputElement->GetMinimum(); + MOZ_ASSERT(!minimum.isNaN()); + + nsAutoString minStr; + ConvertNumberToString(minimum, minStr); + return nsContentUtils::FormatMaybeLocalizedString( + aMessage, nsContentUtils::eDOM_PROPERTIES, + "FormValidationNumberRangeUnderflow", mInputElement->OwnerDoc(), minStr); +} + +auto NumericInputTypeBase::ConvertStringToNumber(const nsAString& aValue) const + -> StringToNumberResult { + return {HTMLInputElement::StringToDecimal(aValue)}; +} + +bool NumericInputTypeBase::ConvertNumberToString( + Decimal aValue, nsAString& aResultString) const { + MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); + + aResultString.Truncate(); + + char buf[32]; + bool ok = aValue.toString(buf, ArrayLength(buf)); + aResultString.AssignASCII(buf); + MOZ_ASSERT(ok, "buf not big enough"); + + return ok; +} + +/* input type=number */ + +bool NumberInputType::IsValueMissing() const { + if (!mInputElement->IsRequired()) { + return false; + } + + if (!IsMutable()) { + return false; + } + + return IsValueEmpty(); +} + +bool NumberInputType::HasBadInput() const { + nsAutoString value; + GetNonFileValueInternal(value); + return !value.IsEmpty() && mInputElement->GetValueAsDecimal().isNaN(); +} + +auto NumberInputType::ConvertStringToNumber(const nsAString& aValue) const + -> StringToNumberResult { + auto result = NumericInputTypeBase::ConvertStringToNumber(aValue); + if (result.mResult.isFinite()) { + return result; + } + // Try to read the localized value from the user. + ICUUtils::LanguageTagIterForContent langTagIter(mInputElement); + result.mLocalized = true; + result.mResult = + Decimal::fromDouble(ICUUtils::ParseNumber(aValue, langTagIter)); + return result; +} + +bool NumberInputType::ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const { + MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); + + aResultString.Truncate(); + ICUUtils::LanguageTagIterForContent langTagIter(mInputElement); + ICUUtils::LocalizeNumber(aValue.toDouble(), langTagIter, aResultString); + return true; +} + +nsresult NumberInputType::GetValueMissingMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationBadInputNumber", + mInputElement->OwnerDoc(), aMessage); +} + +nsresult NumberInputType::GetBadInputMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationBadInputNumber", + mInputElement->OwnerDoc(), aMessage); +} + +bool NumberInputType::IsMutable() const { + return !mInputElement->IsDisabledOrReadOnly(); +} + +/* input type=range */ +void RangeInputType::MinMaxStepAttrChanged() { + // The value may need to change when @min/max/step changes since the value may + // have been invalid and can now change to a valid value, or vice versa. For + // example, consider: <input type=range value=-1 max=1 step=3>. The valid + // range is 0 to 1 while the nearest valid steps are -1 and 2 (the max value + // having prevented there being a valid step in range). Changing @max to/from + // 1 and a number greater than on equal to 3 should change whether we have a + // step mismatch or not. + // The value may also need to change between a value that results in a step + // mismatch and a value that results in overflow. For example, if @max in the + // example above were to change from 1 to -1. + nsAutoString value; + GetNonFileValueInternal(value); + SetValueInternal(value, TextControlState::ValueSetterOption::ByInternalAPI); +} diff --git a/dom/html/input/NumericInputTypes.h b/dom/html/input/NumericInputTypes.h new file mode 100644 index 0000000000..a541961f8d --- /dev/null +++ b/dom/html/input/NumericInputTypes.h @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_NumericInputTypes_h__ +#define mozilla_dom_NumericInputTypes_h__ + +#include "mozilla/dom/InputType.h" + +namespace mozilla::dom { + +class NumericInputTypeBase : public InputType { + public: + ~NumericInputTypeBase() override = default; + + bool IsRangeOverflow() const override; + bool IsRangeUnderflow() const override; + bool HasStepMismatch() const override; + + nsresult GetRangeOverflowMessage(nsAString& aMessage) override; + nsresult GetRangeUnderflowMessage(nsAString& aMessage) override; + + StringToNumberResult ConvertStringToNumber( + const nsAString& aValue) const override; + bool ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const override; + + protected: + explicit NumericInputTypeBase(HTMLInputElement* aInputElement) + : InputType(aInputElement) {} +}; + +// input type=number +class NumberInputType final : public NumericInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) NumberInputType(aInputElement); + } + + bool IsValueMissing() const override; + bool HasBadInput() const override; + + nsresult GetValueMissingMessage(nsAString& aMessage) override; + nsresult GetBadInputMessage(nsAString& aMessage) override; + + StringToNumberResult ConvertStringToNumber(const nsAString&) const override; + bool ConvertNumberToString(Decimal aValue, + nsAString& aResultString) const override; + + protected: + bool IsMutable() const override; + + private: + explicit NumberInputType(HTMLInputElement* aInputElement) + : NumericInputTypeBase(aInputElement) {} +}; + +// input type=range +class RangeInputType : public NumericInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) RangeInputType(aInputElement); + } + + MOZ_CAN_RUN_SCRIPT void MinMaxStepAttrChanged() override; + + private: + explicit RangeInputType(HTMLInputElement* aInputElement) + : NumericInputTypeBase(aInputElement) {} +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_NumericInputTypes_h__ */ diff --git a/dom/html/input/SingleLineTextInputTypes.cpp b/dom/html/input/SingleLineTextInputTypes.cpp new file mode 100644 index 0000000000..18cb12f520 --- /dev/null +++ b/dom/html/input/SingleLineTextInputTypes.cpp @@ -0,0 +1,289 @@ +/* -*- 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/SingleLineTextInputTypes.h" + +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/TextUtils.h" +#include "HTMLSplitOnSpacesTokenizer.h" +#include "nsContentUtils.h" +#include "nsCRTGlue.h" +#include "nsIIDNService.h" +#include "nsIIOService.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" + +using namespace mozilla; +using namespace mozilla::dom; + +bool SingleLineTextInputTypeBase::IsMutable() const { + return !mInputElement->IsDisabledOrReadOnly(); +} + +bool SingleLineTextInputTypeBase::IsTooLong() const { + int32_t maxLength = mInputElement->MaxLength(); + + // Maxlength of -1 means attribute isn't set or parsing error. + if (maxLength == -1) { + return false; + } + + int32_t textLength = mInputElement->InputTextLength(CallerType::System); + + return textLength > maxLength; +} + +bool SingleLineTextInputTypeBase::IsTooShort() const { + int32_t minLength = mInputElement->MinLength(); + + // Minlength of -1 means attribute isn't set or parsing error. + if (minLength == -1) { + return false; + } + + int32_t textLength = mInputElement->InputTextLength(CallerType::System); + + return textLength && textLength < minLength; +} + +bool SingleLineTextInputTypeBase::IsValueMissing() const { + if (!mInputElement->IsRequired()) { + return false; + } + + if (!IsMutable()) { + return false; + } + + return IsValueEmpty(); +} + +Maybe<bool> SingleLineTextInputTypeBase::HasPatternMismatch() const { + if (!mInputElement->HasPatternAttribute()) { + return Some(false); + } + + nsAutoString pattern; + if (!mInputElement->GetAttr(nsGkAtoms::pattern, pattern)) { + return Some(false); + } + + nsAutoString value; + GetNonFileValueInternal(value); + + if (value.IsEmpty()) { + return Some(false); + } + + Document* doc = mInputElement->OwnerDoc(); + Maybe<bool> result = nsContentUtils::IsPatternMatching( + value, std::move(pattern), doc, + mInputElement->HasAttr(nsGkAtoms::multiple)); + return result ? Some(!*result) : Nothing(); +} + +/* input type=url */ + +bool URLInputType::HasTypeMismatch() const { + nsAutoString value; + GetNonFileValueInternal(value); + + if (value.IsEmpty()) { + return false; + } + + /** + * TODO: + * The URL is not checked as the HTML5 specifications want it to be because + * there is no code to check for a valid URI/IRI according to 3986 and 3987 + * RFC's at the moment, see bug 561586. + * + * RFC 3987 (IRI) implementation: bug 42899 + * + * HTML5 specifications: + * http://dev.w3.org/html5/spec/infrastructure.html#valid-url + */ + nsCOMPtr<nsIIOService> ioService = do_GetIOService(); + nsCOMPtr<nsIURI> uri; + + return !NS_SUCCEEDED(ioService->NewURI(NS_ConvertUTF16toUTF8(value), nullptr, + nullptr, getter_AddRefs(uri))); +} + +nsresult URLInputType::GetTypeMismatchMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidURL", + mInputElement->OwnerDoc(), aMessage); +} + +/* input type=email */ + +bool EmailInputType::HasTypeMismatch() const { + nsAutoString value; + GetNonFileValueInternal(value); + + if (value.IsEmpty()) { + return false; + } + + return mInputElement->HasAttr(nsGkAtoms::multiple) + ? !IsValidEmailAddressList(value) + : !IsValidEmailAddress(value); +} + +bool EmailInputType::HasBadInput() const { + // With regards to suffering from bad input the spec says that only the + // punycode conversion works, so we don't care whether the email address is + // valid or not here. (If the email address is invalid then we will be + // suffering from a type mismatch.) + nsAutoString value; + nsAutoCString unused; + uint32_t unused2; + GetNonFileValueInternal(value); + HTMLSplitOnSpacesTokenizer tokenizer(value, ','); + while (tokenizer.hasMoreTokens()) { + if (!PunycodeEncodeEmailAddress(tokenizer.nextToken(), unused, &unused2)) { + return true; + } + } + return false; +} + +nsresult EmailInputType::GetTypeMismatchMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidEmail", + mInputElement->OwnerDoc(), aMessage); +} + +nsresult EmailInputType::GetBadInputMessage(nsAString& aMessage) { + return nsContentUtils::GetMaybeLocalizedString( + nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidEmail", + mInputElement->OwnerDoc(), aMessage); +} + +/* static */ +bool EmailInputType::IsValidEmailAddressList(const nsAString& aValue) { + HTMLSplitOnSpacesTokenizer tokenizer(aValue, ','); + + while (tokenizer.hasMoreTokens()) { + if (!IsValidEmailAddress(tokenizer.nextToken())) { + return false; + } + } + + return !tokenizer.separatorAfterCurrentToken(); +} + +/* static */ +bool EmailInputType::IsValidEmailAddress(const nsAString& aValue) { + // Email addresses can't be empty and can't end with a '.' or '-'. + if (aValue.IsEmpty() || aValue.Last() == '.' || aValue.Last() == '-') { + return false; + } + + uint32_t atPos; + nsAutoCString value; + if (!PunycodeEncodeEmailAddress(aValue, value, &atPos) || + atPos == (uint32_t)kNotFound || atPos == 0 || + atPos == value.Length() - 1) { + // Could not encode, or "@" was not found, or it was at the start or end + // of the input - in all cases, not a valid email address. + return false; + } + + uint32_t length = value.Length(); + uint32_t i = 0; + + // Parsing the username. + for (; i < atPos; ++i) { + char16_t c = value[i]; + + // The username characters have to be in this list to be valid. + if (!(IsAsciiAlpha(c) || IsAsciiDigit(c) || c == '.' || c == '!' || + c == '#' || c == '$' || c == '%' || c == '&' || c == '\'' || + c == '*' || c == '+' || c == '-' || c == '/' || c == '=' || + c == '?' || c == '^' || c == '_' || c == '`' || c == '{' || + c == '|' || c == '}' || c == '~')) { + return false; + } + } + + // Skip the '@'. + ++i; + + // The domain name can't begin with a dot or a dash. + if (value[i] == '.' || value[i] == '-') { + return false; + } + + // Parsing the domain name. + for (; i < length; ++i) { + char16_t c = value[i]; + + if (c == '.') { + // A dot can't follow a dot or a dash. + if (value[i - 1] == '.' || value[i - 1] == '-') { + return false; + } + } else if (c == '-') { + // A dash can't follow a dot. + if (value[i - 1] == '.') { + return false; + } + } else if (!(IsAsciiAlpha(c) || IsAsciiDigit(c) || c == '-')) { + // The domain characters have to be in this list to be valid. + return false; + } + } + + return true; +} + +/* static */ +bool EmailInputType::PunycodeEncodeEmailAddress(const nsAString& aEmail, + nsAutoCString& aEncodedEmail, + uint32_t* aIndexOfAt) { + nsAutoCString value = NS_ConvertUTF16toUTF8(aEmail); + *aIndexOfAt = (uint32_t)value.FindChar('@'); + + if (*aIndexOfAt == (uint32_t)kNotFound || *aIndexOfAt == value.Length() - 1) { + aEncodedEmail = value; + return true; + } + + nsCOMPtr<nsIIDNService> idnSrv = do_GetService(NS_IDNSERVICE_CONTRACTID); + if (!idnSrv) { + NS_ERROR("nsIIDNService isn't present!"); + return false; + } + + uint32_t indexOfDomain = *aIndexOfAt + 1; + + const nsDependentCSubstring domain = Substring(value, indexOfDomain); + bool ace; + if (NS_SUCCEEDED(idnSrv->IsACE(domain, &ace)) && !ace) { + nsAutoCString domainACE; + if (NS_FAILED(idnSrv->ConvertUTF8toACE(domain, domainACE))) { + return false; + } + + // Bug 1788115 removed the 63 character limit from the + // IDNService::ConvertUTF8toACE so we check for that limit here as required + // by the spec: https://html.spec.whatwg.org/#valid-e-mail-address + nsCCharSeparatedTokenizer tokenizer(domainACE, '.'); + while (tokenizer.hasMoreTokens()) { + if (tokenizer.nextToken().Length() > 63) { + return false; + } + } + + value.Replace(indexOfDomain, domain.Length(), domainACE); + } + + aEncodedEmail = value; + return true; +} diff --git a/dom/html/input/SingleLineTextInputTypes.h b/dom/html/input/SingleLineTextInputTypes.h new file mode 100644 index 0000000000..afacc917a3 --- /dev/null +++ b/dom/html/input/SingleLineTextInputTypes.h @@ -0,0 +1,158 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_SingleLineTextInputTypes_h__ +#define mozilla_dom_SingleLineTextInputTypes_h__ + +#include "mozilla/dom/InputType.h" + +namespace mozilla::dom { + +class SingleLineTextInputTypeBase : public InputType { + public: + ~SingleLineTextInputTypeBase() override = default; + + bool MinAndMaxLengthApply() const final { return true; } + bool IsTooLong() const final; + bool IsTooShort() const final; + bool IsValueMissing() const final; + // Can return Nothing() if the JS engine failed to evaluate the pattern. + Maybe<bool> HasPatternMismatch() const final; + + protected: + explicit SingleLineTextInputTypeBase(HTMLInputElement* aInputElement) + : InputType(aInputElement) {} + + bool IsMutable() const override; +}; + +// input type=text +class TextInputType : public SingleLineTextInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) TextInputType(aInputElement); + } + + private: + explicit TextInputType(HTMLInputElement* aInputElement) + : SingleLineTextInputTypeBase(aInputElement) {} +}; + +// input type=search +class SearchInputType : public SingleLineTextInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) SearchInputType(aInputElement); + } + + private: + explicit SearchInputType(HTMLInputElement* aInputElement) + : SingleLineTextInputTypeBase(aInputElement) {} +}; + +// input type=tel +class TelInputType : public SingleLineTextInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) TelInputType(aInputElement); + } + + private: + explicit TelInputType(HTMLInputElement* aInputElement) + : SingleLineTextInputTypeBase(aInputElement) {} +}; + +// input type=url +class URLInputType : public SingleLineTextInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) URLInputType(aInputElement); + } + + bool HasTypeMismatch() const override; + + nsresult GetTypeMismatchMessage(nsAString& aMessage) override; + + private: + explicit URLInputType(HTMLInputElement* aInputElement) + : SingleLineTextInputTypeBase(aInputElement) {} +}; + +// input type=email +class EmailInputType : public SingleLineTextInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) EmailInputType(aInputElement); + } + + bool HasTypeMismatch() const override; + bool HasBadInput() const override; + + nsresult GetTypeMismatchMessage(nsAString& aMessage) override; + nsresult GetBadInputMessage(nsAString& aMessage) override; + + private: + explicit EmailInputType(HTMLInputElement* aInputElement) + : SingleLineTextInputTypeBase(aInputElement) {} + + /** + * This helper method returns true if aValue is a valid email address. + * This is following the HTML5 specification: + * http://dev.w3.org/html5/spec/forms.html#valid-e-mail-address + * + * @param aValue the email address to check. + * @result whether the given string is a valid email address. + */ + static bool IsValidEmailAddress(const nsAString& aValue); + + /** + * This helper method returns true if aValue is a valid email address list. + * Email address list is a list of email address separated by comas (,) which + * can be surrounded by space charecters. + * This is following the HTML5 specification: + * http://dev.w3.org/html5/spec/forms.html#valid-e-mail-address-list + * + * @param aValue the email address list to check. + * @result whether the given string is a valid email address list. + */ + static bool IsValidEmailAddressList(const nsAString& aValue); + + /** + * Takes aEmail and attempts to convert everything after the first "@" + * character (if anything) to punycode before returning the complete result + * via the aEncodedEmail out-param. The aIndexOfAt out-param is set to the + * index of the "@" character. + * + * If no "@" is found in aEmail, aEncodedEmail is simply set to aEmail and + * the aIndexOfAt out-param is set to kNotFound. + * + * Returns true in all cases unless an attempt to punycode encode fails. If + * false is returned, aEncodedEmail has not been set. + * + * This function exists because ConvertUTF8toACE() splits on ".", meaning that + * for 'user.name@sld.tld' it would treat "name@sld" as a label. We want to + * encode the domain part only. + */ + static bool PunycodeEncodeEmailAddress(const nsAString& aEmail, + nsAutoCString& aEncodedEmail, + uint32_t* aIndexOfAt); +}; + +// input type=password +class PasswordInputType : public SingleLineTextInputTypeBase { + public: + static InputType* Create(HTMLInputElement* aInputElement, void* aMemory) { + return new (aMemory) PasswordInputType(aInputElement); + } + + private: + explicit PasswordInputType(HTMLInputElement* aInputElement) + : SingleLineTextInputTypeBase(aInputElement) {} +}; + +} // namespace mozilla::dom + +#endif /* mozilla_dom_SingleLineTextInputTypes_h__ */ diff --git a/dom/html/input/moz.build b/dom/html/input/moz.build new file mode 100644 index 0000000000..fa468f11e7 --- /dev/null +++ b/dom/html/input/moz.build @@ -0,0 +1,36 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +EXPORTS.mozilla.dom += [ + "ButtonInputTypes.h", + "CheckableInputTypes.h", + "ColorInputType.h", + "DateTimeInputTypes.h", + "FileInputType.h", + "HiddenInputType.h", + "InputType.h", + "NumericInputTypes.h", + "SingleLineTextInputTypes.h", +] + +UNIFIED_SOURCES += [ + "CheckableInputTypes.cpp", + "DateTimeInputTypes.cpp", + "FileInputType.cpp", + "InputType.cpp", + "NumericInputTypes.cpp", + "SingleLineTextInputTypes.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/html", + "/layout/forms", +] + +FINAL_LIBRARY = "xul" diff --git a/dom/html/moz.build b/dom/html/moz.build new file mode 100644 index 0000000000..f3fdd30917 --- /dev/null +++ b/dom/html/moz.build @@ -0,0 +1,247 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "DOM: Core & HTML") + +DIRS += ["input"] + +MOCHITEST_MANIFESTS += [ + "test/dialog/mochitest.toml", + "test/forms/mochitest.toml", + "test/forms/without_selectionchange/mochitest.toml", + "test/mochitest.toml", +] + +MOCHITEST_CHROME_MANIFESTS += [ + "test/chrome.toml", + "test/forms/chrome.toml", +] + +BROWSER_CHROME_MANIFESTS += ["test/browser.toml"] + +EXPORTS += [ + "nsGenericHTMLElement.h", + "nsGenericHTMLFrameElement.h", + "nsHTMLDocument.h", + "nsIConstraintValidation.h", + "nsIFormControl.h", + "nsIHTMLCollection.h", + "nsIRadioVisitor.h", +] + +EXPORTS.mozilla += [ + "TextControlElement.h", + "TextControlState.h", + "TextInputListener.h", +] + +EXPORTS.mozilla.dom += [ + "ConstraintValidation.h", + "CustomStateSet.h", + "ElementInternals.h", + "FetchPriority.h", + "HTMLAllCollection.h", + "HTMLAnchorElement.h", + "HTMLAreaElement.h", + "HTMLAudioElement.h", + "HTMLBodyElement.h", + "HTMLBRElement.h", + "HTMLButtonElement.h", + "HTMLCanvasElement.h", + "HTMLDataElement.h", + "HTMLDataListElement.h", + "HTMLDetailsElement.h", + "HTMLDialogElement.h", + "HTMLDivElement.h", + "HTMLDNSPrefetch.h", + "HTMLElement.h", + "HTMLEmbedElement.h", + "HTMLFieldSetElement.h", + "HTMLFontElement.h", + "HTMLFormControlsCollection.h", + "HTMLFormElement.h", + "HTMLFormSubmission.h", + "HTMLFrameElement.h", + "HTMLFrameSetElement.h", + "HTMLHeadingElement.h", + "HTMLHRElement.h", + "HTMLIFrameElement.h", + "HTMLImageElement.h", + "HTMLInputElement.h", + "HTMLLabelElement.h", + "HTMLLegendElement.h", + "HTMLLIElement.h", + "HTMLLinkElement.h", + "HTMLMapElement.h", + "HTMLMarqueeElement.h", + "HTMLMediaElement.h", + "HTMLMenuElement.h", + "HTMLMetaElement.h", + "HTMLMeterElement.h", + "HTMLModElement.h", + "HTMLObjectElement.h", + "HTMLOptGroupElement.h", + "HTMLOptionElement.h", + "HTMLOptionsCollection.h", + "HTMLOutputElement.h", + "HTMLParagraphElement.h", + "HTMLPictureElement.h", + "HTMLPreElement.h", + "HTMLProgressElement.h", + "HTMLScriptElement.h", + "HTMLSelectElement.h", + "HTMLSharedElement.h", + "HTMLSharedListElement.h", + "HTMLSlotElement.h", + "HTMLSourceElement.h", + "HTMLSpanElement.h", + "HTMLStyleElement.h", + "HTMLSummaryElement.h", + "HTMLTableCaptionElement.h", + "HTMLTableCellElement.h", + "HTMLTableColElement.h", + "HTMLTableElement.h", + "HTMLTableRowElement.h", + "HTMLTableSectionElement.h", + "HTMLTemplateElement.h", + "HTMLTextAreaElement.h", + "HTMLTimeElement.h", + "HTMLTitleElement.h", + "HTMLTrackElement.h", + "HTMLUnknownElement.h", + "HTMLVideoElement.h", + "ImageDocument.h", + "MediaDocument.h", + "MediaError.h", + "nsBrowserElement.h", + "PlayPromise.h", + "RadioNodeList.h", + "TextTrackManager.h", + "TimeRanges.h", + "ValidityState.h", +] + +UNIFIED_SOURCES += [ + "ConstraintValidation.cpp", + "CustomStateSet.cpp", + "ElementInternals.cpp", + "FetchPriority.cpp", + "HTMLAllCollection.cpp", + "HTMLAnchorElement.cpp", + "HTMLAreaElement.cpp", + "HTMLAudioElement.cpp", + "HTMLBodyElement.cpp", + "HTMLBRElement.cpp", + "HTMLButtonElement.cpp", + "HTMLCanvasElement.cpp", + "HTMLDataElement.cpp", + "HTMLDataListElement.cpp", + "HTMLDetailsElement.cpp", + "HTMLDialogElement.cpp", + "HTMLDivElement.cpp", + "HTMLDNSPrefetch.cpp", + "HTMLElement.cpp", + "HTMLEmbedElement.cpp", + "HTMLFieldSetElement.cpp", + "HTMLFontElement.cpp", + "HTMLFormControlsCollection.cpp", + "HTMLFormElement.cpp", + "HTMLFormSubmission.cpp", + "HTMLFrameElement.cpp", + "HTMLFrameSetElement.cpp", + "HTMLHeadingElement.cpp", + "HTMLHRElement.cpp", + "HTMLIFrameElement.cpp", + "HTMLImageElement.cpp", + "HTMLInputElement.cpp", + "HTMLLabelElement.cpp", + "HTMLLegendElement.cpp", + "HTMLLIElement.cpp", + "HTMLLinkElement.cpp", + "HTMLMapElement.cpp", + "HTMLMarqueeElement.cpp", + "HTMLMediaElement.cpp", + "HTMLMenuElement.cpp", + "HTMLMetaElement.cpp", + "HTMLMeterElement.cpp", + "HTMLModElement.cpp", + "HTMLObjectElement.cpp", + "HTMLOptGroupElement.cpp", + "HTMLOptionElement.cpp", + "HTMLOptionsCollection.cpp", + "HTMLOutputElement.cpp", + "HTMLParagraphElement.cpp", + "HTMLPictureElement.cpp", + "HTMLPreElement.cpp", + "HTMLProgressElement.cpp", + "HTMLScriptElement.cpp", + "HTMLSelectElement.cpp", + "HTMLSharedElement.cpp", + "HTMLSharedListElement.cpp", + "HTMLSlotElement.cpp", + "HTMLSourceElement.cpp", + "HTMLSpanElement.cpp", + "HTMLStyleElement.cpp", + "HTMLSummaryElement.cpp", + "HTMLTableCaptionElement.cpp", + "HTMLTableCellElement.cpp", + "HTMLTableColElement.cpp", + "HTMLTableElement.cpp", + "HTMLTableRowElement.cpp", + "HTMLTableSectionElement.cpp", + "HTMLTemplateElement.cpp", + "HTMLTextAreaElement.cpp", + "HTMLTimeElement.cpp", + "HTMLTitleElement.cpp", + "HTMLTrackElement.cpp", + "HTMLUnknownElement.cpp", + "HTMLVideoElement.cpp", + "ImageDocument.cpp", + "MediaDocument.cpp", + "MediaError.cpp", + "nsBrowserElement.cpp", + "nsDOMStringMap.cpp", + "nsGenericHTMLElement.cpp", + "nsGenericHTMLFrameElement.cpp", + "nsHTMLContentSink.cpp", + "nsHTMLDocument.cpp", + "nsIConstraintValidation.cpp", + "nsRadioVisitor.cpp", + "PlayPromise.cpp", + "RadioNodeList.cpp", + "TextControlState.cpp", + "TextTrackManager.cpp", + "TimeRanges.cpp", + "ValidityState.cpp", + "VideoDocument.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +LOCAL_INCLUDES += [ + "/caps", + "/docshell/base", + "/dom/base", + "/dom/canvas", + "/dom/html/input", + "/dom/media", + "/dom/security", + "/dom/xul", + "/image", + "/layout/forms", + "/layout/generic", + "/layout/style", + "/layout/tables", + "/layout/xul", + "/netwerk/base", + "/parser/htmlparser", +] + +FINAL_LIBRARY = "xul" + +if CONFIG["MOZ_ANDROID_HLS_SUPPORT"]: + DEFINES["MOZ_ANDROID_HLS_SUPPORT"] = True diff --git a/dom/html/nsBrowserElement.cpp b/dom/html/nsBrowserElement.cpp new file mode 100644 index 0000000000..69284e77ab --- /dev/null +++ b/dom/html/nsBrowserElement.cpp @@ -0,0 +1,57 @@ +/* -*- 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 "nsBrowserElement.h" + +#include "mozilla/Preferences.h" +#include "mozilla/dom/Promise.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ToJSValue.h" + +#include "nsComponentManagerUtils.h" +#include "nsFrameLoader.h" +#include "nsINode.h" + +#include "js/Wrapper.h" + +using namespace mozilla::dom; + +namespace mozilla { + +bool nsBrowserElement::IsBrowserElementOrThrow(ErrorResult& aRv) { + if (mBrowserElementAPI) { + return true; + } + aRv.Throw(NS_ERROR_DOM_INVALID_NODE_TYPE_ERR); + return false; +} + +void nsBrowserElement::InitBrowserElementAPI() { + RefPtr<nsFrameLoader> frameLoader = GetFrameLoader(); + NS_ENSURE_TRUE_VOID(frameLoader); + + if (!frameLoader->OwnerIsMozBrowserFrame()) { + return; + } + + if (!mBrowserElementAPI) { + mBrowserElementAPI = + do_CreateInstance("@mozilla.org/dom/browser-element-api;1"); + if (NS_WARN_IF(!mBrowserElementAPI)) { + return; + } + } + mBrowserElementAPI->SetFrameLoader(frameLoader); +} + +void nsBrowserElement::DestroyBrowserElementFrameScripts() { + if (!mBrowserElementAPI) { + return; + } + mBrowserElementAPI->DestroyFrameScripts(); +} + +} // namespace mozilla diff --git a/dom/html/nsBrowserElement.h b/dom/html/nsBrowserElement.h new file mode 100644 index 0000000000..c81529de33 --- /dev/null +++ b/dom/html/nsBrowserElement.h @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsBrowserElement_h +#define nsBrowserElement_h + +#include "mozilla/dom/BindingDeclarations.h" + +#include "nsCOMPtr.h" +#include "nsIBrowserElementAPI.h" + +class nsFrameLoader; + +namespace mozilla { + +namespace dom { +class Promise; +} // namespace dom + +class ErrorResult; + +/** + * A helper class for browser-element frames + */ +class nsBrowserElement { + public: + nsBrowserElement() = default; + virtual ~nsBrowserElement() = default; + + void SendMouseEvent(const nsAString& aType, uint32_t aX, uint32_t aY, + uint32_t aButton, uint32_t aClickCount, + uint32_t aModifiers, ErrorResult& aRv); + void GoBack(ErrorResult& aRv); + void GoForward(ErrorResult& aRv); + void Reload(bool aHardReload, ErrorResult& aRv); + void Stop(ErrorResult& aRv); + + already_AddRefed<dom::Promise> GetCanGoBack(ErrorResult& aRv); + already_AddRefed<dom::Promise> GetCanGoForward(ErrorResult& aRv); + + protected: + virtual already_AddRefed<nsFrameLoader> GetFrameLoader() = 0; + + void InitBrowserElementAPI(); + void DestroyBrowserElementFrameScripts(); + nsCOMPtr<nsIBrowserElementAPI> mBrowserElementAPI; + + private: + bool IsBrowserElementOrThrow(ErrorResult& aRv); +}; + +} // namespace mozilla + +#endif // nsBrowserElement_h diff --git a/dom/html/nsDOMStringMap.cpp b/dom/html/nsDOMStringMap.cpp new file mode 100644 index 0000000000..f8975cc02e --- /dev/null +++ b/dom/html/nsDOMStringMap.cpp @@ -0,0 +1,242 @@ +/* -*- 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 "nsDOMStringMap.h" + +#include "jsapi.h" +#include "nsError.h" +#include "nsGenericHTMLElement.h" +#include "nsContentUtils.h" +#include "mozilla/dom/DOMStringMapBinding.h" +#include "mozilla/dom/MutationEventBinding.h" + +using namespace mozilla; +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsDOMStringMap) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsDOMStringMap) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElement) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsDOMStringMap) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER + // Check that mElement exists in case the unlink code is run more than once. + if (tmp->mElement) { + // Call back to element to null out weak reference to this object. + tmp->mElement->ClearDataset(); + tmp->mElement->RemoveMutationObserver(tmp); + tmp->mElement = nullptr; + } + tmp->mExpandoAndGeneration.OwnerUnlinked(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsDOMStringMap) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(nsDOMStringMap) +NS_IMPL_CYCLE_COLLECTING_RELEASE(nsDOMStringMap) + +nsDOMStringMap::nsDOMStringMap(Element* aElement) + : mElement(aElement), mRemovingProp(false) { + mElement->AddMutationObserver(this); +} + +nsDOMStringMap::~nsDOMStringMap() { + // Check if element still exists, may have been unlinked by cycle collector. + if (mElement) { + // Call back to element to null out weak reference to this object. + mElement->ClearDataset(); + mElement->RemoveMutationObserver(this); + } +} + +DocGroup* nsDOMStringMap::GetDocGroup() const { + return mElement ? mElement->GetDocGroup() : nullptr; +} + +/* virtual */ +JSObject* nsDOMStringMap::WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) { + return DOMStringMap_Binding::Wrap(cx, this, aGivenProto); +} + +void nsDOMStringMap::NamedGetter(const nsAString& aProp, bool& found, + DOMString& aResult) const { + nsAutoString attr; + + if (!DataPropToAttr(aProp, attr)) { + found = false; + return; + } + + found = mElement->GetAttr(attr, aResult); +} + +void nsDOMStringMap::NamedSetter(const nsAString& aProp, + const nsAString& aValue, ErrorResult& rv) { + nsAutoString attr; + if (!DataPropToAttr(aProp, attr)) { + rv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return; + } + + nsresult res = nsContentUtils::CheckQName(attr, false); + if (NS_FAILED(res)) { + rv.Throw(res); + return; + } + + RefPtr<nsAtom> attrAtom = NS_Atomize(attr); + MOZ_ASSERT(attrAtom, "Should be infallible"); + + res = mElement->SetAttr(kNameSpaceID_None, attrAtom, aValue, true); + if (NS_FAILED(res)) { + rv.Throw(res); + } +} + +void nsDOMStringMap::NamedDeleter(const nsAString& aProp, bool& found) { + // Currently removing property, attribute is already removed. + if (mRemovingProp) { + found = false; + return; + } + + nsAutoString attr; + if (!DataPropToAttr(aProp, attr)) { + found = false; + return; + } + + RefPtr<nsAtom> attrAtom = NS_Atomize(attr); + MOZ_ASSERT(attrAtom, "Should be infallible"); + + found = mElement->HasAttr(attrAtom); + + if (found) { + mRemovingProp = true; + mElement->UnsetAttr(kNameSpaceID_None, attrAtom, true); + mRemovingProp = false; + } +} + +void nsDOMStringMap::GetSupportedNames(nsTArray<nsString>& aNames) { + uint32_t attrCount = mElement->GetAttrCount(); + + // Iterate through all the attributes and add property + // names corresponding to data attributes to return array. + for (uint32_t i = 0; i < attrCount; ++i) { + const nsAttrName* attrName = mElement->GetAttrNameAt(i); + // Skip the ones that are not in the null namespace + if (attrName->NamespaceID() != kNameSpaceID_None) { + continue; + } + + nsAutoString prop; + if (!AttrToDataProp(nsDependentAtomString(attrName->LocalName()), prop)) { + continue; + } + + aNames.AppendElement(prop); + } +} + +/** + * Converts a dataset property name to the corresponding data attribute name. + * (ex. aBigFish to data-a-big-fish). + */ +bool nsDOMStringMap::DataPropToAttr(const nsAString& aProp, + nsAutoString& aResult) { + // aResult is an autostring, so don't worry about setting its capacity: + // SetCapacity is slow even when it's a no-op and we already have enough + // storage there for most cases, probably. + aResult.AppendLiteral("data-"); + + // Iterate property by character to form attribute name. + // Return syntax error if there is a sequence of "-" followed by a character + // in the range "a" to "z". + // Replace capital characters with "-" followed by lower case character. + // Otherwise, simply append character to attribute name. + const char16_t* start = aProp.BeginReading(); + const char16_t* end = aProp.EndReading(); + const char16_t* cur = start; + for (; cur < end; ++cur) { + const char16_t* next = cur + 1; + if (char16_t('-') == *cur && next < end && char16_t('a') <= *next && + *next <= char16_t('z')) { + // Syntax error if character following "-" is in range "a" to "z". + return false; + } + + if (char16_t('A') <= *cur && *cur <= char16_t('Z')) { + // Append the characters in the range [start, cur) + aResult.Append(start, cur - start); + // Uncamel-case characters in the range of "A" to "Z". + aResult.Append(char16_t('-')); + aResult.Append(*cur - 'A' + 'a'); + start = next; // We've already appended the thing at *cur + } + } + + aResult.Append(start, cur - start); + + return true; +} + +/** + * Converts a data attribute name to the corresponding dataset property name. + * (ex. data-a-big-fish to aBigFish). + */ +bool nsDOMStringMap::AttrToDataProp(const nsAString& aAttr, + nsAutoString& aResult) { + // If the attribute name does not begin with "data-" then it can not be + // a data attribute. + if (!StringBeginsWith(aAttr, u"data-"_ns)) { + return false; + } + + // Start reading attribute from first character after "data-". + const char16_t* cur = aAttr.BeginReading() + 5; + const char16_t* end = aAttr.EndReading(); + + // Don't try to mess with aResult's capacity: the probably-no-op SetCapacity() + // call is not that fast. + + // Iterate through attrName by character to form property name. + // If there is a sequence of "-" followed by a character in the range "a" to + // "z" then replace with upper case letter. + // Otherwise append character to property name. + for (; cur < end; ++cur) { + const char16_t* next = cur + 1; + if (char16_t('-') == *cur && next < end && char16_t('a') <= *next && + *next <= char16_t('z')) { + // Upper case the lower case letters that follow a "-". + aResult.Append(*next - 'a' + 'A'); + // Consume character to account for "-" character. + ++cur; + } else { + // Simply append character if camel case is not necessary. + aResult.Append(*cur); + } + } + + return true; +} + +void nsDOMStringMap::AttributeChanged(Element* aElement, int32_t aNameSpaceID, + nsAtom* aAttribute, int32_t aModType, + const nsAttrValue* aOldValue) { + if ((aModType == MutationEvent_Binding::ADDITION || + aModType == MutationEvent_Binding::REMOVAL) && + aNameSpaceID == kNameSpaceID_None && + StringBeginsWith(nsDependentAtomString(aAttribute), u"data-"_ns)) { + ++mExpandoAndGeneration.generation; + } +} diff --git a/dom/html/nsDOMStringMap.h b/dom/html/nsDOMStringMap.h new file mode 100644 index 0000000000..a5b3a20832 --- /dev/null +++ b/dom/html/nsDOMStringMap.h @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsDOMStringMap_h +#define nsDOMStringMap_h + +#include "nsCycleCollectionParticipant.h" +#include "nsStubMutationObserver.h" +#include "nsTArray.h" +#include "nsString.h" +#include "nsWrapperCache.h" +#include "js/friend/DOMProxy.h" // JS::ExpandoAndGeneration +#include "js/RootingAPI.h" // JS::Handle + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "mozilla/dom/Element.h" + +namespace mozilla { +class ErrorResult; +namespace dom { +class DOMString; +class DocGroup; +} // namespace dom +} // namespace mozilla + +class nsDOMStringMap : public nsStubMutationObserver, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(nsDOMStringMap) + + NS_DECL_NSIMUTATIONOBSERVER_ATTRIBUTECHANGED + + nsINode* GetParentObject() { return mElement; } + + mozilla::dom::DocGroup* GetDocGroup() const; + + explicit nsDOMStringMap(mozilla::dom::Element* aElement); + + // WebIDL API + virtual JSObject* WrapObject(JSContext* cx, + JS::Handle<JSObject*> aGivenProto) override; + void NamedGetter(const nsAString& aProp, bool& found, + mozilla::dom::DOMString& aResult) const; + void NamedSetter(const nsAString& aProp, const nsAString& aValue, + mozilla::ErrorResult& rv); + void NamedDeleter(const nsAString& aProp, bool& found); + void GetSupportedNames(nsTArray<nsString>& aNames); + + JS::ExpandoAndGeneration mExpandoAndGeneration; + + private: + virtual ~nsDOMStringMap(); + + protected: + RefPtr<mozilla::dom::Element> mElement; + // Flag to guard against infinite recursion. + bool mRemovingProp; + static bool DataPropToAttr(const nsAString& aProp, nsAutoString& aResult); + static bool AttrToDataProp(const nsAString& aAttr, nsAutoString& aResult); +}; + +#endif diff --git a/dom/html/nsGenericHTMLElement.cpp b/dom/html/nsGenericHTMLElement.cpp new file mode 100644 index 0000000000..de29276fdc --- /dev/null +++ b/dom/html/nsGenericHTMLElement.cpp @@ -0,0 +1,3623 @@ +/* -*- 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/EditorBase.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/IMEContentObserver.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/MappedDeclarationsBuilder.h" +#include "mozilla/Maybe.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/TextEditor.h" +#include "mozilla/TextEvents.h" +#include "mozilla/StaticPrefs_html5.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "mozilla/dom/FetchPriority.h" +#include "mozilla/dom/FormData.h" +#include "nscore.h" +#include "nsGenericHTMLElement.h" +#include "nsCOMPtr.h" +#include "nsAtom.h" +#include "nsQueryObject.h" +#include "mozilla/dom/BindContext.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" +#include "nsIFrameInlines.h" +#include "nsIScrollableFrame.h" +#include "nsView.h" +#include "nsViewManager.h" +#include "nsIWidget.h" +#include "nsRange.h" +#include "nsPresContext.h" +#include "nsError.h" +#include "nsIPrincipal.h" +#include "nsContainerFrame.h" +#include "nsStyleUtil.h" +#include "ReferrerInfo.h" + +#include "mozilla/PresState.h" +#include "nsILayoutHistoryState.h" + +#include "nsHTMLParts.h" +#include "nsContentUtils.h" +#include "mozilla/dom/DirectionalityUtils.h" +#include "mozilla/dom/DocumentOrShadowRoot.h" +#include "nsString.h" +#include "nsGkAtoms.h" +#include "nsDOMCSSDeclaration.h" +#include "nsITextControlFrame.h" +#include "nsIFormControl.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "nsFocusManager.h" + +#include "nsDOMStringMap.h" +#include "nsDOMString.h" + +#include "nsLayoutUtils.h" +#include "mozilla/dom/DocumentInlines.h" +#include "HTMLFieldSetElement.h" +#include "nsTextNode.h" +#include "HTMLBRElement.h" +#include "nsDOMMutationObserver.h" +#include "mozilla/Preferences.h" +#include "mozilla/dom/FromParser.h" +#include "mozilla/dom/Link.h" +#include "mozilla/dom/ScriptLoader.h" + +#include "nsDOMTokenList.h" +#include "nsThreadUtils.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/MouseEventBinding.h" +#include "mozilla/dom/ToggleEvent.h" +#include "mozilla/dom/TouchEvent.h" +#include "mozilla/dom/InputEvent.h" +#include "mozilla/dom/InvokeEvent.h" +#include "mozilla/ErrorResult.h" +#include "nsHTMLDocument.h" +#include "nsGlobalWindowInner.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "imgIContainer.h" +#include "nsComputedDOMStyle.h" +#include "mozilla/dom/HTMLDialogElement.h" +#include "mozilla/dom/HTMLLabelElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/CustomElementRegistry.h" +#include "mozilla/dom/ElementBinding.h" +#include "mozilla/dom/ElementInternals.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static const uint8_t NS_INPUTMODE_NONE = 1; +static const uint8_t NS_INPUTMODE_TEXT = 2; +static const uint8_t NS_INPUTMODE_TEL = 3; +static const uint8_t NS_INPUTMODE_URL = 4; +static const uint8_t NS_INPUTMODE_EMAIL = 5; +static const uint8_t NS_INPUTMODE_NUMERIC = 6; +static const uint8_t NS_INPUTMODE_DECIMAL = 7; +static const uint8_t NS_INPUTMODE_SEARCH = 8; + +static const nsAttrValue::EnumTable kInputmodeTable[] = { + {"none", NS_INPUTMODE_NONE}, + {"text", NS_INPUTMODE_TEXT}, + {"tel", NS_INPUTMODE_TEL}, + {"url", NS_INPUTMODE_URL}, + {"email", NS_INPUTMODE_EMAIL}, + {"numeric", NS_INPUTMODE_NUMERIC}, + {"decimal", NS_INPUTMODE_DECIMAL}, + {"search", NS_INPUTMODE_SEARCH}, + {nullptr, 0}}; + +static const uint8_t NS_ENTERKEYHINT_ENTER = 1; +static const uint8_t NS_ENTERKEYHINT_DONE = 2; +static const uint8_t NS_ENTERKEYHINT_GO = 3; +static const uint8_t NS_ENTERKEYHINT_NEXT = 4; +static const uint8_t NS_ENTERKEYHINT_PREVIOUS = 5; +static const uint8_t NS_ENTERKEYHINT_SEARCH = 6; +static const uint8_t NS_ENTERKEYHINT_SEND = 7; + +static const nsAttrValue::EnumTable kEnterKeyHintTable[] = { + {"enter", NS_ENTERKEYHINT_ENTER}, + {"done", NS_ENTERKEYHINT_DONE}, + {"go", NS_ENTERKEYHINT_GO}, + {"next", NS_ENTERKEYHINT_NEXT}, + {"previous", NS_ENTERKEYHINT_PREVIOUS}, + {"search", NS_ENTERKEYHINT_SEARCH}, + {"send", NS_ENTERKEYHINT_SEND}, + {nullptr, 0}}; + +static const uint8_t NS_AUTOCAPITALIZE_NONE = 1; +static const uint8_t NS_AUTOCAPITALIZE_SENTENCES = 2; +static const uint8_t NS_AUTOCAPITALIZE_WORDS = 3; +static const uint8_t NS_AUTOCAPITALIZE_CHARACTERS = 4; + +static const nsAttrValue::EnumTable kAutocapitalizeTable[] = { + {"none", NS_AUTOCAPITALIZE_NONE}, + {"sentences", NS_AUTOCAPITALIZE_SENTENCES}, + {"words", NS_AUTOCAPITALIZE_WORDS}, + {"characters", NS_AUTOCAPITALIZE_CHARACTERS}, + {"off", NS_AUTOCAPITALIZE_NONE}, + {"on", NS_AUTOCAPITALIZE_SENTENCES}, + {"", 0}, + {nullptr, 0}}; + +static const nsAttrValue::EnumTable* kDefaultAutocapitalize = + &kAutocapitalizeTable[1]; + +nsresult nsGenericHTMLElement::CopyInnerTo(Element* aDst) { + MOZ_ASSERT(!aDst->GetUncomposedDoc(), + "Should not CopyInnerTo an Element in a document"); + + auto reparse = aDst->OwnerDoc() == OwnerDoc() ? ReparseAttributes::No + : ReparseAttributes::Yes; + nsresult rv = Element::CopyInnerTo(aDst, reparse); + NS_ENSURE_SUCCESS(rv, rv); + + // cloning a node must retain its internal nonce slot + nsString* nonce = static_cast<nsString*>(GetProperty(nsGkAtoms::nonce)); + if (nonce) { + static_cast<nsGenericHTMLElement*>(aDst)->SetNonce(*nonce); + } + return NS_OK; +} + +static const nsAttrValue::EnumTable kDirTable[] = { + {"ltr", Directionality::Ltr}, + {"rtl", Directionality::Rtl}, + {"auto", Directionality::Auto}, + {nullptr, 0}, +}; + +namespace { +// See <https://html.spec.whatwg.org/#the-popover-attribute>. +enum class PopoverAttributeKeyword : uint8_t { Auto, EmptyString, Manual }; + +static const char* kPopoverAttributeValueAuto = "auto"; +static const char* kPopoverAttributeValueEmptyString = ""; +static const char* kPopoverAttributeValueManual = "manual"; + +static const nsAttrValue::EnumTable kPopoverTable[] = { + {kPopoverAttributeValueAuto, PopoverAttributeKeyword::Auto}, + {kPopoverAttributeValueEmptyString, PopoverAttributeKeyword::EmptyString}, + {kPopoverAttributeValueManual, PopoverAttributeKeyword::Manual}, + {nullptr, 0}}; + +// See <https://html.spec.whatwg.org/#the-popover-attribute>. +static const nsAttrValue::EnumTable* kPopoverTableInvalidValueDefault = + &kPopoverTable[2]; +} // namespace + +void nsGenericHTMLElement::GetFetchPriority(nsAString& aFetchPriority) const { + // <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>. + GetEnumAttr(nsGkAtoms::fetchpriority, kFetchPriorityAttributeValueAuto, + aFetchPriority); +} + +/* static */ +FetchPriority nsGenericHTMLElement::ToFetchPriority(const nsAString& aValue) { + nsAttrValue attrValue; + ParseFetchPriority(aValue, attrValue); + MOZ_ASSERT(attrValue.Type() == nsAttrValue::eEnum); + return FetchPriority(attrValue.GetEnumValue()); +} + +namespace { +// <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>. +static const nsAttrValue::EnumTable kFetchPriorityEnumTable[] = { + {kFetchPriorityAttributeValueHigh, FetchPriority::High}, + {kFetchPriorityAttributeValueLow, FetchPriority::Low}, + {kFetchPriorityAttributeValueAuto, FetchPriority::Auto}, + {nullptr, 0}}; + +// <https://html.spec.whatwg.org/multipage/urls-and-fetching.html#fetch-priority-attributes>. +static const nsAttrValue::EnumTable* + kFetchPriorityEnumTableInvalidValueDefault = &kFetchPriorityEnumTable[2]; +} // namespace + +FetchPriority nsGenericHTMLElement::GetFetchPriority() const { + const nsAttrValue* fetchpriorityAttribute = + GetParsedAttr(nsGkAtoms::fetchpriority); + if (fetchpriorityAttribute) { + MOZ_ASSERT(fetchpriorityAttribute->Type() == nsAttrValue::eEnum); + return FetchPriority(fetchpriorityAttribute->GetEnumValue()); + } + + return FetchPriority::Auto; +} + +/* static */ +void nsGenericHTMLElement::ParseFetchPriority(const nsAString& aValue, + nsAttrValue& aResult) { + aResult.ParseEnumValue(aValue, kFetchPriorityEnumTable, + false /* aCaseSensitive */, + kFetchPriorityEnumTableInvalidValueDefault); +} + +void nsGenericHTMLElement::AddToNameTable(nsAtom* aName) { + MOZ_ASSERT(HasName(), "Node doesn't have name?"); + Document* doc = GetUncomposedDoc(); + if (doc && !IsInNativeAnonymousSubtree()) { + doc->AddToNameTable(this, aName); + } +} + +void nsGenericHTMLElement::RemoveFromNameTable() { + if (HasName() && CanHaveName(NodeInfo()->NameAtom())) { + if (Document* doc = GetUncomposedDoc()) { + doc->RemoveFromNameTable(this, + GetParsedAttr(nsGkAtoms::name)->GetAtomValue()); + } + } +} + +void nsGenericHTMLElement::GetAccessKeyLabel(nsString& aLabel) { + nsAutoString suffix; + GetAccessKey(suffix); + if (!suffix.IsEmpty()) { + EventStateManager::GetAccessKeyLabelPrefix(this, aLabel); + aLabel.Append(suffix); + } +} + +static bool IsOffsetParent(nsIFrame* aFrame) { + LayoutFrameType frameType = aFrame->Type(); + + if (frameType == LayoutFrameType::TableCell || + frameType == LayoutFrameType::TableWrapper) { + // Per the IDL for Element, only td, th, and table are acceptable + // offsetParents apart from body or positioned elements; we need to check + // the content type as well as the frame type so we ignore anonymous tables + // created by an element with display: table-cell with no actual table + nsIContent* content = aFrame->GetContent(); + + return content->IsAnyOfHTMLElements(nsGkAtoms::table, nsGkAtoms::td, + nsGkAtoms::th); + } + return false; +} + +struct OffsetResult { + Element* mParent = nullptr; + CSSIntRect mRect; +}; + +static OffsetResult GetUnretargetedOffsetsFor(const Element& aElement) { + nsIFrame* frame = aElement.GetPrimaryFrame(); + if (!frame) { + return {}; + } + + nsIFrame* styleFrame = nsLayoutUtils::GetStyleFrame(frame); + + nsIFrame* parent = frame->GetParent(); + nsPoint origin(0, 0); + + nsIContent* offsetParent = nullptr; + Element* docElement = aElement.GetComposedDoc()->GetRootElement(); + nsIContent* content = frame->GetContent(); + + if (content && + (content->IsHTMLElement(nsGkAtoms::body) || content == docElement)) { + parent = frame; + } else { + const bool isPositioned = styleFrame->IsAbsPosContainingBlock(); + const bool isAbsolutelyPositioned = frame->IsAbsolutelyPositioned(); + origin += frame->GetPositionIgnoringScrolling(); + + for (; parent; parent = parent->GetParent()) { + content = parent->GetContent(); + + // Stop at the first ancestor that is positioned. + if (parent->IsAbsPosContainingBlock()) { + offsetParent = content; + break; + } + + // Add the parent's origin to our own to get to the + // right coordinate system. + const bool isOffsetParent = !isPositioned && IsOffsetParent(parent); + if (!isOffsetParent) { + origin += parent->GetPositionIgnoringScrolling(); + } + + if (content) { + // If we've hit the document element, break here. + if (content == docElement) { + break; + } + + // Break if the ancestor frame type makes it suitable as offset parent + // and this element is *not* positioned or if we found the body element. + if (isOffsetParent || content->IsHTMLElement(nsGkAtoms::body)) { + offsetParent = content; + break; + } + } + } + + if (isAbsolutelyPositioned && !offsetParent) { + // If this element is absolutely positioned, but we don't have + // an offset parent it means this element is an absolutely + // positioned child that's not nested inside another positioned + // element, in this case the element's frame's parent is the + // frame for the HTML element so we fail to find the body in the + // parent chain. We want the offset parent in this case to be + // the body, so we just get the body element from the document. + // + // We use GetBodyElement() here, not GetBody(), because we don't want to + // end up with framesets here. + offsetParent = aElement.GetComposedDoc()->GetBodyElement(); + } + } + + // Make the position relative to the padding edge. + if (parent) { + const nsStyleBorder* border = parent->StyleBorder(); + origin.x -= border->GetComputedBorderWidth(eSideLeft); + origin.y -= border->GetComputedBorderWidth(eSideTop); + } + + // Get the union of all rectangles in this and continuation frames. + // It doesn't really matter what we use as aRelativeTo here, since + // we only care about the size. We just have to use something non-null. + nsRect rcFrame = nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame); + rcFrame.MoveTo(origin); + return {Element::FromNodeOrNull(offsetParent), + CSSIntRect::FromAppUnitsRounded(rcFrame)}; +} + +static bool ShouldBeRetargeted(const Element& aReferenceElement, + const Element& aElementToMaybeRetarget) { + ShadowRoot* shadow = aElementToMaybeRetarget.GetContainingShadow(); + if (!shadow) { + return false; + } + for (ShadowRoot* scope = aReferenceElement.GetContainingShadow(); scope; + scope = scope->Host()->GetContainingShadow()) { + if (scope == shadow) { + return false; + } + } + + return true; +} + +Element* nsGenericHTMLElement::GetOffsetRect(CSSIntRect& aRect) { + aRect = CSSIntRect(); + + if (!GetPrimaryFrame(FlushType::Layout)) { + return nullptr; + } + + OffsetResult thisResult = GetUnretargetedOffsetsFor(*this); + aRect = thisResult.mRect; + + Element* parent = thisResult.mParent; + while (parent && ShouldBeRetargeted(*this, *parent)) { + OffsetResult result = GetUnretargetedOffsetsFor(*parent); + aRect += result.mRect.TopLeft(); + parent = result.mParent; + } + + return parent; +} + +bool nsGenericHTMLElement::Spellcheck() { + // Has the state has been explicitly set? + nsIContent* node; + for (node = this; node; node = node->GetParent()) { + if (node->IsHTMLElement()) { + static Element::AttrValuesArray strings[] = {nsGkAtoms::_true, + nsGkAtoms::_false, nullptr}; + switch (node->AsElement()->FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::spellcheck, strings, eCaseMatters)) { + case 0: // spellcheck = "true" + return true; + case 1: // spellcheck = "false" + return false; + } + } + } + + // contenteditable/designMode are spellchecked by default + if (IsEditable()) { + return true; + } + + // Is this a chrome element? + if (nsContentUtils::IsChromeDoc(OwnerDoc())) { + return false; // Not spellchecked by default + } + + // Anything else that's not a form control is not spellchecked by default + nsCOMPtr<nsIFormControl> formControl = do_QueryObject(this); + if (!formControl) { + return false; // Not spellchecked by default + } + + // Is this a multiline plaintext input? + auto controlType = formControl->ControlType(); + if (controlType == FormControlType::Textarea) { + return true; // Spellchecked by default + } + + // Is this anything other than an input text? + // Other inputs are not spellchecked. + if (controlType != FormControlType::InputText) { + return false; // Not spellchecked by default + } + + // Does the user want input text spellchecked by default? + // NOTE: Do not reflect a pref value of 0 back to the DOM getter. + // The web page should not know if the user has disabled spellchecking. + // We'll catch this in the editor itself. + int32_t spellcheckLevel = Preferences::GetInt("layout.spellcheckDefault", 1); + return spellcheckLevel == 2; // "Spellcheck multi- and single-line" +} + +bool nsGenericHTMLElement::InNavQuirksMode(Document* aDoc) { + return aDoc && aDoc->GetCompatibilityMode() == eCompatibility_NavQuirks; +} + +void nsGenericHTMLElement::UpdateEditableState(bool aNotify) { + // XXX Should we do this only when in a document? + ContentEditableTristate value = GetContentEditableValue(); + if (value != eInherit) { + SetEditableFlag(!!value); + UpdateReadOnlyState(aNotify); + return; + } + nsStyledElement::UpdateEditableState(aNotify); +} + +nsresult nsGenericHTMLElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElementBase::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInComposedDoc()) { + RegUnRegAccessKey(true); + } + + if (IsInUncomposedDoc()) { + if (HasName() && CanHaveName(NodeInfo()->NameAtom())) { + aContext.OwnerDoc().AddToNameTable( + this, GetParsedAttr(nsGkAtoms::name)->GetAtomValue()); + } + } + + if (HasFlag(NODE_IS_EDITABLE) && GetContentEditableValue() == eTrue && + IsInComposedDoc()) { + aContext.OwnerDoc().ChangeContentEditableCount(this, +1); + } + + // Hide any nonce from the DOM, but keep the internal value of the + // nonce by copying and resetting the internal nonce value. + if (HasFlag(NODE_HAS_NONCE_AND_HEADER_CSP) && IsInComposedDoc() && + OwnerDoc()->GetBrowsingContext()) { + nsContentUtils::AddScriptRunner(NS_NewRunnableFunction( + "nsGenericHTMLElement::ResetNonce::Runnable", + [self = RefPtr<nsGenericHTMLElement>(this)]() { + nsAutoString nonce; + self->GetNonce(nonce); + self->SetAttr(kNameSpaceID_None, nsGkAtoms::nonce, u""_ns, true); + self->SetNonce(nonce); + })); + } + + // We need to consider a labels element is moved to another subtree + // with different root, it needs to update labels list and its root + // as well. + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (slots && slots->mLabelsList) { + slots->mLabelsList->MaybeResetRoot(SubtreeRoot()); + } + + return rv; +} + +void nsGenericHTMLElement::UnbindFromTree(bool aNullParent) { + if (IsInComposedDoc()) { + // https://html.spec.whatwg.org/#dom-trees:hide-popover-algorithm + // If removedNode's popover attribute is not in the no popover state, then + // run the hide popover algorithm given removedNode, false, false, and + // false. + if (GetPopoverData()) { + HidePopoverWithoutRunningScript(); + } + RegUnRegAccessKey(false); + } + + RemoveFromNameTable(); + + if (GetContentEditableValue() == eTrue) { + if (Document* doc = GetComposedDoc()) { + doc->ChangeContentEditableCount(this, -1); + } + } + + nsStyledElement::UnbindFromTree(aNullParent); + + // Invalidate .labels list. It will be repopulated when used the next time. + nsExtendedDOMSlots* slots = GetExistingExtendedDOMSlots(); + if (slots && slots->mLabelsList) { + slots->mLabelsList->MaybeResetRoot(SubtreeRoot()); + } +} + +HTMLFormElement* nsGenericHTMLElement::FindAncestorForm( + HTMLFormElement* aCurrentForm) { + NS_ASSERTION(!HasAttr(nsGkAtoms::form) || IsHTMLElement(nsGkAtoms::img), + "FindAncestorForm should not be called if @form is set!"); + if (IsInNativeAnonymousSubtree()) { + return nullptr; + } + + nsIContent* content = this; + while (content) { + // If the current ancestor is a form, return it as our form + if (content->IsHTMLElement(nsGkAtoms::form)) { +#ifdef DEBUG + if (!nsContentUtils::IsInSameAnonymousTree(this, content)) { + // It's possible that we started unbinding at |content| or + // some ancestor of it, and |content| and |this| used to all be + // anonymous. Check for this the hard way. + for (nsIContent* child = this; child != content; + child = child->GetParent()) { + NS_ASSERTION(child->ComputeIndexInParentContent().isSome(), + "Walked too far?"); + } + } +#endif + return static_cast<HTMLFormElement*>(content); + } + + nsIContent* prevContent = content; + content = prevContent->GetParent(); + + if (!content && aCurrentForm) { + // We got to the root of the subtree we're in, and we're being removed + // from the DOM (the only time we get into this method with a non-null + // aCurrentForm). Check whether aCurrentForm is in the same subtree. If + // it is, we want to return aCurrentForm, since this case means that + // we're one of those inputs-in-a-table that have a hacked mForm pointer + // and a subtree containing both us and the form got removed from the + // DOM. + if (aCurrentForm->IsInclusiveDescendantOf(prevContent)) { + return aCurrentForm; + } + } + } + + return nullptr; +} + +bool nsGenericHTMLElement::CheckHandleEventForAnchorsPreconditions( + EventChainVisitor& aVisitor) { + MOZ_ASSERT(nsCOMPtr<Link>(do_QueryObject(this)), + "should be called only when |this| implements |Link|"); + // When disconnected, only <a> should navigate away per + // https://html.spec.whatwg.org/#cannot-navigate + return IsInComposedDoc() || IsHTMLElement(nsGkAtoms::a); +} + +void nsGenericHTMLElement::GetEventTargetParentForAnchors( + EventChainPreVisitor& aVisitor) { + nsGenericHTMLElementBase::GetEventTargetParent(aVisitor); + + if (!CheckHandleEventForAnchorsPreconditions(aVisitor)) { + return; + } + + GetEventTargetParentForLinks(aVisitor); +} + +nsresult nsGenericHTMLElement::PostHandleEventForAnchors( + EventChainPostVisitor& aVisitor) { + if (!CheckHandleEventForAnchorsPreconditions(aVisitor)) { + return NS_OK; + } + + return PostHandleEventForLinks(aVisitor); +} + +bool nsGenericHTMLElement::IsHTMLLink(nsIURI** aURI) const { + MOZ_ASSERT(aURI, "Must provide aURI out param"); + + *aURI = GetHrefURIForAnchors().take(); + // We promise out param is non-null if we return true, so base rv on it + return *aURI != nullptr; +} + +already_AddRefed<nsIURI> nsGenericHTMLElement::GetHrefURIForAnchors() const { + // This is used by the three Link implementations and + // nsHTMLStyleElement. + + // Get href= attribute (relative URI). + + // We use the nsAttrValue's copy of the URI string to avoid copying. + nsCOMPtr<nsIURI> uri; + GetURIAttr(nsGkAtoms::href, nullptr, getter_AddRefs(uri)); + + return uri.forget(); +} + +void nsGenericHTMLElement::BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::accesskey) { + // Have to unregister before clearing flag. See UnregAccessKey + RegUnRegAccessKey(false); + if (!aValue) { + UnsetFlags(NODE_HAS_ACCESSKEY); + } + } else if (aName == nsGkAtoms::name) { + // Have to do this before clearing flag. See RemoveFromNameTable + RemoveFromNameTable(); + if (!aValue || aValue->IsEmptyString()) { + ClearHasName(); + } + } else if (aName == nsGkAtoms::contenteditable) { + if (aValue) { + // Set this before the attribute is set so that any subclass code that + // runs before the attribute is set won't think we're missing a + // contenteditable attr when we actually have one. + SetMayHaveContentEditableAttr(); + } + } + if (!aValue && IsEventAttributeName(aName)) { + if (EventListenerManager* manager = GetExistingListenerManager()) { + manager->RemoveEventHandler(GetEventNameForAttr(aName)); + } + } + } + + return nsGenericHTMLElementBase::BeforeSetAttr(aNamespaceID, aName, aValue, + aNotify); +} + +namespace { +constexpr PopoverAttributeState ToPopoverAttributeState( + PopoverAttributeKeyword aPopoverAttributeKeyword) { + // See <https://html.spec.whatwg.org/#the-popover-attribute>. + switch (aPopoverAttributeKeyword) { + case PopoverAttributeKeyword::Auto: + return PopoverAttributeState::Auto; + case PopoverAttributeKeyword::EmptyString: + return PopoverAttributeState::Auto; + case PopoverAttributeKeyword::Manual: + return PopoverAttributeState::Manual; + default: { + MOZ_ASSERT_UNREACHABLE(); + return PopoverAttributeState::None; + } + } +} +} // namespace + +void nsGenericHTMLElement::AfterSetPopoverAttr() { + auto mapPopoverState = [](const nsAttrValue* value) -> PopoverAttributeState { + if (value) { + MOZ_ASSERT(value->Type() == nsAttrValue::eEnum); + const auto popoverAttributeKeyword = + static_cast<PopoverAttributeKeyword>(value->GetEnumValue()); + return ToPopoverAttributeState(popoverAttributeKeyword); + } + + // The missing value default is the no popover state, see + // <https://html.spec.whatwg.org/multipage/popover.html#attr-popover>. + return PopoverAttributeState::None; + }; + + PopoverAttributeState newState = + mapPopoverState(GetParsedAttr(nsGkAtoms::popover)); + + const PopoverAttributeState oldState = GetPopoverAttributeState(); + + if (newState != oldState) { + PopoverPseudoStateUpdate(false, true); + + if (IsPopoverOpen()) { + HidePopoverInternal(/* aFocusPreviousElement = */ true, + /* aFireEvents = */ true, IgnoreErrors()); + // Event handlers could have removed the popover attribute, or changed + // its value. + // https://github.com/whatwg/html/issues/9034 + newState = mapPopoverState(GetParsedAttr(nsGkAtoms::popover)); + } + + if (newState == PopoverAttributeState::None) { + ClearPopoverData(); + RemoveStates(ElementState::POPOVER_OPEN); + } else { + // TODO: what if `HidePopoverInternal` called `ShowPopup()`? + EnsurePopoverData().SetPopoverAttributeState(newState); + } + } +} + +void nsGenericHTMLElement::AfterSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (IsEventAttributeName(aName) && aValue) { + MOZ_ASSERT(aValue->Type() == nsAttrValue::eString, + "Expected string value for script body"); + SetEventHandler(GetEventNameForAttr(aName), aValue->GetStringValue()); + } else if (aNotify && aName == nsGkAtoms::spellcheck) { + SyncEditorsOnSubtree(this); + } else if (aName == nsGkAtoms::popover && + StaticPrefs::dom_element_popover_enabled()) { + nsContentUtils::AddScriptRunner( + NewRunnableMethod("nsGenericHTMLElement::AfterSetPopoverAttr", this, + &nsGenericHTMLElement::AfterSetPopoverAttr)); + } else if (aName == nsGkAtoms::popovertarget) { + ClearExplicitlySetAttrElement(nsGkAtoms::popovertarget); + } else if (aName == nsGkAtoms::dir) { + auto dir = Directionality::Ltr; + // A boolean tracking whether we need to recompute our directionality. + // This needs to happen after we update our internal "dir" attribute + // state but before we call SetDirectionalityOnDescendants. + bool recomputeDirectionality = false; + ElementState dirStates; + if (aValue && aValue->Type() == nsAttrValue::eEnum) { + SetHasValidDir(); + dirStates |= ElementState::HAS_DIR_ATTR; + auto dirValue = Directionality(aValue->GetEnumValue()); + if (dirValue == Directionality::Auto) { + dirStates |= ElementState::HAS_DIR_ATTR_LIKE_AUTO; + } else { + dir = dirValue; + SetDirectionality(dir, aNotify); + if (dirValue == Directionality::Ltr) { + dirStates |= ElementState::HAS_DIR_ATTR_LTR; + } else { + MOZ_ASSERT(dirValue == Directionality::Rtl); + dirStates |= ElementState::HAS_DIR_ATTR_RTL; + } + } + } else { + if (aValue) { + // We have a value, just not a valid one. + dirStates |= ElementState::HAS_DIR_ATTR; + } + ClearHasValidDir(); + if (NodeInfo()->Equals(nsGkAtoms::bdi)) { + dirStates |= ElementState::HAS_DIR_ATTR_LIKE_AUTO; + } else { + recomputeDirectionality = true; + } + } + // Now figure out what's changed about our dir states. + ElementState oldDirStates = State() & ElementState::DIR_ATTR_STATES; + ElementState changedStates = dirStates ^ oldDirStates; + if (!changedStates.IsEmpty()) { + ToggleStates(changedStates, aNotify); + } + if (recomputeDirectionality) { + dir = RecomputeDirectionality(this, aNotify); + } + SetDirectionalityOnDescendants(this, dir, aNotify); + } else if (aName == nsGkAtoms::contenteditable) { + int32_t editableCountDelta = 0; + if (aOldValue && (aOldValue->Equals(u"true"_ns, eIgnoreCase) || + aOldValue->Equals(u""_ns, eIgnoreCase))) { + editableCountDelta = -1; + } + if (aValue && (aValue->Equals(u"true"_ns, eIgnoreCase) || + aValue->Equals(u""_ns, eIgnoreCase))) { + ++editableCountDelta; + } + ChangeEditableState(editableCountDelta); + } else if (aName == nsGkAtoms::accesskey) { + if (aValue && !aValue->Equals(u""_ns, eIgnoreCase)) { + SetFlags(NODE_HAS_ACCESSKEY); + RegUnRegAccessKey(true); + } + } else if (aName == nsGkAtoms::inert) { + if (aValue) { + AddStates(ElementState::INERT); + } else { + RemoveStates(ElementState::INERT); + } + } else if (aName == nsGkAtoms::name) { + if (aValue && !aValue->Equals(u""_ns, eIgnoreCase)) { + // This may not be quite right because we can have subclass code run + // before here. But in practice subclasses don't care about this flag, + // and in particular selector matching does not care. Otherwise we'd + // want to handle it like we handle id attributes (in PreIdMaybeChange + // and PostIdMaybeChange). + SetHasName(); + if (CanHaveName(NodeInfo()->NameAtom())) { + AddToNameTable(aValue->GetAtomValue()); + } + } + } else if (aName == nsGkAtoms::inputmode || + aName == nsGkAtoms::enterkeyhint) { + if (nsFocusManager::GetFocusedElementStatic() == this) { + if (const nsPresContext* presContext = + GetPresContext(eForComposedDoc)) { + IMEContentObserver* observer = + IMEStateManager::GetActiveContentObserver(); + if (observer && observer->IsObserving(*presContext, this)) { + if (RefPtr<EditorBase> editorBase = GetEditorWithoutCreation()) { + IMEState newState; + editorBase->GetPreferredIMEState(&newState); + OwningNonNull<nsGenericHTMLElement> kungFuDeathGrip(*this); + IMEStateManager::UpdateIMEState( + newState, kungFuDeathGrip, *editorBase, + {IMEStateManager::UpdateIMEStateOption::ForceUpdate, + IMEStateManager::UpdateIMEStateOption:: + DontCommitComposition}); + } + } + } + } + } + + // The nonce will be copied over to an internal slot and cleared from the + // Element within BindToTree to avoid CSS Selector nonce exfiltration if + // the CSP list contains a header-delivered CSP. + if (nsGkAtoms::nonce == aName) { + if (aValue) { + SetNonce(aValue->GetStringValue()); + if (OwnerDoc()->GetHasCSPDeliveredThroughHeader()) { + SetFlags(NODE_HAS_NONCE_AND_HEADER_CSP); + } + } else { + RemoveNonce(); + } + } + } + + return nsGenericHTMLElementBase::AfterSetAttr( + aNamespaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +EventListenerManager* nsGenericHTMLElement::GetEventListenerManagerForAttr( + nsAtom* aAttrName, bool* aDefer) { + // Attributes on the body and frameset tags get set on the global object + if ((mNodeInfo->Equals(nsGkAtoms::body) || + mNodeInfo->Equals(nsGkAtoms::frameset)) && + // We only forward some event attributes from body/frameset to window + (0 +#define EVENT(name_, id_, type_, struct_) /* nothing */ +#define FORWARDED_EVENT(name_, id_, type_, struct_) \ + || nsGkAtoms::on##name_ == aAttrName +#define WINDOW_EVENT FORWARDED_EVENT +#include "mozilla/EventNameList.h" // IWYU pragma: keep +#undef WINDOW_EVENT +#undef FORWARDED_EVENT +#undef EVENT + )) { + nsPIDOMWindowInner* win; + + // If we have a document, and it has a window, add the event + // listener on the window (the inner window). If not, proceed as + // normal. + // XXXbz sXBL/XBL2 issue: should we instead use GetComposedDoc() here, + // override BindToTree for those classes and munge event listeners there? + Document* document = OwnerDoc(); + + *aDefer = false; + if ((win = document->GetInnerWindow())) { + nsCOMPtr<EventTarget> piTarget(do_QueryInterface(win)); + + return piTarget->GetOrCreateListenerManager(); + } + + return nullptr; + } + + return nsGenericHTMLElementBase::GetEventListenerManagerForAttr(aAttrName, + aDefer); +} + +#define EVENT(name_, id_, type_, struct_) /* nothing; handled by nsINode */ +#define FORWARDED_EVENT(name_, id_, type_, struct_) \ + EventHandlerNonNull* nsGenericHTMLElement::GetOn##name_() { \ + if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \ + /* XXXbz note to self: add tests for this! */ \ + if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + return globalWin->GetOn##name_(); \ + } \ + return nullptr; \ + } \ + \ + return nsINode::GetOn##name_(); \ + } \ + void nsGenericHTMLElement::SetOn##name_(EventHandlerNonNull* handler) { \ + if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \ + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \ + if (!win) { \ + return; \ + } \ + \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + return globalWin->SetOn##name_(handler); \ + } \ + \ + return nsINode::SetOn##name_(handler); \ + } +#define ERROR_EVENT(name_, id_, type_, struct_) \ + already_AddRefed<EventHandlerNonNull> nsGenericHTMLElement::GetOn##name_() { \ + if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \ + /* XXXbz note to self: add tests for this! */ \ + if (nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow()) { \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + OnErrorEventHandlerNonNull* errorHandler = globalWin->GetOn##name_(); \ + if (errorHandler) { \ + RefPtr<EventHandlerNonNull> handler = \ + new EventHandlerNonNull(errorHandler); \ + return handler.forget(); \ + } \ + } \ + return nullptr; \ + } \ + \ + RefPtr<EventHandlerNonNull> handler = nsINode::GetOn##name_(); \ + return handler.forget(); \ + } \ + void nsGenericHTMLElement::SetOn##name_(EventHandlerNonNull* handler) { \ + if (IsAnyOfHTMLElements(nsGkAtoms::body, nsGkAtoms::frameset)) { \ + nsPIDOMWindowInner* win = OwnerDoc()->GetInnerWindow(); \ + if (!win) { \ + return; \ + } \ + \ + nsGlobalWindowInner* globalWin = nsGlobalWindowInner::Cast(win); \ + RefPtr<OnErrorEventHandlerNonNull> errorHandler; \ + if (handler) { \ + errorHandler = new OnErrorEventHandlerNonNull(handler); \ + } \ + return globalWin->SetOn##name_(errorHandler); \ + } \ + \ + return nsINode::SetOn##name_(handler); \ + } +#include "mozilla/EventNameList.h" // IWYU pragma: keep +#undef ERROR_EVENT +#undef FORWARDED_EVENT +#undef EVENT + +void nsGenericHTMLElement::GetBaseTarget(nsAString& aBaseTarget) const { + OwnerDoc()->GetBaseTarget(aBaseTarget); +} + +//---------------------------------------------------------------------- + +bool nsGenericHTMLElement::ParseAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (aAttribute == nsGkAtoms::dir) { + return aResult.ParseEnumValue(aValue, kDirTable, false); + } + + if (aAttribute == nsGkAtoms::popover && + StaticPrefs::dom_element_popover_enabled()) { + return aResult.ParseEnumValue(aValue, kPopoverTable, false, + kPopoverTableInvalidValueDefault); + } + + if (aAttribute == nsGkAtoms::tabindex) { + return aResult.ParseIntValue(aValue); + } + + if (aAttribute == nsGkAtoms::referrerpolicy) { + return ParseReferrerAttribute(aValue, aResult); + } + + if (aAttribute == nsGkAtoms::name) { + // Store name as an atom. name="" means that the element has no name, + // not that it has an empty string as the name. + if (aValue.IsEmpty()) { + return false; + } + aResult.ParseAtom(aValue); + return true; + } + + if (aAttribute == nsGkAtoms::contenteditable || + aAttribute == nsGkAtoms::translate) { + aResult.ParseAtom(aValue); + return true; + } + + if (aAttribute == nsGkAtoms::rel) { + aResult.ParseAtomArray(aValue); + return true; + } + + if (aAttribute == nsGkAtoms::inputmode) { + return aResult.ParseEnumValue(aValue, kInputmodeTable, false); + } + + if (aAttribute == nsGkAtoms::enterkeyhint) { + return aResult.ParseEnumValue(aValue, kEnterKeyHintTable, false); + } + + if (aAttribute == nsGkAtoms::autocapitalize) { + return aResult.ParseEnumValue(aValue, kAutocapitalizeTable, false); + } + } + + return nsGenericHTMLElementBase::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +bool nsGenericHTMLElement::ParseBackgroundAttribute(int32_t aNamespaceID, + nsAtom* aAttribute, + const nsAString& aValue, + nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None && + aAttribute == nsGkAtoms::background && !aValue.IsEmpty()) { + // Resolve url to an absolute url + Document* doc = OwnerDoc(); + nsCOMPtr<nsIURI> uri; + nsresult rv = nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(uri), aValue, doc, GetBaseURI()); + if (NS_FAILED(rv)) { + return false; + } + aResult.SetTo(uri, &aValue); + return true; + } + + return false; +} + +bool nsGenericHTMLElement::IsAttributeMapped(const nsAtom* aAttribute) const { + static const MappedAttributeEntry* const map[] = {sCommonAttributeMap}; + + return FindAttributeDependence(aAttribute, map); +} + +nsMapRuleToAttributesFunc nsGenericHTMLElement::GetAttributeMappingFunction() + const { + return &MapCommonAttributesInto; +} + +nsIFormControlFrame* nsGenericHTMLElement::GetFormControlFrame( + bool aFlushFrames) { + auto flushType = aFlushFrames ? FlushType::Frames : FlushType::None; + nsIFrame* frame = GetPrimaryFrame(flushType); + if (!frame) { + return nullptr; + } + + if (nsIFormControlFrame* f = do_QueryFrame(frame)) { + return f; + } + + // If we have generated content, the primary frame will be a wrapper frame... + // Our real frame will be in its child list. + // + // FIXME(emilio): I don't think that's true... See bug 155957 for test-cases + // though, we should figure out whether this is still needed. + for (nsIFrame* kid : frame->PrincipalChildList()) { + if (nsIFormControlFrame* f = do_QueryFrame(kid)) { + return f; + } + } + + return nullptr; +} + +static const nsAttrValue::EnumTable kDivAlignTable[] = { + {"left", StyleTextAlign::MozLeft}, + {"right", StyleTextAlign::MozRight}, + {"center", StyleTextAlign::MozCenter}, + {"middle", StyleTextAlign::MozCenter}, + {"justify", StyleTextAlign::Justify}, + {nullptr, 0}}; + +static const nsAttrValue::EnumTable kFrameborderTable[] = { + {"yes", FrameBorderProperty::Yes}, + {"no", FrameBorderProperty::No}, + {"1", FrameBorderProperty::One}, + {"0", FrameBorderProperty::Zero}, + {nullptr, 0}}; + +// TODO(emilio): Nobody uses the parsed attribute here. +static const nsAttrValue::EnumTable kScrollingTable[] = { + {"yes", ScrollingAttribute::Yes}, + {"no", ScrollingAttribute::No}, + {"on", ScrollingAttribute::On}, + {"off", ScrollingAttribute::Off}, + {"scroll", ScrollingAttribute::Scroll}, + {"noscroll", ScrollingAttribute::Noscroll}, + {"auto", ScrollingAttribute::Auto}, + {nullptr, 0}}; + +static const nsAttrValue::EnumTable kTableVAlignTable[] = { + {"top", StyleVerticalAlignKeyword::Top}, + {"middle", StyleVerticalAlignKeyword::Middle}, + {"bottom", StyleVerticalAlignKeyword::Bottom}, + {"baseline", StyleVerticalAlignKeyword::Baseline}, + {nullptr, 0}}; + +bool nsGenericHTMLElement::ParseAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + static const nsAttrValue::EnumTable kAlignTable[] = { + {"left", StyleTextAlign::Left}, + {"right", StyleTextAlign::Right}, + + {"top", StyleVerticalAlignKeyword::Top}, + {"middle", StyleVerticalAlignKeyword::MozMiddleWithBaseline}, + + // Intentionally not bottom. + {"bottom", StyleVerticalAlignKeyword::Baseline}, + + {"center", StyleVerticalAlignKeyword::MozMiddleWithBaseline}, + {"baseline", StyleVerticalAlignKeyword::Baseline}, + + {"texttop", StyleVerticalAlignKeyword::TextTop}, + {"absmiddle", StyleVerticalAlignKeyword::Middle}, + {"abscenter", StyleVerticalAlignKeyword::Middle}, + {"absbottom", StyleVerticalAlignKeyword::Bottom}, + {nullptr, 0}}; + + static_assert(uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::Top) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::MozMiddleWithBaseline) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::Baseline) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::TextTop) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::Middle) && + uint8_t(StyleTextAlign::Left) != + uint8_t(StyleVerticalAlignKeyword::Bottom)); + + static_assert(uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::Top) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::MozMiddleWithBaseline) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::Baseline) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::TextTop) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::Middle) && + uint8_t(StyleTextAlign::Right) != + uint8_t(StyleVerticalAlignKeyword::Bottom)); + + return aResult.ParseEnumValue(aString, kAlignTable, false); +} + +//---------------------------------------- + +static const nsAttrValue::EnumTable kTableHAlignTable[] = { + {"left", StyleTextAlign::Left}, {"right", StyleTextAlign::Right}, + {"center", StyleTextAlign::Center}, {"char", StyleTextAlign::Char}, + {"justify", StyleTextAlign::Justify}, {nullptr, 0}}; + +bool nsGenericHTMLElement::ParseTableHAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kTableHAlignTable, false); +} + +//---------------------------------------- + +// This table is used for td, th, tr, col, thead, tbody and tfoot. +static const nsAttrValue::EnumTable kTableCellHAlignTable[] = { + {"left", StyleTextAlign::MozLeft}, + {"right", StyleTextAlign::MozRight}, + {"center", StyleTextAlign::MozCenter}, + {"char", StyleTextAlign::Char}, + {"justify", StyleTextAlign::Justify}, + {"middle", StyleTextAlign::MozCenter}, + {"absmiddle", StyleTextAlign::Center}, + {nullptr, 0}}; + +bool nsGenericHTMLElement::ParseTableCellHAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kTableCellHAlignTable, false); +} + +//---------------------------------------- + +bool nsGenericHTMLElement::ParseTableVAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kTableVAlignTable, false); +} + +bool nsGenericHTMLElement::ParseDivAlignValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kDivAlignTable, false); +} + +bool nsGenericHTMLElement::ParseImageAttribute(nsAtom* aAttribute, + const nsAString& aString, + nsAttrValue& aResult) { + if (aAttribute == nsGkAtoms::width || aAttribute == nsGkAtoms::height || + aAttribute == nsGkAtoms::hspace || aAttribute == nsGkAtoms::vspace) { + return aResult.ParseHTMLDimension(aString); + } + if (aAttribute == nsGkAtoms::border) { + return aResult.ParseNonNegativeIntValue(aString); + } + return false; +} + +bool nsGenericHTMLElement::ParseReferrerAttribute(const nsAString& aString, + nsAttrValue& aResult) { + using mozilla::dom::ReferrerInfo; + static const nsAttrValue::EnumTable kReferrerPolicyTable[] = { + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::No_referrer), + static_cast<int16_t>(ReferrerPolicy::No_referrer)}, + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Origin), + static_cast<int16_t>(ReferrerPolicy::Origin)}, + {ReferrerInfo::ReferrerPolicyToString( + ReferrerPolicy::Origin_when_cross_origin), + static_cast<int16_t>(ReferrerPolicy::Origin_when_cross_origin)}, + {ReferrerInfo::ReferrerPolicyToString( + ReferrerPolicy::No_referrer_when_downgrade), + static_cast<int16_t>(ReferrerPolicy::No_referrer_when_downgrade)}, + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Unsafe_url), + static_cast<int16_t>(ReferrerPolicy::Unsafe_url)}, + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Strict_origin), + static_cast<int16_t>(ReferrerPolicy::Strict_origin)}, + {ReferrerInfo::ReferrerPolicyToString(ReferrerPolicy::Same_origin), + static_cast<int16_t>(ReferrerPolicy::Same_origin)}, + {ReferrerInfo::ReferrerPolicyToString( + ReferrerPolicy::Strict_origin_when_cross_origin), + static_cast<int16_t>(ReferrerPolicy::Strict_origin_when_cross_origin)}, + {nullptr, ReferrerPolicy::_empty}}; + return aResult.ParseEnumValue(aString, kReferrerPolicyTable, false); +} + +bool nsGenericHTMLElement::ParseFrameborderValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kFrameborderTable, false); +} + +bool nsGenericHTMLElement::ParseScrollingValue(const nsAString& aString, + nsAttrValue& aResult) { + return aResult.ParseEnumValue(aString, kScrollingTable, false); +} + +static inline void MapLangAttributeInto(MappedDeclarationsBuilder& aBuilder) { + const nsAttrValue* langValue = aBuilder.GetAttr(nsGkAtoms::lang); + if (!langValue) { + return; + } + MOZ_ASSERT(langValue->Type() == nsAttrValue::eAtom); + aBuilder.SetIdentAtomValueIfUnset(eCSSProperty__x_lang, + langValue->GetAtomValue()); + if (!aBuilder.PropertyIsSet(eCSSProperty_text_emphasis_position)) { + const nsAtom* lang = langValue->GetAtomValue(); + if (nsStyleUtil::MatchesLanguagePrefix(lang, u"zh")) { + aBuilder.SetKeywordValue(eCSSProperty_text_emphasis_position, + StyleTextEmphasisPosition::UNDER._0); + } else if (nsStyleUtil::MatchesLanguagePrefix(lang, u"ja") || + nsStyleUtil::MatchesLanguagePrefix(lang, u"mn")) { + // This branch is currently no part of the spec. + // See bug 1040668 comment 69 and comment 75. + aBuilder.SetKeywordValue(eCSSProperty_text_emphasis_position, + StyleTextEmphasisPosition::OVER._0); + } + } +} + +/** + * Handle attributes common to all html elements + */ +void nsGenericHTMLElement::MapCommonAttributesIntoExceptHidden( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty__moz_user_modify)) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::contenteditable); + if (value) { + if (value->Equals(nsGkAtoms::_empty, eCaseMatters) || + value->Equals(nsGkAtoms::_true, eIgnoreCase)) { + aBuilder.SetKeywordValue(eCSSProperty__moz_user_modify, + StyleUserModify::ReadWrite); + } else if (value->Equals(nsGkAtoms::_false, eIgnoreCase)) { + aBuilder.SetKeywordValue(eCSSProperty__moz_user_modify, + StyleUserModify::ReadOnly); + } + } + } + + MapLangAttributeInto(aBuilder); +} + +void nsGenericHTMLElement::MapCommonAttributesInto( + MappedDeclarationsBuilder& aBuilder) { + MapCommonAttributesIntoExceptHidden(aBuilder); + if (!aBuilder.PropertyIsSet(eCSSProperty_display)) { + if (aBuilder.GetAttr(nsGkAtoms::hidden)) { + aBuilder.SetKeywordValue(eCSSProperty_display, StyleDisplay::None._0); + } + } +} + +/* static */ +const nsGenericHTMLElement::MappedAttributeEntry + nsGenericHTMLElement::sCommonAttributeMap[] = {{nsGkAtoms::contenteditable}, + {nsGkAtoms::lang}, + {nsGkAtoms::hidden}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sImageMarginSizeAttributeMap[] = {{nsGkAtoms::width}, + {nsGkAtoms::height}, + {nsGkAtoms::hspace}, + {nsGkAtoms::vspace}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sImageAlignAttributeMap[] = {{nsGkAtoms::align}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sDivAlignAttributeMap[] = {{nsGkAtoms::align}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sImageBorderAttributeMap[] = {{nsGkAtoms::border}, + {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sBackgroundAttributeMap[] = { + {nsGkAtoms::background}, {nsGkAtoms::bgcolor}, {nullptr}}; + +/* static */ +const Element::MappedAttributeEntry + nsGenericHTMLElement::sBackgroundColorAttributeMap[] = { + {nsGkAtoms::bgcolor}, {nullptr}}; + +void nsGenericHTMLElement::MapImageAlignAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align); + if (value && value->Type() == nsAttrValue::eEnum) { + int32_t align = value->GetEnumValue(); + if (!aBuilder.PropertyIsSet(eCSSProperty_float)) { + if (align == uint8_t(StyleTextAlign::Left)) { + aBuilder.SetKeywordValue(eCSSProperty_float, StyleFloat::Left); + } else if (align == uint8_t(StyleTextAlign::Right)) { + aBuilder.SetKeywordValue(eCSSProperty_float, StyleFloat::Right); + } + } + if (!aBuilder.PropertyIsSet(eCSSProperty_vertical_align)) { + switch (align) { + case uint8_t(StyleTextAlign::Left): + case uint8_t(StyleTextAlign::Right): + break; + default: + aBuilder.SetKeywordValue(eCSSProperty_vertical_align, align); + break; + } + } + } +} + +void nsGenericHTMLElement::MapDivAlignAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_text_align)) { + // align: enum + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::align); + if (value && value->Type() == nsAttrValue::eEnum) + aBuilder.SetKeywordValue(eCSSProperty_text_align, value->GetEnumValue()); + } +} + +void nsGenericHTMLElement::MapVAlignAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_vertical_align)) { + // align: enum + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::valign); + if (value && value->Type() == nsAttrValue::eEnum) + aBuilder.SetKeywordValue(eCSSProperty_vertical_align, + value->GetEnumValue()); + } +} + +void nsGenericHTMLElement::MapDimensionAttributeInto( + MappedDeclarationsBuilder& aBuilder, nsCSSPropertyID aProp, + const nsAttrValue& aValue) { + MOZ_ASSERT(!aBuilder.PropertyIsSet(aProp), + "Why mapping the same property twice?"); + if (aValue.Type() == nsAttrValue::eInteger) { + return aBuilder.SetPixelValue(aProp, aValue.GetIntegerValue()); + } + if (aValue.Type() == nsAttrValue::ePercent) { + return aBuilder.SetPercentValue(aProp, aValue.GetPercentValue()); + } + if (aValue.Type() == nsAttrValue::eDoubleValue) { + return aBuilder.SetPixelValue(aProp, aValue.GetDoubleValue()); + } +} + +void nsGenericHTMLElement::MapImageMarginAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + // hspace: value + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::hspace)) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_left, *value); + MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_right, *value); + } + + // vspace: value + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::vspace)) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_top, *value); + MapDimensionAttributeInto(aBuilder, eCSSProperty_margin_bottom, *value); + } +} + +void nsGenericHTMLElement::MapWidthAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::width)) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_width, *value); + } +} + +void nsGenericHTMLElement::MapHeightAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::height)) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_height, *value); + } +} + +void nsGenericHTMLElement::DoMapAspectRatio( + const nsAttrValue& aWidth, const nsAttrValue& aHeight, + MappedDeclarationsBuilder& aBuilder) { + Maybe<double> w; + if (aWidth.Type() == nsAttrValue::eInteger) { + w.emplace(aWidth.GetIntegerValue()); + } else if (aWidth.Type() == nsAttrValue::eDoubleValue) { + w.emplace(aWidth.GetDoubleValue()); + } + + Maybe<double> h; + if (aHeight.Type() == nsAttrValue::eInteger) { + h.emplace(aHeight.GetIntegerValue()); + } else if (aHeight.Type() == nsAttrValue::eDoubleValue) { + h.emplace(aHeight.GetDoubleValue()); + } + + if (w && h) { + aBuilder.SetAspectRatio(*w, *h); + } +} + +void nsGenericHTMLElement::MapImageSizeAttributesInto( + MappedDeclarationsBuilder& aBuilder, MapAspectRatio aMapAspectRatio) { + auto* width = aBuilder.GetAttr(nsGkAtoms::width); + auto* height = aBuilder.GetAttr(nsGkAtoms::height); + if (width) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_width, *width); + } + if (height) { + MapDimensionAttributeInto(aBuilder, eCSSProperty_height, *height); + } + if (aMapAspectRatio == MapAspectRatio::Yes && width && height) { + DoMapAspectRatio(*width, *height, aBuilder); + } +} + +void nsGenericHTMLElement::MapAspectRatioInto( + MappedDeclarationsBuilder& aBuilder) { + auto* width = aBuilder.GetAttr(nsGkAtoms::width); + auto* height = aBuilder.GetAttr(nsGkAtoms::height); + if (width && height) { + DoMapAspectRatio(*width, *height, aBuilder); + } +} + +void nsGenericHTMLElement::MapImageBorderAttributeInto( + MappedDeclarationsBuilder& aBuilder) { + // border: pixels + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::border); + if (!value) return; + + nscoord val = 0; + if (value->Type() == nsAttrValue::eInteger) val = value->GetIntegerValue(); + + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_top_width, (float)val); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_right_width, (float)val); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_bottom_width, (float)val); + aBuilder.SetPixelValueIfUnset(eCSSProperty_border_left_width, (float)val); + + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_top_style, + StyleBorderStyle::Solid); + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_right_style, + StyleBorderStyle::Solid); + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_bottom_style, + StyleBorderStyle::Solid); + aBuilder.SetKeywordValueIfUnset(eCSSProperty_border_left_style, + StyleBorderStyle::Solid); + + aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_top_color); + aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_right_color); + aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_bottom_color); + aBuilder.SetCurrentColorIfUnset(eCSSProperty_border_left_color); +} + +void nsGenericHTMLElement::MapBackgroundInto( + MappedDeclarationsBuilder& aBuilder) { + if (!aBuilder.PropertyIsSet(eCSSProperty_background_image)) { + // background + if (const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::background)) { + aBuilder.SetBackgroundImage(*value); + } + } +} + +void nsGenericHTMLElement::MapBGColorInto(MappedDeclarationsBuilder& aBuilder) { + if (aBuilder.PropertyIsSet(eCSSProperty_background_color)) { + return; + } + const nsAttrValue* value = aBuilder.GetAttr(nsGkAtoms::bgcolor); + nscolor color; + if (value && value->GetColorValue(color)) { + aBuilder.SetColorValue(eCSSProperty_background_color, color); + } +} + +void nsGenericHTMLElement::MapBackgroundAttributesInto( + MappedDeclarationsBuilder& aBuilder) { + MapBackgroundInto(aBuilder); + MapBGColorInto(aBuilder); +} + +//---------------------------------------------------------------------- + +int32_t nsGenericHTMLElement::GetIntAttr(nsAtom* aAttr, + int32_t aDefault) const { + const nsAttrValue* attrVal = mAttrs.GetAttr(aAttr); + if (attrVal && attrVal->Type() == nsAttrValue::eInteger) { + return attrVal->GetIntegerValue(); + } + return aDefault; +} + +nsresult nsGenericHTMLElement::SetIntAttr(nsAtom* aAttr, int32_t aValue) { + nsAutoString value; + value.AppendInt(aValue); + + return SetAttr(kNameSpaceID_None, aAttr, value, true); +} + +uint32_t nsGenericHTMLElement::GetUnsignedIntAttr(nsAtom* aAttr, + uint32_t aDefault) const { + const nsAttrValue* attrVal = mAttrs.GetAttr(aAttr); + if (!attrVal || attrVal->Type() != nsAttrValue::eInteger) { + return aDefault; + } + + return attrVal->GetIntegerValue(); +} + +uint32_t nsGenericHTMLElement::GetDimensionAttrAsUnsignedInt( + nsAtom* aAttr, uint32_t aDefault) const { + const nsAttrValue* attrVal = mAttrs.GetAttr(aAttr); + if (!attrVal) { + return aDefault; + } + + if (attrVal->Type() == nsAttrValue::eInteger) { + return attrVal->GetIntegerValue(); + } + + if (attrVal->Type() == nsAttrValue::ePercent) { + // This is a nasty hack. When we parsed the value, we stored it as an + // ePercent, not eInteger, because there was a '%' after it in the string. + // But the spec says to basically re-parse the string as an integer. + // Luckily, we can just return the value we have stored. But + // GetPercentValue() divides it by 100, so we need to multiply it back. + return uint32_t(attrVal->GetPercentValue() * 100.0f); + } + + if (attrVal->Type() == nsAttrValue::eDoubleValue) { + return uint32_t(attrVal->GetDoubleValue()); + } + + // Unfortunately, the set of values that are valid dimensions is not a + // superset of values that are valid unsigned ints. In particular "+100" is + // not a valid dimension, but should parse as the unsigned int "100". So if + // we got here and we don't have a valid dimension value, just try re-parsing + // the string we have as an integer. + nsAutoString val; + attrVal->ToString(val); + nsContentUtils::ParseHTMLIntegerResultFlags result; + int32_t parsedInt = nsContentUtils::ParseHTMLInteger(val, &result); + if ((result & nsContentUtils::eParseHTMLInteger_Error) || parsedInt < 0) { + return aDefault; + } + + return parsedInt; +} + +void nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, + nsAString& aResult) const { + nsCOMPtr<nsIURI> uri; + bool hadAttr = GetURIAttr(aAttr, aBaseAttr, getter_AddRefs(uri)); + if (!hadAttr) { + aResult.Truncate(); + return; + } + + if (!uri) { + // Just return the attr value + GetAttr(aAttr, aResult); + return; + } + + nsAutoCString spec; + uri->GetSpec(spec); + CopyUTF8toUTF16(spec, aResult); +} + +bool nsGenericHTMLElement::GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, + nsIURI** aURI) const { + *aURI = nullptr; + + const nsAttrValue* attr = mAttrs.GetAttr(aAttr); + if (!attr) { + return false; + } + + nsCOMPtr<nsIURI> baseURI = GetBaseURI(); + + if (aBaseAttr) { + nsAutoString baseAttrValue; + if (GetAttr(aBaseAttr, baseAttrValue)) { + nsCOMPtr<nsIURI> baseAttrURI; + nsresult rv = nsContentUtils::NewURIWithDocumentCharset( + getter_AddRefs(baseAttrURI), baseAttrValue, OwnerDoc(), baseURI); + if (NS_FAILED(rv)) { + return true; + } + baseURI.swap(baseAttrURI); + } + } + + // Don't care about return value. If it fails, we still want to + // return true, and *aURI will be null. + nsContentUtils::NewURIWithDocumentCharset(aURI, attr->GetStringValue(), + OwnerDoc(), baseURI); + return true; +} + +bool nsGenericHTMLElement::IsLabelable() const { + return IsAnyOfHTMLElements(nsGkAtoms::progress, nsGkAtoms::meter); +} + +/* static */ +bool nsGenericHTMLElement::MatchLabelsElement(Element* aElement, + int32_t aNamespaceID, + nsAtom* aAtom, void* aData) { + HTMLLabelElement* element = HTMLLabelElement::FromNode(aElement); + return element && element->GetControl() == aData; +} + +already_AddRefed<nsINodeList> nsGenericHTMLElement::Labels() { + MOZ_ASSERT(IsLabelable(), + "Labels() only allow labelable elements to use it."); + nsExtendedDOMSlots* slots = ExtendedDOMSlots(); + + if (!slots->mLabelsList) { + slots->mLabelsList = + new nsLabelsNodeList(SubtreeRoot(), MatchLabelsElement, nullptr, this); + } + + RefPtr<nsLabelsNodeList> labels = slots->mLabelsList; + return labels.forget(); +} + +// static +bool nsGenericHTMLElement::LegacyTouchAPIEnabled(JSContext* aCx, + JSObject* aGlobal) { + return TouchEvent::LegacyAPIEnabled(aCx, aGlobal); +} + +bool nsGenericHTMLElement::IsFormControlDefaultFocusable( + bool aWithMouse) const { + if (!aWithMouse) { + return true; + } + switch (StaticPrefs::accessibility_mouse_focuses_formcontrol()) { + case 0: + return false; + case 1: + return true; + default: + return !IsInChromeDocument(); + } +} + +//---------------------------------------------------------------------- + +nsGenericHTMLFormElement::nsGenericHTMLFormElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElement(std::move(aNodeInfo)) { + // We should add the ElementState::ENABLED bit here as needed, but that + // depends on our type, which is not initialized yet. So we have to do this + // in subclasses. Same for a couple other bits. +} + +void nsGenericHTMLFormElement::ClearForm(bool aRemoveFromForm, + bool aUnbindOrDelete) { + MOZ_ASSERT(IsFormAssociatedElement()); + + HTMLFormElement* form = GetFormInternal(); + NS_ASSERTION((form != nullptr) == HasFlag(ADDED_TO_FORM), + "Form control should have had flag set correctly"); + + if (!form) { + return; + } + + if (aRemoveFromForm) { + nsAutoString nameVal, idVal; + GetAttr(nsGkAtoms::name, nameVal); + GetAttr(nsGkAtoms::id, idVal); + + form->RemoveElement(this, true); + + if (!nameVal.IsEmpty()) { + form->RemoveElementFromTable(this, nameVal); + } + + if (!idVal.IsEmpty()) { + form->RemoveElementFromTable(this, idVal); + } + } + + UnsetFlags(ADDED_TO_FORM); + SetFormInternal(nullptr, false); + AfterClearForm(aUnbindOrDelete); +} + +nsresult nsGenericHTMLFormElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsFormAssociatedElement()) { + // If @form is set, the element *has* to be in a composed document, + // otherwise it wouldn't be possible to find an element with the + // corresponding id. If @form isn't set, the element *has* to have a parent, + // otherwise it wouldn't be possible to find a form ancestor. We should not + // call UpdateFormOwner if none of these conditions are fulfilled. + if (HasAttr(nsGkAtoms::form) ? IsInComposedDoc() : aParent.IsContent()) { + UpdateFormOwner(true, nullptr); + } + } + + // Set parent fieldset which should be used for the disabled state. + UpdateFieldSet(false); + return NS_OK; +} + +void nsGenericHTMLFormElement::UnbindFromTree(bool aNullParent) { + // Save state before doing anything else. + SaveState(); + + if (IsFormAssociatedElement()) { + if (HTMLFormElement* form = GetFormInternal()) { + // Might need to unset form + if (aNullParent) { + // No more parent means no more form + ClearForm(true, true); + } else { + // Recheck whether we should still have an form. + if (HasAttr(nsGkAtoms::form) || !FindAncestorForm(form)) { + ClearForm(true, true); + } else { + UnsetFlags(MAYBE_ORPHAN_FORM_ELEMENT); + } + } + } + + // We have to remove the form id observer if there was one. + // We will re-add one later if needed (during bind to tree). + if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None, + nsGkAtoms::form)) { + RemoveFormIdObserver(); + } + } + + nsGenericHTMLElement::UnbindFromTree(aNullParent); + + // The element might not have a fieldset anymore. + UpdateFieldSet(false); +} + +void nsGenericHTMLFormElement::BeforeSetAttr(int32_t aNameSpaceID, + nsAtom* aName, + const nsAttrValue* aValue, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && IsFormAssociatedElement()) { + nsAutoString tmp; + HTMLFormElement* form = GetFormInternal(); + + // remove the control from the hashtable as needed + + if (form && (aName == nsGkAtoms::name || aName == nsGkAtoms::id)) { + GetAttr(aName, tmp); + + if (!tmp.IsEmpty()) { + form->RemoveElementFromTable(this, tmp); + } + } + + if (form && aName == nsGkAtoms::type) { + GetAttr(nsGkAtoms::name, tmp); + + if (!tmp.IsEmpty()) { + form->RemoveElementFromTable(this, tmp); + } + + GetAttr(nsGkAtoms::id, tmp); + + if (!tmp.IsEmpty()) { + form->RemoveElementFromTable(this, tmp); + } + + form->RemoveElement(this, false); + } + + if (aName == nsGkAtoms::form) { + // If @form isn't set or set to the empty string, there were no observer + // so we don't have to remove it. + if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None, + nsGkAtoms::form)) { + // The current form id observer is no longer needed. + // A new one may be added in AfterSetAttr. + RemoveFormIdObserver(); + } + } + } + + return nsGenericHTMLElement::BeforeSetAttr(aNameSpaceID, aName, aValue, + aNotify); +} + +void nsGenericHTMLFormElement::AfterSetAttr( + int32_t aNameSpaceID, nsAtom* aName, const nsAttrValue* aValue, + const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aNameSpaceID == kNameSpaceID_None && IsFormAssociatedElement()) { + HTMLFormElement* form = GetFormInternal(); + + // add the control to the hashtable as needed + if (form && (aName == nsGkAtoms::name || aName == nsGkAtoms::id) && + aValue && !aValue->IsEmptyString()) { + MOZ_ASSERT(aValue->Type() == nsAttrValue::eAtom, + "Expected atom value for name/id"); + form->AddElementToTable(this, + nsDependentAtomString(aValue->GetAtomValue())); + } + + if (form && aName == nsGkAtoms::type) { + nsAutoString tmp; + + GetAttr(nsGkAtoms::name, tmp); + + if (!tmp.IsEmpty()) { + form->AddElementToTable(this, tmp); + } + + GetAttr(nsGkAtoms::id, tmp); + + if (!tmp.IsEmpty()) { + form->AddElementToTable(this, tmp); + } + + form->AddElement(this, false, aNotify); + } + + if (aName == nsGkAtoms::form) { + // We need a new form id observer. + DocumentOrShadowRoot* docOrShadow = + GetUncomposedDocOrConnectedShadowRoot(); + if (docOrShadow) { + Element* formIdElement = nullptr; + if (aValue && !aValue->IsEmptyString()) { + formIdElement = AddFormIdObserver(); + } + + // Because we have a new @form value (or no more @form), we have to + // update our form owner. + UpdateFormOwner(false, formIdElement); + } + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void nsGenericHTMLFormElement::ForgetFieldSet(nsIContent* aFieldset) { + MOZ_DIAGNOSTIC_ASSERT(IsFormAssociatedElement()); + if (GetFieldSetInternal() == aFieldset) { + SetFieldSetInternal(nullptr); + } +} + +Element* nsGenericHTMLFormElement::AddFormIdObserver() { + MOZ_ASSERT(IsFormAssociatedElement()); + + nsAutoString formId; + DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); + GetAttr(nsGkAtoms::form, formId); + NS_ASSERTION(!formId.IsEmpty(), + "@form value should not be the empty string!"); + RefPtr<nsAtom> atom = NS_Atomize(formId); + + return docOrShadow->AddIDTargetObserver(atom, FormIdUpdated, this, false); +} + +void nsGenericHTMLFormElement::RemoveFormIdObserver() { + MOZ_ASSERT(IsFormAssociatedElement()); + + DocumentOrShadowRoot* docOrShadow = GetUncomposedDocOrConnectedShadowRoot(); + if (!docOrShadow) { + return; + } + + nsAutoString formId; + GetAttr(nsGkAtoms::form, formId); + NS_ASSERTION(!formId.IsEmpty(), + "@form value should not be the empty string!"); + RefPtr<nsAtom> atom = NS_Atomize(formId); + + docOrShadow->RemoveIDTargetObserver(atom, FormIdUpdated, this, false); +} + +/* static */ +bool nsGenericHTMLFormElement::FormIdUpdated(Element* aOldElement, + Element* aNewElement, + void* aData) { + nsGenericHTMLFormElement* element = + static_cast<nsGenericHTMLFormElement*>(aData); + + NS_ASSERTION(element->IsHTMLElement(), "aData should be an HTML element"); + + element->UpdateFormOwner(false, aNewElement); + + return true; +} + +bool nsGenericHTMLFormElement::IsElementDisabledForEvents(WidgetEvent* aEvent, + nsIFrame* aFrame) { + MOZ_ASSERT(aEvent); + + // Allow dispatch of CustomEvent and untrusted Events. + if (!aEvent->IsTrusted()) { + return false; + } + + switch (aEvent->mMessage) { + case eAnimationStart: + case eAnimationEnd: + case eAnimationIteration: + case eAnimationCancel: + case eFormChange: + case eMouseMove: + case eMouseOver: + case eMouseOut: + case eMouseEnter: + case eMouseLeave: + case ePointerMove: + case ePointerOver: + case ePointerOut: + case ePointerEnter: + case ePointerLeave: + case eTransitionCancel: + case eTransitionEnd: + case eTransitionRun: + case eTransitionStart: + case eWheel: + case eLegacyMouseLineOrPageScroll: + case eLegacyMousePixelScroll: + return false; + case eFocus: + case eBlur: + case eFocusIn: + case eFocusOut: + case eKeyPress: + case eKeyUp: + case eKeyDown: + if (StaticPrefs::dom_forms_always_allow_key_and_focus_events_enabled()) { + return false; + } + [[fallthrough]]; + case ePointerDown: + case ePointerUp: + case ePointerCancel: + case ePointerGotCapture: + case ePointerLostCapture: + if (StaticPrefs::dom_forms_always_allow_pointer_events_enabled()) { + return false; + } + [[fallthrough]]; + default: + break; + } + + if (aEvent->mSpecifiedEventType == nsGkAtoms::oninput) { + return false; + } + + // FIXME(emilio): This poking at the style of the frame is slightly bogus + // unless we flush before every event, which we don't really want to do. + if (aFrame && aFrame->StyleUI()->UserInput() == StyleUserInput::None) { + return true; + } + + return IsDisabled(); +} + +void nsGenericHTMLFormElement::UpdateFormOwner(bool aBindToTree, + Element* aFormIdElement) { + MOZ_ASSERT(IsFormAssociatedElement()); + MOZ_ASSERT(!aBindToTree || !aFormIdElement, + "aFormIdElement shouldn't be set if aBindToTree is true!"); + + HTMLFormElement* form = GetFormInternal(); + if (!aBindToTree) { + ClearForm(true, false); + form = nullptr; + } + + HTMLFormElement* oldForm = form; + if (!form) { + // If @form is set, we have to use that to find the form. + nsAutoString formId; + if (GetAttr(nsGkAtoms::form, formId)) { + if (!formId.IsEmpty()) { + Element* element = nullptr; + + if (aBindToTree) { + element = AddFormIdObserver(); + } else { + element = aFormIdElement; + } + + NS_ASSERTION(!IsInComposedDoc() || + element == GetUncomposedDocOrConnectedShadowRoot() + ->GetElementById(formId), + "element should be equals to the current element " + "associated with the id in @form!"); + + if (element && element->IsHTMLElement(nsGkAtoms::form) && + nsContentUtils::IsInSameAnonymousTree(this, element)) { + form = static_cast<HTMLFormElement*>(element); + SetFormInternal(form, aBindToTree); + } + } + } else { + // We now have a parent, so we may have picked up an ancestor form. Search + // for it. Note that if form is already set we don't want to do this, + // because that means someone (probably the content sink) has already set + // it to the right value. Also note that even if being bound here didn't + // change our parent, we still need to search, since our parent chain + // probably changed _somewhere_. + form = FindAncestorForm(); + SetFormInternal(form, aBindToTree); + } + } + + if (form && !HasFlag(ADDED_TO_FORM)) { + // Now we need to add ourselves to the form + nsAutoString nameVal, idVal; + GetAttr(nsGkAtoms::name, nameVal); + GetAttr(nsGkAtoms::id, idVal); + + SetFlags(ADDED_TO_FORM); + + // Notify only if we just found this form. + form->AddElement(this, true, oldForm == nullptr); + + if (!nameVal.IsEmpty()) { + form->AddElementToTable(this, nameVal); + } + + if (!idVal.IsEmpty()) { + form->AddElementToTable(this, idVal); + } + } +} + +void nsGenericHTMLFormElement::UpdateFieldSet(bool aNotify) { + if (IsInNativeAnonymousSubtree() || !IsFormAssociatedElement()) { + MOZ_ASSERT_IF(IsFormAssociatedElement(), !GetFieldSetInternal()); + return; + } + + nsIContent* parent = nullptr; + nsIContent* prev = nullptr; + HTMLFieldSetElement* fieldset = GetFieldSetInternal(); + + for (parent = GetParent(); parent; + prev = parent, parent = parent->GetParent()) { + HTMLFieldSetElement* parentFieldset = HTMLFieldSetElement::FromNode(parent); + if (parentFieldset && (!prev || parentFieldset->GetFirstLegend() != prev)) { + if (fieldset == parentFieldset) { + // We already have the right fieldset; + return; + } + + if (fieldset) { + fieldset->RemoveElement(this); + } + SetFieldSetInternal(parentFieldset); + parentFieldset->AddElement(this); + + // The disabled state may have changed + FieldSetDisabledChanged(aNotify); + return; + } + } + + // No fieldset found. + if (fieldset) { + fieldset->RemoveElement(this); + SetFieldSetInternal(nullptr); + // The disabled state may have changed + FieldSetDisabledChanged(aNotify); + } +} + +void nsGenericHTMLFormElement::UpdateDisabledState(bool aNotify) { + if (!CanBeDisabled()) { + return; + } + + HTMLFieldSetElement* fieldset = GetFieldSetInternal(); + const bool isDisabled = + HasAttr(nsGkAtoms::disabled) || (fieldset && fieldset->IsDisabled()); + + const ElementState disabledStates = + isDisabled ? ElementState::DISABLED : ElementState::ENABLED; + + ElementState oldDisabledStates = State() & ElementState::DISABLED_STATES; + ElementState changedStates = disabledStates ^ oldDisabledStates; + + if (!changedStates.IsEmpty()) { + ToggleStates(changedStates, aNotify); + if (DoesReadOnlyApply()) { + // :disabled influences :read-only / :read-write. + UpdateReadOnlyState(aNotify); + } + } +} + +bool nsGenericHTMLFormElement::IsReadOnlyInternal() const { + if (DoesReadOnlyApply()) { + return IsDisabled() || GetBoolAttr(nsGkAtoms::readonly); + } + return nsGenericHTMLElement::IsReadOnlyInternal(); +} + +void nsGenericHTMLFormElement::FieldSetDisabledChanged(bool aNotify) { + UpdateDisabledState(aNotify); +} + +void nsGenericHTMLFormElement::SaveSubtreeState() { + SaveState(); + + nsGenericHTMLElement::SaveSubtreeState(); +} + +//---------------------------------------------------------------------- + +void nsGenericHTMLElement::Click(CallerType aCallerType) { + if (HandlingClick()) { + return; + } + + // There are two notions of disabled. + // "disabled": + // https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fe-disabled + // "actually disabled": + // https://html.spec.whatwg.org/multipage/semantics-other.html#concept-element-disabled + // click() reads the former but IsDisabled() is for the latter. <fieldset> is + // included only in the latter, so we exclude it here. + // XXX(krosylight): What about <optgroup>? And should we add a separate method + // for this? + if (IsDisabled() && + !(mNodeInfo->Equals(nsGkAtoms::fieldset) && + StaticPrefs::dom_forms_fieldset_disable_only_descendants_enabled())) { + return; + } + + // Strong in case the event kills it + nsCOMPtr<Document> doc = GetComposedDoc(); + + RefPtr<nsPresContext> context; + if (doc) { + PresShell* presShell = doc->GetPresShell(); + if (!presShell) { + // We need the nsPresContext for dispatching the click event. In some + // rare cases we need to flush notifications to force creation of the + // nsPresContext here (for example when a script calls button.click() + // from script early during page load). We only flush the notifications + // if the PresShell hasn't been created yet, to limit the performance + // impact. + doc->FlushPendingNotifications(FlushType::EnsurePresShellInitAndFrames); + presShell = doc->GetPresShell(); + } + if (presShell) { + context = presShell->GetPresContext(); + } + } + + SetHandlingClick(); + + // Mark this event trusted if Click() is called from system code. + WidgetMouseEvent event(aCallerType == CallerType::System, eMouseClick, + nullptr, WidgetMouseEvent::eReal); + event.mFlags.mIsPositionless = true; + event.mInputSource = MouseEvent_Binding::MOZ_SOURCE_UNKNOWN; + + EventDispatcher::Dispatch(this, context, &event); + + ClearHandlingClick(); +} + +bool nsGenericHTMLElement::IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) { + MOZ_ASSERT(aIsFocusable); + MOZ_ASSERT(aTabIndex); + if (ShadowRoot* root = GetShadowRoot()) { + if (root->DelegatesFocus()) { + *aIsFocusable = false; + return true; + } + } + + if (!IsInComposedDoc() || IsInDesignMode()) { + // In designMode documents we only allow focusing the document. + *aTabIndex = -1; + *aIsFocusable = false; + return true; + } + + *aTabIndex = TabIndex(); + bool disabled = false; + bool disallowOverridingFocusability = true; + Maybe<int32_t> attrVal = GetTabIndexAttrValue(); + if (IsEditingHost()) { + // Editable roots should always be focusable. + disallowOverridingFocusability = true; + + // Ignore the disabled attribute in editable contentEditable/designMode + // roots. + if (attrVal.isNothing()) { + // The default value for tabindex should be 0 for editable + // contentEditable roots. + *aTabIndex = 0; + } + } else { + disallowOverridingFocusability = false; + + // Just check for disabled attribute on form controls + disabled = IsDisabled(); + if (disabled) { + *aTabIndex = -1; + } + } + + // If a tabindex is specified at all, or the default tabindex is 0, we're + // focusable. + *aIsFocusable = (*aTabIndex >= 0 || (!disabled && attrVal.isSome())); + return disallowOverridingFocusability; +} + +Result<bool, nsresult> nsGenericHTMLElement::PerformAccesskey( + bool aKeyCausesActivation, bool aIsTrustedEvent) { + RefPtr<nsPresContext> presContext = GetPresContext(eForComposedDoc); + if (!presContext) { + return Err(NS_ERROR_UNEXPECTED); + } + + // It's hard to say what HTML4 wants us to do in all cases. + bool focused = true; + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + fm->SetFocus(this, nsIFocusManager::FLAG_BYKEY); + + // Return true if the element became the current focus within its window. + nsPIDOMWindowOuter* window = OwnerDoc()->GetWindow(); + focused = window && window->GetFocusedElement() == this; + } + + if (aKeyCausesActivation) { + // Click on it if the users prefs indicate to do so. + AutoHandlingUserInputStatePusher userInputStatePusher(aIsTrustedEvent); + AutoPopupStatePusher popupStatePusher( + aIsTrustedEvent ? PopupBlocker::openAllowed : PopupBlocker::openAbused); + DispatchSimulatedClick(this, aIsTrustedEvent, presContext); + return focused; + } + + // If the accesskey won't cause the activation and the focus isn't changed, + // either. Return error so EventStateManager would try to find next element + // to handle the accesskey. + return focused ? Result<bool, nsresult>{focused} : Err(NS_ERROR_ABORT); +} + +void nsGenericHTMLElement::HandleKeyboardActivation( + EventChainPostVisitor& aVisitor) { + MOZ_ASSERT(aVisitor.mEvent->HasKeyEventMessage()); + MOZ_ASSERT(aVisitor.mEvent->IsTrusted()); + + // If focused element is different from this element, it may be editable. + // In that case, associated editor for the element should handle the keyboard + // instead. Therefore, if this is not the focused element, we should not + // handle the event here. Note that this element may be an editing host, + // i.e., focused and editable. In the case, keyboard events should be + // handled by the focused element instead of associated editor because + // Chrome handles the case so. For compatibility with Chrome, we follow them. + if (nsFocusManager::GetFocusedElementStatic() != this) { + return; + } + + const auto message = aVisitor.mEvent->mMessage; + const WidgetKeyboardEvent* keyEvent = aVisitor.mEvent->AsKeyboardEvent(); + if (nsEventStatus_eIgnore != aVisitor.mEventStatus) { + if (message == eKeyUp && keyEvent->mKeyCode == NS_VK_SPACE) { + // Unset the flag even if the event is default-prevented or something. + UnsetFlags(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD); + } + return; + } + + bool shouldActivate = false; + switch (message) { + case eKeyDown: + if (keyEvent->ShouldWorkAsSpaceKey()) { + SetFlags(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD); + } + return; + case eKeyPress: + shouldActivate = keyEvent->mKeyCode == NS_VK_RETURN; + if (keyEvent->ShouldWorkAsSpaceKey()) { + // Consume 'space' key to prevent scrolling the page down. + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; + } + break; + case eKeyUp: + shouldActivate = keyEvent->ShouldWorkAsSpaceKey() && + HasFlag(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD); + if (shouldActivate) { + UnsetFlags(HTML_ELEMENT_ACTIVE_FOR_KEYBOARD); + } + break; + default: + MOZ_ASSERT_UNREACHABLE("why didn't we bail out earlier?"); + break; + } + + if (!shouldActivate) { + return; + } + + RefPtr<nsPresContext> presContext = aVisitor.mPresContext; + DispatchSimulatedClick(this, aVisitor.mEvent->IsTrusted(), presContext); + aVisitor.mEventStatus = nsEventStatus_eConsumeNoDefault; +} + +nsresult nsGenericHTMLElement::DispatchSimulatedClick( + nsGenericHTMLElement* aElement, bool aIsTrusted, + nsPresContext* aPresContext) { + WidgetMouseEvent event(aIsTrusted, eMouseClick, nullptr, + WidgetMouseEvent::eReal); + event.mInputSource = MouseEvent_Binding::MOZ_SOURCE_KEYBOARD; + event.mFlags.mIsPositionless = true; + return EventDispatcher::Dispatch(aElement, aPresContext, &event); +} + +already_AddRefed<EditorBase> nsGenericHTMLElement::GetAssociatedEditor() { + // If contenteditable is ever implemented, it might need to do something + // different here? + + RefPtr<TextEditor> textEditor = GetTextEditorInternal(); + return textEditor.forget(); +} + +// static +void nsGenericHTMLElement::SyncEditorsOnSubtree(nsIContent* content) { + /* Sync this node */ + nsGenericHTMLElement* element = FromNode(content); + if (element) { + if (RefPtr<EditorBase> editorBase = element->GetAssociatedEditor()) { + editorBase->SyncRealTimeSpell(); + } + } + + /* Sync all children */ + for (nsIContent* child = content->GetFirstChild(); child; + child = child->GetNextSibling()) { + SyncEditorsOnSubtree(child); + } +} + +static void MakeContentDescendantsEditable(nsIContent* aContent) { + // If aContent is not an element, we just need to update its + // internal editable state and don't need to notify anyone about + // that. For elements, we need to send a ElementStateChanged + // notification. + if (!aContent->IsElement()) { + aContent->UpdateEditableState(false); + return; + } + + Element* element = aContent->AsElement(); + + element->UpdateEditableState(true); + + for (nsIContent* child = aContent->GetFirstChild(); child; + child = child->GetNextSibling()) { + if (!child->IsElement() || + !child->AsElement()->HasAttr(nsGkAtoms::contenteditable)) { + MakeContentDescendantsEditable(child); + } + } +} + +void nsGenericHTMLElement::ChangeEditableState(int32_t aChange) { + Document* document = GetComposedDoc(); + if (!document) { + return; + } + + Document::EditingState previousEditingState = Document::EditingState::eOff; + if (aChange != 0) { + document->ChangeContentEditableCount(this, aChange); + previousEditingState = document->GetEditingState(); + } + + // MakeContentDescendantsEditable is going to call ElementStateChanged for + // this element and all descendants if editable state has changed. + // We might as well wrap it all in one script blocker. + nsAutoScriptBlocker scriptBlocker; + MakeContentDescendantsEditable(this); + + // If the document already had contenteditable and JS adds new + // contenteditable, that might cause changing editing host to current editing + // host's ancestor. In such case, HTMLEditor needs to know that + // synchronously to update selection limitter. + // Additionally, elements in shadow DOM is not editable in the normal cases, + // but if its content has `contenteditable`, only in it can be ediable. + // So we don't need to notify HTMLEditor of this change only when we're not + // in shadow DOM and the composed document is in design mode. + if (IsInDesignMode() && !IsInShadowTree() && aChange > 0 && + previousEditingState == Document::EditingState::eContentEditable) { + if (HTMLEditor* htmlEditor = + nsContentUtils::GetHTMLEditor(document->GetPresContext())) { + htmlEditor->NotifyEditingHostMaybeChanged(); + } + } +} + +//---------------------------------------------------------------------- + +nsGenericHTMLFormControlElement::nsGenericHTMLFormControlElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, FormControlType aType) + : nsGenericHTMLFormElement(std::move(aNodeInfo)), + nsIFormControl(aType), + mForm(nullptr), + mFieldSet(nullptr) {} + +nsGenericHTMLFormControlElement::~nsGenericHTMLFormControlElement() { + if (mFieldSet) { + mFieldSet->RemoveElement(this); + } + + // Check that this element doesn't know anything about its form at this point. + NS_ASSERTION(!mForm, "mForm should be null at this point!"); +} + +NS_IMPL_ISUPPORTS_INHERITED(nsGenericHTMLFormControlElement, + nsGenericHTMLFormElement, nsIFormControl) + +nsINode* nsGenericHTMLFormControlElement::GetScopeChainParent() const { + return mForm ? mForm : nsGenericHTMLElement::GetScopeChainParent(); +} + +nsIContent::IMEState nsGenericHTMLFormControlElement::GetDesiredIMEState() { + TextEditor* textEditor = GetTextEditorInternal(); + if (!textEditor) { + return nsGenericHTMLFormElement::GetDesiredIMEState(); + } + IMEState state; + nsresult rv = textEditor->GetPreferredIMEState(&state); + if (NS_FAILED(rv)) { + return nsGenericHTMLFormElement::GetDesiredIMEState(); + } + return state; +} + +void nsGenericHTMLFormControlElement::GetAutocapitalize( + nsAString& aValue) const { + if (nsContentUtils::HasNonEmptyAttr(this, kNameSpaceID_None, + nsGkAtoms::autocapitalize)) { + nsGenericHTMLFormElement::GetAutocapitalize(aValue); + return; + } + + if (mForm && IsAutocapitalizeInheriting()) { + mForm->GetAutocapitalize(aValue); + } +} + +bool nsGenericHTMLFormControlElement::IsHTMLFocusable(bool aWithMouse, + bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLFormElement::IsHTMLFocusable(aWithMouse, aIsFocusable, + aTabIndex)) { + return true; + } + + *aIsFocusable = *aIsFocusable && IsFormControlDefaultFocusable(aWithMouse); + return false; +} + +void nsGenericHTMLFormControlElement::GetEventTargetParent( + EventChainPreVisitor& aVisitor) { + if (aVisitor.mEvent->IsTrusted() && (aVisitor.mEvent->mMessage == eFocus || + aVisitor.mEvent->mMessage == eBlur)) { + // We have to handle focus/blur event to change focus states in + // PreHandleEvent to prevent it breaks event target chain creation. + aVisitor.mWantsPreHandleEvent = true; + } + nsGenericHTMLFormElement::GetEventTargetParent(aVisitor); +} + +nsresult nsGenericHTMLFormControlElement::PreHandleEvent( + EventChainVisitor& aVisitor) { + if (aVisitor.mEvent->IsTrusted()) { + switch (aVisitor.mEvent->mMessage) { + case eFocus: { + // Check to see if focus has bubbled up from a form control's + // child textfield or button. If that's the case, don't focus + // this parent file control -- leave focus on the child. + nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); + if (formControlFrame && + aVisitor.mEvent->mOriginalTarget == static_cast<nsINode*>(this)) { + formControlFrame->SetFocus(true, true); + } + break; + } + case eBlur: { + nsIFormControlFrame* formControlFrame = GetFormControlFrame(true); + if (formControlFrame) { + formControlFrame->SetFocus(false, false); + } + break; + } + default: + break; + } + } + return nsGenericHTMLFormElement::PreHandleEvent(aVisitor); +} + +HTMLFieldSetElement* nsGenericHTMLFormControlElement::GetFieldSet() { + return GetFieldSetInternal(); +} + +void nsGenericHTMLFormControlElement::SetForm(HTMLFormElement* aForm) { + MOZ_ASSERT(aForm, "Don't pass null here"); + NS_ASSERTION(!mForm, + "We don't support switching from one non-null form to another."); + + SetFormInternal(aForm, false); +} + +void nsGenericHTMLFormControlElement::ClearForm(bool aRemoveFromForm, + bool aUnbindOrDelete) { + nsGenericHTMLFormElement::ClearForm(aRemoveFromForm, aUnbindOrDelete); +} + +bool nsGenericHTMLFormControlElement::IsLabelable() const { + auto type = ControlType(); + return (IsInputElement(type) && type != FormControlType::InputHidden) || + IsButtonElement(type) || type == FormControlType::Output || + type == FormControlType::Select || type == FormControlType::Textarea; +} + +bool nsGenericHTMLFormControlElement::CanBeDisabled() const { + auto type = ControlType(); + // It's easier to test the types that _cannot_ be disabled + return type != FormControlType::Object && type != FormControlType::Output; +} + +bool nsGenericHTMLFormControlElement::DoesReadOnlyApply() const { + auto type = ControlType(); + if (!IsInputElement(type) && type != FormControlType::Textarea) { + return false; + } + + switch (type) { + case FormControlType::InputHidden: + case FormControlType::InputButton: + case FormControlType::InputImage: + case FormControlType::InputReset: + case FormControlType::InputSubmit: + case FormControlType::InputRadio: + case FormControlType::InputFile: + case FormControlType::InputCheckbox: + case FormControlType::InputRange: + case FormControlType::InputColor: + return false; +#ifdef DEBUG + case FormControlType::Textarea: + case FormControlType::InputText: + case FormControlType::InputPassword: + case FormControlType::InputSearch: + case FormControlType::InputTel: + case FormControlType::InputEmail: + case FormControlType::InputUrl: + case FormControlType::InputNumber: + case FormControlType::InputDate: + case FormControlType::InputTime: + case FormControlType::InputMonth: + case FormControlType::InputWeek: + case FormControlType::InputDatetimeLocal: + return true; + default: + MOZ_ASSERT_UNREACHABLE("Unexpected input type in DoesReadOnlyApply()"); + return true; +#else // DEBUG + default: + return true; +#endif // DEBUG + } +} + +void nsGenericHTMLFormControlElement::SetFormInternal(HTMLFormElement* aForm, + bool aBindToTree) { + if (aForm) { + BeforeSetForm(aForm, aBindToTree); + } + + // keep a *weak* ref to the form here + mForm = aForm; +} + +HTMLFormElement* nsGenericHTMLFormControlElement::GetFormInternal() const { + return mForm; +} + +HTMLFieldSetElement* nsGenericHTMLFormControlElement::GetFieldSetInternal() + const { + return mFieldSet; +} + +void nsGenericHTMLFormControlElement::SetFieldSetInternal( + HTMLFieldSetElement* aFieldset) { + mFieldSet = aFieldset; +} + +void nsGenericHTMLFormControlElement::UpdateRequiredState(bool aIsRequired, + bool aNotify) { +#ifdef DEBUG + auto type = ControlType(); +#endif + MOZ_ASSERT(IsInputElement(type) || type == FormControlType::Select || + type == FormControlType::Textarea, + "This should be called only on types that @required applies"); + +#ifdef DEBUG + if (HTMLInputElement* input = HTMLInputElement::FromNode(this)) { + MOZ_ASSERT( + input->DoesRequiredApply(), + "This should be called only on input types that @required applies"); + } +#endif + + ElementState requiredStates; + if (aIsRequired) { + requiredStates |= ElementState::REQUIRED; + } else { + requiredStates |= ElementState::OPTIONAL_; + } + + ElementState oldRequiredStates = State() & ElementState::REQUIRED_STATES; + ElementState changedStates = requiredStates ^ oldRequiredStates; + + if (!changedStates.IsEmpty()) { + ToggleStates(changedStates, aNotify); + } +} + +bool nsGenericHTMLFormControlElement::IsAutocapitalizeInheriting() const { + auto type = ControlType(); + return IsInputElement(type) || IsButtonElement(type) || + type == FormControlType::Fieldset || type == FormControlType::Output || + type == FormControlType::Select || type == FormControlType::Textarea; +} + +nsresult nsGenericHTMLFormControlElement::SubmitDirnameDir( + FormData* aFormData) { + // Submit dirname=dir if element has non-empty dirname attribute + if (HasAttr(nsGkAtoms::dirname)) { + nsAutoString dirname; + GetAttr(nsGkAtoms::dirname, dirname); + if (!dirname.IsEmpty()) { + const Directionality dir = GetDirectionality(); + MOZ_ASSERT(dir == Directionality::Ltr || dir == Directionality::Rtl, + "The directionality of an element is either ltr or rtl"); + return aFormData->AddNameValuePair( + dirname, dir == Directionality::Ltr ? u"ltr"_ns : u"rtl"_ns); + } + } + return NS_OK; +} + +//---------------------------------------------------------------------- + +static const nsAttrValue::EnumTable kPopoverTargetActionTable[] = { + {"toggle", PopoverTargetAction::Toggle}, + {"show", PopoverTargetAction::Show}, + {"hide", PopoverTargetAction::Hide}, + {nullptr, 0}}; + +static const nsAttrValue::EnumTable* kPopoverTargetActionDefault = + &kPopoverTargetActionTable[0]; + +nsGenericHTMLFormControlElementWithState:: + nsGenericHTMLFormControlElementWithState( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser, FormControlType aType) + : nsGenericHTMLFormControlElement(std::move(aNodeInfo), aType), + mControlNumber(!!(aFromParser & FROM_PARSER_NETWORK) + ? OwnerDoc()->GetNextControlNumber() + : -1) { + mStateKey.SetIsVoid(true); +} + +bool nsGenericHTMLFormControlElementWithState::ParseAttribute( + int32_t aNamespaceID, nsAtom* aAttribute, const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, nsAttrValue& aResult) { + if (aNamespaceID == kNameSpaceID_None) { + if (StaticPrefs::dom_element_popover_enabled()) { + if (aAttribute == nsGkAtoms::popovertargetaction) { + return aResult.ParseEnumValue(aValue, kPopoverTargetActionTable, false, + kPopoverTargetActionDefault); + } + if (aAttribute == nsGkAtoms::popovertarget) { + aResult.ParseAtom(aValue); + return true; + } + } + + if (StaticPrefs::dom_element_invokers_enabled()) { + if (aAttribute == nsGkAtoms::invokeaction) { + aResult.ParseAtom(aValue); + return true; + } + if (aAttribute == nsGkAtoms::invoketarget) { + aResult.ParseAtom(aValue); + return true; + } + } + } + + return nsGenericHTMLFormControlElement::ParseAttribute( + aNamespaceID, aAttribute, aValue, aMaybeScriptedPrincipal, aResult); +} + +mozilla::dom::Element* +nsGenericHTMLFormControlElementWithState::GetPopoverTargetElement() const { + return GetAttrAssociatedElement(nsGkAtoms::popovertarget); +} + +void nsGenericHTMLFormControlElementWithState::SetPopoverTargetElement( + mozilla::dom::Element* aElement) { + ExplicitlySetAttrElement(nsGkAtoms::popovertarget, aElement); +} + +void nsGenericHTMLFormControlElementWithState::HandlePopoverTargetAction() { + RefPtr<nsGenericHTMLElement> target = GetEffectivePopoverTargetElement(); + if (!target) { + return; + } + + auto action = PopoverTargetAction::Toggle; + if (const nsAttrValue* value = + GetParsedAttr(nsGkAtoms::popovertargetaction)) { + MOZ_ASSERT(value->Type() == nsAttrValue::eEnum); + action = static_cast<PopoverTargetAction>(value->GetEnumValue()); + } + + bool canHide = action == PopoverTargetAction::Hide || + action == PopoverTargetAction::Toggle; + bool shouldHide = canHide && target->IsPopoverOpen(); + bool canShow = action == PopoverTargetAction::Show || + action == PopoverTargetAction::Toggle; + bool shouldShow = canShow && !target->IsPopoverOpen(); + + if (shouldHide) { + target->HidePopover(IgnoreErrors()); + } else if (shouldShow) { + target->ShowPopoverInternal(this, IgnoreErrors()); + } +} + +void nsGenericHTMLFormControlElementWithState::GetInvokeAction( + nsAString& aValue) const { + GetInvokeAction()->ToString(aValue); +} + +nsAtom* nsGenericHTMLFormControlElementWithState::GetInvokeAction() const { + const nsAttrValue* attr = GetParsedAttr(nsGkAtoms::invokeaction); + if (attr && attr->GetAtomValue() != nsGkAtoms::_empty) { + return attr->GetAtomValue(); + } + return nsGkAtoms::_auto; +} + +mozilla::dom::Element* +nsGenericHTMLFormControlElementWithState::GetInvokeTargetElement() const { + if (StaticPrefs::dom_element_invokers_enabled()) { + return GetAttrAssociatedElement(nsGkAtoms::invoketarget); + } + return nullptr; +} + +void nsGenericHTMLFormControlElementWithState::SetInvokeTargetElement( + mozilla::dom::Element* aElement) { + ExplicitlySetAttrElement(nsGkAtoms::invoketarget, aElement); +} + +void nsGenericHTMLFormControlElementWithState::HandleInvokeTargetAction() { + // 1. Let invokee be node's invoke target element. + RefPtr<Element> invokee = GetInvokeTargetElement(); + + // 2. If invokee is null, then return. + if (!invokee) { + return; + } + + // 3. Let action be node's invokeaction attribute + // 4. If action is null or empty, then let action be the string "auto". + RefPtr<nsAtom> aAction = GetInvokeAction(); + MOZ_ASSERT(!aAction->IsEmpty(), "Action should not be empty"); + + // 5. Let notCancelled be the result of firing an event named invoke at + // invokee with its action set to action, its invoker set to node, + // and its cancelable attribute initialized to true. + InvokeEventInit init; + aAction->ToString(init.mAction); + init.mInvoker = this; + init.mCancelable = true; + init.mComposed = true; + RefPtr<Event> event = InvokeEvent::Constructor(this, u"invoke"_ns, init); + event->SetTrusted(true); + event->SetTarget(invokee); + + EventDispatcher::DispatchDOMEvent(invokee, nullptr, event, nullptr, nullptr); + + // 6. If notCancelled is true and invokee has an associated invocation action + // algorithm then run the invokee's invocation action algorithm given action. + if (event->DefaultPrevented()) { + return; + } + + invokee->HandleInvokeInternal(aAction, IgnoreErrors()); +} + +void nsGenericHTMLFormControlElementWithState::GenerateStateKey() { + // Keep the key if already computed + if (!mStateKey.IsVoid()) { + return; + } + + Document* doc = GetUncomposedDoc(); + if (!doc) { + mStateKey.Truncate(); + return; + } + + // Generate the state key + nsContentUtils::GenerateStateKey(this, doc, mStateKey); + + // If the state key is blank, this is anonymous content or for whatever + // reason we are not supposed to save/restore state: keep it as such. + if (!mStateKey.IsEmpty()) { + // Add something unique to content so layout doesn't muck us up. + mStateKey += "-C"; + } +} + +PresState* nsGenericHTMLFormControlElementWithState::GetPrimaryPresState() { + if (mStateKey.IsEmpty()) { + return nullptr; + } + + nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(false); + + if (!history) { + return nullptr; + } + + // Get the pres state for this key, if it doesn't exist, create one. + PresState* result = history->GetState(mStateKey); + if (!result) { + UniquePtr<PresState> newState = NewPresState(); + result = newState.get(); + history->AddState(mStateKey, std::move(newState)); + } + + return result; +} + +already_AddRefed<nsILayoutHistoryState> +nsGenericHTMLFormElement::GetLayoutHistory(bool aRead) { + nsCOMPtr<Document> doc = GetUncomposedDoc(); + if (!doc) { + return nullptr; + } + + // + // Get the history + // + nsCOMPtr<nsILayoutHistoryState> history = doc->GetLayoutHistoryState(); + if (!history) { + return nullptr; + } + + if (aRead && !history->HasStates()) { + return nullptr; + } + + return history.forget(); +} + +bool nsGenericHTMLFormControlElementWithState::RestoreFormControlState() { + MOZ_ASSERT(!mStateKey.IsVoid(), + "GenerateStateKey must already have been called"); + + if (mStateKey.IsEmpty()) { + return false; + } + + nsCOMPtr<nsILayoutHistoryState> history = GetLayoutHistory(true); + if (!history) { + return false; + } + + // Get the pres state for this key + PresState* state = history->GetState(mStateKey); + if (state) { + bool result = RestoreState(state); + history->RemoveState(mStateKey); + return result; + } + + return false; +} + +void nsGenericHTMLFormControlElementWithState::NodeInfoChanged( + Document* aOldDoc) { + nsGenericHTMLFormControlElement::NodeInfoChanged(aOldDoc); + + // We need to regenerate the state key now we're in a new document. Clearing + // mControlNumber means we stop considering this control to be parser + // inserted, and we'll generate a state key based on its position in the + // document rather than the order it was inserted into the document. + mControlNumber = -1; + mStateKey.SetIsVoid(true); +} + +void nsGenericHTMLFormControlElementWithState::GetFormAction(nsString& aValue) { + auto type = ControlType(); + if (!IsInputElement(type) && !IsButtonElement(type)) { + return; + } + + if (!GetAttr(nsGkAtoms::formaction, aValue) || aValue.IsEmpty()) { + Document* document = OwnerDoc(); + nsIURI* docURI = document->GetDocumentURI(); + if (docURI) { + nsAutoCString spec; + nsresult rv = docURI->GetSpec(spec); + if (NS_FAILED(rv)) { + return; + } + + CopyUTF8toUTF16(spec, aValue); + } + } else { + GetURIAttr(nsGkAtoms::formaction, nullptr, aValue); + } +} + +bool nsGenericHTMLElement::IsEventAttributeNameInternal(nsAtom* aName) { + return nsContentUtils::IsEventAttributeName(aName, EventNameType_HTML); +} + +/** + * Construct a URI from a string, as an element.src attribute + * would be set to. Helper for the media elements. + */ +nsresult nsGenericHTMLElement::NewURIFromString(const nsAString& aURISpec, + nsIURI** aURI) { + NS_ENSURE_ARG_POINTER(aURI); + + *aURI = nullptr; + + nsCOMPtr<Document> doc = OwnerDoc(); + + nsresult rv = nsContentUtils::NewURIWithDocumentCharset(aURI, aURISpec, doc, + GetBaseURI()); + NS_ENSURE_SUCCESS(rv, rv); + + bool equal; + if (aURISpec.IsEmpty() && doc->GetDocumentURI() && + NS_SUCCEEDED(doc->GetDocumentURI()->Equals(*aURI, &equal)) && equal) { + // Assume an element can't point to a fragment of its embedding + // document. Fail here instead of returning the recursive URI + // and waiting for the subsequent load to fail. + NS_RELEASE(*aURI); + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + return NS_OK; +} + +void nsGenericHTMLElement::GetInnerText(mozilla::dom::DOMString& aValue, + mozilla::ErrorResult& aError) { + // innerText depends on layout. For example, white space processing is + // something that happens during reflow and which must be reflected by + // innerText. So for: + // + // <div style="white-space:normal"> A B C </div> + // + // innerText should give "A B C". + // + // The approach taken here to avoid the expense of reflow is to flush style + // and then see whether it's necessary to flush layout afterwards. Flushing + // layout can be skipped if we can detect that the element or its descendants + // are not dirty. + + // Obtain the composed doc to handle elements in Shadow DOM. + Document* doc = GetComposedDoc(); + if (doc) { + doc->FlushPendingNotifications(FlushType::Style); + } + + // Elements with `display: content` will not have a frame. To handle Shadow + // DOM, walk the flattened tree looking for parent frame. + nsIFrame* frame = GetPrimaryFrame(); + if (IsDisplayContents()) { + for (Element* parent = GetFlattenedTreeParentElement(); parent; + parent = parent->GetFlattenedTreeParentElement()) { + frame = parent->GetPrimaryFrame(); + if (frame) { + break; + } + } + } + + // Check for dirty reflow roots in the subtree from targetFrame; this requires + // a reflow flush. + bool dirty = frame && frame->PresShell()->FrameIsAncestorOfDirtyRoot(frame); + + // The way we do that is by checking whether the element has either of the two + // dirty bits (NS_FRAME_IS_DIRTY or NS_FRAME_HAS_DIRTY_DESCENDANTS) or if any + // ancestor has NS_FRAME_IS_DIRTY. We need to check for NS_FRAME_IS_DIRTY on + // ancestors since that is something that implies NS_FRAME_IS_DIRTY on all + // descendants. + dirty |= frame && frame->HasAnyStateBits(NS_FRAME_HAS_DIRTY_CHILDREN); + while (!dirty && frame) { + dirty |= frame->HasAnyStateBits(NS_FRAME_IS_DIRTY); + frame = frame->GetInFlowParent(); + } + + // Flush layout if we determined a reflow is required. + if (dirty && doc) { + doc->FlushPendingNotifications(FlushType::Layout); + } + + if (!IsRendered()) { + GetTextContentInternal(aValue, aError); + } else { + nsRange::GetInnerTextNoFlush(aValue, aError, this); + } +} + +static already_AddRefed<nsINode> TextToNode(const nsAString& aString, + nsNodeInfoManager* aNim) { + nsString str; + const char16_t* s = aString.BeginReading(); + const char16_t* end = aString.EndReading(); + RefPtr<DocumentFragment> fragment; + while (true) { + if (s != end && *s == '\r' && s + 1 != end && s[1] == '\n') { + // a \r\n pair should only generate one <br>, so just skip the \r + ++s; + } + if (s == end || *s == '\r' || *s == '\n') { + if (!str.IsEmpty()) { + RefPtr<nsTextNode> textContent = new (aNim) nsTextNode(aNim); + textContent->SetText(str, true); + if (!fragment) { + if (s == end) { + return textContent.forget(); + } + fragment = new (aNim) DocumentFragment(aNim); + } + fragment->AppendChildTo(textContent, true, IgnoreErrors()); + } + if (s == end) { + break; + } + str.Truncate(); + RefPtr<NodeInfo> ni = aNim->GetNodeInfo( + nsGkAtoms::br, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + auto* nim = ni->NodeInfoManager(); + RefPtr<HTMLBRElement> br = new (nim) HTMLBRElement(ni.forget()); + if (!fragment) { + if (s + 1 == end) { + return br.forget(); + } + fragment = new (aNim) DocumentFragment(aNim); + } + fragment->AppendChildTo(br, true, IgnoreErrors()); + } else { + str.Append(*s); + } + ++s; + } + return fragment.forget(); +} + +void nsGenericHTMLElement::SetInnerText(const nsAString& aValue) { + RefPtr<nsINode> node = TextToNode(aValue, NodeInfo()->NodeInfoManager()); + ReplaceChildren(node, IgnoreErrors()); +} + +// https://html.spec.whatwg.org/#merge-with-the-next-text-node +static void MergeWithNextTextNode(Text& aText, ErrorResult& aRv) { + RefPtr<Text> nextSibling = Text::FromNodeOrNull(aText.GetNextSibling()); + if (!nextSibling) { + return; + } + nsAutoString data; + nextSibling->GetData(data); + aText.AppendData(data, aRv); + nextSibling->Remove(); +} + +// https://html.spec.whatwg.org/#dom-outertext +void nsGenericHTMLElement::SetOuterText(const nsAString& aValue, + ErrorResult& aRv) { + nsCOMPtr<nsINode> parent = GetParentNode(); + if (!parent) { + return aRv.ThrowNoModificationAllowedError("Element has no parent"); + } + + RefPtr<nsINode> next = GetNextSibling(); + RefPtr<nsINode> previous = GetPreviousSibling(); + + // Batch possible DOMSubtreeModified events. + mozAutoSubtreeModified subtree(OwnerDoc(), nullptr); + + nsNodeInfoManager* nim = NodeInfo()->NodeInfoManager(); + RefPtr<nsINode> node = TextToNode(aValue, nim); + if (!node) { + // This doesn't match the spec, see + // https://github.com/whatwg/html/issues/7508 + node = new (nim) nsTextNode(nim); + } + parent->ReplaceChild(*node, *this, aRv); + if (aRv.Failed()) { + return; + } + + if (next) { + if (RefPtr<Text> text = Text::FromNodeOrNull(next->GetPreviousSibling())) { + MergeWithNextTextNode(*text, aRv); + if (aRv.Failed()) { + return; + } + } + } + if (auto* text = Text::FromNodeOrNull(previous)) { + MergeWithNextTextNode(*text, aRv); + } +} + +// This should be true when `:open` should match. +bool nsGenericHTMLElement::PopoverOpen() const { + if (PopoverData* popoverData = GetPopoverData()) { + return popoverData->GetPopoverVisibilityState() == + PopoverVisibilityState::Showing; + } + return false; +} + +// https://html.spec.whatwg.org/#check-popover-validity +bool nsGenericHTMLElement::CheckPopoverValidity( + PopoverVisibilityState aExpectedState, Document* aExpectedDocument, + ErrorResult& aRv) { + if (GetPopoverAttributeState() == PopoverAttributeState::None) { + aRv.ThrowNotSupportedError("Element is in the no popover state"); + return false; + } + + if (GetPopoverData()->GetPopoverVisibilityState() != aExpectedState) { + return false; + } + + if (!IsInComposedDoc()) { + aRv.ThrowInvalidStateError("Element is not connected"); + return false; + } + + if (aExpectedDocument && aExpectedDocument != OwnerDoc()) { + aRv.ThrowInvalidStateError("Element is moved to other document"); + return false; + } + + if (auto* dialog = HTMLDialogElement::FromNode(this)) { + if (dialog->IsInTopLayer()) { + aRv.ThrowInvalidStateError("Element is a modal <dialog> element"); + return false; + } + } + + if (State().HasState(ElementState::FULLSCREEN)) { + aRv.ThrowInvalidStateError("Element is fullscreen"); + return false; + } + + return true; +} + +PopoverAttributeState nsGenericHTMLElement::GetPopoverAttributeState() const { + return GetPopoverData() ? GetPopoverData()->GetPopoverAttributeState() + : PopoverAttributeState::None; +} + +void nsGenericHTMLElement::PopoverPseudoStateUpdate(bool aOpen, bool aNotify) { + SetStates(ElementState::POPOVER_OPEN, aOpen, aNotify); +} + +already_AddRefed<ToggleEvent> nsGenericHTMLElement::CreateToggleEvent( + const nsAString& aEventType, const nsAString& aOldState, + const nsAString& aNewState, Cancelable aCancelable) { + ToggleEventInit init; + init.mBubbles = false; + init.mOldState = aOldState; + init.mNewState = aNewState; + init.mCancelable = aCancelable == Cancelable::eYes; + RefPtr<ToggleEvent> event = ToggleEvent::Constructor(this, aEventType, init); + event->SetTrusted(true); + event->SetTarget(this); + return event.forget(); +} + +bool nsGenericHTMLElement::FireToggleEvent(PopoverVisibilityState aOldState, + PopoverVisibilityState aNewState, + const nsAString& aType) { + auto stringForState = [](PopoverVisibilityState state) { + return state == PopoverVisibilityState::Hidden ? u"closed"_ns : u"open"_ns; + }; + const auto cancelable = aType == u"beforetoggle"_ns && + aNewState == PopoverVisibilityState::Showing + ? Cancelable::eYes + : Cancelable::eNo; + RefPtr event = CreateToggleEvent(aType, stringForState(aOldState), + stringForState(aNewState), cancelable); + EventDispatcher::DispatchDOMEvent(this, nullptr, event, nullptr, nullptr); + return event->DefaultPrevented(); +} + +// https://html.spec.whatwg.org/#queue-a-popover-toggle-event-task +void nsGenericHTMLElement::QueuePopoverEventTask( + PopoverVisibilityState aOldState) { + auto* data = GetPopoverData(); + MOZ_ASSERT(data, "Should have popover data"); + + if (auto* queuedToggleEventTask = data->GetToggleEventTask()) { + aOldState = queuedToggleEventTask->GetOldState(); + } + + auto task = + MakeRefPtr<PopoverToggleEventTask>(do_GetWeakReference(this), aOldState); + data->SetToggleEventTask(task); + OwnerDoc()->Dispatch(task.forget()); +} + +void nsGenericHTMLElement::RunPopoverToggleEventTask( + PopoverToggleEventTask* aTask, PopoverVisibilityState aOldState) { + auto* data = GetPopoverData(); + if (!data) { + return; + } + + auto* popoverToggleEventTask = data->GetToggleEventTask(); + if (!popoverToggleEventTask || aTask != popoverToggleEventTask) { + return; + } + data->ClearToggleEventTask(); + // Intentionally ignore the return value here as only on open event the + // cancelable attribute is initialized to true for beforetoggle event. + FireToggleEvent(aOldState, data->GetPopoverVisibilityState(), u"toggle"_ns); +} + +// https://html.spec.whatwg.org/#dom-showpopover +void nsGenericHTMLElement::ShowPopover(ErrorResult& aRv) { + return ShowPopoverInternal(nullptr, aRv); +} +void nsGenericHTMLElement::ShowPopoverInternal(Element* aInvoker, + ErrorResult& aRv) { + if (!CheckPopoverValidity(PopoverVisibilityState::Hidden, nullptr, aRv)) { + return; + } + RefPtr<Document> document = OwnerDoc(); + + MOZ_ASSERT(!GetPopoverData() || !GetPopoverData()->GetInvoker()); + MOZ_ASSERT(!OwnerDoc()->TopLayerContains(*this)); + + bool wasShowingOrHiding = GetPopoverData()->IsShowingOrHiding(); + GetPopoverData()->SetIsShowingOrHiding(true); + auto cleanupShowingFlag = MakeScopeExit([&]() { + if (auto* popoverData = GetPopoverData()) { + popoverData->SetIsShowingOrHiding(wasShowingOrHiding); + } + }); + + // Fire beforetoggle event and re-check popover validity. + if (FireToggleEvent(PopoverVisibilityState::Hidden, + PopoverVisibilityState::Showing, u"beforetoggle"_ns)) { + return; + } + if (!CheckPopoverValidity(PopoverVisibilityState::Hidden, document, aRv)) { + return; + } + + bool shouldRestoreFocus = false; + nsWeakPtr originallyFocusedElement; + if (IsAutoPopover()) { + auto originalState = GetPopoverAttributeState(); + RefPtr<nsINode> ancestor = GetTopmostPopoverAncestor(aInvoker, true); + if (!ancestor) { + ancestor = document; + } + document->HideAllPopoversUntil(*ancestor, false, + /* aFireEvents = */ !wasShowingOrHiding); + if (GetPopoverAttributeState() != originalState) { + aRv.ThrowInvalidStateError( + "The value of the popover attribute was changed while hiding the " + "popover."); + return; + } + + // TODO: Handle if document changes, see + // https://github.com/whatwg/html/issues/9177 + if (!IsAutoPopover() || + !CheckPopoverValidity(PopoverVisibilityState::Hidden, document, aRv)) { + return; + } + + shouldRestoreFocus = !document->GetTopmostAutoPopover(); + // Let originallyFocusedElement be document's focused area of the document's + // DOM anchor. + if (nsIContent* unretargetedFocus = + document->GetUnretargetedFocusedContent()) { + originallyFocusedElement = + do_GetWeakReference(unretargetedFocus->AsElement()); + } + } + + document->AddPopoverToTopLayer(*this); + + PopoverPseudoStateUpdate(true, true); + + { + auto* popoverData = GetPopoverData(); + popoverData->SetPopoverVisibilityState(PopoverVisibilityState::Showing); + popoverData->SetInvoker(aInvoker); + } + + // Run the popover focusing steps given element. + FocusPopover(); + if (shouldRestoreFocus && + GetPopoverAttributeState() != PopoverAttributeState::None) { + GetPopoverData()->SetPreviouslyFocusedElement(originallyFocusedElement); + } + + // Queue popover toggle event task. + QueuePopoverEventTask(PopoverVisibilityState::Hidden); +} + +void nsGenericHTMLElement::HidePopoverWithoutRunningScript() { + HidePopoverInternal(/* aFocusPreviousElement = */ false, + /* aFireEvents = */ false, IgnoreErrors()); +} + +// https://html.spec.whatwg.org/#dom-hidepopover +void nsGenericHTMLElement::HidePopover(ErrorResult& aRv) { + HidePopoverInternal(/* aFocusPreviousElement = */ true, + /* aFireEvents = */ true, aRv); +} + +void nsGenericHTMLElement::HidePopoverInternal(bool aFocusPreviousElement, + bool aFireEvents, + ErrorResult& aRv) { + OwnerDoc()->HidePopover(*this, aFocusPreviousElement, aFireEvents, aRv); +} + +void nsGenericHTMLElement::ForgetPreviouslyFocusedElementAfterHidingPopover() { + auto* data = GetPopoverData(); + MOZ_ASSERT(data, "Should have popover data"); + data->SetPreviouslyFocusedElement(nullptr); +} + +void nsGenericHTMLElement::FocusPreviousElementAfterHidingPopover() { + auto* data = GetPopoverData(); + MOZ_ASSERT(data, "Should have popover data"); + + RefPtr<Element> control = + do_QueryReferent(data->GetPreviouslyFocusedElement().get()); + data->SetPreviouslyFocusedElement(nullptr); + + if (!control) { + return; + } + + // Step 14.2 at + // https://html.spec.whatwg.org/multipage/popover.html#hide-popover-algorithm + // If focusPreviousElement is true and document's focused area of the + // document's DOM anchor is a shadow-including inclusive descendant of + // element, then run the focusing steps for previouslyFocusedElement; + nsIContent* currentFocus = OwnerDoc()->GetUnretargetedFocusedContent(); + if (currentFocus && + currentFocus->IsShadowIncludingInclusiveDescendantOf(this)) { + FocusOptions options; + options.mPreventScroll = true; + control->Focus(options, CallerType::NonSystem, IgnoreErrors()); + } +} + +// https://html.spec.whatwg.org/multipage/popover.html#dom-togglepopover +bool nsGenericHTMLElement::TogglePopover(const Optional<bool>& aForce, + ErrorResult& aRv) { + if (PopoverOpen() && (!aForce.WasPassed() || !aForce.Value())) { + HidePopover(aRv); + } else if (!aForce.WasPassed() || aForce.Value()) { + ShowPopover(aRv); + } else { + CheckPopoverValidity(GetPopoverData() + ? GetPopoverData()->GetPopoverVisibilityState() + : PopoverVisibilityState::Showing, + nullptr, aRv); + } + + return PopoverOpen(); +} + +// https://html.spec.whatwg.org/multipage/popover.html#popover-focusing-steps +void nsGenericHTMLElement::FocusPopover() { + if (auto* dialog = HTMLDialogElement::FromNode(this)) { + return MOZ_KnownLive(dialog)->FocusDialog(); + } + + if (RefPtr<Document> doc = GetComposedDoc()) { + doc->FlushPendingNotifications(FlushType::Frames); + } + + RefPtr<Element> control = GetBoolAttr(nsGkAtoms::autofocus) + ? this + : GetAutofocusDelegate(false /* aWithMouse */); + + if (!control) { + return; + } + FocusCandidate(control, false /* aClearUpFocus */); +} + +void nsGenericHTMLElement::FocusCandidate(Element* aControl, + bool aClearUpFocus) { + // 1) Run the focusing steps given control. + IgnoredErrorResult rv; + if (RefPtr<Element> elementToFocus = nsFocusManager::GetTheFocusableArea( + aControl, nsFocusManager::ProgrammaticFocusFlags(FocusOptions()))) { + elementToFocus->Focus(FocusOptions(), CallerType::NonSystem, rv); + if (rv.Failed()) { + return; + } + } else if (aClearUpFocus) { + if (RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager()) { + // Clear the focus which ends up making the body gets focused + nsCOMPtr<nsPIDOMWindowOuter> outerWindow = OwnerDoc()->GetWindow(); + fm->ClearFocus(outerWindow); + } + } + + // 2) Let topDocument be the active document of control's node document's + // browsing context's top-level browsing context. + // 3) If control's node document's origin is not the same as the origin of + // topDocument, then return. + BrowsingContext* bc = aControl->OwnerDoc()->GetBrowsingContext(); + if (bc && bc->IsInProcess() && bc->SameOriginWithTop()) { + if (nsCOMPtr<nsIDocShell> docShell = bc->Top()->GetDocShell()) { + if (Document* topDocument = docShell->GetExtantDocument()) { + // 4) Empty topDocument's autofocus candidates. + // 5) Set topDocument's autofocus processed flag to true. + topDocument->SetAutoFocusFired(); + } + } + } +} + +already_AddRefed<ElementInternals> nsGenericHTMLElement::AttachInternals( + ErrorResult& aRv) { + // ElementInternals is only available on autonomous custom element, so throws + // an error by default. The spec steps are implemented in HTMLElement because + // ElementInternals needs to hold a pointer to HTMLElement in order to forward + // form operation to it. + aRv.ThrowNotSupportedError(nsPrintfCString( + "Cannot attach ElementInternals to a customized built-in or non-custom " + "element " + "'%s'", + NS_ConvertUTF16toUTF8(NodeInfo()->NameAtom()->GetUTF16String()).get())); + return nullptr; +} + +ElementInternals* nsGenericHTMLElement::GetInternals() const { + if (CustomElementData* data = GetCustomElementData()) { + return data->GetElementInternals(); + } + return nullptr; +} + +bool nsGenericHTMLElement::IsFormAssociatedCustomElements() const { + if (CustomElementData* data = GetCustomElementData()) { + return data->IsFormAssociated(); + } + return false; +} + +void nsGenericHTMLElement::GetAutocapitalize(nsAString& aValue) const { + GetEnumAttr(nsGkAtoms::autocapitalize, nullptr, kDefaultAutocapitalize->tag, + aValue); +} + +bool nsGenericHTMLElement::Translate() const { + if (const nsAttrValue* attr = mAttrs.GetAttr(nsGkAtoms::translate)) { + if (attr->IsEmptyString() || attr->Equals(nsGkAtoms::yes, eIgnoreCase)) { + return true; + } + if (attr->Equals(nsGkAtoms::no, eIgnoreCase)) { + return false; + } + } + return nsGenericHTMLElementBase::Translate(); +} + +void nsGenericHTMLElement::GetPopover(nsString& aPopover) const { + GetHTMLEnumAttr(nsGkAtoms::popover, aPopover); + if (aPopover.IsEmpty() && !DOMStringIsNull(aPopover)) { + aPopover.Assign(NS_ConvertUTF8toUTF16(kPopoverAttributeValueAuto)); + } +} diff --git a/dom/html/nsGenericHTMLElement.h b/dom/html/nsGenericHTMLElement.h new file mode 100644 index 0000000000..f6e7d2415d --- /dev/null +++ b/dom/html/nsGenericHTMLElement.h @@ -0,0 +1,1461 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsGenericHTMLElement_h___ +#define nsGenericHTMLElement_h___ + +#include "mozilla/Attributes.h" +#include "mozilla/EventForwards.h" +#include "nsNameSpaceManager.h" // for kNameSpaceID_None +#include "nsIFormControl.h" +#include "nsGkAtoms.h" +#include "nsContentCreatorFunctions.h" +#include "nsStyledElement.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/DOMRect.h" +#include "mozilla/dom/ValidityState.h" +#include "mozilla/dom/PopoverData.h" +#include "mozilla/dom/ToggleEvent.h" + +#include <cstdint> + +class nsDOMTokenList; +class nsIFormControlFrame; +class nsIFrame; +class nsILayoutHistoryState; +class nsIURI; +struct nsSize; + +enum nsCSSPropertyID : int32_t; + +namespace mozilla { +class EditorBase; +class ErrorResult; +class EventChainPostVisitor; +class EventChainPreVisitor; +class EventChainVisitor; +class EventListenerManager; +class PresState; +namespace dom { +class ElementInternals; +class HTMLFormElement; +enum class FetchPriority : uint8_t; +} // namespace dom +} // namespace mozilla + +using nsGenericHTMLElementBase = nsStyledElement; + +/** + * A common superclass for HTML elements + */ +class nsGenericHTMLElement : public nsGenericHTMLElementBase { + public: + using Element::Focus; + using Element::SetTabIndex; + explicit nsGenericHTMLElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo) + : nsGenericHTMLElementBase(std::move(aNodeInfo)) { + NS_ASSERTION(mNodeInfo->NamespaceID() == kNameSpaceID_XHTML, + "Unexpected namespace"); + AddStatesSilently(mozilla::dom::ElementState::LTR); + } + + NS_INLINE_DECL_REFCOUNTING_INHERITED(nsGenericHTMLElement, + nsGenericHTMLElementBase) + + NS_IMPL_FROMNODE(nsGenericHTMLElement, kNameSpaceID_XHTML) + + // From Element + nsresult CopyInnerTo(mozilla::dom::Element* aDest); + + void GetTitle(mozilla::dom::DOMString& aTitle) { + GetHTMLAttr(nsGkAtoms::title, aTitle); + } + void SetTitle(const nsAString& aTitle) { + SetHTMLAttr(nsGkAtoms::title, aTitle); + } + void GetLang(mozilla::dom::DOMString& aLang) { + GetHTMLAttr(nsGkAtoms::lang, aLang); + } + void SetLang(const nsAString& aLang) { SetHTMLAttr(nsGkAtoms::lang, aLang); } + bool Translate() const override; + void SetTranslate(bool aTranslate, mozilla::ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::translate, aTranslate ? u"yes"_ns : u"no"_ns, + aError); + } + void GetDir(nsAString& aDir) { GetHTMLEnumAttr(nsGkAtoms::dir, aDir); } + void SetDir(const nsAString& aDir, mozilla::ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::dir, aDir, aError); + } + void GetPopover(nsString& aPopover) const; + void SetPopover(const nsAString& aPopover, mozilla::ErrorResult& aError) { + SetOrRemoveNullableStringAttr(nsGkAtoms::popover, aPopover, aError); + } + bool Hidden() const { return GetBoolAttr(nsGkAtoms::hidden); } + void SetHidden(bool aHidden, mozilla::ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::hidden, aHidden, aError); + } + bool Inert() const { return GetBoolAttr(nsGkAtoms::inert); } + void SetInert(bool aInert, mozilla::ErrorResult& aError) { + SetHTMLBoolAttr(nsGkAtoms::inert, aInert, aError); + } + MOZ_CAN_RUN_SCRIPT void Click(mozilla::dom::CallerType aCallerType); + void GetAccessKey(nsString& aAccessKey) { + GetHTMLAttr(nsGkAtoms::accesskey, aAccessKey); + } + void SetAccessKey(const nsAString& aAccessKey, mozilla::ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::accesskey, aAccessKey, aError); + } + void GetAccessKeyLabel(nsString& aAccessKeyLabel); + virtual bool Draggable() const { + return AttrValueIs(kNameSpaceID_None, nsGkAtoms::draggable, + nsGkAtoms::_true, eIgnoreCase); + } + void SetDraggable(bool aDraggable, mozilla::ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::draggable, aDraggable ? u"true"_ns : u"false"_ns, + aError); + } + void GetContentEditable(nsString& aContentEditable) { + ContentEditableTristate value = GetContentEditableValue(); + if (value == eTrue) { + aContentEditable.AssignLiteral("true"); + } else if (value == eFalse) { + aContentEditable.AssignLiteral("false"); + } else { + aContentEditable.AssignLiteral("inherit"); + } + } + void SetContentEditable(const nsAString& aContentEditable, + mozilla::ErrorResult& aError) { + if (aContentEditable.LowerCaseEqualsLiteral("inherit")) { + UnsetHTMLAttr(nsGkAtoms::contenteditable, aError); + } else if (aContentEditable.LowerCaseEqualsLiteral("true")) { + SetHTMLAttr(nsGkAtoms::contenteditable, u"true"_ns, aError); + } else if (aContentEditable.LowerCaseEqualsLiteral("false")) { + SetHTMLAttr(nsGkAtoms::contenteditable, u"false"_ns, aError); + } else { + aError.Throw(NS_ERROR_DOM_SYNTAX_ERR); + } + } + bool IsContentEditable() { + for (nsIContent* node = this; node; node = node->GetParent()) { + nsGenericHTMLElement* element = FromNode(node); + if (element) { + ContentEditableTristate value = element->GetContentEditableValue(); + if (value != eInherit) { + return value == eTrue; + } + } + } + return false; + } + + mozilla::dom::PopoverAttributeState GetPopoverAttributeState() const; + void PopoverPseudoStateUpdate(bool aOpen, bool aNotify); + bool PopoverOpen() const; + bool CheckPopoverValidity(mozilla::dom::PopoverVisibilityState aExpectedState, + Document* aExpectedDocument, ErrorResult& aRv); + already_AddRefed<mozilla::dom::ToggleEvent> CreateToggleEvent( + const nsAString& aEventType, const nsAString& aOldState, + const nsAString& aNewState, mozilla::Cancelable); + /** Returns true if the event has been cancelled. */ + MOZ_CAN_RUN_SCRIPT bool FireToggleEvent( + mozilla::dom::PopoverVisibilityState aOldState, + mozilla::dom::PopoverVisibilityState aNewState, const nsAString& aType); + MOZ_CAN_RUN_SCRIPT void QueuePopoverEventTask( + mozilla::dom::PopoverVisibilityState aOldState); + MOZ_CAN_RUN_SCRIPT void RunPopoverToggleEventTask( + mozilla::dom::PopoverToggleEventTask* aTask, + mozilla::dom::PopoverVisibilityState aOldState); + MOZ_CAN_RUN_SCRIPT void ShowPopover(ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void ShowPopoverInternal(Element* aInvoker, + ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT_BOUNDARY void HidePopoverWithoutRunningScript(); + MOZ_CAN_RUN_SCRIPT void HidePopoverInternal(bool aFocusPreviousElement, + bool aFireEvents, + ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void HidePopover(ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT bool TogglePopover( + const mozilla::dom::Optional<bool>& aForce, ErrorResult& aRv); + MOZ_CAN_RUN_SCRIPT void FocusPopover(); + void ForgetPreviouslyFocusedElementAfterHidingPopover(); + MOZ_CAN_RUN_SCRIPT void FocusPreviousElementAfterHidingPopover(); + + MOZ_CAN_RUN_SCRIPT void FocusCandidate(Element*, bool aClearUpFocus); + + void SetNonce(const nsAString& aNonce) { + SetProperty(nsGkAtoms::nonce, new nsString(aNonce), + nsINode::DeleteProperty<nsString>, /* aTransfer = */ true); + } + void RemoveNonce() { RemoveProperty(nsGkAtoms::nonce); } + void GetNonce(nsAString& aNonce) const { + nsString* cspNonce = static_cast<nsString*>(GetProperty(nsGkAtoms::nonce)); + if (cspNonce) { + aNonce = *cspNonce; + } + } + + /** Returns whether a form control should be default-focusable. */ + bool IsFormControlDefaultFocusable(bool aWithMouse) const; + + /** + * Returns the count of descendants (inclusive of this node) in + * the uncomposed document that are explicitly set as editable. + */ + uint32_t EditableInclusiveDescendantCount(); + + bool Spellcheck(); + void SetSpellcheck(bool aSpellcheck, mozilla::ErrorResult& aError) { + SetHTMLAttr(nsGkAtoms::spellcheck, aSpellcheck ? u"true"_ns : u"false"_ns, + aError); + } + + MOZ_CAN_RUN_SCRIPT + void GetInnerText(mozilla::dom::DOMString& aValue, ErrorResult& aError); + MOZ_CAN_RUN_SCRIPT + void GetOuterText(mozilla::dom::DOMString& aValue, ErrorResult& aError) { + return GetInnerText(aValue, aError); + } + MOZ_CAN_RUN_SCRIPT void SetInnerText(const nsAString& aValue); + MOZ_CAN_RUN_SCRIPT void SetOuterText(const nsAString& aValue, + ErrorResult& aRv); + + void GetInputMode(nsAString& aValue) { + GetEnumAttr(nsGkAtoms::inputmode, nullptr, aValue); + } + void SetInputMode(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::inputmode, aValue, aRv); + } + virtual void GetAutocapitalize(nsAString& aValue) const; + void SetAutocapitalize(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::autocapitalize, aValue, aRv); + } + + void GetEnterKeyHint(nsAString& aValue) const { + GetEnumAttr(nsGkAtoms::enterkeyhint, nullptr, aValue); + } + void SetEnterKeyHint(const nsAString& aValue, ErrorResult& aRv) { + SetHTMLAttr(nsGkAtoms::enterkeyhint, aValue, aRv); + } + + /** + * Determine whether an attribute is an event (onclick, etc.) + * @param aName the attribute + * @return whether the name is an event handler name + */ + bool IsEventAttributeNameInternal(nsAtom* aName) override; + +#define EVENT(name_, id_, type_, struct_) /* nothing; handled by nsINode */ +// The using nsINode::Get/SetOn* are to avoid warnings about shadowing the XPCOM +// getter and setter on nsINode. +#define FORWARDED_EVENT(name_, id_, type_, struct_) \ + using nsINode::GetOn##name_; \ + using nsINode::SetOn##name_; \ + mozilla::dom::EventHandlerNonNull* GetOn##name_(); \ + void SetOn##name_(mozilla::dom::EventHandlerNonNull* handler); +#define ERROR_EVENT(name_, id_, type_, struct_) \ + using nsINode::GetOn##name_; \ + using nsINode::SetOn##name_; \ + already_AddRefed<mozilla::dom::EventHandlerNonNull> GetOn##name_(); \ + void SetOn##name_(mozilla::dom::EventHandlerNonNull* handler); +#include "mozilla/EventNameList.h" // IWYU pragma: keep +#undef ERROR_EVENT +#undef FORWARDED_EVENT +#undef EVENT + mozilla::dom::Element* GetOffsetParent() { + mozilla::CSSIntRect rcFrame; + return GetOffsetRect(rcFrame); + } + int32_t OffsetTop() { + mozilla::CSSIntRect rcFrame; + GetOffsetRect(rcFrame); + + return rcFrame.y; + } + int32_t OffsetLeft() { + mozilla::CSSIntRect rcFrame; + GetOffsetRect(rcFrame); + + return rcFrame.x; + } + int32_t OffsetWidth() { + mozilla::CSSIntRect rcFrame; + GetOffsetRect(rcFrame); + + return rcFrame.Width(); + } + int32_t OffsetHeight() { + mozilla::CSSIntRect rcFrame; + GetOffsetRect(rcFrame); + + return rcFrame.Height(); + } + + // These methods are already implemented in nsIContent but we want something + // faster for HTMLElements ignoring the namespace checking. + // This is safe because we already know that we are in the HTML namespace. + inline bool IsHTMLElement() const { return true; } + + inline bool IsHTMLElement(nsAtom* aTag) const { + return mNodeInfo->Equals(aTag); + } + + template <typename First, typename... Args> + inline bool IsAnyOfHTMLElements(First aFirst, Args... aArgs) const { + return IsNodeInternal(aFirst, aArgs...); + } + + // https://html.spec.whatwg.org/multipage/custom-elements.html#dom-attachinternals + virtual already_AddRefed<mozilla::dom::ElementInternals> AttachInternals( + ErrorResult& aRv); + + mozilla::dom::ElementInternals* GetInternals() const; + + bool IsFormAssociatedCustomElements() const; + + // Returns true if the event should not be handled from GetEventTargetParent. + virtual bool IsDisabledForEvents(mozilla::WidgetEvent* aEvent) { + return false; + } + + bool Autofocus() const { return GetBoolAttr(nsGkAtoms::autofocus); } + void SetAutofocus(bool aVal, ErrorResult& aRv) { + SetHTMLBoolAttr(nsGkAtoms::autofocus, aVal, aRv); + } + + protected: + virtual ~nsGenericHTMLElement() = default; + + public: + // Implementation for nsIContent + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + + Focusable IsFocusableWithoutStyle(bool aWithMouse) override { + Focusable result; + IsHTMLFocusable(aWithMouse, &result.mFocusable, &result.mTabIndex); + return result; + } + /** + * Returns true if a subclass is not allowed to override the value returned + * in aIsFocusable. + */ + virtual bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex); + MOZ_CAN_RUN_SCRIPT + mozilla::Result<bool, nsresult> PerformAccesskey( + bool aKeyCausesActivation, bool aIsTrustedEvent) override; + + /** + * Check if an event for an anchor can be handled + * @return true if the event can be handled, false otherwise + */ + bool CheckHandleEventForAnchorsPreconditions( + mozilla::EventChainVisitor& aVisitor); + void GetEventTargetParentForAnchors(mozilla::EventChainPreVisitor& aVisitor); + MOZ_CAN_RUN_SCRIPT + nsresult PostHandleEventForAnchors(mozilla::EventChainPostVisitor& aVisitor); + bool IsHTMLLink(nsIURI** aURI) const; + + // HTML element methods + void Compact() { mAttrs.Compact(); } + + void UpdateEditableState(bool aNotify) override; + + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + bool ParseBackgroundAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, nsAttrValue& aResult); + + NS_IMETHOD_(bool) IsAttributeMapped(const nsAtom* aAttribute) const override; + nsMapRuleToAttributesFunc GetAttributeMappingFunction() const override; + + /** + * Get the base target for any links within this piece + * of content. Generally, this is the document's base target, + * but certain content carries a local base for backward + * compatibility. + * + * @param aBaseTarget the base target [OUT] + */ + void GetBaseTarget(nsAString& aBaseTarget) const; + + /** + * Get the primary form control frame for this element. Same as + * GetPrimaryFrame(), except it QI's to nsIFormControlFrame. + * + * @param aFlush whether to flush out frames so that they're up to date. + * @return the primary frame as nsIFormControlFrame + */ + nsIFormControlFrame* GetFormControlFrame(bool aFlushFrames); + + //---------------------------------------- + + /** + * Parse an alignment attribute (top/middle/bottom/baseline) + * + * @param aString the string to parse + * @param aResult the resulting HTMLValue + * @return whether the value was parsed + */ + static bool ParseAlignValue(const nsAString& aString, nsAttrValue& aResult); + + /** + * Parse a div align string to value (left/right/center/middle/justify) + * + * @param aString the string to parse + * @param aResult the resulting HTMLValue + * @return whether the value was parsed + */ + static bool ParseDivAlignValue(const nsAString& aString, + nsAttrValue& aResult); + + /** + * Convert a table halign string to value (left/right/center/char/justify) + * + * @param aString the string to parse + * @param aResult the resulting HTMLValue + * @return whether the value was parsed + */ + static bool ParseTableHAlignValue(const nsAString& aString, + nsAttrValue& aResult); + + /** + * Convert a table cell halign string to value + * + * @param aString the string to parse + * @param aResult the resulting HTMLValue + * @return whether the value was parsed + */ + static bool ParseTableCellHAlignValue(const nsAString& aString, + nsAttrValue& aResult); + + /** + * Convert a table valign string to value (left/right/center/char/justify/ + * abscenter/absmiddle/middle) + * + * @param aString the string to parse + * @param aResult the resulting HTMLValue + * @return whether the value was parsed + */ + static bool ParseTableVAlignValue(const nsAString& aString, + nsAttrValue& aResult); + + /** + * Convert an image attribute to value (width, height, hspace, vspace, border) + * + * @param aAttribute the attribute to parse + * @param aString the string to parse + * @param aResult the resulting HTMLValue + * @return whether the value was parsed + */ + static bool ParseImageAttribute(nsAtom* aAttribute, const nsAString& aString, + nsAttrValue& aResult); + + static bool ParseReferrerAttribute(const nsAString& aString, + nsAttrValue& aResult); + + /** + * Convert a frameborder string to value (yes/no/1/0) + * + * @param aString the string to parse + * @param aResult the resulting HTMLValue + * @return whether the value was parsed + */ + static bool ParseFrameborderValue(const nsAString& aString, + nsAttrValue& aResult); + + /** + * Convert a scrolling string to value (yes/no/on/off/scroll/noscroll/auto) + * + * @param aString the string to parse + * @param aResult the resulting HTMLValue + * @return whether the value was parsed + */ + static bool ParseScrollingValue(const nsAString& aString, + nsAttrValue& aResult); + + /* + * Attribute Mapping Helpers + */ + + /** + * A style attribute mapping function for the most common attributes, to be + * called by subclasses' attribute mapping functions. Currently handles + * dir, lang and hidden, could handle others. + * + * @param aAttributes the list of attributes to map + * @param aData the returned rule data [INOUT] + * @see GetAttributeMappingFunction + */ + static void MapCommonAttributesInto(mozilla::MappedDeclarationsBuilder&); + /** + * Same as MapCommonAttributesInto except that it does not handle hidden. + * @see GetAttributeMappingFunction + */ + static void MapCommonAttributesIntoExceptHidden( + mozilla::MappedDeclarationsBuilder&); + + static const MappedAttributeEntry sCommonAttributeMap[]; + static const MappedAttributeEntry sImageMarginSizeAttributeMap[]; + static const MappedAttributeEntry sImageBorderAttributeMap[]; + static const MappedAttributeEntry sImageAlignAttributeMap[]; + static const MappedAttributeEntry sDivAlignAttributeMap[]; + static const MappedAttributeEntry sBackgroundAttributeMap[]; + static const MappedAttributeEntry sBackgroundColorAttributeMap[]; + + /** + * Helper to map the align attribute. + * @see GetAttributeMappingFunction + */ + static void MapImageAlignAttributeInto(mozilla::MappedDeclarationsBuilder&); + + /** + * Helper to map the align attribute for things like <div>, <h1>, etc. + * @see GetAttributeMappingFunction + */ + static void MapDivAlignAttributeInto(mozilla::MappedDeclarationsBuilder&); + + /** + * Helper to map the valign attribute for things like <col>, <tr>, <section>. + * @see GetAttributeMappingFunction + */ + static void MapVAlignAttributeInto(mozilla::MappedDeclarationsBuilder&); + + /** + * Helper to map the image border attribute. + * @see GetAttributeMappingFunction + */ + static void MapImageBorderAttributeInto(mozilla::MappedDeclarationsBuilder&); + /** + * Helper to map the image margin attribute into a style struct. + * + * @param aAttributes the list of attributes to map + * @param aData the returned rule data [INOUT] + * @see GetAttributeMappingFunction + */ + static void MapImageMarginAttributeInto(mozilla::MappedDeclarationsBuilder&); + + /** + * Helper to map a given dimension (width/height) into the declaration + * block, handling percentages and numbers. + */ + static void MapDimensionAttributeInto(mozilla::MappedDeclarationsBuilder&, + nsCSSPropertyID, const nsAttrValue&); + + /** + * Maps the aspect ratio given width and height attributes. + */ + static void DoMapAspectRatio(const nsAttrValue& aWidth, + const nsAttrValue& aHeight, + mozilla::MappedDeclarationsBuilder&); + + // Whether to map the width and height attributes to aspect-ratio. + enum class MapAspectRatio { No, Yes }; + + /** + * Helper to map the image position attribute into a style struct. + */ + static void MapImageSizeAttributesInto(mozilla::MappedDeclarationsBuilder&, + MapAspectRatio = MapAspectRatio::No); + + /** + * Helper to map the width and height attributes into the aspect-ratio + * property. + * + * If you also map the width/height attributes to width/height (as you should + * for any HTML element that isn't <canvas>) then you should use + * MapImageSizeAttributesInto instead, passing MapAspectRatio::Yes instead, as + * that'd be faster. + */ + static void MapAspectRatioInto(mozilla::MappedDeclarationsBuilder&); + + /** + * Helper to map `width` attribute into a style struct. + * + * @param aAttributes the list of attributes to map + * @param aData the returned rule data [INOUT] + * @see GetAttributeMappingFunction + */ + static void MapWidthAttributeInto(mozilla::MappedDeclarationsBuilder&); + + /** + * Helper to map `height` attribute. + * @see GetAttributeMappingFunction + */ + static void MapHeightAttributeInto(mozilla::MappedDeclarationsBuilder&); + /** + * Helper to map the background attribute + * @see GetAttributeMappingFunction + */ + static void MapBackgroundInto(mozilla::MappedDeclarationsBuilder&); + /** + * Helper to map the bgcolor attribute + * @see GetAttributeMappingFunction + */ + static void MapBGColorInto(mozilla::MappedDeclarationsBuilder&); + /** + * Helper to map the background attributes (currently background and bgcolor) + * @see GetAttributeMappingFunction + */ + static void MapBackgroundAttributesInto(mozilla::MappedDeclarationsBuilder&); + /** + * Helper to map the scrolling attribute on FRAME and IFRAME. + * @see GetAttributeMappingFunction + */ + static void MapScrollingAttributeInto(mozilla::MappedDeclarationsBuilder&); + + // Form Helper Routines + /** + * Find an ancestor of this content node which is a form (could be null) + * @param aCurrentForm the current form for this node. If this is + * non-null, and no ancestor form is found, and the current form is in + * a connected subtree with the node, the current form will be + * returned. This is needed to handle cases when HTML elements have a + * current form that they're not descendants of. + * @note This method should not be called if the element has a form attribute. + */ + mozilla::dom::HTMLFormElement* FindAncestorForm( + mozilla::dom::HTMLFormElement* aCurrentForm = nullptr); + + /** + * See if the document being tested has nav-quirks mode enabled. + * @param doc the document + */ + static bool InNavQuirksMode(Document*); + + /** + * Gets the absolute URI value of an attribute, by resolving any relative + * URIs in the attribute against the baseuri of the element. If the attribute + * isn't a relative URI the value of the attribute is returned as is. Only + * works for attributes in null namespace. + * + * @param aAttr name of attribute. + * @param aBaseAttr name of base attribute. + * @param aResult result value [out] + */ + void GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, nsAString& aResult) const; + + /** + * Gets the absolute URI values of an attribute, by resolving any relative + * URIs in the attribute against the baseuri of the element. If a substring + * isn't a relative URI, the substring is returned as is. Only works for + * attributes in null namespace. + */ + bool GetURIAttr(nsAtom* aAttr, nsAtom* aBaseAttr, nsIURI** aURI) const; + + bool IsHidden() const { return HasAttr(nsGkAtoms::hidden); } + + bool IsLabelable() const override; + + static bool MatchLabelsElement(Element* aElement, int32_t aNamespaceID, + nsAtom* aAtom, void* aData); + + already_AddRefed<nsINodeList> Labels(); + + static bool LegacyTouchAPIEnabled(JSContext* aCx, JSObject* aObj); + + static inline bool CanHaveName(nsAtom* aTag) { + return aTag == nsGkAtoms::img || aTag == nsGkAtoms::form || + aTag == nsGkAtoms::embed || aTag == nsGkAtoms::object; + } + static inline bool ShouldExposeNameAsHTMLDocumentProperty(Element* aElement) { + return aElement->IsHTMLElement() && + CanHaveName(aElement->NodeInfo()->NameAtom()); + } + static inline bool ShouldExposeIdAsHTMLDocumentProperty(Element* aElement) { + if (aElement->IsHTMLElement(nsGkAtoms::object)) { + return true; + } + + // Per spec, <img> is exposed by id only if it also has a nonempty + // name (which doesn't have to match the id or anything). + // HasName() is true precisely when name is nonempty. + return aElement->IsHTMLElement(nsGkAtoms::img) && aElement->HasName(); + } + + virtual inline void ResultForDialogSubmit(nsAString& aResult) { + GetAttr(nsGkAtoms::value, aResult); + } + + // <https://html.spec.whatwg.org/#fetch-priority-attribute>. + static mozilla::dom::FetchPriority ToFetchPriority(const nsAString& aValue); + + void GetFetchPriority(nsAString& aFetchPriority) const; + + void SetFetchPriority(const nsAString& aFetchPriority) { + SetHTMLAttr(nsGkAtoms::fetchpriority, aFetchPriority); + } + + protected: + mozilla::dom::FetchPriority GetFetchPriority() const; + + static void ParseFetchPriority(const nsAString& aValue, nsAttrValue& aResult); + + private: + /** + * Add/remove this element to the documents name cache + */ + void AddToNameTable(nsAtom* aName); + void RemoveFromNameTable(); + + /** + * Register or unregister an access key to this element based on the + * accesskey attribute. + */ + void RegUnRegAccessKey(bool aDoReg) override { + if (!HasFlag(NODE_HAS_ACCESSKEY)) { + return; + } + + nsStyledElement::RegUnRegAccessKey(aDoReg); + } + + protected: + void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + // TODO: Convert AfterSetAttr to MOZ_CAN_RUN_SCRIPT and get rid of + // kungFuDeathGrip in it. + MOZ_CAN_RUN_SCRIPT_BOUNDARY void AfterSetAttr( + int32_t aNamespaceID, nsAtom* aName, const nsAttrValue* aValue, + const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + + MOZ_CAN_RUN_SCRIPT void AfterSetPopoverAttr(); + + mozilla::EventListenerManager* GetEventListenerManagerForAttr( + nsAtom* aAttrName, bool* aDefer) override; + + /** + * Handles dispatching a simulated click on `this` on space or enter. + * TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230) + */ + MOZ_CAN_RUN_SCRIPT_BOUNDARY void HandleKeyboardActivation( + mozilla::EventChainPostVisitor&); + + /** Dispatch a simulated mouse click by keyboard to the given element. */ + MOZ_CAN_RUN_SCRIPT static nsresult DispatchSimulatedClick( + nsGenericHTMLElement* aElement, bool aIsTrusted, + nsPresContext* aPresContext); + + /** + * Create a URI for the given aURISpec string. + * Returns INVALID_STATE_ERR and nulls *aURI if aURISpec is empty + * and the document's URI matches the element's base URI. + */ + nsresult NewURIFromString(const nsAString& aURISpec, nsIURI** aURI); + + void GetHTMLAttr(nsAtom* aName, nsAString& aResult) const { + GetAttr(aName, aResult); + } + void GetHTMLAttr(nsAtom* aName, mozilla::dom::DOMString& aResult) const { + GetAttr(aName, aResult); + } + void GetHTMLEnumAttr(nsAtom* aName, nsAString& aResult) const { + GetEnumAttr(aName, nullptr, aResult); + } + void GetHTMLURIAttr(nsAtom* aName, nsAString& aResult) const { + GetURIAttr(aName, nullptr, aResult); + } + + void SetHTMLAttr(nsAtom* aName, const nsAString& aValue) { + SetAttr(kNameSpaceID_None, aName, aValue, true); + } + void SetHTMLAttr(nsAtom* aName, const nsAString& aValue, + mozilla::ErrorResult& aError) { + SetAttr(aName, aValue, aError); + } + void SetHTMLAttr(nsAtom* aName, const nsAString& aValue, + nsIPrincipal* aTriggeringPrincipal, + mozilla::ErrorResult& aError) { + SetAttr(aName, aValue, aTriggeringPrincipal, aError); + } + void UnsetHTMLAttr(nsAtom* aName, mozilla::ErrorResult& aError) { + UnsetAttr(aName, aError); + } + void SetHTMLBoolAttr(nsAtom* aName, bool aValue, + mozilla::ErrorResult& aError) { + if (aValue) { + SetHTMLAttr(aName, u""_ns, aError); + } else { + UnsetHTMLAttr(aName, aError); + } + } + template <typename T> + void SetHTMLIntAttr(nsAtom* aName, T aValue, mozilla::ErrorResult& aError) { + nsAutoString value; + value.AppendInt(aValue); + + SetHTMLAttr(aName, value, aError); + } + + /** + * Gets the integer-value of an attribute, returns specified default value + * if the attribute isn't set or isn't set to an integer. Only works for + * attributes in null namespace. + * + * @param aAttr name of attribute. + * @param aDefault default-value to return if attribute isn't set. + */ + int32_t GetIntAttr(nsAtom* aAttr, int32_t aDefault) const; + + /** + * Sets value of attribute to specified integer. Only works for attributes + * in null namespace. + * + * @param aAttr name of attribute. + * @param aValue Integer value of attribute. + */ + nsresult SetIntAttr(nsAtom* aAttr, int32_t aValue); + + /** + * Gets the unsigned integer-value of an attribute, returns specified default + * value if the attribute isn't set or isn't set to an integer. Only works for + * attributes in null namespace. + * + * @param aAttr name of attribute. + * @param aDefault default-value to return if attribute isn't set. + */ + uint32_t GetUnsignedIntAttr(nsAtom* aAttr, uint32_t aDefault) const; + + /** + * Sets value of attribute to specified unsigned integer. Only works for + * attributes in null namespace. + * + * @param aAttr name of attribute. + * @param aValue Integer value of attribute. + * @param aDefault Default value (in case value is out of range). If the spec + * doesn't provide one, should be 1 if the value is limited to + * nonzero values, and 0 otherwise. + */ + void SetUnsignedIntAttr(nsAtom* aName, uint32_t aValue, uint32_t aDefault, + mozilla::ErrorResult& aError) { + nsAutoString value; + if (aValue > INT32_MAX) { + value.AppendInt(aDefault); + } else { + value.AppendInt(aValue); + } + + SetHTMLAttr(aName, value, aError); + } + + /** + * Gets the unsigned integer-value of an attribute that is stored as a + * dimension (i.e. could be an integer or a percentage), returns specified + * default value if the attribute isn't set or isn't set to a dimension. Only + * works for attributes in null namespace. + * + * @param aAttr name of attribute. + * @param aDefault default-value to return if attribute isn't set. + */ + uint32_t GetDimensionAttrAsUnsignedInt(nsAtom* aAttr, + uint32_t aDefault) const; + + enum class Reflection { + Unlimited, + OnlyPositive, + }; + + /** + * Sets value of attribute to specified double. Only works for attributes + * in null namespace. + * + * Implements + * https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-double + * + * @param aAttr name of attribute. + * @param aValue Double value of attribute. + */ + template <Reflection Limited = Reflection::Unlimited> + void SetDoubleAttr(nsAtom* aAttr, double aValue, mozilla::ErrorResult& aRv) { + // 1. If the reflected IDL attribute is limited to only positive numbers and + // the given value is not greater than 0, then return. + if (Limited == Reflection::OnlyPositive && aValue <= 0) { + return; + } + + // 2. Run this's set the content attribute with the given value, converted + // to the best representation of the number as a floating-point number. + nsAutoString value; + value.AppendFloat(aValue); + + SetHTMLAttr(aAttr, value, aRv); + } + + /** + * Locates the EditorBase associated with this node. In general this is + * equivalent to GetEditorInternal(), but for designmode or contenteditable, + * this may need to get an editor that's not actually on this element's + * associated TextControlFrame. This is used by the spellchecking routines + * to get the editor affected by changing the spellcheck attribute on this + * node. + */ + virtual already_AddRefed<mozilla::EditorBase> GetAssociatedEditor(); + + /** + * Get the frame's offset information for offsetTop/Left/Width/Height. + * Returns the parent the offset is relative to. + * @note This method flushes pending notifications (FlushType::Layout). + * @param aRect the offset information [OUT] + */ + mozilla::dom::Element* GetOffsetRect(mozilla::CSSIntRect& aRect); + + /** + * Ensures all editors associated with a subtree are synced, for purposes of + * spellchecking. + */ + static void SyncEditorsOnSubtree(nsIContent* content); + + enum ContentEditableTristate { eInherit = -1, eFalse = 0, eTrue = 1 }; + + /** + * Returns eTrue if the element has a contentEditable attribute and its value + * is "true" or an empty string. Returns eFalse if the element has a + * contentEditable attribute and its value is "false". Otherwise returns + * eInherit. + */ + ContentEditableTristate GetContentEditableValue() const { + static const Element::AttrValuesArray values[] = { + nsGkAtoms::_false, nsGkAtoms::_true, nsGkAtoms::_empty, nullptr}; + + if (!MayHaveContentEditableAttr()) return eInherit; + + int32_t value = FindAttrValueIn( + kNameSpaceID_None, nsGkAtoms::contenteditable, values, eIgnoreCase); + + return value > 0 ? eTrue : (value == 0 ? eFalse : eInherit); + } + + // Used by A, AREA, LINK, and STYLE. + already_AddRefed<nsIURI> GetHrefURIForAnchors() const; + + private: + void ChangeEditableState(int32_t aChange); +}; + +namespace mozilla::dom { +class HTMLFieldSetElement; +} // namespace mozilla::dom + +#define HTML_ELEMENT_FLAG_BIT(n_) \ + NODE_FLAG_BIT(ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + (n_)) + +// HTMLElement specific bits +enum { + // Used to handle keyboard activation. + HTML_ELEMENT_ACTIVE_FOR_KEYBOARD = HTML_ELEMENT_FLAG_BIT(0), + // Similar to HTMLInputElement's mInhibitRestoration, used to prevent + // form-associated custom elements not created by a network parser from + // being restored. + HTML_ELEMENT_INHIBIT_RESTORATION = HTML_ELEMENT_FLAG_BIT(1), + + // Remaining bits are type specific. + HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET = + ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 2, +}; + +ASSERT_NODE_FLAGS_SPACE(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET); + +#define FORM_ELEMENT_FLAG_BIT(n_) \ + NODE_FLAG_BIT(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + (n_)) + +// Form element specific bits +enum { + // If this flag is set on an nsGenericHTMLFormElement or an HTMLImageElement, + // that means that we have added ourselves to our mForm. It's possible to + // have a non-null mForm, but not have this flag set. That happens when the + // form is set via the content sink. + ADDED_TO_FORM = FORM_ELEMENT_FLAG_BIT(0), + + // If this flag is set on an nsGenericHTMLFormElement or an HTMLImageElement, + // that means that its form is in the process of being unbound from the tree, + // and this form element hasn't re-found its form in + // nsGenericHTMLFormElement::UnbindFromTree yet. + MAYBE_ORPHAN_FORM_ELEMENT = FORM_ELEMENT_FLAG_BIT(1), + + // If this flag is set on an nsGenericHTMLElement or an HTMLImageElement, then + // the element might be in the past names map of its form. + MAY_BE_IN_PAST_NAMES_MAP = FORM_ELEMENT_FLAG_BIT(2) +}; + +// NOTE: I don't think it's possible to have both ADDED_TO_FORM and +// MAYBE_ORPHAN_FORM_ELEMENT set at the same time, so if it becomes an issue we +// can probably merge them into the same bit. --bz + +ASSERT_NODE_FLAGS_SPACE(HTML_ELEMENT_TYPE_SPECIFIC_BITS_OFFSET + 3); + +#undef FORM_ELEMENT_FLAG_BIT + +/** + * A helper class for form elements that can contain children + */ +class nsGenericHTMLFormElement : public nsGenericHTMLElement { + public: + nsGenericHTMLFormElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo); + + // nsIContent + void SaveSubtreeState() override; + nsresult BindToTree(BindContext&, nsINode& aParent) override; + void UnbindFromTree(bool aNullParent = true) override; + + /** + * This callback is called by a fieldest on all its elements whenever its + * disabled attribute is changed so the element knows its disabled state + * might have changed. + * + * @note Classes redefining this method should not do any content + * state updates themselves but should just make sure to call into + * nsGenericHTMLFormElement::FieldSetDisabledChanged. + */ + virtual void FieldSetDisabledChanged(bool aNotify); + + void FieldSetFirstLegendChanged(bool aNotify) { UpdateFieldSet(aNotify); } + + /** + * This callback is called by a fieldset on all it's elements when it's being + * destroyed. When called, the elements should check that aFieldset is there + * first parent fieldset and null mFieldset in that case only. + * + * @param aFieldSet The fieldset being removed. + */ + void ForgetFieldSet(nsIContent* aFieldset); + + void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete); + + /** + * Get the layout history object for a particular piece of content. + * + * @param aRead if true, won't return a layout history state if the + * layout history state is empty. + * @return the history state object + */ + already_AddRefed<nsILayoutHistoryState> GetLayoutHistory(bool aRead); + + // Sets the user-interacted flag in + // https://html.spec.whatwg.org/#user-interacted, if it applies. + virtual void SetUserInteracted(bool aNotify) {} + + protected: + virtual ~nsGenericHTMLFormElement() = default; + + void BeforeSetAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValue* aValue, bool aNotify) override; + + void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, const nsAttrValue* aOldValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) override; + + virtual void BeforeSetForm(mozilla::dom::HTMLFormElement* aForm, + bool aBindToTree) {} + + virtual void AfterClearForm(bool aUnbindOrDelete) {} + + /** + * Check our disabled content attribute and fieldset's (if it exists) disabled + * state to decide whether our disabled flag should be toggled. + */ + virtual void UpdateDisabledState(bool aNotify); + bool IsReadOnlyInternal() const final; + + virtual void SetFormInternal(mozilla::dom::HTMLFormElement* aForm, + bool aBindToTree) {} + + virtual mozilla::dom::HTMLFormElement* GetFormInternal() const { + return nullptr; + } + + virtual mozilla::dom::HTMLFieldSetElement* GetFieldSetInternal() const { + return nullptr; + } + + virtual void SetFieldSetInternal( + mozilla::dom::HTMLFieldSetElement* aFieldset) {} + + /** + * This method will update the form owner, using @form or looking to a parent. + * + * @param aBindToTree Whether the element is being attached to the tree. + * @param aFormIdElement The element associated with the id in @form. If + * aBindToTree is false, aFormIdElement *must* contain the element associated + * with the id in @form. Otherwise, it *must* be null. + * + * @note Callers of UpdateFormOwner have to be sure the element is in a + * document (GetUncomposedDoc() != nullptr). + */ + virtual void UpdateFormOwner(bool aBindToTree, Element* aFormIdElement); + + /** + * This method will update mFieldset and set it to the first fieldset parent. + */ + void UpdateFieldSet(bool aNotify); + + /** + * Add a form id observer which will observe when the element with the id in + * @form will change. + * + * @return The element associated with the current id in @form (may be null). + */ + Element* AddFormIdObserver(); + + /** + * Remove the form id observer. + */ + void RemoveFormIdObserver(); + + /** + * This method is a a callback for IDTargetObserver (from Document). + * It will be called each time the element associated with the id in @form + * changes. + */ + static bool FormIdUpdated(Element* aOldElement, Element* aNewElement, + void* aData); + + // Returns true if the event should not be handled from GetEventTargetParent + bool IsElementDisabledForEvents(mozilla::WidgetEvent* aEvent, + nsIFrame* aFrame); + + /** + * Returns if the control can be disabled. + */ + virtual bool CanBeDisabled() const { return false; } + + /** + * Returns if the readonly attribute applies. + */ + virtual bool DoesReadOnlyApply() const { return false; } + + /** + * Returns true if the element is a form associated element. + * See https://html.spec.whatwg.org/#form-associated-element. + */ + virtual bool IsFormAssociatedElement() const { return false; } + + /** + * Save to presentation state. The form element will determine whether it + * has anything to save and if so, create an entry in the layout history for + * its pres context. + */ + virtual void SaveState() {} +}; + +class nsGenericHTMLFormControlElement : public nsGenericHTMLFormElement, + public nsIFormControl { + public: + nsGenericHTMLFormControlElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, FormControlType); + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMPL_FROMNODE_HELPER(nsGenericHTMLFormControlElement, + IsHTMLFormControlElement()) + + // nsINode + nsINode* GetScopeChainParent() const override; + bool IsHTMLFormControlElement() const final { return true; } + + // nsIContent + IMEState GetDesiredIMEState() override; + + // nsGenericHTMLElement + // autocapitalize attribute support + void GetAutocapitalize(nsAString& aValue) const override; + bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + + // EventTarget + void GetEventTargetParent(mozilla::EventChainPreVisitor& aVisitor) override; + nsresult PreHandleEvent(mozilla::EventChainVisitor& aVisitor) override; + + // nsIFormControl + mozilla::dom::HTMLFieldSetElement* GetFieldSet() override; + mozilla::dom::HTMLFormElement* GetForm() const override { return mForm; } + void SetForm(mozilla::dom::HTMLFormElement* aForm) override; + void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) override; + + protected: + virtual ~nsGenericHTMLFormControlElement(); + + // Element + bool IsLabelable() const override; + + // nsGenericHTMLFormElement + bool CanBeDisabled() const override; + bool DoesReadOnlyApply() const override; + void SetFormInternal(mozilla::dom::HTMLFormElement* aForm, + bool aBindToTree) override; + mozilla::dom::HTMLFormElement* GetFormInternal() const override; + mozilla::dom::HTMLFieldSetElement* GetFieldSetInternal() const override; + void SetFieldSetInternal( + mozilla::dom::HTMLFieldSetElement* aFieldset) override; + bool IsFormAssociatedElement() const override { return true; } + + /** + * Update our required/optional flags to match the given aIsRequired boolean. + */ + void UpdateRequiredState(bool aIsRequired, bool aNotify); + + bool IsAutocapitalizeInheriting() const; + + nsresult SubmitDirnameDir(mozilla::dom::FormData* aFormData); + + /** The form that contains this control */ + mozilla::dom::HTMLFormElement* mForm; + + /* This is a pointer to our closest fieldset parent if any */ + mozilla::dom::HTMLFieldSetElement* mFieldSet; +}; + +enum class PopoverTargetAction : uint8_t { + Toggle, + Show, + Hide, +}; + +class nsGenericHTMLFormControlElementWithState + : public nsGenericHTMLFormControlElement { + public: + nsGenericHTMLFormControlElementWithState( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser, FormControlType); + + bool IsGenericHTMLFormControlElementWithState() const final { return true; } + NS_IMPL_FROMNODE_HELPER(nsGenericHTMLFormControlElementWithState, + IsGenericHTMLFormControlElementWithState()) + + // Element + bool ParseAttribute(int32_t aNamespaceID, nsAtom* aAttribute, + const nsAString& aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + nsAttrValue& aResult) override; + + // PopoverInvokerElement + mozilla::dom::Element* GetPopoverTargetElement() const; + void SetPopoverTargetElement(mozilla::dom::Element*); + void GetPopoverTargetAction(nsAString& aValue) const { + GetHTMLEnumAttr(nsGkAtoms::popovertargetaction, aValue); + } + void SetPopoverTargetAction(const nsAString& aValue) { + SetHTMLAttr(nsGkAtoms::popovertargetaction, aValue); + } + + // InvokerElement + mozilla::dom::Element* GetInvokeTargetElement() const; + void SetInvokeTargetElement(mozilla::dom::Element*); + void GetInvokeAction(nsAString& aValue) const; + nsAtom* GetInvokeAction() const; + void SetInvokeAction(const nsAString& aValue) { + SetHTMLAttr(nsGkAtoms::invokeaction, aValue); + } + + /** + * https://html.spec.whatwg.org/#popover-target-attribute-activation-behavior + */ + MOZ_CAN_RUN_SCRIPT void HandlePopoverTargetAction(); + + MOZ_CAN_RUN_SCRIPT void HandleInvokeTargetAction(); + + /** + * Get the presentation state for a piece of content, or create it if it does + * not exist. Generally used by SaveState(). + */ + mozilla::PresState* GetPrimaryPresState(); + + /** + * Called when we have been cloned and adopted, and the information of the + * node has been changed. + */ + void NodeInfoChanged(Document* aOldDoc) override; + + void GetFormAction(nsString& aValue); + + protected: + /** + * Restore from presentation state. You pass in the presentation state for + * this form control (generated with GenerateStateKey() + "-C") and the form + * control will grab its state from there. + * + * @param aState the pres state to use to restore the control + * @return true if the form control was a checkbox and its + * checked state was restored, false otherwise. + */ + virtual bool RestoreState(mozilla::PresState* aState) { return false; } + + /** + * Restore the state for a form control in response to the element being + * inserted into the document by the parser. Ends up calling RestoreState(). + * + * GenerateStateKey() must already have been called. + * + * @return false if RestoreState() was not called, the return + * value of RestoreState() otherwise. + */ + bool RestoreFormControlState(); + + /* Generates the state key for saving the form state in the session if not + computed already. The result is stored in mStateKey. */ + void GenerateStateKey(); + + int32_t GetParserInsertedControlNumberForStateKey() const override { + return mControlNumber; + } + + /* Used to store the key to that element in the session. Is void until + GenerateStateKey has been used */ + nsCString mStateKey; + + // A number for this form control that is unique within its owner document. + // This is only set to a number for elements inserted into the document by + // the parser from the network. Otherwise, it is -1. + int32_t mControlNumber; +}; + +#define NS_INTERFACE_MAP_ENTRY_IF_TAG(_interface, _tag) \ + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(_interface, \ + mNodeInfo->Equals(nsGkAtoms::_tag)) + +namespace mozilla::dom { + +using HTMLContentCreatorFunction = + nsGenericHTMLElement* (*)(already_AddRefed<mozilla::dom::NodeInfo>&&, + mozilla::dom::FromParser); + +} // namespace mozilla::dom + +/** + * A macro to declare the NS_NewHTMLXXXElement() functions. + */ +#define NS_DECLARE_NS_NEW_HTML_ELEMENT(_elementName) \ + namespace mozilla { \ + namespace dom { \ + class HTML##_elementName##Element; \ + } \ + } \ + nsGenericHTMLElement* NS_NewHTML##_elementName##Element( \ + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, \ + mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER); + +#define NS_DECLARE_NS_NEW_HTML_ELEMENT_AS_SHARED(_elementName) \ + inline nsGenericHTMLElement* NS_NewHTML##_elementName##Element( \ + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, \ + mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER) { \ + return NS_NewHTMLSharedElement(std::move(aNodeInfo), aFromParser); \ + } + +/** + * A macro to implement the NS_NewHTMLXXXElement() functions. + */ +#define NS_IMPL_NS_NEW_HTML_ELEMENT(_elementName) \ + nsGenericHTMLElement* NS_NewHTML##_elementName##Element( \ + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, \ + mozilla::dom::FromParser aFromParser) { \ + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); \ + auto* nim = nodeInfo->NodeInfoManager(); \ + MOZ_ASSERT(nim); \ + return new (nim) \ + mozilla::dom::HTML##_elementName##Element(nodeInfo.forget()); \ + } + +#define NS_IMPL_NS_NEW_HTML_ELEMENT_CHECK_PARSER(_elementName) \ + nsGenericHTMLElement* NS_NewHTML##_elementName##Element( \ + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, \ + mozilla::dom::FromParser aFromParser) { \ + RefPtr<mozilla::dom::NodeInfo> nodeInfo(aNodeInfo); \ + auto* nim = nodeInfo->NodeInfoManager(); \ + MOZ_ASSERT(nim); \ + return new (nim) mozilla::dom::HTML##_elementName##Element( \ + nodeInfo.forget(), aFromParser); \ + } + +// Here, we expand 'NS_DECLARE_NS_NEW_HTML_ELEMENT()' by hand. +// (Calling the macro directly (with no args) produces compiler warnings.) +nsGenericHTMLElement* NS_NewHTMLElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER); + +// Distinct from the above in order to have function pointer that compared +// unequal to a function pointer to the above. +nsGenericHTMLElement* NS_NewCustomElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser = mozilla::dom::NOT_FROM_PARSER); + +NS_DECLARE_NS_NEW_HTML_ELEMENT(Shared) +NS_DECLARE_NS_NEW_HTML_ELEMENT(SharedList) + +NS_DECLARE_NS_NEW_HTML_ELEMENT(Anchor) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Area) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Audio) +NS_DECLARE_NS_NEW_HTML_ELEMENT(BR) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Body) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Button) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Canvas) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Content) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Mod) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Data) +NS_DECLARE_NS_NEW_HTML_ELEMENT(DataList) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Details) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Dialog) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Div) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Embed) +NS_DECLARE_NS_NEW_HTML_ELEMENT(FieldSet) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Font) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Form) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Frame) +NS_DECLARE_NS_NEW_HTML_ELEMENT(FrameSet) +NS_DECLARE_NS_NEW_HTML_ELEMENT(HR) +NS_DECLARE_NS_NEW_HTML_ELEMENT_AS_SHARED(Head) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Heading) +NS_DECLARE_NS_NEW_HTML_ELEMENT_AS_SHARED(Html) +NS_DECLARE_NS_NEW_HTML_ELEMENT(IFrame) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Image) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Input) +NS_DECLARE_NS_NEW_HTML_ELEMENT(LI) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Label) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Legend) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Link) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Marquee) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Map) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Menu) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Meta) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Meter) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Object) +NS_DECLARE_NS_NEW_HTML_ELEMENT(OptGroup) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Option) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Output) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Paragraph) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Picture) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Pre) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Progress) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Script) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Select) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Slot) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Source) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Span) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Style) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Summary) +NS_DECLARE_NS_NEW_HTML_ELEMENT(TableCaption) +NS_DECLARE_NS_NEW_HTML_ELEMENT(TableCell) +NS_DECLARE_NS_NEW_HTML_ELEMENT(TableCol) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Table) +NS_DECLARE_NS_NEW_HTML_ELEMENT(TableRow) +NS_DECLARE_NS_NEW_HTML_ELEMENT(TableSection) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Tbody) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Template) +NS_DECLARE_NS_NEW_HTML_ELEMENT(TextArea) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Tfoot) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Thead) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Time) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Title) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Track) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Unknown) +NS_DECLARE_NS_NEW_HTML_ELEMENT(Video) + +#endif /* nsGenericHTMLElement_h___ */ diff --git a/dom/html/nsGenericHTMLFrameElement.cpp b/dom/html/nsGenericHTMLFrameElement.cpp new file mode 100644 index 0000000000..ae2c4dcce5 --- /dev/null +++ b/dom/html/nsGenericHTMLFrameElement.cpp @@ -0,0 +1,363 @@ +/* -*- 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 "nsGenericHTMLFrameElement.h" + +#include "mozilla/dom/Document.h" +#include "mozilla/dom/HTMLIFrameElement.h" +#include "mozilla/dom/XULFrameElement.h" +#include "mozilla/dom/BrowserBridgeChild.h" +#include "mozilla/dom/WindowProxyHolder.h" +#include "mozilla/Preferences.h" +#include "mozilla/PresShell.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/ErrorResult.h" +#include "nsAttrValueInlines.h" +#include "nsContentUtils.h" +#include "nsIDocShell.h" +#include "nsIFrame.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIPermissionManager.h" +#include "nsPresContext.h" +#include "nsServiceManagerUtils.h" +#include "nsSubDocumentFrame.h" +#include "nsAttrValueOrString.h" + +using namespace mozilla; +using namespace mozilla::dom; + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsGenericHTMLFrameElement) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(nsGenericHTMLFrameElement, + nsGenericHTMLElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameLoader) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBrowserElementAPI) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(nsGenericHTMLFrameElement, + nsGenericHTMLElement) + if (tmp->mFrameLoader) { + tmp->mFrameLoader->Destroy(); + } + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFrameLoader) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mBrowserElementAPI) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED( + nsGenericHTMLFrameElement, nsGenericHTMLElement, nsFrameLoaderOwner, + nsIDOMMozBrowserFrame, nsIMozBrowserFrame, nsGenericHTMLFrameElement) + +NS_IMETHODIMP +nsGenericHTMLFrameElement::GetMozbrowser(bool* aValue) { + *aValue = GetBoolAttr(nsGkAtoms::mozbrowser); + return NS_OK; +} +NS_IMETHODIMP +nsGenericHTMLFrameElement::SetMozbrowser(bool aValue) { + return SetBoolAttr(nsGkAtoms::mozbrowser, aValue); +} + +int32_t nsGenericHTMLFrameElement::TabIndexDefault() { return 0; } + +nsGenericHTMLFrameElement::~nsGenericHTMLFrameElement() { + if (mFrameLoader) { + mFrameLoader->Destroy(); + } +} + +Document* nsGenericHTMLFrameElement::GetContentDocument( + nsIPrincipal& aSubjectPrincipal) { + RefPtr<BrowsingContext> bc = GetContentWindowInternal(); + if (!bc) { + return nullptr; + } + + nsPIDOMWindowOuter* window = bc->GetDOMWindow(); + if (!window) { + // Either our browsing context contents are out-of-process (in which case + // clearly this is a cross-origin call and we should return null), or our + // browsing context is torn-down enough to no longer have a window or a + // document, and we should still return null. + return nullptr; + } + Document* doc = window->GetDoc(); + if (!doc) { + return nullptr; + } + + // Return null for cross-origin contentDocument. + if (!aSubjectPrincipal.SubsumesConsideringDomain(doc->NodePrincipal())) { + return nullptr; + } + return doc; +} + +BrowsingContext* nsGenericHTMLFrameElement::GetContentWindowInternal() { + EnsureFrameLoader(); + + if (!mFrameLoader) { + return nullptr; + } + + if (mFrameLoader->DepthTooGreat()) { + // Claim to have no contentWindow + return nullptr; + } + + RefPtr<BrowsingContext> bc = mFrameLoader->GetBrowsingContext(); + return bc; +} + +Nullable<WindowProxyHolder> nsGenericHTMLFrameElement::GetContentWindow() { + RefPtr<BrowsingContext> bc = GetContentWindowInternal(); + if (!bc) { + return nullptr; + } + return WindowProxyHolder(bc); +} + +void nsGenericHTMLFrameElement::EnsureFrameLoader() { + if (!IsInComposedDoc() || mFrameLoader || OwnerDoc()->IsStaticDocument()) { + // If frame loader is there, we just keep it around, cached + return; + } + + // Strangely enough, this method doesn't actually ensure that the + // frameloader exists. It's more of a best-effort kind of thing. + mFrameLoader = nsFrameLoader::Create(this, mNetworkCreated); +} + +void nsGenericHTMLFrameElement::SwapFrameLoaders( + HTMLIFrameElement& aOtherLoaderOwner, ErrorResult& rv) { + if (&aOtherLoaderOwner == this) { + // nothing to do + return; + } + + aOtherLoaderOwner.SwapFrameLoaders(this, rv); +} + +void nsGenericHTMLFrameElement::SwapFrameLoaders( + XULFrameElement& aOtherLoaderOwner, ErrorResult& rv) { + aOtherLoaderOwner.SwapFrameLoaders(this, rv); +} + +void nsGenericHTMLFrameElement::SwapFrameLoaders( + nsFrameLoaderOwner* aOtherLoaderOwner, mozilla::ErrorResult& rv) { + if (RefPtr<Document> doc = GetComposedDoc()) { + // SwapWithOtherLoader relies on frames being up-to-date. + doc->FlushPendingNotifications(FlushType::Frames); + } + + RefPtr<nsFrameLoader> loader = GetFrameLoader(); + RefPtr<nsFrameLoader> otherLoader = aOtherLoaderOwner->GetFrameLoader(); + if (!loader || !otherLoader) { + rv.Throw(NS_ERROR_NOT_IMPLEMENTED); + return; + } + + rv = loader->SwapWithOtherLoader(otherLoader, this, aOtherLoaderOwner); +} + +void nsGenericHTMLFrameElement::LoadSrc() { + // Waiting for lazy load, do nothing. + if (mLazyLoading) { + return; + } + + EnsureFrameLoader(); + + if (!mFrameLoader) { + return; + } + + bool origSrc = !mSrcLoadHappened; + mSrcLoadHappened = true; + mFrameLoader->LoadFrame(origSrc); +} + +nsresult nsGenericHTMLFrameElement::BindToTree(BindContext& aContext, + nsINode& aParent) { + nsresult rv = nsGenericHTMLElement::BindToTree(aContext, aParent); + NS_ENSURE_SUCCESS(rv, rv); + + if (IsInComposedDoc()) { + NS_ASSERTION(!nsContentUtils::IsSafeToRunScript(), + "Missing a script blocker!"); + + AUTO_PROFILER_LABEL("nsGenericHTMLFrameElement::BindToTree", OTHER); + + // We're in a document now. Kick off the frame load. + LoadSrc(); + } + + // We're now in document and scripts may move us, so clear + // the mNetworkCreated flag. + mNetworkCreated = false; + return rv; +} + +void nsGenericHTMLFrameElement::UnbindFromTree(bool aNullParent) { + if (mFrameLoader) { + // This iframe is being taken out of the document, destroy the + // iframe's frame loader (doing that will tear down the window in + // this iframe). + // XXXbz we really want to only partially destroy the frame + // loader... we don't want to tear down the docshell. Food for + // later bug. + mFrameLoader->Destroy(); + mFrameLoader = nullptr; + } + + nsGenericHTMLElement::UnbindFromTree(aNullParent); +} + +/* static */ +ScrollbarPreference nsGenericHTMLFrameElement::MapScrollingAttribute( + const nsAttrValue* aValue) { + if (aValue && aValue->Type() == nsAttrValue::eEnum) { + auto scrolling = static_cast<ScrollingAttribute>(aValue->GetEnumValue()); + if (scrolling == ScrollingAttribute::Off || + scrolling == ScrollingAttribute::Noscroll || + scrolling == ScrollingAttribute::No) { + return ScrollbarPreference::Never; + } + } + return ScrollbarPreference::Auto; +} + +/* virtual */ +void nsGenericHTMLFrameElement::AfterSetAttr( + int32_t aNameSpaceID, nsAtom* aName, const nsAttrValue* aValue, + const nsAttrValue* aOldValue, nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify) { + if (aValue) { + nsAttrValueOrString value(aValue); + AfterMaybeChangeAttr(aNameSpaceID, aName, &value, aMaybeScriptedPrincipal, + aNotify); + } else { + AfterMaybeChangeAttr(aNameSpaceID, aName, nullptr, aMaybeScriptedPrincipal, + aNotify); + } + + if (aNameSpaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::scrolling) { + if (mFrameLoader) { + ScrollbarPreference pref = MapScrollingAttribute(aValue); + if (nsDocShell* docshell = mFrameLoader->GetExistingDocShell()) { + docshell->SetScrollbarPreference(pref); + } else if (auto* child = mFrameLoader->GetBrowserBridgeChild()) { + // NOTE(emilio): We intentionally don't deal with the + // GetBrowserParent() case, and only deal with the fission iframe + // case. We could make it work, but it's a bit of boilerplate for + // something that we don't use, and we'd need to think how it + // interacts with the scrollbar window flags... + child->SendScrollbarPreferenceChanged(pref); + } + } + } else if (aName == nsGkAtoms::mozbrowser) { + mReallyIsBrowser = !!aValue && XRE_IsParentProcess() && + NodePrincipal()->IsSystemPrincipal(); + } + } + + return nsGenericHTMLElement::AfterSetAttr( + aNameSpaceID, aName, aValue, aOldValue, aMaybeScriptedPrincipal, aNotify); +} + +void nsGenericHTMLFrameElement::OnAttrSetButNotChanged( + int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString& aValue, + bool aNotify) { + AfterMaybeChangeAttr(aNamespaceID, aName, &aValue, nullptr, aNotify); + + return nsGenericHTMLElement::OnAttrSetButNotChanged(aNamespaceID, aName, + aValue, aNotify); +} + +void nsGenericHTMLFrameElement::AfterMaybeChangeAttr( + int32_t aNamespaceID, nsAtom* aName, const nsAttrValueOrString* aValue, + nsIPrincipal* aMaybeScriptedPrincipal, bool aNotify) { + if (aNamespaceID == kNameSpaceID_None) { + if (aName == nsGkAtoms::src) { + mSrcTriggeringPrincipal = nsContentUtils::GetAttrTriggeringPrincipal( + this, aValue ? aValue->String() : u""_ns, aMaybeScriptedPrincipal); + if (!IsHTMLElement(nsGkAtoms::iframe) || !HasAttr(nsGkAtoms::srcdoc)) { + // Don't propagate error here. The attribute was successfully + // set or removed; that's what we should reflect. + LoadSrc(); + } + } else if (aName == nsGkAtoms::name) { + // Propagate "name" to the browsing context per HTML5. + RefPtr<BrowsingContext> bc = + mFrameLoader ? mFrameLoader->GetExtantBrowsingContext() : nullptr; + if (bc) { + MOZ_ALWAYS_SUCCEEDS(bc->SetName(aValue ? aValue->String() : u""_ns)); + } + } + } +} + +void nsGenericHTMLFrameElement::DestroyContent() { + if (mFrameLoader) { + mFrameLoader->Destroy(); + mFrameLoader = nullptr; + } + + nsGenericHTMLElement::DestroyContent(); +} + +nsresult nsGenericHTMLFrameElement::CopyInnerTo(Element* aDest) { + nsresult rv = nsGenericHTMLElement::CopyInnerTo(aDest); + NS_ENSURE_SUCCESS(rv, rv); + + Document* doc = aDest->OwnerDoc(); + if (doc->IsStaticDocument() && mFrameLoader) { + nsGenericHTMLFrameElement* dest = + static_cast<nsGenericHTMLFrameElement*>(aDest); + doc->AddPendingFrameStaticClone(dest, mFrameLoader); + } + + return rv; +} + +bool nsGenericHTMLFrameElement::IsHTMLFocusable(bool aWithMouse, + bool* aIsFocusable, + int32_t* aTabIndex) { + if (nsGenericHTMLElement::IsHTMLFocusable(aWithMouse, aIsFocusable, + aTabIndex)) { + return true; + } + + *aIsFocusable = true; + return false; +} + +/** + * Return true if this frame element really is a mozbrowser. (It + * needs to have the right attributes, and its creator must have the right + * permissions.) + */ +/* [infallible] */ +nsresult nsGenericHTMLFrameElement::GetReallyIsBrowser(bool* aOut) { + *aOut = mReallyIsBrowser; + return NS_OK; +} + +NS_IMETHODIMP +nsGenericHTMLFrameElement::InitializeBrowserAPI() { + MOZ_ASSERT(mFrameLoader); + InitBrowserElementAPI(); + return NS_OK; +} + +NS_IMETHODIMP +nsGenericHTMLFrameElement::DestroyBrowserFrameScripts() { + MOZ_ASSERT(mFrameLoader); + DestroyBrowserElementFrameScripts(); + return NS_OK; +} diff --git a/dom/html/nsGenericHTMLFrameElement.h b/dom/html/nsGenericHTMLFrameElement.h new file mode 100644 index 0000000000..4ac6401721 --- /dev/null +++ b/dom/html/nsGenericHTMLFrameElement.h @@ -0,0 +1,173 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsGenericHTMLFrameElement_h +#define nsGenericHTMLFrameElement_h + +#include "mozilla/Attributes.h" +#include "mozilla/dom/nsBrowserElement.h" + +#include "nsFrameLoader.h" +#include "nsFrameLoaderOwner.h" +#include "nsGenericHTMLElement.h" +#include "nsIMozBrowserFrame.h" + +namespace mozilla { +class ErrorResult; + +namespace dom { +class BrowserParent; +template <typename> +struct Nullable; +class WindowProxyHolder; +class XULFrameElement; +} // namespace dom +} // namespace mozilla + +#define NS_GENERICHTMLFRAMEELEMENT_IID \ + { \ + 0x8190db72, 0xdab0, 0x4d72, { \ + 0x94, 0x26, 0x87, 0x5f, 0x5a, 0x8a, 0x2a, 0xe5 \ + } \ + } + +/** + * A helper class for frame elements + */ +class nsGenericHTMLFrameElement : public nsGenericHTMLElement, + public nsFrameLoaderOwner, + public mozilla::nsBrowserElement, + public nsIMozBrowserFrame { + public: + nsGenericHTMLFrameElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + mozilla::dom::FromParser aFromParser) + : nsGenericHTMLElement(std::move(aNodeInfo)), + mSrcLoadHappened(false), + mNetworkCreated(aFromParser == mozilla::dom::FROM_PARSER_NETWORK), + mBrowserFrameListenersRegistered(false), + mReallyIsBrowser(false) {} + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSIDOMMOZBROWSERFRAME + NS_DECL_NSIMOZBROWSERFRAME + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_GENERICHTMLFRAMEELEMENT_IID) + + // nsIContent + virtual bool IsHTMLFocusable(bool aWithMouse, bool* aIsFocusable, + int32_t* aTabIndex) override; + virtual nsresult BindToTree(BindContext&, nsINode& aParent) override; + virtual void UnbindFromTree(bool aNullParent = true) override; + virtual void DestroyContent() override; + + nsresult CopyInnerTo(mozilla::dom::Element* aDest); + + virtual int32_t TabIndexDefault() override; + + virtual nsIMozBrowserFrame* GetAsMozBrowserFrame() override { return this; } + + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsGenericHTMLFrameElement, + nsGenericHTMLElement) + + void SwapFrameLoaders(mozilla::dom::HTMLIFrameElement& aOtherLoaderOwner, + mozilla::ErrorResult& aError); + + void SwapFrameLoaders(mozilla::dom::XULFrameElement& aOtherLoaderOwner, + mozilla::ErrorResult& aError); + + void SwapFrameLoaders(nsFrameLoaderOwner* aOtherLoaderOwner, + mozilla::ErrorResult& rv); + + /** + * Helper method to map a HTML 'scrolling' attribute value (which can be null) + * to a ScrollbarPreference value value. scrolling="no" (and its synonyms) + * map to Never, and anything else to Auto. + */ + static mozilla::ScrollbarPreference MapScrollingAttribute(const nsAttrValue*); + + nsIPrincipal* GetSrcTriggeringPrincipal() const { + return mSrcTriggeringPrincipal; + } + + // Needed for nsBrowserElement + already_AddRefed<nsFrameLoader> GetFrameLoader() override { + return nsFrameLoaderOwner::GetFrameLoader(); + } + + protected: + virtual ~nsGenericHTMLFrameElement(); + + // This doesn't really ensure a frame loader in all cases, only when + // it makes sense. + void EnsureFrameLoader(); + void LoadSrc(); + Document* GetContentDocument(nsIPrincipal& aSubjectPrincipal); + mozilla::dom::Nullable<mozilla::dom::WindowProxyHolder> GetContentWindow(); + + virtual void AfterSetAttr(int32_t aNameSpaceID, nsAtom* aName, + const nsAttrValue* aValue, + const nsAttrValue* aOldValue, + nsIPrincipal* aSubjectPrincipal, + bool aNotify) override; + virtual void OnAttrSetButNotChanged(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString& aValue, + bool aNotify) override; + + nsCOMPtr<nsIPrincipal> mSrcTriggeringPrincipal; + + /** + * True if we have already loaded the frame's original src + */ + bool mSrcLoadHappened; + + /** + * True when the element is created by the parser using the + * NS_FROM_PARSER_NETWORK flag. + * If the element is modified, it may lose the flag. + */ + bool mNetworkCreated; + + bool mBrowserFrameListenersRegistered; + bool mReallyIsBrowser; + + // This flag is only used by <iframe>. See HTMLIFrameElement:: + // FullscreenFlag() for details. It is placed here so that we + // do not bloat any struct. + bool mFullscreenFlag = false; + + /** + * Represents the iframe is deferred loading until this element gets visible. + * We just do not load if set and leave specific elements to set it (see + * HTMLIFrameElement). + */ + bool mLazyLoading = false; + + private: + void GetManifestURL(nsAString& aOut); + + /** + * This function is called by AfterSetAttr and OnAttrSetButNotChanged. + * It will be called whether the value is being set or unset. + * + * @param aNamespaceID the namespace of the attr being set + * @param aName the localname of the attribute being set + * @param aValue the value being set or null if the value is being unset + * @param aNotify Whether we plan to notify document observers. + */ + void AfterMaybeChangeAttr(int32_t aNamespaceID, nsAtom* aName, + const nsAttrValueOrString* aValue, + nsIPrincipal* aMaybeScriptedPrincipal, + bool aNotify); + + mozilla::dom::BrowsingContext* GetContentWindowInternal(); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsGenericHTMLFrameElement, + NS_GENERICHTMLFRAMEELEMENT_IID) + +#endif // nsGenericHTMLFrameElement_h diff --git a/dom/html/nsHTMLContentSink.cpp b/dom/html/nsHTMLContentSink.cpp new file mode 100644 index 0000000000..0c22b3e9aa --- /dev/null +++ b/dom/html/nsHTMLContentSink.cpp @@ -0,0 +1,937 @@ +/* -*- 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/. */ + +/** + * This file is near-OBSOLETE. It is used for about:blank only and for the + * HTML element factory. + * Don't bother adding new stuff in this file. + */ + +#include "mozilla/ArrayUtils.h" + +#include "nsContentSink.h" +#include "nsCOMPtr.h" +#include "nsHTMLTags.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsIHTMLContentSink.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIURI.h" +#include "mozilla/dom/NodeInfo.h" +#include "mozilla/dom/ScriptLoader.h" +#include "nsCRT.h" +#include "prtime.h" +#include "mozilla/Logging.h" +#include "nsIContent.h" +#include "mozilla/dom/CustomElementRegistry.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/MutationObservers.h" +#include "mozilla/Preferences.h" + +#include "nsGenericHTMLElement.h" + +#include "nsIScriptElement.h" + +#include "nsDocElementCreatedNotificationRunner.h" +#include "nsGkAtoms.h" +#include "nsContentUtils.h" +#include "nsIChannel.h" +#include "mozilla/dom/Document.h" +#include "nsStubDocumentObserver.h" +#include "nsHTMLDocument.h" +#include "nsTArray.h" +#include "nsTextFragment.h" +#include "nsIScriptGlobalObject.h" +#include "nsNameSpaceManager.h" + +#include "nsError.h" +#include "nsContentPolicyUtils.h" +#include "nsIDocShell.h" +#include "nsIScriptContext.h" + +#include "nsLayoutCID.h" + +#include "nsEscape.h" +#include "nsNodeInfoManager.h" +#include "nsContentCreatorFunctions.h" +#include "mozAutoDocUpdate.h" +#include "nsTextNode.h" + +using namespace mozilla; +using namespace mozilla::dom; + +//---------------------------------------------------------------------- + +nsGenericHTMLElement* NS_NewHTMLNOTUSEDElement( + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) { + MOZ_ASSERT_UNREACHABLE("The element ctor should never be called"); + return nullptr; +} + +#define HTML_TAG(_tag, _classname, _interfacename) \ + NS_NewHTML##_classname##Element, +#define HTML_OTHER(_tag) NS_NewHTMLNOTUSEDElement, +static const HTMLContentCreatorFunction sHTMLContentCreatorFunctions[] = { + NS_NewHTMLUnknownElement, +#include "nsHTMLTagList.h" +#undef HTML_TAG +#undef HTML_OTHER + NS_NewHTMLUnknownElement}; + +class SinkContext; +class HTMLContentSink; + +/** + * This class is near-OBSOLETE. It is used for about:blank only. + * Don't bother adding new stuff in this file. + */ +class HTMLContentSink : public nsContentSink, public nsIHTMLContentSink { + public: + friend class SinkContext; + + HTMLContentSink(); + + nsresult Init(Document* aDoc, nsIURI* aURI, nsISupports* aContainer, + nsIChannel* aChannel); + + // nsISupports + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(HTMLContentSink, nsContentSink) + + // nsIContentSink + NS_IMETHOD WillParse(void) override; + NS_IMETHOD WillBuildModel(nsDTDMode aDTDMode) override; + NS_IMETHOD DidBuildModel(bool aTerminated) override; + NS_IMETHOD WillInterrupt(void) override; + void WillResume() override; + NS_IMETHOD SetParser(nsParserBase* aParser) override; + virtual void FlushPendingNotifications(FlushType aType) override; + virtual void SetDocumentCharset(NotNull<const Encoding*> aEncoding) override; + virtual nsISupports* GetTarget() override; + virtual bool IsScriptExecuting() override; + virtual bool WaitForPendingSheets() override; + virtual void ContinueInterruptedParsingAsync() override; + + // nsIHTMLContentSink + NS_IMETHOD OpenContainer(ElementType aNodeType) override; + NS_IMETHOD CloseContainer(ElementType aTag) override; + + protected: + virtual ~HTMLContentSink(); + + RefPtr<nsHTMLDocument> mHTMLDocument; + + // The maximum length of a text run + int32_t mMaxTextRun; + + RefPtr<nsGenericHTMLElement> mRoot; + RefPtr<nsGenericHTMLElement> mBody; + RefPtr<nsGenericHTMLElement> mHead; + + AutoTArray<SinkContext*, 8> mContextStack; + SinkContext* mCurrentContext; + SinkContext* mHeadContext; + + // Boolean indicating whether we've seen a <head> tag that might have had + // attributes once already. + bool mHaveSeenHead; + + // Boolean indicating whether we've notified insertion of our root content + // yet. We want to make sure to only do this once. + bool mNotifiedRootInsertion; + + nsresult FlushTags() override; + + // Routines for tags that require special handling + nsresult CloseHTML(); + nsresult OpenBody(); + nsresult CloseBody(); + + void CloseHeadContext(); + + // nsContentSink overrides + void UpdateChildCounts() override; + + void NotifyInsert(nsIContent* aContent, nsIContent* aChildContent); + void NotifyRootInsertion(); + + private: + void ContinueInterruptedParsingIfEnabled(); +}; + +class SinkContext { + public: + explicit SinkContext(HTMLContentSink* aSink); + ~SinkContext(); + + nsresult Begin(nsHTMLTag aNodeType, nsGenericHTMLElement* aRoot, + uint32_t aNumFlushed, int32_t aInsertionPoint); + nsresult OpenBody(); + nsresult CloseBody(); + nsresult End(); + + nsresult GrowStack(); + nsresult FlushTags(); + + bool IsCurrentContainer(nsHTMLTag aTag) const; + + void DidAddContent(nsIContent* aContent); + void UpdateChildCounts(); + + private: + // Function to check whether we've notified for the current content. + // What this actually does is check whether we've notified for all + // of the parent's kids. + bool HaveNotifiedForCurrentContent() const; + + public: + HTMLContentSink* mSink; + int32_t mNotifyLevel; + + struct Node { + nsHTMLTag mType; + nsGenericHTMLElement* mContent; + uint32_t mNumFlushed; + int32_t mInsertionPoint; + + nsIContent* Add(nsIContent* child); + }; + + Node* mStack; + int32_t mStackSize; + int32_t mStackPos; +}; + +nsresult NS_NewHTMLElement(Element** aResult, + already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser, nsAtom* aIsAtom, + mozilla::dom::CustomElementDefinition* aDefinition) { + RefPtr<mozilla::dom::NodeInfo> nodeInfo = aNodeInfo; + + NS_ASSERTION( + nodeInfo->NamespaceEquals(kNameSpaceID_XHTML), + "Trying to create HTML elements that don't have the XHTML namespace"); + + return nsContentUtils::NewXULOrHTMLElement(aResult, nodeInfo, aFromParser, + aIsAtom, aDefinition); +} + +already_AddRefed<nsGenericHTMLElement> CreateHTMLElement( + uint32_t aNodeType, already_AddRefed<mozilla::dom::NodeInfo>&& aNodeInfo, + FromParser aFromParser) { + NS_ASSERTION( + aNodeType <= NS_HTML_TAG_MAX || aNodeType == eHTMLTag_userdefined, + "aNodeType is out of bounds"); + + HTMLContentCreatorFunction cb = sHTMLContentCreatorFunctions[aNodeType]; + + NS_ASSERTION(cb != NS_NewHTMLNOTUSEDElement, + "Don't know how to construct tag element!"); + + RefPtr<nsGenericHTMLElement> result = cb(std::move(aNodeInfo), aFromParser); + + return result.forget(); +} + +//---------------------------------------------------------------------- + +SinkContext::SinkContext(HTMLContentSink* aSink) + : mSink(aSink), + mNotifyLevel(0), + mStack(nullptr), + mStackSize(0), + mStackPos(0) { + MOZ_COUNT_CTOR(SinkContext); +} + +SinkContext::~SinkContext() { + MOZ_COUNT_DTOR(SinkContext); + + if (mStack) { + for (int32_t i = 0; i < mStackPos; i++) { + NS_RELEASE(mStack[i].mContent); + } + delete[] mStack; + } +} + +nsresult SinkContext::Begin(nsHTMLTag aNodeType, nsGenericHTMLElement* aRoot, + uint32_t aNumFlushed, int32_t aInsertionPoint) { + if (mStackSize < 1) { + nsresult rv = GrowStack(); + if (NS_FAILED(rv)) { + return rv; + } + } + + mStack[0].mType = aNodeType; + mStack[0].mContent = aRoot; + mStack[0].mNumFlushed = aNumFlushed; + mStack[0].mInsertionPoint = aInsertionPoint; + NS_ADDREF(aRoot); + mStackPos = 1; + + return NS_OK; +} + +bool SinkContext::IsCurrentContainer(nsHTMLTag aTag) const { + return aTag == mStack[mStackPos - 1].mType; +} + +void SinkContext::DidAddContent(nsIContent* aContent) { + if ((mStackPos == 2) && (mSink->mBody == mStack[1].mContent)) { + // We just finished adding something to the body + mNotifyLevel = 0; + } + + // If we just added content to a node for which + // an insertion happen, we need to do an immediate + // notification for that insertion. + if (0 < mStackPos && mStack[mStackPos - 1].mInsertionPoint != -1 && + mStack[mStackPos - 1].mNumFlushed < + mStack[mStackPos - 1].mContent->GetChildCount()) { + nsIContent* parent = mStack[mStackPos - 1].mContent; + mSink->NotifyInsert(parent, aContent); + mStack[mStackPos - 1].mNumFlushed = parent->GetChildCount(); + } else if (mSink->IsTimeToNotify()) { + FlushTags(); + } +} + +nsresult SinkContext::OpenBody() { + if (mStackPos <= 0) { + NS_ERROR("container w/o parent"); + + return NS_ERROR_FAILURE; + } + + nsresult rv; + if (mStackPos + 1 > mStackSize) { + rv = GrowStack(); + if (NS_FAILED(rv)) { + return rv; + } + } + + RefPtr<mozilla::dom::NodeInfo> nodeInfo = + mSink->mNodeInfoManager->GetNodeInfo( + nsGkAtoms::body, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + NS_ENSURE_TRUE(nodeInfo, NS_ERROR_UNEXPECTED); + + // Make the content object + RefPtr<nsGenericHTMLElement> body = + NS_NewHTMLBodyElement(nodeInfo.forget(), FROM_PARSER_NETWORK); + if (!body) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mStack[mStackPos].mType = eHTMLTag_body; + body.forget(&mStack[mStackPos].mContent); + mStack[mStackPos].mNumFlushed = 0; + mStack[mStackPos].mInsertionPoint = -1; + ++mStackPos; + mStack[mStackPos - 2].Add(mStack[mStackPos - 1].mContent); + + return NS_OK; +} + +bool SinkContext::HaveNotifiedForCurrentContent() const { + if (0 < mStackPos) { + nsIContent* parent = mStack[mStackPos - 1].mContent; + return mStack[mStackPos - 1].mNumFlushed == parent->GetChildCount(); + } + + return true; +} + +nsIContent* SinkContext::Node::Add(nsIContent* child) { + NS_ASSERTION(mContent, "No parent to insert/append into!"); + if (mInsertionPoint != -1) { + NS_ASSERTION(mNumFlushed == mContent->GetChildCount(), + "Inserting multiple children without flushing."); + nsCOMPtr<nsIContent> nodeToInsertBefore = + mContent->GetChildAt_Deprecated(mInsertionPoint++); + mContent->InsertChildBefore(child, nodeToInsertBefore, false, + IgnoreErrors()); + } else { + mContent->AppendChildTo(child, false, IgnoreErrors()); + } + return child; +} + +nsresult SinkContext::CloseBody() { + NS_ASSERTION(mStackPos > 0, "stack out of bounds. wrong context probably!"); + + if (mStackPos <= 0) { + return NS_OK; // Fix crash - Ref. bug 45975 or 45007 + } + + --mStackPos; + NS_ASSERTION(mStack[mStackPos].mType == eHTMLTag_body, + "Tag mismatch. Closing tag on wrong context or something?"); + + nsGenericHTMLElement* content = mStack[mStackPos].mContent; + + content->Compact(); + + // If we're in a state where we do append notifications as + // we go up the tree, and we're at the level where the next + // notification needs to be done, do the notification. + if (mNotifyLevel >= mStackPos) { + // Check to see if new content has been added after our last + // notification + + if (mStack[mStackPos].mNumFlushed < content->GetChildCount()) { + mSink->NotifyAppend(content, mStack[mStackPos].mNumFlushed); + mStack[mStackPos].mNumFlushed = content->GetChildCount(); + } + + // Indicate that notification has now happened at this level + mNotifyLevel = mStackPos - 1; + } + + DidAddContent(content); + NS_IF_RELEASE(content); + + return NS_OK; +} + +nsresult SinkContext::End() { + for (int32_t i = 0; i < mStackPos; i++) { + NS_RELEASE(mStack[i].mContent); + } + + mStackPos = 0; + + return NS_OK; +} + +nsresult SinkContext::GrowStack() { + int32_t newSize = mStackSize * 2; + if (newSize == 0) { + newSize = 32; + } + + Node* stack = new Node[newSize]; + + if (mStackPos != 0) { + memcpy(stack, mStack, sizeof(Node) * mStackPos); + delete[] mStack; + } + + mStack = stack; + mStackSize = newSize; + + return NS_OK; +} + +/** + * NOTE!! Forked into nsXMLContentSink. Please keep in sync. + * + * Flush all elements that have been seen so far such that + * they are visible in the tree. Specifically, make sure + * that they are all added to their respective parents. + * Also, do notification at the top for all content that + * has been newly added so that the frame tree is complete. + */ +nsresult SinkContext::FlushTags() { + mSink->mDeferredFlushTags = false; + uint32_t oldUpdates = mSink->mUpdatesInNotification; + + ++(mSink->mInNotification); + mSink->mUpdatesInNotification = 0; + { + // Scope so we call EndUpdate before we decrease mInNotification + mozAutoDocUpdate updateBatch(mSink->mDocument, true); + + // Start from the base of the stack (growing downward) and do + // a notification from the node that is closest to the root of + // tree for any content that has been added. + + // Note that we can start at stackPos == 0 here, because it's the caller's + // responsibility to handle flushing interactions between contexts (see + // HTMLContentSink::BeginContext). + int32_t stackPos = 0; + bool flushed = false; + uint32_t childCount; + nsGenericHTMLElement* content; + + while (stackPos < mStackPos) { + content = mStack[stackPos].mContent; + childCount = content->GetChildCount(); + + if (!flushed && (mStack[stackPos].mNumFlushed < childCount)) { + if (mStack[stackPos].mInsertionPoint != -1) { + // We might have popped the child off our stack already + // but not notified on it yet, which is why we have to get it + // directly from its parent node. + + int32_t childIndex = mStack[stackPos].mInsertionPoint - 1; + nsIContent* child = content->GetChildAt_Deprecated(childIndex); + // Child not on stack anymore; can't assert it's correct + NS_ASSERTION(!(mStackPos > (stackPos + 1)) || + (child == mStack[stackPos + 1].mContent), + "Flushing the wrong child."); + mSink->NotifyInsert(content, child); + } else { + mSink->NotifyAppend(content, mStack[stackPos].mNumFlushed); + } + + flushed = true; + } + + mStack[stackPos].mNumFlushed = childCount; + stackPos++; + } + mNotifyLevel = mStackPos - 1; + } + --(mSink->mInNotification); + + if (mSink->mUpdatesInNotification > 1) { + UpdateChildCounts(); + } + + mSink->mUpdatesInNotification = oldUpdates; + + return NS_OK; +} + +/** + * NOTE!! Forked into nsXMLContentSink. Please keep in sync. + */ +void SinkContext::UpdateChildCounts() { + // Start from the top of the stack (growing upwards) and see if any + // new content has been appended. If so, we recognize that reflows + // have been generated for it and we should make sure that no + // further reflows occur. Note that we have to include stackPos == 0 + // to properly notify on kids of <html>. + int32_t stackPos = mStackPos - 1; + while (stackPos >= 0) { + Node& node = mStack[stackPos]; + node.mNumFlushed = node.mContent->GetChildCount(); + + stackPos--; + } + + mNotifyLevel = mStackPos - 1; +} + +nsresult NS_NewHTMLContentSink(nsIHTMLContentSink** aResult, Document* aDoc, + nsIURI* aURI, nsISupports* aContainer, + nsIChannel* aChannel) { + NS_ENSURE_ARG_POINTER(aResult); + + RefPtr<HTMLContentSink> it = new HTMLContentSink(); + + nsresult rv = it->Init(aDoc, aURI, aContainer, aChannel); + + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = it; + NS_ADDREF(*aResult); + + return NS_OK; +} + +HTMLContentSink::HTMLContentSink() + : mMaxTextRun(0), + mCurrentContext(nullptr), + mHeadContext(nullptr), + mHaveSeenHead(false), + mNotifiedRootInsertion(false) {} + +HTMLContentSink::~HTMLContentSink() { + if (mNotificationTimer) { + mNotificationTimer->Cancel(); + } + + if (mCurrentContext == mHeadContext && !mContextStack.IsEmpty()) { + // Pop off the second html context if it's not done earlier + mContextStack.RemoveLastElement(); + } + + for (int32_t i = 0, numContexts = mContextStack.Length(); i < numContexts; + i++) { + SinkContext* sc = mContextStack.ElementAt(i); + if (sc) { + sc->End(); + if (sc == mCurrentContext) { + mCurrentContext = nullptr; + } + + delete sc; + } + } + + if (mCurrentContext == mHeadContext) { + mCurrentContext = nullptr; + } + + delete mCurrentContext; + + delete mHeadContext; +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(HTMLContentSink, nsContentSink, + mHTMLDocument, mRoot, mBody, mHead) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(HTMLContentSink, nsContentSink, + nsIContentSink, nsIHTMLContentSink) + +nsresult HTMLContentSink::Init(Document* aDoc, nsIURI* aURI, + nsISupports* aContainer, nsIChannel* aChannel) { + NS_ENSURE_TRUE(aContainer, NS_ERROR_NULL_POINTER); + + nsresult rv = nsContentSink::Init(aDoc, aURI, aContainer, aChannel); + if (NS_FAILED(rv)) { + return rv; + } + + aDoc->AddObserver(this); + mIsDocumentObserver = true; + mHTMLDocument = aDoc->AsHTMLDocument(); + + NS_ASSERTION(mDocShell, "oops no docshell!"); + + // Changed from 8192 to greatly improve page loading performance on + // large pages. See bugzilla bug 77540. + mMaxTextRun = Preferences::GetInt("content.maxtextrun", 8191); + + RefPtr<mozilla::dom::NodeInfo> nodeInfo; + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::html, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + // Make root part + mRoot = NS_NewHTMLHtmlElement(nodeInfo.forget()); + if (!mRoot) { + return NS_ERROR_OUT_OF_MEMORY; + } + + NS_ASSERTION(mDocument->GetChildCount() == 0, + "Document should have no kids here!"); + ErrorResult error; + mDocument->AppendChildTo(mRoot, false, error); + if (error.Failed()) { + return error.StealNSResult(); + } + + // Make head part + nodeInfo = mNodeInfoManager->GetNodeInfo( + nsGkAtoms::head, nullptr, kNameSpaceID_XHTML, nsINode::ELEMENT_NODE); + + mHead = NS_NewHTMLHeadElement(nodeInfo.forget()); + if (NS_FAILED(rv)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mRoot->AppendChildTo(mHead, false, IgnoreErrors()); + + mCurrentContext = new SinkContext(this); + mCurrentContext->Begin(eHTMLTag_html, mRoot, 0, -1); + mContextStack.AppendElement(mCurrentContext); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLContentSink::WillParse(void) { return WillParseImpl(); } + +NS_IMETHODIMP +HTMLContentSink::WillBuildModel(nsDTDMode aDTDMode) { + WillBuildModelImpl(); + + mDocument->SetCompatibilityMode(aDTDMode == eDTDMode_full_standards + ? eCompatibility_FullStandards + : eCompatibility_NavQuirks); + + // Notify document that the load is beginning + mDocument->BeginLoad(); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLContentSink::DidBuildModel(bool aTerminated) { + DidBuildModelImpl(aTerminated); + + // Reflow the last batch of content + if (mBody) { + mCurrentContext->FlushTags(); + } else if (!mLayoutStarted) { + // We never saw the body, and layout never got started. Force + // layout *now*, to get an initial reflow. + // NOTE: only force the layout if we are NOT destroying the + // docshell. If we are destroying it, then starting layout will + // likely cause us to crash, or at best waste a lot of time as we + // are just going to tear it down anyway. + bool bDestroying = true; + if (mDocShell) { + mDocShell->IsBeingDestroyed(&bDestroying); + } + + if (!bDestroying) { + StartLayout(false); + } + } + + ScrollToRef(); + + // Make sure we no longer respond to document mutations. We've flushed all + // our notifications out, so there's no need to do anything else here. + + // XXXbz I wonder whether we could End() our contexts here too, or something, + // just to make sure we no longer notify... Or is the mIsDocumentObserver + // thing sufficient? + mDocument->RemoveObserver(this); + mIsDocumentObserver = false; + + mDocument->EndLoad(); + + DropParserAndPerfHint(); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLContentSink::SetParser(nsParserBase* aParser) { + MOZ_ASSERT(aParser, "Should have a parser here!"); + mParser = aParser; + return NS_OK; +} + +nsresult HTMLContentSink::CloseHTML() { + if (mHeadContext) { + if (mCurrentContext == mHeadContext) { + // Pop off the second html context if it's not done earlier + mCurrentContext = mContextStack.PopLastElement(); + } + + mHeadContext->End(); + + delete mHeadContext; + mHeadContext = nullptr; + } + + return NS_OK; +} + +nsresult HTMLContentSink::OpenBody() { + CloseHeadContext(); // do this just in case if the HEAD was left open! + + // if we already have a body we're done + if (mBody) { + return NS_OK; + } + + nsresult rv = mCurrentContext->OpenBody(); + + if (NS_FAILED(rv)) { + return rv; + } + + mBody = mCurrentContext->mStack[mCurrentContext->mStackPos - 1].mContent; + + if (mCurrentContext->mStackPos > 1) { + int32_t parentIndex = mCurrentContext->mStackPos - 2; + nsGenericHTMLElement* parent = + mCurrentContext->mStack[parentIndex].mContent; + int32_t numFlushed = mCurrentContext->mStack[parentIndex].mNumFlushed; + int32_t childCount = parent->GetChildCount(); + NS_ASSERTION(numFlushed < childCount, "Already notified on the body?"); + + int32_t insertionPoint = + mCurrentContext->mStack[parentIndex].mInsertionPoint; + + // XXX: I have yet to see a case where numFlushed is non-zero and + // insertionPoint is not -1, but this code will try to handle + // those cases too. + + uint32_t oldUpdates = mUpdatesInNotification; + mUpdatesInNotification = 0; + if (insertionPoint != -1) { + NotifyInsert(parent, mBody); + } else { + NotifyAppend(parent, numFlushed); + } + mCurrentContext->mStack[parentIndex].mNumFlushed = childCount; + if (mUpdatesInNotification > 1) { + UpdateChildCounts(); + } + mUpdatesInNotification = oldUpdates; + } + + StartLayout(false); + + return NS_OK; +} + +nsresult HTMLContentSink::CloseBody() { + // Flush out anything that's left + mCurrentContext->FlushTags(); + mCurrentContext->CloseBody(); + + return NS_OK; +} + +NS_IMETHODIMP +HTMLContentSink::OpenContainer(ElementType aElementType) { + nsresult rv = NS_OK; + + switch (aElementType) { + case eBody: + rv = OpenBody(); + break; + case eHTML: + if (mRoot) { + // If we've already hit this code once, then we're done + if (!mNotifiedRootInsertion) { + NotifyRootInsertion(); + } + } + break; + } + + return rv; +} + +NS_IMETHODIMP +HTMLContentSink::CloseContainer(const ElementType aTag) { + nsresult rv = NS_OK; + + switch (aTag) { + case eBody: + rv = CloseBody(); + break; + case eHTML: + rv = CloseHTML(); + break; + } + + return rv; +} + +NS_IMETHODIMP +HTMLContentSink::WillInterrupt() { return WillInterruptImpl(); } + +void HTMLContentSink::WillResume() { WillResumeImpl(); } + +void HTMLContentSink::CloseHeadContext() { + if (mCurrentContext) { + if (!mCurrentContext->IsCurrentContainer(eHTMLTag_head)) return; + + mCurrentContext->FlushTags(); + } + + if (!mContextStack.IsEmpty()) { + mCurrentContext = mContextStack.PopLastElement(); + } +} + +void HTMLContentSink::NotifyInsert(nsIContent* aContent, + nsIContent* aChildContent) { + mInNotification++; + + { + // Scope so we call EndUpdate before we decrease mInNotification + // Note that aContent->OwnerDoc() may be different to mDocument already. + MOZ_AUTO_DOC_UPDATE(aContent ? aContent->OwnerDoc() : mDocument.get(), + true); + MutationObservers::NotifyContentInserted(NODE_FROM(aContent, mDocument), + aChildContent); + mLastNotificationTime = PR_Now(); + } + + mInNotification--; +} + +void HTMLContentSink::NotifyRootInsertion() { + MOZ_ASSERT(!mNotifiedRootInsertion, "Double-notifying on root?"); + NS_ASSERTION(!mLayoutStarted, + "How did we start layout without notifying on root?"); + // Now make sure to notify that we have now inserted our root. If + // there has been no initial reflow yet it'll be a no-op, but if + // there has been one we need this to get its frames constructed. + // Note that if mNotifiedRootInsertion is true we don't notify here, + // since that just means there are multiple <html> tags in the + // document; in those cases we just want to put all the attrs on one + // tag. + mNotifiedRootInsertion = true; + NotifyInsert(nullptr, mRoot); + + // Now update the notification information in all our + // contexts, since we just inserted the root and notified on + // our whole tree + UpdateChildCounts(); + + nsContentUtils::AddScriptRunner( + new nsDocElementCreatedNotificationRunner(mDocument)); +} + +void HTMLContentSink::UpdateChildCounts() { + uint32_t numContexts = mContextStack.Length(); + for (uint32_t i = 0; i < numContexts; i++) { + SinkContext* sc = mContextStack.ElementAt(i); + + sc->UpdateChildCounts(); + } + + mCurrentContext->UpdateChildCounts(); +} + +void HTMLContentSink::FlushPendingNotifications(FlushType aType) { + // Only flush tags if we're not doing the notification ourselves + // (since we aren't reentrant) + if (!mInNotification) { + // Only flush if we're still a document observer (so that our child counts + // should be correct). + if (mIsDocumentObserver) { + if (aType >= FlushType::ContentAndNotify) { + FlushTags(); + } + } + if (aType >= FlushType::EnsurePresShellInitAndFrames) { + // Make sure that layout has started so that the reflow flush + // will actually happen. + StartLayout(true); + } + } +} + +nsresult HTMLContentSink::FlushTags() { + if (!mNotifiedRootInsertion) { + NotifyRootInsertion(); + return NS_OK; + } + + return mCurrentContext ? mCurrentContext->FlushTags() : NS_OK; +} + +void HTMLContentSink::SetDocumentCharset(NotNull<const Encoding*> aEncoding) { + MOZ_ASSERT_UNREACHABLE("<meta charset> case doesn't occur with about:blank"); +} + +nsISupports* HTMLContentSink::GetTarget() { return ToSupports(mDocument); } + +bool HTMLContentSink::IsScriptExecuting() { return IsScriptExecutingImpl(); } + +void HTMLContentSink::ContinueInterruptedParsingIfEnabled() { + if (mParser && mParser->IsParserEnabled()) { + static_cast<nsIParser*>(mParser.get())->ContinueInterruptedParsing(); + } +} + +bool HTMLContentSink::WaitForPendingSheets() { + return nsContentSink::WaitForPendingSheets(); +} + +void HTMLContentSink::ContinueInterruptedParsingAsync() { + nsCOMPtr<nsIRunnable> ev = NewRunnableMethod( + "HTMLContentSink::ContinueInterruptedParsingIfEnabled", this, + &HTMLContentSink::ContinueInterruptedParsingIfEnabled); + mHTMLDocument->Dispatch(ev.forget()); +} diff --git a/dom/html/nsHTMLDocument.cpp b/dom/html/nsHTMLDocument.cpp new file mode 100644 index 0000000000..26165bb622 --- /dev/null +++ b/dom/html/nsHTMLDocument.cpp @@ -0,0 +1,747 @@ +/* -*- 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 "nsHTMLDocument.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/PresShell.h" +#include "mozilla/StaticPrefs_intl.h" +#include "nsCommandManager.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsPrintfCString.h" +#include "nsReadableUtils.h" +#include "nsUnicharUtils.h" +#include "nsIHTMLContentSink.h" +#include "nsIProtocolHandler.h" +#include "nsIXMLContentSink.h" +#include "nsHTMLParts.h" +#include "nsGkAtoms.h" +#include "nsPresContext.h" +#include "nsPIDOMWindow.h" +#include "nsDOMString.h" +#include "nsIStreamListener.h" +#include "nsIURI.h" +#include "nsNetUtil.h" +#include "nsIDocumentViewer.h" +#include "nsDocShell.h" +#include "nsDocShellLoadTypes.h" +#include "nsIScriptContext.h" +#include "nsContentList.h" +#include "nsError.h" +#include "nsIPrincipal.h" +#include "nsJSPrincipals.h" +#include "nsAttrName.h" + +#include "nsNetCID.h" +#include "mozilla/parser/PrototypeDocumentParser.h" +#include "mozilla/dom/PrototypeDocumentContentSink.h" +#include "nsNameSpaceManager.h" +#include "nsGenericHTMLElement.h" +#include "mozilla/css/Loader.h" +#include "nsFrameSelection.h" + +#include "nsContentUtils.h" +#include "nsJSUtils.h" +#include "DocumentInlines.h" +#include "nsICachingChannel.h" +#include "nsIScriptElement.h" +#include "nsArrayUtils.h" + +// AHMED 12-2 +#include "nsBidiUtils.h" + +#include "mozilla/Encoding.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/IdentifierMapEntry.h" +#include "mozilla/LoadInfo.h" +#include "nsNodeInfoManager.h" +#include "nsRange.h" +#include "mozAutoDocUpdate.h" +#include "nsCCUncollectableMarker.h" +#include "nsHtml5Module.h" +#include "mozilla/dom/Element.h" +#include "mozilla/Preferences.h" +#include "nsMimeTypes.h" +#include "nsIRequest.h" +#include "nsHtml5TreeOpExecutor.h" +#include "nsHtml5Parser.h" +#include "nsParser.h" +#include "nsSandboxFlags.h" +#include "mozilla/dom/HTMLBodyElement.h" +#include "mozilla/dom/HTMLDocumentBinding.h" +#include "mozilla/dom/nsCSPContext.h" +#include "mozilla/dom/Selection.h" +#include "mozilla/dom/ShadowIncludingTreeIterator.h" +#include "nsCharsetSource.h" +#include "nsFocusManager.h" +#include "nsIFrame.h" +#include "nsIContent.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StyleSheet.h" +#include "mozilla/StyleSheetInlines.h" +#include "mozilla/Unused.h" + +using namespace mozilla; +using namespace mozilla::dom; + +#include "prtime.h" + +// #define DEBUG_charset + +// ================================================================== +// = +// ================================================================== + +static bool IsAsciiCompatible(const Encoding* aEncoding) { + return aEncoding->IsAsciiCompatible() || aEncoding == ISO_2022_JP_ENCODING; +} + +nsresult NS_NewHTMLDocument(Document** aInstancePtrResult, + nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal, + bool aLoadedAsData) { + RefPtr<nsHTMLDocument> doc = new nsHTMLDocument(); + + nsresult rv = doc->Init(aPrincipal, aPartitionedPrincipal); + + if (NS_FAILED(rv)) { + *aInstancePtrResult = nullptr; + return rv; + } + + doc->SetLoadedAsData(aLoadedAsData, /* aConsiderForMemoryReporting */ true); + doc.forget(aInstancePtrResult); + + return NS_OK; +} + +nsHTMLDocument::nsHTMLDocument() + : Document("text/html"), + mContentListHolder(nullptr), + mNumForms(0), + mLoadFlags(0), + mWarnedWidthHeight(false), + mIsPlainText(false), + mViewSource(false) { + mType = eHTML; + mDefaultElementType = kNameSpaceID_XHTML; + mCompatMode = eCompatibility_NavQuirks; +} + +nsHTMLDocument::~nsHTMLDocument() = default; + +JSObject* nsHTMLDocument::WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return HTMLDocument_Binding::Wrap(aCx, this, aGivenProto); +} + +nsresult nsHTMLDocument::Init(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) { + nsresult rv = Document::Init(aPrincipal, aPartitionedPrincipal); + NS_ENSURE_SUCCESS(rv, rv); + + // Now reset the compatibility mode of the CSSLoader + // to match our compat mode. + CSSLoader()->SetCompatibilityMode(mCompatMode); + + return NS_OK; +} + +void nsHTMLDocument::Reset(nsIChannel* aChannel, nsILoadGroup* aLoadGroup) { + Document::Reset(aChannel, aLoadGroup); + + if (aChannel) { + aChannel->GetLoadFlags(&mLoadFlags); + } +} + +void nsHTMLDocument::ResetToURI(nsIURI* aURI, nsILoadGroup* aLoadGroup, + nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) { + mLoadFlags = nsIRequest::LOAD_NORMAL; + + Document::ResetToURI(aURI, aLoadGroup, aPrincipal, aPartitionedPrincipal); + + mImages = nullptr; + mApplets = nullptr; + mEmbeds = nullptr; + mLinks = nullptr; + mAnchors = nullptr; + mScripts = nullptr; + + mForms = nullptr; + + // Make the content type default to "text/html", we are a HTML + // document, after all. Once we start getting data, this may be + // changed. + SetContentType(nsDependentCString("text/html")); +} + +void nsHTMLDocument::TryReloadCharset(nsIDocumentViewer* aViewer, + int32_t& aCharsetSource, + NotNull<const Encoding*>& aEncoding) { + if (aViewer) { + int32_t reloadEncodingSource; + const auto reloadEncoding = + aViewer->GetReloadEncodingAndSource(&reloadEncodingSource); + if (kCharsetUninitialized != reloadEncodingSource) { + aViewer->ForgetReloadEncoding(); + + if (reloadEncodingSource <= aCharsetSource || + !IsAsciiCompatible(aEncoding)) { + return; + } + + if (reloadEncoding && IsAsciiCompatible(reloadEncoding)) { + aCharsetSource = reloadEncodingSource; + aEncoding = WrapNotNull(reloadEncoding); + } + } + } +} + +void nsHTMLDocument::TryUserForcedCharset(nsIDocumentViewer* aViewer, + nsIDocShell* aDocShell, + int32_t& aCharsetSource, + NotNull<const Encoding*>& aEncoding, + bool& aForceAutoDetection) { + auto resetForce = MakeScopeExit([&] { + if (aDocShell) { + nsDocShell::Cast(aDocShell)->ResetForcedAutodetection(); + } + }); + + if (aCharsetSource >= kCharsetFromOtherComponent) { + return; + } + + // mCharacterSet not updated yet for channel, so check aEncoding, too. + if (WillIgnoreCharsetOverride() || !IsAsciiCompatible(aEncoding)) { + return; + } + + if (aDocShell && nsDocShell::Cast(aDocShell)->GetForcedAutodetection()) { + // This is the Character Encoding menu code path in Firefox + aForceAutoDetection = true; + } +} + +void nsHTMLDocument::TryParentCharset(nsIDocShell* aDocShell, + int32_t& aCharsetSource, + NotNull<const Encoding*>& aEncoding, + bool& aForceAutoDetection) { + if (!aDocShell) { + return; + } + if (aCharsetSource >= kCharsetFromOtherComponent) { + return; + } + + int32_t parentSource; + const Encoding* parentCharset; + nsCOMPtr<nsIPrincipal> parentPrincipal; + aDocShell->GetParentCharset(parentCharset, &parentSource, + getter_AddRefs(parentPrincipal)); + if (!parentCharset) { + return; + } + if (kCharsetFromInitialUserForcedAutoDetection == parentSource || + kCharsetFromFinalUserForcedAutoDetection == parentSource) { + if (WillIgnoreCharsetOverride() || + !IsAsciiCompatible(aEncoding) || // if channel said UTF-16 + !IsAsciiCompatible(parentCharset)) { + return; + } + aEncoding = WrapNotNull(parentCharset); + aCharsetSource = kCharsetFromParentFrame; + aForceAutoDetection = true; + return; + } + + if (aCharsetSource >= kCharsetFromParentFrame) { + return; + } + + if (kCharsetFromInitialAutoDetectionASCII <= parentSource) { + // Make sure that's OK + if (!NodePrincipal()->Equals(parentPrincipal) || + !IsAsciiCompatible(parentCharset)) { + return; + } + + aEncoding = WrapNotNull(parentCharset); + aCharsetSource = kCharsetFromParentFrame; + } +} + +// Using a prototype document is only allowed with chrome privilege. +bool ShouldUsePrototypeDocument(nsIChannel* aChannel, Document* aDoc) { + if (!aChannel || !aDoc || + !StaticPrefs::dom_prototype_document_cache_enabled()) { + return false; + } + return nsContentUtils::IsChromeDoc(aDoc); +} + +nsresult nsHTMLDocument::StartDocumentLoad( + const char* aCommand, nsIChannel* aChannel, nsILoadGroup* aLoadGroup, + nsISupports* aContainer, nsIStreamListener** aDocListener, bool aReset) { + if (!aCommand) { + MOZ_ASSERT(false, "Command is mandatory"); + return NS_ERROR_INVALID_POINTER; + } + if (mType != eHTML) { + MOZ_ASSERT(mType == eXHTML); + MOZ_ASSERT(false, "Must not set HTML doc to XHTML mode before load start."); + return NS_ERROR_DOM_INVALID_STATE_ERR; + } + + nsAutoCString contentType; + aChannel->GetContentType(contentType); + + bool view = + !strcmp(aCommand, "view") || !strcmp(aCommand, "external-resource"); + mViewSource = !strcmp(aCommand, "view-source"); + bool asData = !strcmp(aCommand, kLoadAsData); + if (!(view || mViewSource || asData)) { + MOZ_ASSERT(false, "Bad parser command"); + return NS_ERROR_INVALID_ARG; + } + + bool html = contentType.EqualsLiteral(TEXT_HTML); + bool xhtml = !html && (contentType.EqualsLiteral(APPLICATION_XHTML_XML) || + contentType.EqualsLiteral(APPLICATION_WAPXHTML_XML)); + mIsPlainText = + !html && !xhtml && nsContentUtils::IsPlainTextType(contentType); + if (!(html || xhtml || mIsPlainText || mViewSource)) { + MOZ_ASSERT(false, "Channel with bad content type."); + return NS_ERROR_INVALID_ARG; + } + + bool forceUtf8 = + mIsPlainText && nsContentUtils::IsUtf8OnlyPlainTextType(contentType); + + bool loadAsHtml5 = true; + + if (!mViewSource && xhtml) { + // We're parsing XHTML as XML, remember that. + mType = eXHTML; + SetCompatibilityMode(eCompatibility_FullStandards); + loadAsHtml5 = false; + } + + // TODO: Proper about:blank treatment is bug 543435 + if (loadAsHtml5 && view) { + // mDocumentURI hasn't been set, yet, so get the URI from the channel + nsCOMPtr<nsIURI> uri; + aChannel->GetOriginalURI(getter_AddRefs(uri)); + // Adapted from nsDocShell: + // GetSpec can be expensive for some URIs, so check the scheme first. + if (uri && uri->SchemeIs("about")) { + if (uri->GetSpecOrDefault().EqualsLiteral("about:blank")) { + loadAsHtml5 = false; + } + } + } + + nsresult rv = Document::StartDocumentLoad(aCommand, aChannel, aLoadGroup, + aContainer, aDocListener, aReset); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIURI> uri; + rv = aChannel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(aContainer)); + + bool loadWithPrototype = false; + RefPtr<nsHtml5Parser> html5Parser; + if (loadAsHtml5) { + html5Parser = nsHtml5Module::NewHtml5Parser(); + mParser = html5Parser; + if (mIsPlainText) { + if (mViewSource) { + html5Parser->MarkAsNotScriptCreated("view-source-plain"); + } else { + html5Parser->MarkAsNotScriptCreated("plain-text"); + } + } else if (mViewSource && !html) { + html5Parser->MarkAsNotScriptCreated("view-source-xml"); + } else { + html5Parser->MarkAsNotScriptCreated(aCommand); + } + } else if (xhtml && ShouldUsePrototypeDocument(aChannel, this)) { + loadWithPrototype = true; + nsCOMPtr<nsIURI> originalURI; + aChannel->GetOriginalURI(getter_AddRefs(originalURI)); + mParser = new mozilla::parser::PrototypeDocumentParser(originalURI, this); + } else { + mParser = new nsParser(); + } + + // Look for the parent document. Note that at this point we don't have our + // content viewer set up yet, and therefore do not have a useful + // mParentDocument. + + // in this block of code, if we get an error result, we return it + // but if we get a null pointer, that's perfectly legal for parent + // and parentViewer + nsCOMPtr<nsIDocShellTreeItem> parentAsItem; + if (docShell) { + docShell->GetInProcessSameTypeParent(getter_AddRefs(parentAsItem)); + } + + nsCOMPtr<nsIDocShell> parent(do_QueryInterface(parentAsItem)); + nsCOMPtr<nsIDocumentViewer> parentViewer; + if (parent) { + rv = parent->GetDocViewer(getter_AddRefs(parentViewer)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIDocumentViewer> viewer; + if (docShell) { + docShell->GetDocViewer(getter_AddRefs(viewer)); + } + if (!viewer) { + viewer = std::move(parentViewer); + } + + nsAutoCString urlSpec; + uri->GetSpec(urlSpec); +#ifdef DEBUG_charset + printf("Determining charset for %s\n", urlSpec.get()); +#endif + + // These are the charset source and charset for our document + bool forceAutoDetection = false; + int32_t charsetSource = kCharsetUninitialized; + auto encoding = UTF_8_ENCODING; + + // For error reporting and referrer policy setting + nsHtml5TreeOpExecutor* executor = nullptr; + if (loadAsHtml5) { + executor = static_cast<nsHtml5TreeOpExecutor*>(mParser->GetContentSink()); + } + + if (forceUtf8) { + charsetSource = kCharsetFromUtf8OnlyMime; + } else if (!IsHTMLDocument() || !docShell) { // no docshell for text/html XHR + charsetSource = + IsHTMLDocument() ? kCharsetFromFallback : kCharsetFromDocTypeDefault; + TryChannelCharset(aChannel, charsetSource, encoding, executor); + } else { + NS_ASSERTION(docShell, "Unexpected null value"); + + // The following will try to get the character encoding from various + // sources. Each Try* function will return early if the source is already + // at least as large as any of the sources it might look at. Some of + // these functions (like TryReloadCharset and TryParentCharset) can set + // charsetSource to various values depending on where the charset they + // end up finding originally comes from. + + // Try the channel's charset (e.g., charset from HTTP + // "Content-Type" header) first. This way, we get to reject overrides in + // TryParentCharset and TryUserForcedCharset if the channel said UTF-16. + // This is to avoid socially engineered XSS by adding user-supplied + // content to a UTF-16 site such that the byte have a dangerous + // interpretation as ASCII and the user can be lured to using the + // charset menu. + TryChannelCharset(aChannel, charsetSource, encoding, executor); + + TryUserForcedCharset(viewer, docShell, charsetSource, encoding, + forceAutoDetection); + + TryReloadCharset(viewer, charsetSource, encoding); // For encoding reload + TryParentCharset(docShell, charsetSource, encoding, forceAutoDetection); + } + + SetDocumentCharacterSetSource(charsetSource); + SetDocumentCharacterSet(encoding); + + // Set the parser as the stream listener for the document loader... + rv = NS_OK; + nsCOMPtr<nsIStreamListener> listener = mParser->GetStreamListener(); + listener.forget(aDocListener); + +#ifdef DEBUG_charset + printf(" charset = %s source %d\n", charset.get(), charsetSource); +#endif + mParser->SetDocumentCharset(encoding, charsetSource, forceAutoDetection); + mParser->SetCommand(aCommand); + + if (!IsHTMLDocument()) { + MOZ_ASSERT(!loadAsHtml5); + if (loadWithPrototype) { + nsCOMPtr<nsIContentSink> sink; + NS_NewPrototypeDocumentContentSink(getter_AddRefs(sink), this, uri, + docShell, aChannel); + mParser->SetContentSink(sink); + } else { + nsCOMPtr<nsIXMLContentSink> xmlsink; + NS_NewXMLContentSink(getter_AddRefs(xmlsink), this, uri, docShell, + aChannel); + mParser->SetContentSink(xmlsink); + } + } else { + if (loadAsHtml5) { + html5Parser->Initialize(this, uri, docShell, aChannel); + } else { + // about:blank *only* + nsCOMPtr<nsIHTMLContentSink> htmlsink; + NS_NewHTMLContentSink(getter_AddRefs(htmlsink), this, uri, docShell, + aChannel); + mParser->SetContentSink(htmlsink); + } + } + + // parser the content of the URI + mParser->Parse(uri); + + return rv; +} + +bool nsHTMLDocument::UseWidthDeviceWidthFallbackViewport() const { + if (mIsPlainText) { + // Plain text documents are simple enough that font inflation doesn't offer + // any appreciable advantage over defaulting to "width=device-width" and + // subsequently turning on word-wrapping. + return true; + } + return Document::UseWidthDeviceWidthFallbackViewport(); +} + +Element* nsHTMLDocument::GetUnfocusedKeyEventTarget() { + if (nsGenericHTMLElement* body = GetBody()) { + return body; + } + return Document::GetUnfocusedKeyEventTarget(); +} + +bool nsHTMLDocument::IsRegistrableDomainSuffixOfOrEqualTo( + const nsAString& aHostSuffixString, const nsACString& aOrigHost) { + // https://html.spec.whatwg.org/multipage/browsers.html#is-a-registrable-domain-suffix-of-or-is-equal-to + if (aHostSuffixString.IsEmpty()) { + return false; + } + + nsCOMPtr<nsIURI> origURI = CreateInheritingURIForHost(aOrigHost); + if (!origURI) { + // Error: failed to parse input domain + return false; + } + + nsCOMPtr<nsIURI> newURI = + RegistrableDomainSuffixOfInternal(aHostSuffixString, origURI); + if (!newURI) { + // Error: illegal domain + return false; + } + return true; +} + +void nsHTMLDocument::AddedForm() { ++mNumForms; } + +void nsHTMLDocument::RemovedForm() { --mNumForms; } + +int32_t nsHTMLDocument::GetNumFormsSynchronous() const { return mNumForms; } + +bool nsHTMLDocument::ResolveName(JSContext* aCx, const nsAString& aName, + JS::MutableHandle<JS::Value> aRetval, + ErrorResult& aError) { + IdentifierMapEntry* entry = mIdentifierMap.GetEntry(aName); + if (!entry) { + return false; + } + + nsBaseContentList* list = entry->GetNameContentList(); + uint32_t length = list ? list->Length() : 0; + + nsIContent* node; + if (length > 0) { + if (length > 1) { + // The list contains more than one element, return the whole list. + if (!ToJSValue(aCx, list, aRetval)) { + aError.NoteJSContextException(aCx); + return false; + } + return true; + } + + // Only one element in the list, return the element instead of returning + // the list. + node = list->Item(0); + } else { + // No named items were found, see if there's one registerd by id for aName. + Element* e = entry->GetIdElement(); + + if (!e || !nsGenericHTMLElement::ShouldExposeIdAsHTMLDocumentProperty(e)) { + return false; + } + + node = e; + } + + if (!ToJSValue(aCx, node, aRetval)) { + aError.NoteJSContextException(aCx); + return false; + } + + return true; +} + +void nsHTMLDocument::GetSupportedNames(nsTArray<nsString>& aNames) { + for (const auto& entry : mIdentifierMap) { + if (entry.HasNameElement() || + entry.HasIdElementExposedAsHTMLDocumentProperty()) { + aNames.AppendElement(entry.GetKeyAsString()); + } + } +} + +//---------------------------- + +// forms related stuff + +bool nsHTMLDocument::MatchFormControls(Element* aElement, int32_t aNamespaceID, + nsAtom* aAtom, void* aData) { + return aElement->IsHTMLFormControlElement(); +} + +nsresult nsHTMLDocument::Clone(dom::NodeInfo* aNodeInfo, + nsINode** aResult) const { + NS_ASSERTION(aNodeInfo->NodeInfoManager() == mNodeInfoManager, + "Can't import this document into another document!"); + + RefPtr<nsHTMLDocument> clone = new nsHTMLDocument(); + nsresult rv = CloneDocHelper(clone.get()); + NS_ENSURE_SUCCESS(rv, rv); + + // State from nsHTMLDocument + clone->mLoadFlags = mLoadFlags; + + clone.forget(aResult); + return NS_OK; +} + +/* virtual */ +void nsHTMLDocument::DocAddSizeOfExcludingThis( + nsWindowSizes& aWindowSizes) const { + Document::DocAddSizeOfExcludingThis(aWindowSizes); + + // Measurement of the following members may be added later if DMD finds it is + // worthwhile: + // - mLinks + // - mAnchors +} + +bool nsHTMLDocument::WillIgnoreCharsetOverride() { + if (mEncodingMenuDisabled) { + return true; + } + if (mType != eHTML) { + MOZ_ASSERT(mType == eXHTML); + return true; + } + if (mCharacterSetSource >= kCharsetFromByteOrderMark) { + return true; + } + if (!mCharacterSet->IsAsciiCompatible() && + mCharacterSet != ISO_2022_JP_ENCODING) { + return true; + } + nsIURI* uri = GetOriginalURI(); + if (uri) { + if (uri->SchemeIs("about")) { + return true; + } + bool isResource; + nsresult rv = NS_URIChainHasFlags( + uri, nsIProtocolHandler::URI_IS_UI_RESOURCE, &isResource); + if (NS_FAILED(rv) || isResource) { + return true; + } + } + + switch (mCharacterSetSource) { + case kCharsetUninitialized: + case kCharsetFromFallback: + case kCharsetFromDocTypeDefault: + case kCharsetFromInitialAutoDetectionWouldHaveBeenUTF8: + case kCharsetFromInitialAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD: + case kCharsetFromFinalAutoDetectionWouldHaveBeenUTF8InitialWasASCII: + case kCharsetFromFinalAutoDetectionWouldNotHaveBeenUTF8DependedOnTLD: + case kCharsetFromParentFrame: + case kCharsetFromXmlDeclaration: + case kCharsetFromMetaTag: + case kCharsetFromChannel: + return false; + } + + bool potentialEffect = false; + nsIPrincipal* parentPrincipal = NodePrincipal(); + + auto subDoc = [&potentialEffect, parentPrincipal](Document& aSubDoc) { + if (parentPrincipal->Equals(aSubDoc.NodePrincipal()) && + !aSubDoc.WillIgnoreCharsetOverride()) { + potentialEffect = true; + return CallState::Stop; + } + return CallState::Continue; + }; + EnumerateSubDocuments(subDoc); + + return !potentialEffect; +} + +void nsHTMLDocument::GetFormsAndFormControls(nsContentList** aFormList, + nsContentList** aFormControlList) { + RefPtr<ContentListHolder> holder = mContentListHolder; + if (!holder) { + // Flush our content model so it'll be up to date + // If this becomes unnecessary and the following line is removed, + // please also remove the corresponding flush operation from + // nsHtml5TreeBuilderCppSupplement.h. (Look for "See bug 497861." there.) + // XXXsmaug nsHtml5TreeBuilderCppSupplement doesn't seem to have such flush + // anymore. + FlushPendingNotifications(FlushType::Content); + + RefPtr<nsContentList> htmlForms = GetExistingForms(); + if (!htmlForms) { + // If the document doesn't have an existing forms content list, create a + // new one which will be released soon by ContentListHolder. The idea is + // that we don't have that list hanging around for a long time and slowing + // down future DOM mutations. + // + // Please keep this in sync with Document::Forms(). + htmlForms = new nsContentList(this, kNameSpaceID_XHTML, nsGkAtoms::form, + nsGkAtoms::form, + /* aDeep = */ true, + /* aLiveList = */ true); + } + + RefPtr<nsContentList> htmlFormControls = new nsContentList( + this, nsHTMLDocument::MatchFormControls, nullptr, nullptr, + /* aDeep = */ true, + /* aMatchAtom = */ nullptr, + /* aMatchNameSpaceId = */ kNameSpaceID_None, + /* aFuncMayDependOnAttr = */ true, + /* aLiveList = */ true); + + holder = new ContentListHolder(this, htmlForms, htmlFormControls); + RefPtr<ContentListHolder> runnable = holder; + if (NS_SUCCEEDED(Dispatch(runnable.forget()))) { + mContentListHolder = holder; + } + } + + NS_ADDREF(*aFormList = holder->mFormList); + NS_ADDREF(*aFormControlList = holder->mFormControlList); +} diff --git a/dom/html/nsHTMLDocument.h b/dom/html/nsHTMLDocument.h new file mode 100644 index 0000000000..652ffd91db --- /dev/null +++ b/dom/html/nsHTMLDocument.h @@ -0,0 +1,213 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsHTMLDocument_h___ +#define nsHTMLDocument_h___ + +#include "mozilla/Attributes.h" +#include "nsContentList.h" +#include "mozilla/dom/Document.h" +#include "nsIHTMLCollection.h" +#include "nsIScriptElement.h" +#include "nsTArray.h" + +#include "PLDHashTable.h" +#include "nsThreadUtils.h" +#include "mozilla/dom/HTMLSharedElement.h" +#include "mozilla/dom/BindingDeclarations.h" + +class nsCommandManager; +class nsIURI; +class nsIDocShell; +class nsICachingChannel; +class nsILoadGroup; + +namespace mozilla::dom { +template <typename T> +struct Nullable; +class WindowProxyHolder; +} // namespace mozilla::dom + +class nsHTMLDocument : public mozilla::dom::Document { + protected: + using ReferrerPolicy = mozilla::dom::ReferrerPolicy; + using Document = mozilla::dom::Document; + using Encoding = mozilla::Encoding; + template <typename T> + using NotNull = mozilla::NotNull<T>; + + public: + using Document::SetDocumentURI; + + nsHTMLDocument(); + virtual nsresult Init(nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) override; + + // Document + virtual void Reset(nsIChannel* aChannel, nsILoadGroup* aLoadGroup) override; + virtual void ResetToURI(nsIURI* aURI, nsILoadGroup* aLoadGroup, + nsIPrincipal* aPrincipal, + nsIPrincipal* aPartitionedPrincipal) override; + + virtual nsresult StartDocumentLoad(const char* aCommand, nsIChannel* aChannel, + nsILoadGroup* aLoadGroup, + nsISupports* aContainer, + nsIStreamListener** aDocListener, + bool aReset = true) override; + + protected: + virtual bool UseWidthDeviceWidthFallbackViewport() const override; + + public: + mozilla::dom::Element* GetUnfocusedKeyEventTarget() override; + + nsContentList* GetExistingForms() const { return mForms; } + + bool IsPlainText() const { return mIsPlainText; } + + bool IsViewSource() const { return mViewSource; } + + // Returns whether an object was found for aName. + bool ResolveName(JSContext* aCx, const nsAString& aName, + JS::MutableHandle<JS::Value> aRetval, + mozilla::ErrorResult& aError); + + /** + * Called when form->BindToTree() is called so that document knows + * immediately when a form is added + */ + void AddedForm(); + /** + * Called when form->SetDocument() is called so that document knows + * immediately when a form is removed + */ + void RemovedForm(); + /** + * Called to get a better count of forms than document.forms can provide + * without calling FlushPendingNotifications (bug 138892). + */ + // XXXbz is this still needed now that we can flush just content, + // not the rest? + int32_t GetNumFormsSynchronous() const; + void SetIsXHTML(bool aXHTML) { mType = (aXHTML ? eXHTML : eHTML); } + + virtual nsresult Clone(mozilla::dom::NodeInfo*, + nsINode** aResult) const override; + + using mozilla::dom::DocumentOrShadowRoot::GetElementById; + + virtual void DocAddSizeOfExcludingThis( + nsWindowSizes& aWindowSizes) const override; + // DocAddSizeOfIncludingThis is inherited from Document. + + virtual bool WillIgnoreCharsetOverride() override; + + // WebIDL API + virtual JSObject* WrapNode(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override; + bool IsRegistrableDomainSuffixOfOrEqualTo(const nsAString& aHostSuffixString, + const nsACString& aOrigHost); + void NamedGetter(JSContext* cx, const nsAString& aName, bool& aFound, + JS::MutableHandle<JSObject*> aRetval, + mozilla::ErrorResult& rv) { + JS::Rooted<JS::Value> v(cx); + if ((aFound = ResolveName(cx, aName, &v, rv))) { + SetUseCounter(mozilla::eUseCounter_custom_HTMLDocumentNamedGetterHit); + aRetval.set(v.toObjectOrNull()); + } + } + void GetSupportedNames(nsTArray<nsString>& aNames); + // We're picking up GetLocation from Document + already_AddRefed<mozilla::dom::Location> GetLocation() const { + return Document::GetLocation(); + } + + static bool MatchFormControls(mozilla::dom::Element* aElement, + int32_t aNamespaceID, nsAtom* aAtom, + void* aData); + + void GetFormsAndFormControls(nsContentList** aFormList, + nsContentList** aFormControlList); + + protected: + ~nsHTMLDocument(); + + nsresult GetBodySize(int32_t* aWidth, int32_t* aHeight); + + nsIContent* MatchId(nsIContent* aContent, const nsAString& aId); + + static void DocumentWriteTerminationFunc(nsISupports* aRef); + + // A helper class to keep nsContentList objects alive for a short period of + // time. Note, when the final Release is called on an nsContentList object, it + // removes itself from MutationObserver list. + class ContentListHolder : public mozilla::Runnable { + public: + ContentListHolder(nsHTMLDocument* aDocument, nsContentList* aFormList, + nsContentList* aFormControlList) + : mozilla::Runnable("ContentListHolder"), + mDocument(aDocument), + mFormList(aFormList), + mFormControlList(aFormControlList) {} + + ~ContentListHolder() { + MOZ_ASSERT(!mDocument->mContentListHolder || + mDocument->mContentListHolder == this); + mDocument->mContentListHolder = nullptr; + } + + RefPtr<nsHTMLDocument> mDocument; + RefPtr<nsContentList> mFormList; + RefPtr<nsContentList> mFormControlList; + }; + + friend class ContentListHolder; + ContentListHolder* mContentListHolder; + + /** # of forms in the document, synchronously set */ + int32_t mNumForms; + + static void TryReloadCharset(nsIDocumentViewer* aViewer, + int32_t& aCharsetSource, + NotNull<const Encoding*>& aEncoding); + void TryUserForcedCharset(nsIDocumentViewer* aViewer, nsIDocShell* aDocShell, + int32_t& aCharsetSource, + NotNull<const Encoding*>& aEncoding, + bool& aForceAutoDetection); + void TryParentCharset(nsIDocShell* aDocShell, int32_t& charsetSource, + NotNull<const Encoding*>& aEncoding, + bool& aForceAutoDetection); + + // Load flags of the document's channel + uint32_t mLoadFlags; + + bool mWarnedWidthHeight; + + /** + * Set to true once we know that we are loading plain text content. + */ + bool mIsPlainText; + + /** + * Set to true once we know that we are viewing source. + */ + bool mViewSource; +}; + +namespace mozilla::dom { + +inline nsHTMLDocument* Document::AsHTMLDocument() { + MOZ_ASSERT(IsHTMLOrXHTML()); + return static_cast<nsHTMLDocument*>(this); +} + +inline const nsHTMLDocument* Document::AsHTMLDocument() const { + MOZ_ASSERT(IsHTMLOrXHTML()); + return static_cast<const nsHTMLDocument*>(this); +} + +} // namespace mozilla::dom + +#endif /* nsHTMLDocument_h___ */ diff --git a/dom/html/nsIConstraintValidation.cpp b/dom/html/nsIConstraintValidation.cpp new file mode 100644 index 0000000000..6ccda2ea7e --- /dev/null +++ b/dom/html/nsIConstraintValidation.cpp @@ -0,0 +1,135 @@ +/* -*- 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 "nsIConstraintValidation.h" + +#include "nsGenericHTMLElement.h" +#include "mozilla/dom/CustomEvent.h" +#include "mozilla/dom/HTMLFormElement.h" +#include "mozilla/dom/HTMLFieldSetElement.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "mozilla/dom/ValidityState.h" +#include "nsIFormControl.h" +#include "nsISimpleEnumerator.h" +#include "nsContentUtils.h" + +const uint16_t nsIConstraintValidation::sContentSpecifiedMaxLengthMessage = 256; + +using namespace mozilla; +using namespace mozilla::dom; + +nsIConstraintValidation::nsIConstraintValidation() + : mValidityBitField(0) + // By default, all elements are subjects to constraint validation. + , + mBarredFromConstraintValidation(false) {} + +nsIConstraintValidation::~nsIConstraintValidation() = default; + +mozilla::dom::ValidityState* nsIConstraintValidation::Validity() { + if (!mValidity) { + mValidity = new mozilla::dom::ValidityState(this); + } + + return mValidity; +} + +bool nsIConstraintValidation::CheckValidity(nsIContent& aEventTarget, + bool* aEventDefaultAction) const { + if (!IsCandidateForConstraintValidation() || IsValid()) { + return true; + } + + nsContentUtils::DispatchTrustedEvent( + aEventTarget.OwnerDoc(), &aEventTarget, u"invalid"_ns, CanBubble::eNo, + Cancelable::eYes, Composed::eDefault, aEventDefaultAction); + return false; +} + +bool nsIConstraintValidation::ReportValidity() { + nsCOMPtr<Element> element = do_QueryInterface(this); + MOZ_ASSERT(element, "This class should be inherited by HTML elements only!"); + + bool defaultAction = true; + if (CheckValidity(*element, &defaultAction)) { + return true; + } + + if (!defaultAction) { + return false; + } + + AutoTArray<RefPtr<Element>, 1> invalidElements; + invalidElements.AppendElement(element); + + AutoJSAPI jsapi; + if (!jsapi.Init(element->GetOwnerGlobal())) { + return false; + } + JS::Rooted<JS::Value> detail(jsapi.cx()); + if (!ToJSValue(jsapi.cx(), invalidElements, &detail)) { + return false; + } + + RefPtr<CustomEvent> event = + NS_NewDOMCustomEvent(element->OwnerDoc(), nullptr, nullptr); + event->InitCustomEvent(jsapi.cx(), u"MozInvalidForm"_ns, + /* CanBubble */ true, + /* Cancelable */ true, detail); + event->SetTrusted(true); + event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true; + + element->DispatchEvent(*event); + return false; +} + +void nsIConstraintValidation::SetValidityState(ValidityStateType aState, + bool aValue) { + bool previousValidity = IsValid(); + + if (aValue) { + mValidityBitField |= aState; + } else { + mValidityBitField &= ~aState; + } + + // Inform the form and fieldset elements if our validity has changed. + if (previousValidity != IsValid() && IsCandidateForConstraintValidation()) { + nsCOMPtr<nsIFormControl> formCtrl = do_QueryInterface(this); + NS_ASSERTION(formCtrl, "This interface should be used by form elements!"); + + if (HTMLFormElement* form = formCtrl->GetForm()) { + form->UpdateValidity(IsValid()); + } + if (HTMLFieldSetElement* fieldSet = formCtrl->GetFieldSet()) { + fieldSet->UpdateValidity(IsValid()); + } + } +} + +void nsIConstraintValidation::SetBarredFromConstraintValidation(bool aBarred) { + bool previousBarred = mBarredFromConstraintValidation; + + mBarredFromConstraintValidation = aBarred; + + // Inform the form and fieldset elements if our status regarding constraint + // validation is going to change. + if (!IsValid() && previousBarred != mBarredFromConstraintValidation) { + nsCOMPtr<nsIFormControl> formCtrl = do_QueryInterface(this); + NS_ASSERTION(formCtrl, "This interface should be used by form elements!"); + + // If the element is going to be barred from constraint validation, we can + // inform the form and fieldset that we are now valid. Otherwise, we are now + // invalid. + if (HTMLFormElement* form = formCtrl->GetForm()) { + form->UpdateValidity(aBarred); + } + HTMLFieldSetElement* fieldSet = formCtrl->GetFieldSet(); + if (fieldSet) { + fieldSet->UpdateValidity(aBarred); + } + } +} diff --git a/dom/html/nsIConstraintValidation.h b/dom/html/nsIConstraintValidation.h new file mode 100644 index 0000000000..14f7cee77f --- /dev/null +++ b/dom/html/nsIConstraintValidation.h @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsIConstraintValidition_h___ +#define nsIConstraintValidition_h___ + +#include "nsISupports.h" + +class nsIContent; + +namespace mozilla::dom { +class ValidityState; +} // namespace mozilla::dom + +#define NS_ICONSTRAINTVALIDATION_IID \ + { \ + 0x983829da, 0x1aaf, 0x449c, { \ + 0xa3, 0x06, 0x85, 0xd4, 0xf0, 0x31, 0x1c, 0xf6 \ + } \ + } + +/** + * This interface is for form elements implementing the validity constraint API. + * See: http://dev.w3.org/html5/spec/forms.html#the-constraint-validation-api + * + * This interface has to be implemented by all elements implementing the API + * and only them. + */ +class nsIConstraintValidation : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_ICONSTRAINTVALIDATION_IID); + + friend class mozilla::dom::ValidityState; + + static const uint16_t sContentSpecifiedMaxLengthMessage; + + virtual ~nsIConstraintValidation(); + + bool IsValid() const { return mValidityBitField == 0; } + + bool IsCandidateForConstraintValidation() const { + return !mBarredFromConstraintValidation; + } + + enum ValidityStateType { + VALIDITY_STATE_VALUE_MISSING = 0x1 << 0, + VALIDITY_STATE_TYPE_MISMATCH = 0x1 << 1, + VALIDITY_STATE_PATTERN_MISMATCH = 0x1 << 2, + VALIDITY_STATE_TOO_LONG = 0x1 << 3, + VALIDITY_STATE_TOO_SHORT = 0x1 << 4, + VALIDITY_STATE_RANGE_UNDERFLOW = 0x1 << 5, + VALIDITY_STATE_RANGE_OVERFLOW = 0x1 << 6, + VALIDITY_STATE_STEP_MISMATCH = 0x1 << 7, + VALIDITY_STATE_BAD_INPUT = 0x1 << 8, + VALIDITY_STATE_CUSTOM_ERROR = 0x1 << 9, + }; + + void SetValidityState(ValidityStateType aState, bool aValue); + + /** + * Check the validity of this object. If it is not valid, file a "invalid" + * event on the aEventTarget. + * + * @param aEventTarget The target of the event. + * @param aDefaultAction Set to true if default action should be taken, + * see EventTarget::DispatchEvent. + * @return whether it's valid. + */ + bool CheckValidity(nsIContent& aEventTarget, + bool* aEventDefaultAction = nullptr) const; + + // Web IDL binding methods + bool WillValidate() const { return IsCandidateForConstraintValidation(); } + mozilla::dom::ValidityState* Validity(); + bool ReportValidity(); + + protected: + // You can't instantiate an object from that class. + nsIConstraintValidation(); + + bool GetValidityState(ValidityStateType aState) const { + return mValidityBitField & aState; + } + + void SetBarredFromConstraintValidation(bool aBarred); + + /** + * A pointer to the ValidityState object. + */ + RefPtr<mozilla::dom::ValidityState> mValidity; + + private: + /** + * A bitfield representing the current validity state of the element. + * Each bit represent an error. All bits to zero means the element is valid. + */ + int16_t mValidityBitField; + + /** + * Keeps track whether the element is barred from constraint validation. + */ + bool mBarredFromConstraintValidation; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIConstraintValidation, + NS_ICONSTRAINTVALIDATION_IID) + +#endif // nsIConstraintValidation_h___ diff --git a/dom/html/nsIFormControl.h b/dom/html/nsIFormControl.h new file mode 100644 index 0000000000..051f83215f --- /dev/null +++ b/dom/html/nsIFormControl.h @@ -0,0 +1,288 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsIFormControl_h___ +#define nsIFormControl_h___ + +#include "mozilla/EventForwards.h" +#include "mozilla/StaticPrefs_dom.h" +#include "nsISupports.h" + +namespace mozilla { +class PresState; +namespace dom { +class Element; +class FormData; +class HTMLFieldSetElement; +class HTMLFormElement; +} // namespace dom +} // namespace mozilla + +// Elements with different types, the value is used as a mask. +// When changing the order, adding or removing elements, be sure to update +// the static_assert checks accordingly. +constexpr uint8_t kFormControlButtonElementMask = 0x40; // 0b01000000 +constexpr uint8_t kFormControlInputElementMask = 0x80; // 0b10000000 + +enum class FormControlType : uint8_t { + Fieldset = 1, + Output, + Select, + Textarea, + Object, + FormAssociatedCustomElement, + + LastWithoutSubtypes = FormAssociatedCustomElement, + + ButtonButton = kFormControlButtonElementMask + 1, + ButtonReset, + ButtonSubmit, + LastButtonElement = ButtonSubmit, + + InputButton = kFormControlInputElementMask + 1, + InputCheckbox, + InputColor, + InputDate, + InputEmail, + InputFile, + InputHidden, + InputReset, + InputImage, + InputMonth, + InputNumber, + InputPassword, + InputRadio, + InputSearch, + InputSubmit, + InputTel, + InputText, + InputTime, + InputUrl, + InputRange, + InputWeek, + InputDatetimeLocal, + LastInputElement = InputDatetimeLocal, +}; + +static_assert(uint8_t(FormControlType::LastWithoutSubtypes) < + kFormControlButtonElementMask, + "Too many FormControlsTypes without sub-types"); +static_assert(uint8_t(FormControlType::LastButtonElement) < + kFormControlInputElementMask, + "Too many ButtonElementTypes"); +static_assert(uint32_t(FormControlType::LastInputElement) < (1 << 8), + "Too many form control types"); + +#define NS_IFORMCONTROL_IID \ + { \ + 0x4b89980c, 0x4dcd, 0x428f, { \ + 0xb7, 0xad, 0x43, 0x5b, 0x93, 0x29, 0x79, 0xec \ + } \ + } + +/** + * Interface which all form controls (e.g. buttons, checkboxes, text, + * radio buttons, select, etc) implement in addition to their dom specific + * interface. + */ +class nsIFormControl : public nsISupports { + public: + nsIFormControl(FormControlType aType) : mType(aType) {} + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IFORMCONTROL_IID) + + /** + * Get the fieldset for this form control. + * @return the fieldset + */ + virtual mozilla::dom::HTMLFieldSetElement* GetFieldSet() = 0; + + /** + * Get the form for this form control. + * @return the form + */ + virtual mozilla::dom::HTMLFormElement* GetForm() const = 0; + + /** + * Set the form for this form control. + * @param aForm the form. This must not be null. + * + * @note that when setting the form the control is not added to the + * form. It adds itself when it gets bound to the tree thereafter, + * so that it can be properly sorted with the other controls in the + * form. + */ + virtual void SetForm(mozilla::dom::HTMLFormElement* aForm) = 0; + + /** + * Tell the control to forget about its form. + * + * @param aRemoveFromForm set false if you do not want this element removed + * from the form. (Used by nsFormControlList::Clear()) + * @param aUnbindOrDelete set true if the element is being deleted or unbound + * from tree. + */ + virtual void ClearForm(bool aRemoveFromForm, bool aUnbindOrDelete) = 0; + + /** + * Get the type of this control as an int (see NS_FORM_* above) + * @return the type of this control + */ + FormControlType ControlType() const { return mType; } + + /** + * Reset this form control (as it should be when the user clicks the Reset + * button) + */ + NS_IMETHOD Reset() = 0; + + /** + * Tells the form control to submit its names and values to the form data + * object + * + * @param aFormData the form data to notify of names/values/files to submit + */ + NS_IMETHOD + SubmitNamesValues(mozilla::dom::FormData* aFormData) = 0; + + /** + * Returns whether this is a control which submits the form when activated by + * the user. + * @return whether this is a submit control. + */ + inline bool IsSubmitControl() const; + + /** + * Returns whether this is a text control. + * @param aExcludePassword to have NS_FORM_INPUT_PASSWORD returning false. + * @return whether this is a text control. + */ + inline bool IsTextControl(bool aExcludePassword) const; + + /** + * Returns whether this is a single line text control. + * @param aExcludePassword to have NS_FORM_INPUT_PASSWORD returning false. + * @return whether this is a single line text control. + */ + inline bool IsSingleLineTextControl(bool aExcludePassword) const; + + /** + * Returns whether this is a submittable form control. + * @return whether this is a submittable form control. + */ + inline bool IsSubmittableControl() const; + + /** + * https://html.spec.whatwg.org/multipage/forms.html#concept-button + */ + inline bool IsConceptButton() const; + + /** + * Returns whether this is an ordinal button or a concept button that has no + * form associated. + */ + inline bool IsButtonControl() const; + + /** + * Returns whether this form control can have draggable children. + * @return whether this form control can have draggable children. + */ + inline bool AllowDraggableChildren() const; + + // Returns a number for this form control that is unique within its + // owner document. This is used by nsContentUtils::GenerateStateKey + // to identify form controls that are inserted into the document by + // the parser. -1 is returned for form controls with no state or + // which were inserted into the document by some other means than + // the parser from the network. + virtual int32_t GetParserInsertedControlNumberForStateKey() const { + return -1; + }; + + protected: + /** + * Returns whether mType corresponds to a single line text control type. + * @param aExcludePassword to have NS_FORM_INPUT_PASSWORD ignored. + * @param aType the type to be tested. + * @return whether mType corresponds to a single line text control type. + */ + inline static bool IsSingleLineTextControl(bool aExcludePassword, + FormControlType); + + inline static bool IsButtonElement(FormControlType aType) { + return uint8_t(aType) & kFormControlButtonElementMask; + } + + inline static bool IsInputElement(FormControlType aType) { + return uint8_t(aType) & kFormControlInputElementMask; + } + + FormControlType mType; +}; + +bool nsIFormControl::IsSubmitControl() const { + FormControlType type = ControlType(); + return type == FormControlType::InputSubmit || + type == FormControlType::InputImage || + type == FormControlType::ButtonSubmit; +} + +bool nsIFormControl::IsTextControl(bool aExcludePassword) const { + FormControlType type = ControlType(); + return type == FormControlType::Textarea || + IsSingleLineTextControl(aExcludePassword, type); +} + +bool nsIFormControl::IsSingleLineTextControl(bool aExcludePassword) const { + return IsSingleLineTextControl(aExcludePassword, ControlType()); +} + +/*static*/ +bool nsIFormControl::IsSingleLineTextControl(bool aExcludePassword, + FormControlType aType) { + switch (aType) { + case FormControlType::InputText: + case FormControlType::InputEmail: + case FormControlType::InputSearch: + case FormControlType::InputTel: + case FormControlType::InputUrl: + case FormControlType::InputNumber: + // TODO: those are temporary until bug 773205 is fixed. + case FormControlType::InputMonth: + case FormControlType::InputWeek: + return true; + case FormControlType::InputPassword: + return !aExcludePassword; + default: + return false; + } +} + +bool nsIFormControl::IsSubmittableControl() const { + auto type = ControlType(); + return type == FormControlType::Object || type == FormControlType::Textarea || + type == FormControlType::Select || IsButtonElement(type) || + IsInputElement(type); +} + +bool nsIFormControl::IsConceptButton() const { + auto type = ControlType(); + return IsSubmitControl() || type == FormControlType::InputReset || + type == FormControlType::InputButton || IsButtonElement(type); +} + +bool nsIFormControl::IsButtonControl() const { + return IsConceptButton() && (!GetForm() || !IsSubmitControl()); +} + +bool nsIFormControl::AllowDraggableChildren() const { + auto type = ControlType(); + return type == FormControlType::Object || type == FormControlType::Fieldset || + type == FormControlType::Output; +} + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIFormControl, NS_IFORMCONTROL_IID) + +#endif /* nsIFormControl_h___ */ diff --git a/dom/html/nsIHTMLCollection.h b/dom/html/nsIHTMLCollection.h new file mode 100644 index 0000000000..17d0c1fbcf --- /dev/null +++ b/dom/html/nsIHTMLCollection.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsIHTMLCollection_h___ +#define nsIHTMLCollection_h___ + +#include "nsISupports.h" +#include "nsStringFwd.h" +#include "nsTArrayForwardDeclare.h" +#include "nsWrapperCache.h" +#include "js/TypeDecls.h" + +class nsINode; + +namespace mozilla::dom { +class Element; +} // namespace mozilla::dom + +// IID for the nsIHTMLCollection interface +#define NS_IHTMLCOLLECTION_IID \ + { \ + 0x4e169191, 0x5196, 0x4e17, { \ + 0xa4, 0x79, 0xd5, 0x35, 0x0b, 0x5b, 0x0a, 0xcd \ + } \ + } + +/** + * An internal interface + */ +class nsIHTMLCollection : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IHTMLCOLLECTION_IID) + + /** + * Get the root node for this HTML collection. + */ + virtual nsINode* GetParentObject() = 0; + + virtual uint32_t Length() = 0; + virtual mozilla::dom::Element* GetElementAt(uint32_t index) = 0; + mozilla::dom::Element* Item(uint32_t index) { return GetElementAt(index); } + mozilla::dom::Element* IndexedGetter(uint32_t index, bool& aFound) { + mozilla::dom::Element* item = Item(index); + aFound = !!item; + return item; + } + mozilla::dom::Element* NamedItem(const nsAString& aName) { + bool dummy; + return NamedGetter(aName, dummy); + } + mozilla::dom::Element* NamedGetter(const nsAString& aName, bool& aFound) { + return GetFirstNamedElement(aName, aFound); + } + virtual mozilla::dom::Element* GetFirstNamedElement(const nsAString& aName, + bool& aFound) = 0; + + virtual void GetSupportedNames(nsTArray<nsString>& aNames) = 0; + + JSObject* GetWrapperPreserveColor() { + return GetWrapperPreserveColorInternal(); + } + JSObject* GetWrapper() { + JSObject* obj = GetWrapperPreserveColor(); + if (obj) { + JS::ExposeObjectToActiveJS(obj); + } + return obj; + } + void PreserveWrapper(nsISupports* aScriptObjectHolder) { + PreserveWrapperInternal(aScriptObjectHolder); + } + virtual JSObject* WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) = 0; + + protected: + // Hook for calling nsWrapperCache::GetWrapperPreserveColor. + virtual JSObject* GetWrapperPreserveColorInternal() = 0; + // Hook for calling nsWrapperCache::PreserveWrapper. + virtual void PreserveWrapperInternal(nsISupports* aScriptObjectHolder) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIHTMLCollection, NS_IHTMLCOLLECTION_IID) + +#endif /* nsIHTMLCollection_h___ */ diff --git a/dom/html/nsIRadioVisitor.h b/dom/html/nsIRadioVisitor.h new file mode 100644 index 0000000000..308d75170a --- /dev/null +++ b/dom/html/nsIRadioVisitor.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsIRadioVisitor_h___ +#define nsIRadioVisitor_h___ + +#include "nsISupports.h" + +namespace mozilla::dom { +class HTMLInputElement; +} // namespace mozilla::dom + +// IID for the nsIRadioControl interface +#define NS_IRADIOVISITOR_IID \ + { \ + 0xc6bed232, 0x1181, 0x4ab2, { \ + 0xa1, 0xda, 0x55, 0xc2, 0x13, 0x6d, 0xea, 0x3d \ + } \ + } + +/** + * This interface is used for the text control frame to store its value away + * into the content. + */ +class nsIRadioVisitor : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IRADIOVISITOR_IID) + + /** + * Visit a node in the tree. This is meant to be called on all radios in a + * group, sequentially. (Each radio group implementor may define + * sequentially in their own way, it just has to be the same every time.) + * Currently all radio groups are ordered in the order they appear in the + * document. Radio group implementors should honor the return value of the + * method and stop iterating if the return value is false. + * + * @param aRadio the radio button in question (must be nullptr and QI'able to + * nsIRadioControlElement) + */ + virtual bool Visit(mozilla::dom::HTMLInputElement* aRadio) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIRadioVisitor, NS_IRADIOVISITOR_IID) + +#endif // nsIRadioVisitor_h___ diff --git a/dom/html/nsRadioVisitor.cpp b/dom/html/nsRadioVisitor.cpp new file mode 100644 index 0000000000..b7670f4774 --- /dev/null +++ b/dom/html/nsRadioVisitor.cpp @@ -0,0 +1,49 @@ +/* -*- 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 "nsRadioVisitor.h" +#include "mozilla/dom/HTMLInputElement.h" +#include "nsIConstraintValidation.h" + +using namespace mozilla::dom; + +NS_IMPL_ISUPPORTS(nsRadioVisitor, nsIRadioVisitor) + +bool nsRadioSetCheckedChangedVisitor::Visit(HTMLInputElement* aRadio) { + NS_ASSERTION(aRadio, "Visit() passed a null button!"); + aRadio->SetCheckedChangedInternal(mCheckedChanged); + return true; +} + +bool nsRadioGetCheckedChangedVisitor::Visit(HTMLInputElement* aRadio) { + if (aRadio == mExcludeElement) { + return true; + } + + NS_ASSERTION(aRadio, "Visit() passed a null button!"); + *mCheckedChanged = aRadio->GetCheckedChanged(); + return false; +} + +bool nsRadioSetValueMissingState::Visit(HTMLInputElement* aRadio) { + if (aRadio == mExcludeElement) { + return true; + } + + aRadio->SetValidityState( + nsIConstraintValidation::VALIDITY_STATE_VALUE_MISSING, mValidity); + aRadio->UpdateValidityElementStates(true); + return true; +} + +bool nsRadioUpdateStateVisitor::Visit(HTMLInputElement* aRadio) { + if (aRadio == mExcludeElement) { + return true; + } + aRadio->UpdateIndeterminateState(true); + aRadio->UpdateValidityElementStates(true); + return true; +} diff --git a/dom/html/nsRadioVisitor.h b/dom/html/nsRadioVisitor.h new file mode 100644 index 0000000000..0ec20a25a0 --- /dev/null +++ b/dom/html/nsRadioVisitor.h @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsRadioVisitor_h__ +#define _nsRadioVisitor_h__ + +#include "mozilla/Attributes.h" +#include "nsIRadioVisitor.h" + +using mozilla::dom::HTMLInputElement; + +/** + * nsRadioVisitor is the base class implementing nsIRadioVisitor and inherited + * by all radio visitors. + */ +class nsRadioVisitor : public nsIRadioVisitor { + protected: + virtual ~nsRadioVisitor() = default; + + public: + nsRadioVisitor() = default; + + NS_DECL_ISUPPORTS + + virtual bool Visit(HTMLInputElement* aRadio) override = 0; +}; + +/** + * The following declarations are radio visitors inheriting from nsRadioVisitor. + */ + +/** + * nsRadioSetCheckedChangedVisitor is calling SetCheckedChanged with the given + * parameter to all radio elements in the group. + */ +class nsRadioSetCheckedChangedVisitor : public nsRadioVisitor { + public: + explicit nsRadioSetCheckedChangedVisitor(bool aCheckedChanged) + : mCheckedChanged(aCheckedChanged) {} + + virtual bool Visit(HTMLInputElement* aRadio) override; + + protected: + bool mCheckedChanged; +}; + +/** + * nsRadioGetCheckedChangedVisitor is getting the current checked changed value. + * Getting it from one radio element is the group is enough given that all + * elements should have the same value. + */ +class nsRadioGetCheckedChangedVisitor : public nsRadioVisitor { + public: + nsRadioGetCheckedChangedVisitor(bool* aCheckedChanged, + HTMLInputElement* aExcludeElement) + : mCheckedChanged(aCheckedChanged), mExcludeElement(aExcludeElement) {} + + virtual bool Visit(HTMLInputElement* aRadio) override; + + protected: + bool* mCheckedChanged; + HTMLInputElement* mExcludeElement; +}; + +/** + * nsRadioSetValueMissingState is calling SetValueMissingState with the given + * parameter to all radio elements in the group. + * It is also calling ContentStatesChanged if needed. + */ +class nsRadioSetValueMissingState : public nsRadioVisitor { + public: + nsRadioSetValueMissingState(HTMLInputElement* aExcludeElement, bool aValidity) + : mExcludeElement(aExcludeElement), mValidity(aValidity) {} + + virtual bool Visit(HTMLInputElement* aRadio) override; + + protected: + HTMLInputElement* mExcludeElement; + bool mValidity; +}; + +class nsRadioUpdateStateVisitor : public nsRadioVisitor { + public: + explicit nsRadioUpdateStateVisitor(HTMLInputElement* aExcludeElement) + : mExcludeElement(aExcludeElement) {} + + virtual bool Visit(HTMLInputElement* aRadio) override; + + protected: + HTMLInputElement* mExcludeElement; +}; + +#endif // _nsRadioVisitor_h__ diff --git a/dom/html/reftests/41464-1-ref.html b/dom/html/reftests/41464-1-ref.html new file mode 100644 index 0000000000..3b68fca6d7 --- /dev/null +++ b/dom/html/reftests/41464-1-ref.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<title>Dynamic manipulation of textarea.wrap</title> +<link rel=help href=http://www.whatwg.org/html5/#dom-textarea-wrap> +<link rel=author title=Ms2ger href=mailto:ms2ger@gmail.com> +<textarea wrap=soft cols=20>01234567890 01234567890 01234567890</textarea> diff --git a/dom/html/reftests/41464-1a.html b/dom/html/reftests/41464-1a.html new file mode 100644 index 0000000000..f0569347fe --- /dev/null +++ b/dom/html/reftests/41464-1a.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<title>Dynamic manipulation of textarea.wrap</title> +<link rel=help href=http://www.whatwg.org/html5/#dom-textarea-wrap> +<link rel=author title=Ms2ger href=mailto:ms2ger@gmail.com> +<textarea wrap=off cols=20>01234567890 01234567890 01234567890</textarea> +<script> +document.getElementsByTagName("textarea")[0].wrap = "soft"; +</script> diff --git a/dom/html/reftests/41464-1b.html b/dom/html/reftests/41464-1b.html new file mode 100644 index 0000000000..13c42518cc --- /dev/null +++ b/dom/html/reftests/41464-1b.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<title>Dynamic manipulation of textarea.wrap</title> +<link rel=help href=http://www.whatwg.org/html5/#dom-textarea-wrap> +<link rel=author title=Ms2ger href=mailto:ms2ger@gmail.com> +<textarea wrap=off cols=20>01234567890 01234567890 01234567890</textarea> +<script> +document.getElementsByTagName("textarea")[0].setAttribute("wrap", "soft"); +</script> diff --git a/dom/html/reftests/468263-1a.html b/dom/html/reftests/468263-1a.html new file mode 100644 index 0000000000..93ad7df34e --- /dev/null +++ b/dom/html/reftests/468263-1a.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> + <img id="image2" src="pass.png"> +</body> +</html> diff --git a/dom/html/reftests/468263-1b.html b/dom/html/reftests/468263-1b.html new file mode 100644 index 0000000000..e637e30945 --- /dev/null +++ b/dom/html/reftests/468263-1b.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> + <input id="image3" type="image" src="pass.png"> +</body> +</html> diff --git a/dom/html/reftests/468263-1c.html b/dom/html/reftests/468263-1c.html new file mode 100644 index 0000000000..14c2b2b378 --- /dev/null +++ b/dom/html/reftests/468263-1c.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> + <object id="image4" type="image/png" data="pass.png"></object> +</body> +</html> diff --git a/dom/html/reftests/468263-1d.html b/dom/html/reftests/468263-1d.html new file mode 100644 index 0000000000..53740e596f --- /dev/null +++ b/dom/html/reftests/468263-1d.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<html> +<body> + <object id="image5" type="text/html" data="data:text/html,<b>Testing</b>"></object> +</body> +</html> diff --git a/dom/html/reftests/468263-2-alternate-ref.html b/dom/html/reftests/468263-2-alternate-ref.html new file mode 100644 index 0000000000..c3bdb9da93 --- /dev/null +++ b/dom/html/reftests/468263-2-alternate-ref.html @@ -0,0 +1,8 @@ +<!DOCTYPE HTML> +<html> +<body> + <img id="image1" src=""> + <img id="image2"> + <input id="image3" type="image"> +</body> +</html> diff --git a/dom/html/reftests/468263-2-ref.html b/dom/html/reftests/468263-2-ref.html new file mode 100644 index 0000000000..6e7f6cb360 --- /dev/null +++ b/dom/html/reftests/468263-2-ref.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<body> + <img id="image1" src=""> + <img id="image2"> + <input id="image3" type="image"> + <object id="image4" type="image/png"> + <object id="image5" type="text/html"></object> +</body> +</html> diff --git a/dom/html/reftests/468263-2.html b/dom/html/reftests/468263-2.html new file mode 100644 index 0000000000..d3d447d07e --- /dev/null +++ b/dom/html/reftests/468263-2.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<body onload="document.getElementById('image1').setAttribute('src', ''); document.getElementById('image2').removeAttribute('src'); document.getElementById('image3').removeAttribute('src'); document.getElementById('image4').removeAttribute('data'); document.getElementById('image5').removeAttribute('data');"> + <img id="image1" src="pass.png"> + <img id="image2" src="pass.png"> + <input id="image3" type="image" src="pass.png"> + <object id="image4" type="image/png" data="pass.png"></object> + <object id="image5" type="text/html" data="data:text/html,<b>Testing</b>"></object> +</body> +</html> diff --git a/dom/html/reftests/484200-1-ref.html b/dom/html/reftests/484200-1-ref.html new file mode 100644 index 0000000000..39ef12261d --- /dev/null +++ b/dom/html/reftests/484200-1-ref.html @@ -0,0 +1,11 @@ +<html> +<head> +<title>Test for Bug 484200</title> +<style> +p { background:lime } +</style> +</head> +<body> +<p>xxxxx</p> +</body> +</html> diff --git a/dom/html/reftests/484200-1.html b/dom/html/reftests/484200-1.html new file mode 100644 index 0000000000..c2663d1aea --- /dev/null +++ b/dom/html/reftests/484200-1.html @@ -0,0 +1,11 @@ +<html> +<head> +<title>Test for Bug 484200</title> +<style src=style> +p { background:lime } +</style> +</head> +<body> +<p>xxxxx</p> +</body> +</html> diff --git a/dom/html/reftests/485377-ref.html b/dom/html/reftests/485377-ref.html new file mode 100644 index 0000000000..e98cbbb71f --- /dev/null +++ b/dom/html/reftests/485377-ref.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<title>The mark element</title> +<p>Foo <span style="background: yellow; color: black;">bar</span> baz. diff --git a/dom/html/reftests/485377.html b/dom/html/reftests/485377.html new file mode 100644 index 0000000000..9f91f99805 --- /dev/null +++ b/dom/html/reftests/485377.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<title>The mark element</title> +<p>Foo <mark>bar</mark> baz. diff --git a/dom/html/reftests/52019-1-ref.html b/dom/html/reftests/52019-1-ref.html new file mode 100644 index 0000000000..73bf05f681 --- /dev/null +++ b/dom/html/reftests/52019-1-ref.html @@ -0,0 +1,11 @@ +<html>
+<head>
+<title>Test for Bug 52019</title>
+</head>
+<body>
+<font>font</font>
+<font>font</font>
+<font size="+2">font</font>
+<font size="-2">font</font>
+</body>
+</html>
diff --git a/dom/html/reftests/52019-1.html b/dom/html/reftests/52019-1.html new file mode 100644 index 0000000000..09cff148fa --- /dev/null +++ b/dom/html/reftests/52019-1.html @@ -0,0 +1,11 @@ +<html>
+<head>
+<title>Test for Bug 52019</title>
+</head>
+<body>
+<font size="+.5">font</font>
+<font size="-.5">font</font>
+<font size="+2.5">font</font>
+<font size="-2.5">font</font>
+</body>
+</html>
diff --git a/dom/html/reftests/557840-ref.html b/dom/html/reftests/557840-ref.html new file mode 100644 index 0000000000..ea72d50f5f --- /dev/null +++ b/dom/html/reftests/557840-ref.html @@ -0,0 +1,3 @@ +<!doctype html> +<title>Canvas and hspace, vspace</title> +<canvas style="background: black;"></canvas> diff --git a/dom/html/reftests/557840.html b/dom/html/reftests/557840.html new file mode 100644 index 0000000000..4aed5092a8 --- /dev/null +++ b/dom/html/reftests/557840.html @@ -0,0 +1,3 @@ +<!doctype html> +<title>Canvas and hspace, vspace</title> +<canvas hspace="42" vspace="42" style="background: black;"></canvas> diff --git a/dom/html/reftests/560059-video-dimensions-ref.html b/dom/html/reftests/560059-video-dimensions-ref.html new file mode 100644 index 0000000000..f3424de689 --- /dev/null +++ b/dom/html/reftests/560059-video-dimensions-ref.html @@ -0,0 +1,3 @@ +<!doctype html> +<title>Video dimensions</title> +<video style="border: thin solid black;" width="300" height="150"></video> diff --git a/dom/html/reftests/560059-video-dimensions.html b/dom/html/reftests/560059-video-dimensions.html new file mode 100644 index 0000000000..99373c999a --- /dev/null +++ b/dom/html/reftests/560059-video-dimensions.html @@ -0,0 +1,3 @@ +<!doctype html> +<title>Video dimensions</title> +<video style="border: thin solid black;"></video> diff --git a/dom/html/reftests/573322-no-quirks-ref.html b/dom/html/reftests/573322-no-quirks-ref.html new file mode 100644 index 0000000000..e3f993f2f7 --- /dev/null +++ b/dom/html/reftests/573322-no-quirks-ref.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<style> +div { background: green; border: solid blue; width: 100px; height: 100px; } +</style> +<table border=1 width=50%> +<tr> +<td><div></div>left +</table> +<table border=1 width=50%> +<tr> +<td><div></div>justify +</table> +<table border=1 width=50%> +<tr> +<td style="text-align: -moz-right;"><div></div>right +</table> +<table border=1 width=50%> +<tr> +<td style="text-align: -moz-center;"><div></div>center +</table> +<table border=1 width=50%> +<tr> +<td style="text-align: -moz-center;"><div></div>middle +</table> +<table border=1 width=50%> +<tr> +<td style="text-align: center;"><div></div>absmiddle +</table> diff --git a/dom/html/reftests/573322-no-quirks.html b/dom/html/reftests/573322-no-quirks.html new file mode 100644 index 0000000000..ac27609e36 --- /dev/null +++ b/dom/html/reftests/573322-no-quirks.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<style> +div { background: green; border: solid blue; width: 100px; height: 100px; } +</style> +<table border=1 width=50%> +<tr> +<td align=left><div></div>left +</table> +<table border=1 width=50%> +<tr> +<td align=justify><div></div>justify +</table> +<table border=1 width=50%> +<tr> +<td align=right><div></div>right +</table> +<table border=1 width=50%> +<tr> +<td align=center><div></div>center +</table> +<table border=1 width=50%> +<tr> +<td align=middle><div></div>middle +</table> +<table border=1 width=50%> +<tr> +<td align=absmiddle><div></div>absmiddle +</table> diff --git a/dom/html/reftests/573322-quirks-ref.html b/dom/html/reftests/573322-quirks-ref.html new file mode 100644 index 0000000000..6cc22a58e5 --- /dev/null +++ b/dom/html/reftests/573322-quirks-ref.html @@ -0,0 +1,27 @@ +<style> +div { background: green; border: solid blue; width: 100px; height: 100px; } +</style> +<table border=1 width=50%> +<tr> +<td><div></div>left +</table> +<table border=1 width=50%> +<tr> +<td><div></div>justify +</table> +<table border=1 width=50%> +<tr> +<td style="text-align: -moz-right;"><div></div>right +</table> +<table border=1 width=50%> +<tr> +<td style="text-align: -moz-center;"><div></div>center +</table> +<table border=1 width=50%> +<tr> +<td style="text-align: -moz-center;"><div></div>middle +</table> +<table border=1 width=50%> +<tr> +<td style="text-align: center;"><div></div>absmiddle +</table> diff --git a/dom/html/reftests/573322-quirks.html b/dom/html/reftests/573322-quirks.html new file mode 100644 index 0000000000..35757b1859 --- /dev/null +++ b/dom/html/reftests/573322-quirks.html @@ -0,0 +1,27 @@ +<style> +div { background: green; border: solid blue; width: 100px; height: 100px; } +</style> +<table border=1 width=50%> +<tr> +<td align=left><div></div>left +</table> +<table border=1 width=50%> +<tr> +<td align=justify><div></div>justify +</table> +<table border=1 width=50%> +<tr> +<td align=right><div></div>right +</table> +<table border=1 width=50%> +<tr> +<td align=center><div></div>center +</table> +<table border=1 width=50%> +<tr> +<td align=middle><div></div>middle +</table> +<table border=1 width=50%> +<tr> +<td align=absmiddle><div></div>absmiddle +</table> diff --git a/dom/html/reftests/596455-1a.html b/dom/html/reftests/596455-1a.html new file mode 100644 index 0000000000..60d494072e --- /dev/null +++ b/dom/html/reftests/596455-1a.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html class='reftest-wait'> + <script> + function onLoadHandler() + { + document.getElementById('l').value = document.getElementById('i').value; + document.documentElement.className=''; + } + </script> + <body onload="onLoadHandler();"> + <input type='hidden' value='foo bar' id='i'> + <textarea id='l'></textarea> + </body> +</html> diff --git a/dom/html/reftests/596455-1b.html b/dom/html/reftests/596455-1b.html new file mode 100644 index 0000000000..8478786c12 --- /dev/null +++ b/dom/html/reftests/596455-1b.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html class='reftest-wait'> + <script> + function onLoadHandler() + { + document.getElementById('l').value = document.getElementById('i').value; + document.documentElement.className=''; + } + </script> + <body onload="onLoadHandler();"> + <input value='foo bar' type='hidden' id='i'> + <textarea id='l'></textarea> + </body> +</html> diff --git a/dom/html/reftests/596455-2a.html b/dom/html/reftests/596455-2a.html new file mode 100644 index 0000000000..f78a36c613 --- /dev/null +++ b/dom/html/reftests/596455-2a.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html class='reftest-wait'> + <script> + function onLoadHandler() + { + document.getElementById('l').value = document.getElementById('i').value; + document.documentElement.className=''; + } + </script> + <body onload="onLoadHandler();"> + <input style="display:none;" type='text' value='foo bar' id='i'> + <textarea id='l'></textarea> + </body> +</html> diff --git a/dom/html/reftests/596455-2b.html b/dom/html/reftests/596455-2b.html new file mode 100644 index 0000000000..1867327903 --- /dev/null +++ b/dom/html/reftests/596455-2b.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html class='reftest-wait'> + <script> + function onLoadHandler() + { + document.getElementById('l').value = document.getElementById('i').value; + document.documentElement.className=''; + } + </script> + <body onload="onLoadHandler();"> + <input style="display:none;" value='foo bar' type='text' id='i'> + <textarea id='l'></textarea> + </body> +</html> diff --git a/dom/html/reftests/596455-ref-1.html b/dom/html/reftests/596455-ref-1.html new file mode 100644 index 0000000000..10371df270 --- /dev/null +++ b/dom/html/reftests/596455-ref-1.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> + <body> + <textarea>foo bar</textarea> + </body> +</html> diff --git a/dom/html/reftests/596455-ref-2.html b/dom/html/reftests/596455-ref-2.html new file mode 100644 index 0000000000..0dbc4dbb31 --- /dev/null +++ b/dom/html/reftests/596455-ref-2.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> + <body> + <textarea>foobar</textarea> + </body> +</html> diff --git a/dom/html/reftests/610935-ref.html b/dom/html/reftests/610935-ref.html new file mode 100644 index 0000000000..7a2a41a52a --- /dev/null +++ b/dom/html/reftests/610935-ref.html @@ -0,0 +1,12 @@ +<!doctype html> +<title>Test for bug 610935</title> +<style> +div { width:300px; background:yellow; height:50px; } +table { width:150%; } +td { background:blue; } +</style> +<div> + <table cellspacing="0" cellpadding="0" border="0"> + <tr><td>parent div float=left</td></tr> + </table> +</div> diff --git a/dom/html/reftests/610935.html b/dom/html/reftests/610935.html new file mode 100644 index 0000000000..5495ae3d5a --- /dev/null +++ b/dom/html/reftests/610935.html @@ -0,0 +1,11 @@ +<!doctype html> +<title>Test for bug 610935</title> +<style> +div { width:300px; background:yellow; height:50px; } +td { background:blue; } +</style> +<div> + <table width="150%" cellspacing="0" cellpadding="0" border="0"> + <tr><td>parent div float=left</td></tr> + </table> +</div> diff --git a/dom/html/reftests/649134-1.html b/dom/html/reftests/649134-1.html new file mode 100644 index 0000000000..b38e988304 --- /dev/null +++ b/dom/html/reftests/649134-1.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html><head> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <title>Testcase for bug </title> +<link rel="stylesheet" type="text/css" href="" /> +<!-- + #foo { + /* This doesn't get evaluated */ + color: red; + } + #ie { + border: 5px solid red; + } + #ie { + display: block; + } + #moz { + color: blue; + /* display: none; */ + } +--> + +</head> +<body> + +<p id="foo">foo</p> +<p id="ie">ie</p> +<p id="moz">moz</p> + +</body> +</html> diff --git a/dom/html/reftests/649134-2-ref.html b/dom/html/reftests/649134-2-ref.html new file mode 100644 index 0000000000..d15fae5282 --- /dev/null +++ b/dom/html/reftests/649134-2-ref.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html><head> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <title>Testcase for bug </title> +<style> + #ie { + border: 5px solid red; + } + #ie { + display: block; + } + #moz { + color: blue; + /* display: none; */ + } +</style> +</head> +<body> + +<p id="foo">foo</p> +<p id="ie">ie</p> +<p id="moz">moz</p> + +</body> +</html> diff --git a/dom/html/reftests/649134-2.html b/dom/html/reftests/649134-2.html new file mode 100644 index 0000000000..4d2a5ae50d --- /dev/null +++ b/dom/html/reftests/649134-2.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html><head> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <title>Testcase for bug </title> +<link rel="stylesheet" type="text/css" href=" " /> +<!-- + #foo { + /* This doesn't get evaluated */ + color: red; + } + #ie { + border: 5px solid red; + } + #ie { + display: block; + } + #moz { + color: blue; + /* display: none; */ + } +--> + +</head> +<body> + +<p id="foo">foo</p> +<p id="ie">ie</p> +<p id="moz">moz</p> + +</body> +</html> diff --git a/dom/html/reftests/649134-ref.html b/dom/html/reftests/649134-ref.html new file mode 100644 index 0000000000..2968464bed --- /dev/null +++ b/dom/html/reftests/649134-ref.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html><head> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <title>Testcase for bug </title> +</head> +<body> + +<p id="foo">foo</p> +<p id="ie">ie</p> +<p id="moz">moz</p> + +</body> +</html> diff --git a/dom/html/reftests/741776-1-ref.html b/dom/html/reftests/741776-1-ref.html new file mode 100644 index 0000000000..bc05ae4487 --- /dev/null +++ b/dom/html/reftests/741776-1-ref.html @@ -0,0 +1 @@ +<!DOCTYPE html><meta charset=utf-8><pre>ää diff --git a/dom/html/reftests/741776-1.vtt b/dom/html/reftests/741776-1.vtt new file mode 100644 index 0000000000..b66b11694c --- /dev/null +++ b/dom/html/reftests/741776-1.vtt @@ -0,0 +1 @@ +ää diff --git a/dom/html/reftests/82711-1-ref.html b/dom/html/reftests/82711-1-ref.html new file mode 100644 index 0000000000..e0b25fc9b3 --- /dev/null +++ b/dom/html/reftests/82711-1-ref.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> +<body> +<textarea rows="10" cols="25" wrap="off"> + 0 1 2 3 + 4 5 + + 6 7 8 + 9 + + + This is a long line that could wrap. +</textarea> +</body> +</html> diff --git a/dom/html/reftests/82711-1.html b/dom/html/reftests/82711-1.html new file mode 100644 index 0000000000..70a8c1b23f --- /dev/null +++ b/dom/html/reftests/82711-1.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> +<body> +<textarea rows="10" cols="25" style="white-space: pre"> + 0 1 2 3 + 4 5 + + 6 7 8 + 9 + + + This is a long line that could wrap. +</textarea> +</body> +</html> diff --git a/dom/html/reftests/82711-2-ref.html b/dom/html/reftests/82711-2-ref.html new file mode 100644 index 0000000000..963b9c7141 --- /dev/null +++ b/dom/html/reftests/82711-2-ref.html @@ -0,0 +1,15 @@ +<!doctype html> +<html> +<body> +<textarea rows="10" cols="25" wrap="off" style="white-space: normal"> + 0 1 2 3 + 4 5 + + 6 7 8 + 9 + + + This is a long line that could wrap. +</textarea> +</body> +</html> diff --git a/dom/html/reftests/82711-2.html b/dom/html/reftests/82711-2.html new file mode 100644 index 0000000000..aacd6d481e --- /dev/null +++ b/dom/html/reftests/82711-2.html @@ -0,0 +1,8 @@ +<!doctype html> +<html> +<body> +<textarea rows="10" cols="25" style="white-space: normal"> +0 1 2 3 4 5 6 7 8 9 This is a long line that could wrap. +</textarea> +</body> +</html> diff --git a/dom/html/reftests/autofocus/autofocus-after-body-focus-ref.html b/dom/html/reftests/autofocus/autofocus-after-body-focus-ref.html new file mode 100644 index 0000000000..3801ed7543 --- /dev/null +++ b/dom/html/reftests/autofocus/autofocus-after-body-focus-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body onload="document.getElementsByTagName('input')[0].focus();"> + <input onfocus="document.documentElement.removeAttribute('class');"> + </body> +</html> diff --git a/dom/html/reftests/autofocus/autofocus-after-body-focus.html b/dom/html/reftests/autofocus/autofocus-after-body-focus.html new file mode 100644 index 0000000000..6d43b865a8 --- /dev/null +++ b/dom/html/reftests/autofocus/autofocus-after-body-focus.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body> + <script> + document.body.focus(); + </script> + <input autofocus onfocus="document.documentElement.removeAttribute('class');"> + </body> +</html> diff --git a/dom/html/reftests/autofocus/autofocus-after-load-ref.html b/dom/html/reftests/autofocus/autofocus-after-load-ref.html new file mode 100644 index 0000000000..f28f06f0f3 --- /dev/null +++ b/dom/html/reftests/autofocus/autofocus-after-load-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> + <link rel='stylesheet' type='text/css' href='style.css'> + <body> + <input autofocus><textarea></textarea><select></select><button></button> + </body> +</html> diff --git a/dom/html/reftests/autofocus/autofocus-after-load.html b/dom/html/reftests/autofocus/autofocus-after-load.html new file mode 100644 index 0000000000..753ef183d1 --- /dev/null +++ b/dom/html/reftests/autofocus/autofocus-after-load.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <script> + function loadHandler() + { + var body = document.body; + + var elements = ["input", "textarea", "select", "button"]; + for (var e of elements) { + var el = document.createElement(e); + el.autofocus = true; + body.appendChild(el); + } + + setTimeout(document.documentElement.removeAttribute('class'), 0); + } + </script> + <body onload="loadHandler();"> + </body> +</html> diff --git a/dom/html/reftests/autofocus/autofocus-leaves-iframe-ref.html b/dom/html/reftests/autofocus/autofocus-leaves-iframe-ref.html new file mode 100644 index 0000000000..e74e2753dc --- /dev/null +++ b/dom/html/reftests/autofocus/autofocus-leaves-iframe-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <script> + function loadHandler() + { + frames[0].document.getElementsByTagName('input')[0].onfocus = function() { + document.documentElement.removeAttribute('class'); + } + frames[0].document.getElementsByTagName('input')[0].focus(); + } + </script> + <body onload="loadHandler();"> + <iframe srcdoc="<input>"></iframe> + <input></input> + </body> +</html> diff --git a/dom/html/reftests/autofocus/autofocus-leaves-iframe.html b/dom/html/reftests/autofocus/autofocus-leaves-iframe.html new file mode 100644 index 0000000000..9069156878 --- /dev/null +++ b/dom/html/reftests/autofocus/autofocus-leaves-iframe.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <script> + function frameLoadHandler() + { + var i = document.createElement('input'); + i.autofocus = true; + document.body.appendChild(i); + setTimeout(document.documentElement.removeAttribute('class'), 0); + } + </script> + <body> + <iframe onload="frameLoadHandler();" srcdoc="<input autofocus>"></iframe> + </body> +</html> diff --git a/dom/html/reftests/autofocus/button-create.html b/dom/html/reftests/autofocus/button-create.html new file mode 100644 index 0000000000..ae49d162ad --- /dev/null +++ b/dom/html/reftests/autofocus/button-create.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body> + <script> + var body = document.body; + + var i = document.createElement('button'); + i.autofocus = false; + body.appendChild(i); + + i = document.createElement('button'); + i.autofocus = true; + i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); }; + body.appendChild(i); + + i = document.createElement('button'); + i.autofocus = true; + i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); }; + body.appendChild(i); + </script> + </body> +</html> diff --git a/dom/html/reftests/autofocus/button-load.html b/dom/html/reftests/autofocus/button-load.html new file mode 100644 index 0000000000..a9e28e2bb6 --- /dev/null +++ b/dom/html/reftests/autofocus/button-load.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <script> + function focusHandler() + { + setTimeout(document.documentElement.removeAttribute('class'), 0); + } + </script> + <body> + <button></button><button autofocus onfocus="focusHandler();"></button><button autofocus onfocus="focusHandler();"></button> + </body> +</html> diff --git a/dom/html/reftests/autofocus/button-ref.html b/dom/html/reftests/autofocus/button-ref.html new file mode 100644 index 0000000000..878c8e2681 --- /dev/null +++ b/dom/html/reftests/autofocus/button-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body onload="document.getElementsByTagName('button')[1].focus();"> + <button></button><button onfocus="document.documentElement.removeAttribute('class');"></button><button></button> + </body> +</html> diff --git a/dom/html/reftests/autofocus/input-create.html b/dom/html/reftests/autofocus/input-create.html new file mode 100644 index 0000000000..c6d0c28089 --- /dev/null +++ b/dom/html/reftests/autofocus/input-create.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body> + <script> + var body = document.body; + + var i = document.createElement('input'); + i.autofocus = false; + body.appendChild(i); + + i = document.createElement('input'); + i.autofocus = true; + i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); }; + body.appendChild(i); + + i = document.createElement('input'); + i.autofocus = true; + i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); }; + body.appendChild(i); + </script> + </body> +</html> diff --git a/dom/html/reftests/autofocus/input-load.html b/dom/html/reftests/autofocus/input-load.html new file mode 100644 index 0000000000..d40b49177a --- /dev/null +++ b/dom/html/reftests/autofocus/input-load.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <script> + function focusHandler() + { + document.documentElement.removeAttribute('class'); + } + </script> + <body> + <input><input autofocus onfocus="focusHandler();"><input autofocus onfocus="focusHandler();"> + </body> +</html> diff --git a/dom/html/reftests/autofocus/input-number-ref.html b/dom/html/reftests/autofocus/input-number-ref.html new file mode 100644 index 0000000000..384915edb8 --- /dev/null +++ b/dom/html/reftests/autofocus/input-number-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <!-- In this case we're using reftest-wait to make sure the test doesn't + get snapshotted before it's been focused. We're not testing + invalidation so we don't need to listen for MozReftestInvalidate. + --> + <head> + <meta charset="utf-8"> + </head> + <body onload="document.getElementsByTagName('input')[0].focus();"> + <input onfocus="document.documentElement.removeAttribute('class');" + style="-moz-appearance: none;"> + <!-- div to cover spin box area for type=number to type=text comparison --> + <div style="display:block; position:absolute; background-color:black; width:200px; height:100px; top:0px; left:100px;"> + </body> +</html> + diff --git a/dom/html/reftests/autofocus/input-number.html b/dom/html/reftests/autofocus/input-number.html new file mode 100644 index 0000000000..7816ee9bd9 --- /dev/null +++ b/dom/html/reftests/autofocus/input-number.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <!-- In this case we're using reftest-wait to make sure the test doesn't + get snapshotted before it's been focused. We're not testing + invalidation so we don't need to listen for MozReftestInvalidate. + --> + <head> + <meta charset="utf-8"> + <script> + +function focusHandler() { + setTimeout(function() { + document.documentElement.removeAttribute('class'); + }, 0); +} + + </script> + </head> + <body> + <input type="number" autofocus onfocus="focusHandler();" + style="-moz-appearance: none;"> + <!-- div to cover spin box area for type=number to type=text comparison --> + <div style="display:block; position:absolute; background-color:black; width:200px; height:100px; top:0px; left:100px;"> + </body> +</html> + diff --git a/dom/html/reftests/autofocus/input-ref.html b/dom/html/reftests/autofocus/input-ref.html new file mode 100644 index 0000000000..6e2e546d24 --- /dev/null +++ b/dom/html/reftests/autofocus/input-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body onload="document.getElementsByTagName('input')[1].focus();"> + <input><input onfocus="document.documentElement.removeAttribute('class');"><input> + </body> +</html> diff --git a/dom/html/reftests/autofocus/input-time-ref.html b/dom/html/reftests/autofocus/input-time-ref.html new file mode 100644 index 0000000000..abaa6feeac --- /dev/null +++ b/dom/html/reftests/autofocus/input-time-ref.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <!-- In this case we're using reftest-wait to make sure the test doesn't + get snapshotted before it's been focused. We're not testing + invalidation so we don't need to listen for MozReftestInvalidate. + --> + <head> + <script> + function focusHandler() { + setTimeout(function() { + document.documentElement.removeAttribute("class"); + }, 0); + } + </script> + </head> + <body onload="document.getElementById('t').focus();"> + <input type="time" id="t" onfocus="focusHandler();" + style="-moz-appearance: none;"> + </body> +</html> + + diff --git a/dom/html/reftests/autofocus/input-time.html b/dom/html/reftests/autofocus/input-time.html new file mode 100644 index 0000000000..a86a91bbfc --- /dev/null +++ b/dom/html/reftests/autofocus/input-time.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <!-- In this case we're using reftest-wait to make sure the test doesn't + get snapshotted before it's been focused. We're not testing + invalidation so we don't need to listen for MozReftestInvalidate. + --> + <head> + <script> + function focusHandler() { + setTimeout(function() { + document.documentElement.removeAttribute("class"); + }, 0); + } + </script> + </head> + <body> + <input type="time" autofocus onfocus="focusHandler();" + style="-moz-appearance: none;"> + </body> +</html> + + diff --git a/dom/html/reftests/autofocus/reftest.list b/dom/html/reftests/autofocus/reftest.list new file mode 100644 index 0000000000..03ddce4149 --- /dev/null +++ b/dom/html/reftests/autofocus/reftest.list @@ -0,0 +1,13 @@ +needs-focus == input-load.html input-ref.html +needs-focus == input-create.html input-ref.html +needs-focus == input-number.html input-number-ref.html +fuzzy(0-5,0-1) needs-focus == input-time.html input-time-ref.html # One anti-aliased outline corner. +needs-focus == button-load.html button-ref.html +needs-focus == button-create.html button-ref.html +fuzzy-if(gtkWidget,0-18,0-1) needs-focus == textarea-load.html textarea-ref.html # One anti-aliased corner. +needs-focus == textarea-create.html textarea-ref.html +fuzzy-if(Android,0-10,0-5) needs-focus == select-load.html select-ref.html +fuzzy(0-10,0-5) needs-focus == select-create.html select-ref.html +fuzzy(0-1,0-1) needs-focus == autofocus-after-load.html autofocus-after-load-ref.html +needs-focus == autofocus-leaves-iframe.html autofocus-leaves-iframe-ref.html +fuzzy(0-5,0-1) needs-focus == autofocus-after-body-focus.html autofocus-after-body-focus-ref.html diff --git a/dom/html/reftests/autofocus/select-create.html b/dom/html/reftests/autofocus/select-create.html new file mode 100644 index 0000000000..fd9c29c954 --- /dev/null +++ b/dom/html/reftests/autofocus/select-create.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body> + <script> + var body = document.body; + + var i = document.createElement('select'); + i.autofocus = false; + body.appendChild(i); + + i = document.createElement('select'); + i.autofocus = true; + i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); }; + body.appendChild(i); + + i = document.createElement('select'); + i.autofocus = true; + i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); }; + body.appendChild(i); + </script> + </body> +</html> diff --git a/dom/html/reftests/autofocus/select-load.html b/dom/html/reftests/autofocus/select-load.html new file mode 100644 index 0000000000..976005bec2 --- /dev/null +++ b/dom/html/reftests/autofocus/select-load.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <script> + function focusHandler() + { + setTimeout(document.documentElement.removeAttribute('class'), 0); + } + </script> + <body> + <select></select><select autofocus onfocus="focusHandler();"></select><select autofocus onfocus="focusHandler();"></select> + </body> +</html> diff --git a/dom/html/reftests/autofocus/select-ref.html b/dom/html/reftests/autofocus/select-ref.html new file mode 100644 index 0000000000..7fa9cd6559 --- /dev/null +++ b/dom/html/reftests/autofocus/select-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body onload="document.getElementsByTagName('select')[1].focus();"> + <select></select><select onfocus="document.documentElement.removeAttribute('class');"></select><select></select> + </body> +</html> diff --git a/dom/html/reftests/autofocus/style.css b/dom/html/reftests/autofocus/style.css new file mode 100644 index 0000000000..f0dd5d1498 --- /dev/null +++ b/dom/html/reftests/autofocus/style.css @@ -0,0 +1,10 @@ +:focus { background-color: green; } + +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=1645773 */ +textarea { -moz-appearance: none; } + +/** + * autofocus is considered like a keyboard focus and .focus() isn't. + * We might change that with bug 620056 but for these tests, we don't really care. + */ +::-moz-focus-inner { border: none; } diff --git a/dom/html/reftests/autofocus/textarea-create.html b/dom/html/reftests/autofocus/textarea-create.html new file mode 100644 index 0000000000..e506bb2b72 --- /dev/null +++ b/dom/html/reftests/autofocus/textarea-create.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body> + <script> + var body = document.body; + + var i = document.createElement('textarea'); + i.autofocus = false; + body.appendChild(i); + + i = document.createElement('textarea'); + i.autofocus = true; + i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); }; + body.appendChild(i); + + i = document.createElement('textarea'); + i.autofocus = true; + i.onfocus = function() { setTimeout(document.documentElement.removeAttribute('class'), 0); }; + body.appendChild(i); + </script> + </body> +</html> diff --git a/dom/html/reftests/autofocus/textarea-load.html b/dom/html/reftests/autofocus/textarea-load.html new file mode 100644 index 0000000000..13ab2cb2cc --- /dev/null +++ b/dom/html/reftests/autofocus/textarea-load.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <script> + function focusHandler() + { + setTimeout(document.documentElement.removeAttribute('class'), 0); + } + </script> + <body> + <textarea></textarea><textarea autofocus onfocus="focusHandler();"></textarea><textarea autofocus onfocus="focusHandler();"></textarea> + </body> +</html> diff --git a/dom/html/reftests/autofocus/textarea-ref.html b/dom/html/reftests/autofocus/textarea-ref.html new file mode 100644 index 0000000000..b79bd7abe8 --- /dev/null +++ b/dom/html/reftests/autofocus/textarea-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html class="reftest-wait"> + <link rel='stylesheet' type='text/css' href='style.css'> + <body onload="document.getElementsByTagName('textarea')[1].focus();"> + <textarea></textarea><textarea onfocus="document.documentElement.removeAttribute('class');"></textarea><textarea></textarea> + </body> +</html> diff --git a/dom/html/reftests/body-frame-margin-remove-other-pres-hint-ref.html b/dom/html/reftests/body-frame-margin-remove-other-pres-hint-ref.html new file mode 100644 index 0000000000..1e651db66d --- /dev/null +++ b/dom/html/reftests/body-frame-margin-remove-other-pres-hint-ref.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> + <title></title> +</head> +<body> +<script type="text/javascript"> + function loadFrame() { + document.documentElement.className = ""; + } +</script> +<iframe id=frame onload="loadFrame()" srcdoc="<body><span lang='en'>text</span></body>" marginwidth="100px" marginheight="100px" width=300px height=300px></iframe> +</body> +</html>
\ No newline at end of file diff --git a/dom/html/reftests/body-frame-margin-remove-other-pres-hint.html b/dom/html/reftests/body-frame-margin-remove-other-pres-hint.html new file mode 100644 index 0000000000..16428813af --- /dev/null +++ b/dom/html/reftests/body-frame-margin-remove-other-pres-hint.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> + <title></title> +</head> +<body> +<script type="text/javascript"> + function loadFrame() { + let frame = document.getElementById('frame'); + frame.contentDocument.body.removeAttribute('lang'); + document.documentElement.className = ""; + } +</script> +<iframe id=frame onload="loadFrame()" srcdoc="<body lang='en'>text</body>" marginwidth="100px" marginheight="100px" width=300px height=300px></iframe> +</body> +</html>
\ No newline at end of file diff --git a/dom/html/reftests/body-topmargin-dynamic.html b/dom/html/reftests/body-topmargin-dynamic.html new file mode 100644 index 0000000000..e6c8c505e7 --- /dev/null +++ b/dom/html/reftests/body-topmargin-dynamic.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<body> +this text should have a margin of 100px on the top and left +<p style="direction: rtl">this text should have a margin of 100px on the right</p> +<script type="text/javascript"> + document.body.setAttribute("topmargin", "100px"); + document.body.setAttribute("leftmargin", "100px"); + document.body.setAttribute("rightmargin", "100px"); +</script> +</body> +</html> diff --git a/dom/html/reftests/body-topmargin-ref.html b/dom/html/reftests/body-topmargin-ref.html new file mode 100644 index 0000000000..6530a0ae4b --- /dev/null +++ b/dom/html/reftests/body-topmargin-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<body topmargin="100px" leftmargin="100px" rightmargin="100px"> +this text should have a margin of 100px on the top and left +<p style="direction: rtl">this text should have a margin of 100px on the right</p> +</body> +</html> diff --git a/dom/html/reftests/bug1106522-1.html b/dom/html/reftests/bug1106522-1.html new file mode 100644 index 0000000000..db07c1010e --- /dev/null +++ b/dom/html/reftests/bug1106522-1.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<body> + <picture> + <source srcset="lime100x100.svg" type="image/svg+xml"> + <img src="red.png" width="100" height="100"> + </picture> +</body> +</html> diff --git a/dom/html/reftests/bug1106522-2.html b/dom/html/reftests/bug1106522-2.html new file mode 100644 index 0000000000..15520982fc --- /dev/null +++ b/dom/html/reftests/bug1106522-2.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<body> + <picture> + <source srcset="lime100x100.svg"> + <img src="red.png" width="100" height="100"> + </picture> +</body> +</html> diff --git a/dom/html/reftests/bug1106522-ref.html b/dom/html/reftests/bug1106522-ref.html new file mode 100644 index 0000000000..476c47c12d --- /dev/null +++ b/dom/html/reftests/bug1106522-ref.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<html> +<body> + <img src="lime100x100.svg"> +</body> +</html> diff --git a/dom/html/reftests/bug1196784-no-srcset.html b/dom/html/reftests/bug1196784-no-srcset.html new file mode 100644 index 0000000000..df55d48631 --- /dev/null +++ b/dom/html/reftests/bug1196784-no-srcset.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> + <img height="100" width="100" src="bug1196784.png"> +</body> +</html> diff --git a/dom/html/reftests/bug1196784-with-srcset.html b/dom/html/reftests/bug1196784-with-srcset.html new file mode 100644 index 0000000000..1cd77bad9b --- /dev/null +++ b/dom/html/reftests/bug1196784-with-srcset.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> + <img height="100" width="100" src="bug1196784.png" srcset="bug1196784.png"> +</body> +</html> diff --git a/dom/html/reftests/bug1196784.png b/dom/html/reftests/bug1196784.png Binary files differnew file mode 100644 index 0000000000..8d0ed56825 --- /dev/null +++ b/dom/html/reftests/bug1196784.png diff --git a/dom/html/reftests/bug1228601-video-rotated-ref.html b/dom/html/reftests/bug1228601-video-rotated-ref.html new file mode 100644 index 0000000000..e489c9a75b --- /dev/null +++ b/dom/html/reftests/bug1228601-video-rotated-ref.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> +function done() { + document.documentElement.removeAttribute("class"); +} +</script> +</head> +<body onload="setTimeout(done, 3);"> + <video src="video_rotated.mp4" onended="done()" autoplay="true"> +</body> +</html> diff --git a/dom/html/reftests/bug1228601-video-rotation-90.html b/dom/html/reftests/bug1228601-video-rotation-90.html new file mode 100644 index 0000000000..94c57d7504 --- /dev/null +++ b/dom/html/reftests/bug1228601-video-rotation-90.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> +function done() { + document.documentElement.removeAttribute("class"); +} +</script> +</head> +<body onload="setTimeout(done, 3);"> + <video src="video_rotation_90.mp4" onended="done()" autoplay="true"> +</body> +</html> diff --git a/dom/html/reftests/bug1423850-canvas-video-rotated-ref.html b/dom/html/reftests/bug1423850-canvas-video-rotated-ref.html new file mode 100644 index 0000000000..8927b6e9f7 --- /dev/null +++ b/dom/html/reftests/bug1423850-canvas-video-rotated-ref.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head><script> +function done() { + let video = document.querySelector("video"); + let canvas = document.querySelector("canvas"); + let context = canvas.getContext("2d"); + context.drawImage(video, 30, 50, video.videoWidth, video.videoHeight); + document.documentElement.removeAttribute("class"); +} +</script></head> +<body bgcolor="gray"> + <video src="video_rotated.mp4" onended="done()" autoplay="true"></video> + <canvas width="60" height="100"></canvas> +</body> +</html> diff --git a/dom/html/reftests/bug1423850-canvas-video-rotation-90.html b/dom/html/reftests/bug1423850-canvas-video-rotation-90.html new file mode 100644 index 0000000000..5039e64afa --- /dev/null +++ b/dom/html/reftests/bug1423850-canvas-video-rotation-90.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head><script> +function done() { + let video = document.querySelector("video"); + let canvas = document.querySelector("canvas"); + let context = canvas.getContext("2d"); + context.drawImage(video, 30, 50, video.videoWidth, video.videoHeight); + document.documentElement.removeAttribute("class"); +} +</script></head> +<body bgcolor="gray"> + <video src="video_rotation_90.mp4" onended="done()" autoplay="true"></video> + <canvas width="60" height="100"></canvas> +</body> +</html> diff --git a/dom/html/reftests/bug1512297-ref.html b/dom/html/reftests/bug1512297-ref.html new file mode 100644 index 0000000000..45026e86cc --- /dev/null +++ b/dom/html/reftests/bug1512297-ref.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<html> +<head></head> +<body> +<div><img src="" alt="ALT"></div> +</body> +</html> diff --git a/dom/html/reftests/bug1512297.html b/dom/html/reftests/bug1512297.html new file mode 100644 index 0000000000..55d8d4564f --- /dev/null +++ b/dom/html/reftests/bug1512297.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head class="reftest-wait"></head> +<body> +<div><img src="" alt="ALT"></div> +<script> +var img = document.querySelector('img'); +img.remove(); + +var div = document.querySelector('div'); +div.appendChild(img); +</script> +</body> +</html> diff --git a/dom/html/reftests/bug448564-1_ideal.html b/dom/html/reftests/bug448564-1_ideal.html new file mode 100644 index 0000000000..e93c1771f6 --- /dev/null +++ b/dom/html/reftests/bug448564-1_ideal.html @@ -0,0 +1,13 @@ +<html> +<head> + <link rel="stylesheet" type="text/css" + href="bug448564_forms.css"> + </link> +</head> +<body> + <i><b> + <form>a</form> + <form>b</form> + </b></i> +</body> +</html> diff --git a/dom/html/reftests/bug448564-1_malformed.html b/dom/html/reftests/bug448564-1_malformed.html new file mode 100644 index 0000000000..404517c70e --- /dev/null +++ b/dom/html/reftests/bug448564-1_malformed.html @@ -0,0 +1,19 @@ +<html> +<head> + <link rel="stylesheet" type="text/css" + href="bug448564_forms.css"> + </link> +</head> +<body> + <i><b> + <form>a</form> <!-- These forms should not end up nested! --> + <form>b</form> + <!-- Why does it matter whether we explicitly close this tag? --> + <!-- It matters because nsHTMLTokenizer::ScanDocStructure checks + whether there are any malformed tags before parsing begins, + and, if there are any, residual style tags (<i>, <b>, &c.) + must be pushed inside block elements (e.g., <form>). --> + <div><!-- </div> --> + </b></i> +</body> +</html> diff --git a/dom/html/reftests/bug448564-1_well-formed.html b/dom/html/reftests/bug448564-1_well-formed.html new file mode 100644 index 0000000000..46dbb8bdd0 --- /dev/null +++ b/dom/html/reftests/bug448564-1_well-formed.html @@ -0,0 +1,11 @@ +<html> +<head> + <link rel="stylesheet" type="text/css" + href="bug448564_forms.css"> + </link> +</head> +<body> + <form><i><b>a</b></i></form> + <form><i><b>b</b></i></form> +</body> +</html> diff --git a/dom/html/reftests/bug448564-4a.html b/dom/html/reftests/bug448564-4a.html new file mode 100644 index 0000000000..6fbaf85c2f --- /dev/null +++ b/dom/html/reftests/bug448564-4a.html @@ -0,0 +1,10 @@ +<html> +<body> + <b><i> + <!-- Closing a form causes any open residual style tags to be closed + as well. This test ensures that these tags get reopened. --> + <form>form contents</form> + bold text + </i></b> +</body> +</html> diff --git a/dom/html/reftests/bug448564-4b.html b/dom/html/reftests/bug448564-4b.html new file mode 100644 index 0000000000..f04d5fe48f --- /dev/null +++ b/dom/html/reftests/bug448564-4b.html @@ -0,0 +1,6 @@ +<html> +<body> + <form><b><i>form contents</i></b></form> + <b><i>bold text</i></b> +</body> +</html> diff --git a/dom/html/reftests/bug448564_forms.css b/dom/html/reftests/bug448564_forms.css new file mode 100644 index 0000000000..b98788862c --- /dev/null +++ b/dom/html/reftests/bug448564_forms.css @@ -0,0 +1,2 @@ +/* make nesting obvious */ +form { border: 1px solid black; } diff --git a/dom/html/reftests/bug502168-1_malformed.html b/dom/html/reftests/bug502168-1_malformed.html new file mode 100644 index 0000000000..efe23ac47f --- /dev/null +++ b/dom/html/reftests/bug502168-1_malformed.html @@ -0,0 +1,10 @@ +<html><head> +<title> Bug 502168 - Particular images are displayed multiple times in a formated way - only FF 3.5</title> +</head><body> + +<table><tbody><tr><td >You should see this text only once</td> +<embed type="*" style="display: none;"/> +</td></tr></tbody></table> + +</body> +</html> diff --git a/dom/html/reftests/bug502168-1_well-formed.html b/dom/html/reftests/bug502168-1_well-formed.html new file mode 100644 index 0000000000..5eb25c6b35 --- /dev/null +++ b/dom/html/reftests/bug502168-1_well-formed.html @@ -0,0 +1,9 @@ +<html><head> +<title> Bug 502168 - Particular images are displayed multiple times in a formated way - only FF 3.5</title> +</head><body> + +<embed type="*" style="display: none;"> +<table><tbody><tr><td>You should see this text only once</td> +</tr></tbody></table> + +</body></html> diff --git a/dom/html/reftests/bug917595-1-ref.html b/dom/html/reftests/bug917595-1-ref.html new file mode 100644 index 0000000000..b777751ff8 --- /dev/null +++ b/dom/html/reftests/bug917595-1-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<style> + iframe { + width: 100%; + height: 100%; + border: 0px; + } +</style> +<script> + document.addEventListener('MozReftestInvalidate', + () => document.documentElement.removeAttribute('class')); +</script> +<body> + <iframe src="bug917595-pixel-rotated.jpg" scrolling="no" marginwidth="0" marginheight="0"></iframe> +</body> +</html> diff --git a/dom/html/reftests/bug917595-exif-rotated.jpg b/dom/html/reftests/bug917595-exif-rotated.jpg Binary files differnew file mode 100644 index 0000000000..e7b0c22f35 --- /dev/null +++ b/dom/html/reftests/bug917595-exif-rotated.jpg diff --git a/dom/html/reftests/bug917595-iframe-1.html b/dom/html/reftests/bug917595-iframe-1.html new file mode 100644 index 0000000000..f7fca8232c --- /dev/null +++ b/dom/html/reftests/bug917595-iframe-1.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<style> + iframe { + width: 100%; + height: 100%; + border: 0px; + } +</style> +<script> + document.addEventListener('MozReftestInvalidate', + () => document.documentElement.removeAttribute('class')); +</script> +<body> + <iframe src="bug917595-exif-rotated.jpg" scrolling="no" marginwidth="0" marginheight="0"></iframe> +</body> +</html> diff --git a/dom/html/reftests/bug917595-pixel-rotated.jpg b/dom/html/reftests/bug917595-pixel-rotated.jpg Binary files differnew file mode 100644 index 0000000000..ac39faadad --- /dev/null +++ b/dom/html/reftests/bug917595-pixel-rotated.jpg diff --git a/dom/html/reftests/bug917595-unrotated.jpg b/dom/html/reftests/bug917595-unrotated.jpg Binary files differnew file mode 100644 index 0000000000..a787797c5e --- /dev/null +++ b/dom/html/reftests/bug917595-unrotated.jpg diff --git a/dom/html/reftests/figure-ref.html b/dom/html/reftests/figure-ref.html new file mode 100644 index 0000000000..23ca9f6037 --- /dev/null +++ b/dom/html/reftests/figure-ref.html @@ -0,0 +1,11 @@ +<!doctype html> +<title>The figure element</title> +<link rel=author title=Ms2ger href=ms2ger@gmail.com> +<link rel=help href=http://www.whatwg.org/html5/#the-figure-element> +<style> +body > div { margin: 1em 40px; } +</style> +<div> +<div>Caption</div> +Figure +</div> diff --git a/dom/html/reftests/figure.html b/dom/html/reftests/figure.html new file mode 100644 index 0000000000..ad83670b80 --- /dev/null +++ b/dom/html/reftests/figure.html @@ -0,0 +1,8 @@ +<!doctype html> +<title>The figure element</title> +<link rel=author title=Ms2ger href=ms2ger@gmail.com> +<link rel=help href=http://www.whatwg.org/html5/#the-figure-element> +<figure> +<figcaption>Caption</figcaption> +Figure +</figure> diff --git a/dom/html/reftests/href-attr-change-restyles-ref.html b/dom/html/reftests/href-attr-change-restyles-ref.html new file mode 100644 index 0000000000..4ebaec9249 --- /dev/null +++ b/dom/html/reftests/href-attr-change-restyles-ref.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for bug 549797 - Removing href attribute doesn't remove link styling</title> + <style type="text/css"> + :link, :visited { + color:blue; + } + link { + display:block; + } + #link2::before { + content:"Test link 1"; + } + #link4::before { + content:"Test link 2"; + } + #link6::before { + content:"Test link 3"; + } + </style> +</head> +<body> +<p> + <a>Test anchor 1</a> + <link id="link2"/> + <a href="http://example.com/1">Test anchor 2</a> + <link id="link4" href="http://example.com/1"/> + <a href="">Test anchor 3</a> + <link id="link6" href=""/> +</p> +</body> +</html> diff --git a/dom/html/reftests/href-attr-change-restyles.html b/dom/html/reftests/href-attr-change-restyles.html new file mode 100644 index 0000000000..1fa54bfd6f --- /dev/null +++ b/dom/html/reftests/href-attr-change-restyles.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for bug 549797 - Removing href attribute doesn't remove link styling</title> + <style type="text/css"> + :link, :visited { + color:blue; + } + link { + display:block; + } + #link2::before { + content:"Test link 1"; + } + #link4::before { + content:"Test link 2"; + } + #link6::before { + content:"Test link 3"; + } + </style> +</head> +<body onload="run_test();"> +<script type="text/javascript"> +function run_test() +{ + // Remove the href attributes of the links so they should be restyled as + // non-links. + document.getElementById("link1").removeAttribute("href"); + document.getElementById("link2").removeAttribute("href"); + + // Add the href attribute to the links so they should be restyled as links. + document.getElementById("link3").href = "http://example.com/1"; + document.getElementById("link4").href = "http://example.com/1"; + document.getElementById("link5").setAttribute("href", ""); + document.getElementById("link6").setAttribute("href", ""); +} +</script> +<p> + <a id="link1" href="http://example.com/1">Test anchor 1</a> + <link id="link2" href="http://example.com/1"/> + <a id="link3">Test anchor 2</a> + <link id="link4"/> + <a id="link5">Test anchor 3</a> + <link id="link6"/> +</p> +</body> +</html> diff --git a/dom/html/reftests/iframe-with-image-src.html b/dom/html/reftests/iframe-with-image-src.html new file mode 100644 index 0000000000..554abc60db --- /dev/null +++ b/dom/html/reftests/iframe-with-image-src.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<html> +<body> + <iframe height="100" width="100" style="border: none;" src="bug1196784.png"></iframe> +</body> +</html> diff --git a/dom/html/reftests/image-load-shortcircuit-1.html b/dom/html/reftests/image-load-shortcircuit-1.html new file mode 100644 index 0000000000..28e16b7464 --- /dev/null +++ b/dom/html/reftests/image-load-shortcircuit-1.html @@ -0,0 +1,8 @@ +<html> +<div></div> +<script> + var d = (new DOMParser()).parseFromString("<img src=pass.png>", "text/html"); + var n = d.adoptNode(d.querySelector('img')); + document.querySelector('div').appendChild(n); +</script> +</html> diff --git a/dom/html/reftests/image-load-shortcircuit-2.html b/dom/html/reftests/image-load-shortcircuit-2.html new file mode 100644 index 0000000000..3c4baa43be --- /dev/null +++ b/dom/html/reftests/image-load-shortcircuit-2.html @@ -0,0 +1,10 @@ +<html> +<body> +<template id="template"> +<img src="pass.png" alt="Alt Text" /> +</template> +<script> + document.body.appendChild(document.getElementById('template').content.children[0].cloneNode(1)); +</script> +</body> +</html> diff --git a/dom/html/reftests/image-load-shortcircuit-ref.html b/dom/html/reftests/image-load-shortcircuit-ref.html new file mode 100644 index 0000000000..7dd28922d4 --- /dev/null +++ b/dom/html/reftests/image-load-shortcircuit-ref.html @@ -0,0 +1 @@ +<div><img src=pass.png></div> diff --git a/dom/html/reftests/lime100x100.svg b/dom/html/reftests/lime100x100.svg new file mode 100644 index 0000000000..8bdec62c1f --- /dev/null +++ b/dom/html/reftests/lime100x100.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" version="1.1" + width="100" height="100"> + <rect width="100%" height="100%" fill="lime"/> +</svg> diff --git a/dom/html/reftests/pass.png b/dom/html/reftests/pass.png Binary files differnew file mode 100644 index 0000000000..3b30b1de7c --- /dev/null +++ b/dom/html/reftests/pass.png diff --git a/dom/html/reftests/pre-1-ref.html b/dom/html/reftests/pre-1-ref.html new file mode 100644 index 0000000000..a79b4f46a4 --- /dev/null +++ b/dom/html/reftests/pre-1-ref.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<div style="width: 15em"> +<pre> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre wrap> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre wrap> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre wrap> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +</div> +12 diff --git a/dom/html/reftests/pre-1.html b/dom/html/reftests/pre-1.html new file mode 100644 index 0000000000..1b21bcd746 --- /dev/null +++ b/dom/html/reftests/pre-1.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<div style="width: 15em"> +<pre> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre width=12> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre cols=12> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre wrap> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre wrap width=12> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +<pre wrap cols=12> +MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM MMMMM +</pre> +</div> +<script>document.write(document.querySelectorAll('pre')[1].width);</script> diff --git a/dom/html/reftests/red.png b/dom/html/reftests/red.png Binary files differnew file mode 100644 index 0000000000..aa9ce25263 --- /dev/null +++ b/dom/html/reftests/red.png diff --git a/dom/html/reftests/reftest.list b/dom/html/reftests/reftest.list new file mode 100644 index 0000000000..77dd250af9 --- /dev/null +++ b/dom/html/reftests/reftest.list @@ -0,0 +1,78 @@ +# autofocus attribute (we can't test with mochitests) +include autofocus/reftest.list +include toblob-todataurl/reftest.list + +== 41464-1a.html 41464-1-ref.html +== 41464-1b.html 41464-1-ref.html +== 52019-1.html 52019-1-ref.html +== 82711-1.html 82711-1-ref.html +== 82711-2.html 82711-2-ref.html +!= 82711-1-ref.html 82711-2-ref.html +!= 468263-1a.html about:blank +!= 468263-1b.html about:blank +!= 468263-1c.html about:blank +!= 468263-1d.html about:blank +== 468263-2.html 468263-2-ref.html +== 468263-2.html 468263-2-alternate-ref.html +== 484200-1.html 484200-1-ref.html +== 485377.html 485377-ref.html +== 557840.html 557840-ref.html +== 560059-video-dimensions.html 560059-video-dimensions-ref.html +== 573322-quirks.html 573322-quirks-ref.html +== 573322-no-quirks.html 573322-no-quirks-ref.html +== 596455-1a.html 596455-ref-1.html +== 596455-1b.html 596455-ref-1.html +== 596455-2a.html 596455-ref-2.html +== 596455-2b.html 596455-ref-2.html +== 610935.html 610935-ref.html +== 649134-1.html 649134-ref.html +skip-if(Android) == 649134-2.html 649134-2-ref.html +== 741776-1.vtt 741776-1-ref.html + +== bug448564-1_malformed.html bug448564-1_well-formed.html +== bug448564-1_malformed.html bug448564-1_ideal.html + +== bug448564-4a.html bug448564-4b.html +== bug502168-1_malformed.html bug502168-1_well-formed.html + +== responsive-image-load-shortcircuit.html responsive-image-load-shortcircuit-ref.html +== image-load-shortcircuit-1.html image-load-shortcircuit-ref.html +== image-load-shortcircuit-2.html image-load-shortcircuit-ref.html + +# Test that image documents taken into account CSS properties like +# image-orientation when determining the size of the image. +# (Fuzzy necessary due to pixel-wise comparison of different JPEGs. +# The vast majority of the fuzziness comes from Linux and WinXP.) +skip-if(isCoverageBuild) fuzzy(0-2,0-830) random-if(useDrawSnapshot) == bug917595-iframe-1.html bug917595-1-ref.html +fuzzy(0-3,0-7544) fuzzy-if(!geckoview,2-3,50-7544) == bug917595-exif-rotated.jpg bug917595-pixel-rotated.jpg # bug 1060869 + +# Test support for SVG-as-image in <picture> elements. +== bug1106522-1.html bug1106522-ref.html +== bug1106522-2.html bug1106522-ref.html + +== href-attr-change-restyles.html href-attr-change-restyles-ref.html +== figure.html figure-ref.html +== pre-1.html pre-1-ref.html +== table-border-1.html table-border-1-ref.html +== table-border-2.html table-border-2-ref.html +!= table-border-2.html table-border-2-notref.html + +# Test imageset is using permissions.default.image +pref(permissions.default.image,1) HTTP == bug1196784-with-srcset.html bug1196784-no-srcset.html +pref(permissions.default.image,2) HTTP == bug1196784-with-srcset.html bug1196784-no-srcset.html +# Test <iframe src=image> +pref(permissions.default.image,1) HTTP == iframe-with-image-src.html bug1196784-no-srcset.html +pref(permissions.default.image,2) HTTP == iframe-with-image-src.html about:blank + +# Test video with rotation information can be rotated. +fails-if(geckoview&&!swgl) == bug1228601-video-rotation-90.html bug1228601-video-rotated-ref.html # bug 1558285 for geckoview +fuzzy(0-1,0-30) fails-if(geckoview&&!swgl) random-if(geckoview&&swgl) == bug1423850-canvas-video-rotation-90.html bug1423850-canvas-video-rotated-ref.html # bug 1558285 for geckoview + +== bug1512297.html bug1512297-ref.html + +# Test that dynamically setting body margin attributes updates style appropriately +== body-topmargin-dynamic.html body-topmargin-ref.html + +# Test that dynamically removing a nonmargin mapped attribute does not +# destroy margins inherited from the frame. +== body-frame-margin-remove-other-pres-hint.html body-frame-margin-remove-other-pres-hint-ref.html diff --git a/dom/html/reftests/responsive-image-load-shortcircuit-ref.html b/dom/html/reftests/responsive-image-load-shortcircuit-ref.html new file mode 100644 index 0000000000..59d8925ba5 --- /dev/null +++ b/dom/html/reftests/responsive-image-load-shortcircuit-ref.html @@ -0,0 +1 @@ +<iframe srcdoc="<img src=pass.png>" width="300px"></iframe> diff --git a/dom/html/reftests/responsive-image-load-shortcircuit.html b/dom/html/reftests/responsive-image-load-shortcircuit.html new file mode 100644 index 0000000000..1cfb92cb20 --- /dev/null +++ b/dom/html/reftests/responsive-image-load-shortcircuit.html @@ -0,0 +1,15 @@ +<html class="reftest-wait"> +<iframe srcdoc="<img srcset=red.png>" width="150px"></iframe> +<script> + var iframe = document.querySelector('iframe'); + iframe.onload = function() { + var doc = iframe.contentDocument; + var img = doc.querySelector('img'); + img.srcset = "pass.png"; + iframe.width = "300px"; + img.onload = function() { + document.documentElement.classList.remove('reftest-wait'); + }; + } +</script> +</html> diff --git a/dom/html/reftests/table-border-1-ref.html b/dom/html/reftests/table-border-1-ref.html new file mode 100644 index 0000000000..ceac88e9a3 --- /dev/null +++ b/dom/html/reftests/table-border-1-ref.html @@ -0,0 +1,46 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Table borders</title> +<style> +table { + border-width: 1px; + border-style: outset; +} +td { + border-width: 1px; + border-style: inset; +} +</style> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> diff --git a/dom/html/reftests/table-border-1.html b/dom/html/reftests/table-border-1.html new file mode 100644 index 0000000000..12bfb2af46 --- /dev/null +++ b/dom/html/reftests/table-border-1.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Table borders</title> +<table border> +<tr><td>Test +</table> +<table border=""> +<tr><td>Test +</table> +<table border=null> +<tr><td>Test +</table> +<table border=undefined> +<tr><td>Test +</table> +<table border=foo> +<tr><td>Test +</table> +<table border=1> +<tr><td>Test +</table> +<table border=1foo> +<tr><td>Test +</table> +<table border=1%> +<tr><td>Test +</table> +<table border=-1> +<tr><td>Test +</table> +<table border=-1foo> +<tr><td>Test +</table> +<table border=-1%> +<tr><td>Test +</table> diff --git a/dom/html/reftests/table-border-2-notref.html b/dom/html/reftests/table-border-2-notref.html new file mode 100644 index 0000000000..7558e5271a --- /dev/null +++ b/dom/html/reftests/table-border-2-notref.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Table borders</title> +<style> +table { + border-width: 1px; + border-style: outset; +} +td { + border-width: 1px; + border-style: inset; +} +</style> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> diff --git a/dom/html/reftests/table-border-2-ref.html b/dom/html/reftests/table-border-2-ref.html new file mode 100644 index 0000000000..36d1e45106 --- /dev/null +++ b/dom/html/reftests/table-border-2-ref.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Table borders</title> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> +<table> +<tr><td>Test +</table> diff --git a/dom/html/reftests/table-border-2.html b/dom/html/reftests/table-border-2.html new file mode 100644 index 0000000000..4f209545c2 --- /dev/null +++ b/dom/html/reftests/table-border-2.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Table borders</title> +<table border=0> +<tr><td>Test +</table> +<table border=0foo> +<tr><td>Test +</table> +<table border=0%> +<tr><td>Test +</table> +<table border=+0> +<tr><td>Test +</table> +<table border=+0foo> +<tr><td>Test +</table> +<table border=+0%> +<tr><td>Test +</table> +<table border=-0> +<tr><td>Test +</table> +<table border=-0foo> +<tr><td>Test +</table> +<table border=-0%> +<tr><td>Test +</table> diff --git a/dom/html/reftests/toblob-todataurl/blob.js b/dom/html/reftests/toblob-todataurl/blob.js new file mode 100644 index 0000000000..4ed9fdb372 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/blob.js @@ -0,0 +1,68 @@ +function init() { + function end() { + document.documentElement.className = ''; + } + + function next() { + compressAndDisplay(original, end); + } + + var original = getImageFromDataUrl(sample); + setImgLoadListener(original, next); +} + +function compressAndDisplay(image, next) { + var canvas = document.createElement('canvas'); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + var ctx = canvas.getContext('2d'); + ctx.drawImage(image, 0, 0); + + function gotBlob(blob) { + var img = getImageFromBlob(blob); + setImgLoadListener(img, next); + document.body.appendChild(img); + } + + // I want to test passing 'undefined' as quality as well + if ('quality' in window) { + canvas.toBlob(gotBlob, 'image/jpeg', quality); + } else { + canvas.toBlob(gotBlob, 'image/jpeg'); + } +} + +function setImgLoadListener(img, func) { + if (img.complete) { + func.call(img, { target: img}); + } else { + img.addEventListener('load', func); + } +} + +function naturalDimensionsHandler(e) { + var img = e.target; + img.width = img.naturalWidth; + img.height = img.naturalHeight; +} + +function getImageFromBlob(blob) { + var img = document.createElement('img'); + img.src = window.URL.createObjectURL(blob); + setImgLoadListener(img, naturalDimensionsHandler); + setImgLoadListener(img, function(e) { + window.URL.revokeObjectURL(e.target.src); + }); + + return img; +} + +function getImageFromDataUrl(url) { + var img = document.createElement('img'); + img.src = url; + setImgLoadListener(img, naturalDimensionsHandler); + + return img; +} + +init(); diff --git a/dom/html/reftests/toblob-todataurl/dataurl.js b/dom/html/reftests/toblob-todataurl/dataurl.js new file mode 100644 index 0000000000..8ffba1fa8e --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/dataurl.js @@ -0,0 +1,56 @@ +function init() { + function end() { + document.documentElement.className = ''; + } + + function next() { + compressAndDisplay(original, end); + } + + var original = getImageFromDataUrl(sample); + setImgLoadListener(original, next); +} + +function compressAndDisplay(image, next) { + var canvas = document.createElement('canvas'); + canvas.width = image.naturalWidth; + canvas.height = image.naturalHeight; + var ctx = canvas.getContext('2d'); + ctx.drawImage(image, 0, 0); + + var dataUrl; + // I want to test passing undefined as well + if ('quality' in window) { + dataUrl = canvas.toDataURL('image/jpeg', quality); + } else { + dataUrl = canvas.toDataURL('image/jpeg'); + } + + var img = getImageFromDataUrl(dataUrl); + setImgLoadListener(img, next); + document.body.appendChild(img); +} + +function setImgLoadListener(img, func) { + if (img.complete) { + func.call(img, { target: img}); + } else { + img.addEventListener('load', func); + } +} + +function naturalDimensionsHandler(e) { + var img = e.target; + img.width = img.naturalWidth; + img.height = img.naturalHeight; +} + +function getImageFromDataUrl(url) { + var img = document.createElement('img'); + img.src = url; + setImgLoadListener(img, naturalDimensionsHandler); + + return img; +} + +init(); diff --git a/dom/html/reftests/toblob-todataurl/images/original.png b/dom/html/reftests/toblob-todataurl/images/original.png Binary files differnew file mode 100644 index 0000000000..c2da5b3597 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/images/original.png diff --git a/dom/html/reftests/toblob-todataurl/images/q0.jpg b/dom/html/reftests/toblob-todataurl/images/q0.jpg Binary files differnew file mode 100644 index 0000000000..eb41ad3e93 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/images/q0.jpg diff --git a/dom/html/reftests/toblob-todataurl/images/q100.jpg b/dom/html/reftests/toblob-todataurl/images/q100.jpg Binary files differnew file mode 100644 index 0000000000..aaa79f2d31 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/images/q100.jpg diff --git a/dom/html/reftests/toblob-todataurl/images/q25.jpg b/dom/html/reftests/toblob-todataurl/images/q25.jpg Binary files differnew file mode 100644 index 0000000000..d8b1c9bfb2 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/images/q25.jpg diff --git a/dom/html/reftests/toblob-todataurl/images/q50.jpg b/dom/html/reftests/toblob-todataurl/images/q50.jpg Binary files differnew file mode 100644 index 0000000000..f93356ef22 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/images/q50.jpg diff --git a/dom/html/reftests/toblob-todataurl/images/q75.jpg b/dom/html/reftests/toblob-todataurl/images/q75.jpg Binary files differnew file mode 100644 index 0000000000..6c25c55a1a --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/images/q75.jpg diff --git a/dom/html/reftests/toblob-todataurl/images/q92.jpg b/dom/html/reftests/toblob-todataurl/images/q92.jpg Binary files differnew file mode 100644 index 0000000000..1de242a171 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/images/q92.jpg diff --git a/dom/html/reftests/toblob-todataurl/quality-0-ref.html b/dom/html/reftests/toblob-todataurl/quality-0-ref.html new file mode 100644 index 0000000000..3d6923fd39 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/quality-0-ref.html @@ -0,0 +1,2 @@ +<!doctype html> +<html><body><img src='images/q0.jpg'/></table></body></html> diff --git a/dom/html/reftests/toblob-todataurl/quality-100-ref.html b/dom/html/reftests/toblob-todataurl/quality-100-ref.html new file mode 100644 index 0000000000..8b157d0ab3 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/quality-100-ref.html @@ -0,0 +1,2 @@ +<!doctype html> +<html><body><img src='images/q100.jpg'/></table></body></html> diff --git a/dom/html/reftests/toblob-todataurl/quality-25-ref.html b/dom/html/reftests/toblob-todataurl/quality-25-ref.html new file mode 100644 index 0000000000..385f2ab356 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/quality-25-ref.html @@ -0,0 +1,2 @@ +<!doctype html> +<html><body><img src='images/q25.jpg'/></table></body></html> diff --git a/dom/html/reftests/toblob-todataurl/quality-50-ref.html b/dom/html/reftests/toblob-todataurl/quality-50-ref.html new file mode 100644 index 0000000000..68b91f43f6 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/quality-50-ref.html @@ -0,0 +1,2 @@ +<!doctype html> +<html><body><img src='images/q50.jpg'/></table></body></html> diff --git a/dom/html/reftests/toblob-todataurl/quality-75-ref.html b/dom/html/reftests/toblob-todataurl/quality-75-ref.html new file mode 100644 index 0000000000..7e610d231b --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/quality-75-ref.html @@ -0,0 +1,2 @@ +<!doctype html> +<html><body><img src='images/q75.jpg'/></table></body></html> diff --git a/dom/html/reftests/toblob-todataurl/quality-92-ref.html b/dom/html/reftests/toblob-todataurl/quality-92-ref.html new file mode 100644 index 0000000000..15a930c942 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/quality-92-ref.html @@ -0,0 +1,2 @@ +<!doctype html> +<html><body><img src='images/q92.jpg'/></table></body></html> diff --git a/dom/html/reftests/toblob-todataurl/reftest.list b/dom/html/reftests/toblob-todataurl/reftest.list new file mode 100644 index 0000000000..efe5a3e7f6 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/reftest.list @@ -0,0 +1,16 @@ +fuzzy-if(Android,0-105,0-482) == toblob-quality-0.html quality-0-ref.html +fuzzy-if(Android,0-38,0-2024) == toblob-quality-25.html quality-25-ref.html +fuzzy-if(Android,0-29,0-2336) == toblob-quality-50.html quality-50-ref.html +fuzzy-if(Android,0-23,0-3533) == toblob-quality-75.html quality-75-ref.html +fuzzy-if(Android,0-16,0-4199) == toblob-quality-92.html quality-92-ref.html +fuzzy-if(Android,0-8,0-2461) == toblob-quality-100.html quality-100-ref.html +fuzzy-if(Android,0-16,0-4199) == toblob-quality-undefined.html quality-92-ref.html +fuzzy-if(Android,0-16,0-4199) == toblob-quality-default.html quality-92-ref.html +fuzzy-if(Android,0-105,0-482) == todataurl-quality-0.html quality-0-ref.html +fuzzy-if(Android,0-38,0-2024) == todataurl-quality-25.html quality-25-ref.html +fuzzy-if(Android,0-29,0-2336) == todataurl-quality-50.html quality-50-ref.html +fuzzy-if(Android,0-23,0-3533) == todataurl-quality-75.html quality-75-ref.html +fuzzy-if(Android,0-16,0-4199) == todataurl-quality-92.html quality-92-ref.html +fuzzy-if(Android,0-8,0-2461) == todataurl-quality-100.html quality-100-ref.html +fuzzy-if(Android,0-16,0-4199) == todataurl-quality-undefined.html quality-92-ref.html +fuzzy-if(Android,0-16,0-4199) == todataurl-quality-default.html quality-92-ref.html
\ No newline at end of file diff --git a/dom/html/reftests/toblob-todataurl/sample.js b/dom/html/reftests/toblob-todataurl/sample.js new file mode 100644 index 0000000000..8948312c93 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/sample.js @@ -0,0 +1,2 @@ +var sample = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADwCAYAAABxLb1rAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3QcMEAgbxBgmbQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAACAASURBVHja5L151OVnWSW69zlfKklVSCVkBBOmhJiQECAJCVOYJ4OIQouzLSC0LY2ubvXq6nuvii677eVV1+peiAOiKDYgIkJkRkgIIQlVFTJPkIHMU1UmQpIavn3/OOf3vs/0O98XvH/keKsbU/V9Z/z93vd5n2fv/eyH+E+PF+wfAtCCf+NR/A71a+07XcGm6Qo2TqfYuLKCTdMpNk5WsHH+s00rs7/vO51i03QFGyYTTAhw/v8mAEiCECYkiPnv5o8ZfjaZ/2wy/zATzh8j9MeY589+r/m7DI8nJhrer7/PZP7+xOyzTQhQxISzL9o/L/rf2/P6689eu7/+xHwPDq9DmN/Z7w//WYH2u/4Y+xrm5+YagLPHTOavRfP3CYk9q8RDuyZ4eM8ED++c4OHdUzy8e/6z3bN/t7+3n02wczexe3WCXavE7j3ErlVi1x5i9+rsf7v29N/tXiV27pn/XbPfDc/pjwV275nM/j1/rQmB6VRYmQrTKTCdrmI6BVamwmQirEyB6WT2+8kKsDJZnT9OmE6AlekqJpP5PZoIJDAlwKlm62vC2e8hcKLZdZoApOb/xfz+C5wAk4lm13iC/vj570jNXnui2T2aqP9s/rqT+WflZPa5p8P3msw+72Sq9vfhO8z+LUxXZp9pOpl999l1mX//FXONwr9XpqvtfVbmz1mZCpwC23dtxo7dB2D7rs3YvnszduzejO3h3/dsuBT37rMN9+yzDffusxX37nshhN1Yhj+TFqgwHrDSY9YbGLXgtecPoMyvaB4g9bcVoOK5ACEB7ZGKH1Xz/z88Yv5+HD6meTQF2Q9Izn8s2BcfghFaMBy+gPnF/H37pZj9juEaSOkDh8uv8nsJ5npQw0cFOPsYcN9k/vf+f2bPnT/JfmwNbyTzeu3e9LWg9p2YPrPsc8xCcI+JX5rmPVgcosP3BGEXDMPjRPXvKbj7NntqXwkU+r0za4/z7zB7GttaFLVwi8T1lu7p8Hk5rJzZJ6HYvrvMhWe/RW0Nyq5huxY4e3HNF4bihWW47vSfTilxUXuavcN2eZcJTntPYVn+TNLdrHajwuIk8xfngqDJ/AZqW1w+aAwbqHi+pHz1bVRhCJ5EGWBY3b0hEqi/ht+s7NuZ/pO3JTN8BTIvLvUPSBsewnUkzXvRLXvY9csUPeympb8fIaTONr7mQSVeh357ZS5Ivh0Kv1Pf1O4x9FHWbUb129c/EoZzYbg2UrEg4/ebZ7U2sNvvHK+zaD+tuaocO9H7/yi5OMt4QtGHFZr76g/A4S3pDmS1C2GulebvI3OSmp/JvJHbZeY6tHWnuPr7daDKbKUfxrDvbZ7fKhITuZciAI5maCOZH8JqYhEsU1oQw14/aXosZcgMCMWNbTMOmZs2klH0j9lvH4edRRtg2I/+IfOjCVY+UWxbl7BBzcdOmSOcbfn57KWf/DSLfbYBNJY9Dl9qyFgULriK09olgHSpnRagFpQP/P4S5xNP8RXIllXS3Uu13T9kwTSBiT4Jb/81eXyxXMNhVpzFLVgpZ2s2GWeVwLQ0TvPPyBakxPn3cCHBBDuy2B5M+bH7fBrfmAorzn7uliKw2sD5cIh5udxJLFdZKGSJQEigh8NbyxgAuSADxEhJW9UDWF8QHTaX5EuuvlPlg5XLnOYnpC0HXAXKlLCSVfpOn/mYiCApLX7JVZIurvcQYAIWenkqKr2tDaEaLSlCAhleRAzZ1XBIqLgNNEdBW6wYL9dG76+5AJxFe3osw2TirAPn8LnZyzSyfnO17FDm3qplspyn5QqHpsn//JcYIgVVrB2g/CHzHelZrHp+xVDZqN48bIecP0rd5XPHq/9YdLm3bAHlAnrfX0qFEmvAxWeqlLlMcrhzLv0mWLY/kzUDXFoMC8oErYMECSee3dS0x/BwmrWMTDVoVJR4wx2Teb+2L0xp0VPR2Wayi49kKoFnJWNIneTLDLWgPDs2JX/q+8U2ZBByZYs9vBmzalvuoJ0DKe2lLcAVIAVF/Mfnc64urT4HadJY+ghOX84rRFhRvRwzUIVC8LS5TT9EzL01a0Ct5FTP2IfMOpzsqmoGoYBSNL+1xbqb486UT/1t2U6TtkuhRpCHO2wAw4Ls1sYxOTyxwJjnWSdzoheyUZbvmOENu6qUsFktYfmL9YdspTJ2cSoYQTyMg/3ivKyU2b9MS3Y44X3167OyBnGLvjiwC1k0IHdAahhL+PkNd6Xy/HPMg5bb9w6MVy5RHCFRvKctnTRS4rEobRl3lTx2yXHkziRr4YswERssIBC2Q0CJZFJRdtFlTwjXw38VKWdFPWzLlZZp2Smf6ywgAYbr4lCG+ecUe9aJmEm791Vet+aeargXVF6zKeFkmYMw7Un5XJQDudIzdIb7zRDc8qGgUJ+wvZZQYLisYJZ/UwFwcaxbX0AceR4j7tA3iN9w/aa1JG4Ut1bfmFUErnBKKCyYGCroYhCFkfN6joHY09khgaMfoIdIVhxdxXQz4RSymKbJAyml7ysyo0D2sCDrZJ6m+h2yjfmp5DLshlHKlWYDkRCvaScAaM4GFfwHR89kKlIX8hlUzNgCJOPvxXyTS55dLZQRrCoSBRwUaNeJBsuWuwNqJErHgAciJ5SpNtOWD8CiTRxMkNb4Hs5YfGCnhhXd1sgAYc1FW1qu7C8EwHjEcLQaXh9TwnU8Rwbjm20KGmypn0OzrK2DvErHvWjxpcj20mUaJDKz6/CbUGIpBy0RBvj2i9hLJtTSisQdFaqEWenNhJlxyERpJUMR28qgdmQnHaVh0mcHLzoMT8V1s5tdIfNhDZ2411AqlVweInMYkTU0TWbCwjKk7h7TrqTqyxQno8FvQ0FDh6/4ENvXCgu5lZAuIS1hQg+5GFKErdzMFJAKWQrFOQRhscA5iTP/bHJ7QI7Nd5+WIUOkOSLoSUsGhHA5AqBLqxYBv9XJsaAkrogHhlTCaZXmeE4gQxoQy754GUhpyqAToQ5qGRt9ySn6zUvRYGhZakEUq3e+Welw4yDTYeKi++eMxIrCd05McdatQxEPDHWobH6olD/2pymGAIf+9EsyJz1EE6CUyMGGBKTsmwXGyC4fMcROfWioPERItQyrMbwmmDYEt3+whAVarZ5kg7IR99CXAgm1HBawKVHbP6lU+guKkr1+D0zkHSoLaSyhsEoJmSydLqBTkcBTBTrl66IIxYQdQY95L08AFMblK/+aYK6Rw9VIYKw+YiifegDpILZKIFcjyWbEJvoddEWEY5+JSl1rQ4B80uQij8rzopcFLD6nnBYvbkabJc+zY0buGP3ASBmkeqbUJDPMsLc5TBSSSIt5yZEVgx5NJv+bl62B9IiJlpzQlsjyuKi+zhksmbdrl390fIQOL/Z3NSJglvjy8iqadWMPCBUKySK9j4BF4lRUHJD2rLdElswXYta/WMBkfiLLys1kqimXoY8w9W0v5WqO9iAYHqHeffRvBwNcK5xnoq2W1DCmLDRloVxCUKru5U/DknlG0FvJnHzqolzXrWC01R0D87VVDLYM+kGaBeBil+kuaRmVBcHpQ2w8iXuQmuF1FqGXlPI4cYzNoxNSYwRDpGGSWbDDCmyCSqKM/uAJZBOVs8+IwfryXmZD96xM0uzvVTbEnvF5CFmOcs4kjXqAN3fOqhF6UT1ngd1FGQg2BSyXRjtKdy04JwHljudOZri8QfRrUZkwoZWD0neN5CqvwlGTGrTfD5cjDK+tRNgtVwb4rwnWXJA5an2l86jWmpkizPtXHUOBzSotrtQBek8pDFiey+86syjD3EUaQ3Dlb0pYiu9KevzIw60dN4wsMS0xYREDExAppkOAiHrVcFMcTEVfwpsPIlOHNj1lIh2UunCsxE8252TNjDF0IpD0oL6r5dV6qxE6dWLAY8jK0j5HF5S36xbIH1q8lLYjCX29mPKSpt97uB6tHJ+TKl4InxleBq6WKdmQZ0ASxyeziWZ7gGIInIYsJFzALw+2yPjGqoOsGezHdABcb/dH9Quts1Qmi/LVhzYFKp0unQpYmmW4rL5CcH3DLLjUyPjRYGws+VQfMn0ywQIj8YLsmIcF8tM8J7fP2U0vedG4Am7qvimLM6h4zAI5YygdvWTF8TSUL6ppsUSmslDyxx9HyDLJ0wYUi+/DdLVc0FM+dOK9GukZNDs96OkUssbYsz5kp+g910plUaScqtbMKFwiEJNji+pqrMBnI7MUgmgLlGYdS0gdTr31LvSxOM6Ja8SPZcgA656nOq3hOkplBppqpDc3EwemZKMvWxCAYatNauJPg+XIbViWujJfUpuTlVWwLhqx6Ht1RZUi1KYDC6ylzeBiNtvxNYWFWesZrAbMZRR2gyRCKmQAytIfxFsRA7sDiIrvI4QG/5GDtK0FpXK6PZ8MB4dG6Vo1rFLZcMNlzb3P1hFCBIjCwGE4bDn0B8d4L9/SN5T2ET6wMhVbsrPoXgknrJokxTPyg3ax47vhoquX1J08k5OlptKYEcII4m3bh65lzQC1ILBxwWHpsJexoBhbcfqFkgWYjbzB91eabM+cQPZkj+mDaMBfdZyMpv1KRmaghqOZr0Kj82U+6XrWYWQu83JDId1tj5FPvRSYaTpsMNgSJEMKjp5RVenXsTya8sy2xDE39VD+WgRIIBHPkcGvUJKi82KIcBYDplWt2N1J+cOnHYAqYeJGLIspUPX1ZO4PWZInDDh3J1pMpsYgjTLBWhopa1MeQt/54fIA20LJorNDTfepXpfPg20XSjfYhXYNhnBvqxwGRDxwKC2ocpncYGLqOpYBilEmVUfKRb3BRI2ZJTDcat4KBxDzQmTxYQblvljLQiL2xCy47TKCfvR5+QCdy0cSEjA1XnrB99AmNsdwFKSRoQDMSjsFongMdvDwnC8YycI+q2+ihg2Jzqgp9hHQSoIQelVVvbrP+F2WIvWspkwSfV+N4vMzTNyocjndGgDH79pN7mUsWJAjtGxRhXjY3Rub1ZqWPac1DtmIdXcZ0X+y2HtKZGRvEaWVmsHf1ybAFn2lozrXiR1NXJwhPVZLYI1kDxyjica/oBZkgLlpIWnS4mtQxQ0O5a+HPzqo24oe+tILHPFeiFKAgQShgkVXEN2pCnrxzehhTGkx2161uyWsK3yvhDvmbo5UmBq2t9RoMJTMVvQsjWJ4skGEOXD7e8dkAJBvR5dHWR6U8o1sLmu3pZ69iuG2MWY7ipBGXtC+7FuDEIzPY/hEtnR1WDm9MYiQyksvxl+g7Zact2RfrkOwo7t+Q7+285x0pEvE4g1p+G+mF3hhGqtHaZLKbOM3agRq82mvsbG6waEFxwttbb9j4A+V3UrkRAkelBd7s3/LSFS1rnVWspUmwUTV25PW2TbN+zMAs865TgGBjIJeKkkmWpLBIs8zLjgx0GFoARs0iAYjI4myXdB0FvisJ2KNoYdGHnaIhhUVRNHxYNaBiUh9yaRnv5u/ifl8dKJuucjOsgRiFvsX2RKNr1QXKSs/icZdp9SUWca5PliAQNDY8n8gZ9iJHF+qsEiFPPZcfjqajHspMUCy5rkxnp0szABjVHSdDTKdAsPiTBajbrF50SpMhufbzPrJb8x7yPxBpZAJmA1tsk+7SCJTzJTamK6N2EsbA3/0I3TBWohWz3IliVAmr6IzabGPpdJD0x52oclYVil0u7igbjtqjJtOlztXUoooMfd7uIfPonxu+souexpwR9G6KQaRzIjVng3jUnDGs3IgKhtJlOQc64IqyBNE3yFuA5cqWb1K3bLfKa3cpwtyUiEDU1+ncnihT3J6+Zw4EZPtI2PSS8UCe/OwBQK9NV5RIxu8SCC91EKhhShrObwHGr3Lhi0tybQwUoBOEg8uZv2Mz15y0h36QZkPCVtitqCggIOZvky/wjsTKaLUtBF28ZrM1coeRBdqGJ2zXS+qQUDtgi70ak2jKNMnoWhZX7CvaYl5TY6IZk9G21ObnGu6NCjnzApcuYK43bujjFutzjV09NcieSnSVB6qrVpFi6BytGxyJhU2aXBjG4LvI3OrYLu/YkJw+83UaNUWDeGEXPp63eZ6AsRjNQMcw+sWGiQsiPVEnstgzyxZVxXWp2YMVIpGSqEnlGO9kjalyFqOeLYp86Zu1/qlq6JXWE7X2HGnILWwnJ+McQJVOHL4rM1Ku61Am7GcDIxpSApD94jdMPRGCNn1vfftwjvkIOBwM7t6VioZH05p8FdbSqsH8uY8Yja4AkYsVA6AEd1Ryro6hOFNEOmYbZSefK7v18EYSssw07shOXDGGQqlvrnzjLpoFtKrMKLATCmond1ZUk7+S5j3MZpcNvZ7skQBsIpjo10dlZIfiwkRInuT01j8BEcRD+mpYX22qE29jLFzKnqqO989Jl83MpsHiEFyUwj7ZqxibbPFEWSByZ0UrT2Phqnr4t15BhI2sljoFZNfm8cqrc9dDWUwtXD1RDx4GbqeeGfp7HkidksslWYb3dNRds2o9y87NJOGezZldkWAZ0rULxHJEmt94ysYdqjrRjJiJI0d3XUDeBH4KytGMqPNtENSxO6qboK1V02wBXtvdZUhicbpscLw1C2wrFNTYKzUlBFasgCokZuzLjeYkZsd2KH83Hwy51KV41b9I10oWsDC1cR2yPWopDt1eJ5QDk+q7Y/i0e/FbLbdzxm2wqXH83LQyzWsbESWIFlgXDs4uCh20DQZQ8dGC19mVG5YMtCF7LyS5pfAyLUm2/oBX2qfz5IljEIZBXzQl6UdpugstMrrQcOxyb9nyKaKxriepTlSh+1AsGauPVt1AIO7wgpMtUKrJiLEwqLzxSGKAUF1fKK1hSkYaeaBYH7WSG4s9fj30pshaHGVW+4EjuCHKl49XHhUAme4XluGBSCyLrlt4FBBqjiJhr3f/lQTsZjpTiMhffOcSj2lFXt3tw66RSPXe6oRHVZ29QytFsoGoD3YqJ3SlomMhqdOz1yQ9CkDDIGoY3GqYFeX1dsslqoPSQXwgQvECDNijNUoj7B5PcvroF0h8Og9e9YCjetA6MkaQZDOqYVh9glH/AabkS0ZYFQmwfSgoYxaVZs0MPJrUJ01a2SQqYFzWNAiXAsae8wGQK2RaY0SHYVgVIuDJsVicXt8EMVbuHaucvqZupmIC5jsJUMsCaKlj03azMwJ+7NuEMBoHI56olAXEjJ8MedjWCSsNBq9mjH0OBQrtp5KbjVyeGcIdIG8KWwby/vmYCHWi8fBRuH+UczMostiZEB4zrsgxg/r5hMQCa9gltEigrWTLydnaDTc+uusRPEgmIUq4rOIZm1jwXVYR0JSgHFk3rI5oIc2zS40YNJuj1V4zER+GI9ELFsz8GTN8pYLSIm6sqj/obAD6E9XFjhNnxwWs0dALGJ0w8PoQXmpKKUz12u1YjQ+hTQSAQ6taolpplM51N2CMt2aGCF+aAKAHw/mrZC4+NZlJ32o6sONga6lB0ojJLPCiW78Ix1ASz8D2G68MBAOqueGDKlWJBgViBnv4I10fSL2SCGGIoenKcA6ZVtYOn+KCWusTZG07u1lqS4/YbA4AfuxHh3ImTPLhu8qlMnyWCFZjmB2ZF+HX8oj+jEeANcK2lrA7FZ3brEvlJuqZRm3OGjaT18rSmvl7KO7MxduJOG45BjPZZ5L52un7kko23nQ3TbI+FZpQLGRv8j77jEzKQoezmZKaGLHGUTGjdJQRvFUTQKzs5eFbEaBwq6rkk0NzOm8PYzsjYIM2jLZaGI6a5JlWYnKxHnNvb6LshuicsYx/pDFYS75TFUazxEGSEeyUiqNuqRQZnjUgJGWVVQ47Kx5QVGxmFDpXdODiaofTBXQPMLjwfHUiXNh1Q+4ZZPAzAKgsEDAXJVTqNOc9djpm/vR+1kNmhAGYFfT03waruS+W87tdRhTWN1wpFmxuenzfxPYrJ2QA8+Ve5NzzPJSC6Xjms3r0E6z6EFQ8K1pMEaeQjmEKfmEMY9KSy4tXDjusKF4kRSiRQDp5yzHJSRLIQzuKvQ6Ndu9YSzr+6RAbw6hwTzVzZZBHlLu2ryC76E5VBEwTy+gD+M7NSIgcfNCwmAn1ycf+upc/Kun4flApwKcgB9QH3B3RokUc3dVdwynKbhsps7crroUJTCxeNbHmHXRQgusRWyxo12DAUDOzGQJARusTPZTzQNGsBjvNH4x6n5gLovRMG7kQiE18FWXFpAgoZs0aGXkNrpS/wsTkWzHhNIHq0qlGxlMt9UsQu5Z6tEJp+w4agzurkc1srQuv2MY1SgzItV8p0Qde5NZhm6NlpGrYuQtwWDeUSGjsykRipkzcZRCMb9EkYlF0OANpNRgpKqaSHDYogxR5MhH5b40ulQtLHkhCqoZU3RbvcgfpEruTksHARoZDNYZsNZ6XLXv0mPo7cwb+KwUG0vsgYWkofD65mjN0hF/hsBZzuVQZCKJYqK3wcKK8YhCmvgV5TzpvWX7lb00QUTpICOTgSotSJV8F71DZidslEvDaDjlJogZp+FYrmaiuHayVsKZOsAfm+JUduYatbUQKIBw2rONNjFZa7XwRsTUircwjDaXNWwwxJ27FkQ5fMEs+oqm88MWWJBKVvHMskhzYnOG0pve/4cZTPRmCAQKJ90lI0HGohoXMMWjGSQWOJx0H7KeeUX9fiH/EBOC5zIx9ckLVayBAXblJofBMXk2KCc3ZDcvw1MaXZoXJrpFMNzMmbWcQYWIxySmn8J+I7PpF8OidF5PZiMVGanYe0gNNWsyZ3gcKZqSmtqRMdsL5gkYYYr9ZGglVjzkHOGA9EFcUaRuDl1Z92PmzmMNchx5j7tWApf7gMGkIVYzLGG0eMNZVV/MrZapr1tFm2DCuOSaD0Q/Q1uMJJKqYtozvgMZ1oZWLSsLzBzli+tXy2XGrMDGSmayG0Q2I8wiSworXcjm551dtMOawwZgkQExF91yQGV38fW2iL4Xqm15jZ8dvq+yg9TRkJRV+ayqVVDe0det29B+Q3+Ouz5fVcCDHEMrWogAwWYqlqdKRq5wji9RL8YiXjtHwcLFRn6aIHK2r6KRnBYEy40rbgvQeOiJGnFIqftiK9zIZuOk938cgRvNPZn7DYbh8H42TCZ5xmY1M3rj2wogtMCS1ZQ4Y4ARu6hQ45+PbRYYMbfPFyJJDLiOGQCj/oi0QiSU9SKyAFYO8M84kMWD+kmqXDUHaYfXxFkdPdNcDxfOyBGhcs8N+8ziRZmz6R22wcSx4lFeSD9LObHjxqxewajAGKGWJBK8G5mfoWLKcfrDLJfJoeE/mxd66cZw3zigcl3wnKoILcBlDJ6WznYVmabytasafaLriYUqktV9sT9ksveEmQ73eYBKZPUQdDheNnmothCLzE3p/9Yytjg5L/bBy0yfs8THJBAty8IChwu3sDc4DUgpr3H+2YiNfg1whzYe+Wwhl3AoW+dkx0EmxxUgmhEx6A1ju4/vJvIGnDS9skykRTHdbFikzOXdUKKnCq5Tb04XaB2U6RQP8p+VocWhbfqggJaLBqikLnGKmXeyRaPUZZjGcbBZ3iNFiCIMtw6SdFi+fQzMAKjL/U12G3HIHqDky2qZAGYF2OaSynUHqbDeX6BNHLqbQktieXqymF+tMNeYKhAV+iqkUkDbCXGKbuV5fh3zibJkGGACoOSBqUdtf7U2kRKtiuJKV9CvW9t7D5TTZ5Ty7GgnwZj2sRXn2qZ8OwLSO+mH01PjM+QtwePk1k6PFXGkPBHMO5AULGgoPHtfsFLGaufdsszIfRZk2UAlu+6cSnmvQoZOgfHJa2wSioJZHEpRVtZVvae5bBdnUdio6uND0bkjYI1pdQyle5xU6YdPW9NVX35bwopw0zdRtRf2rEupjdN3zHDkkImXgRibA9PukCIabgTSWRG+ZCywCoCuam9bJG/hGiaqKjJEqlSXsoK7KxFsLBdJ1yAe/ycVH1PVaTw09rP36ZpTNTX20w4XwqhZpT3pXb+sCtA8Rt9it8bEoJm/yhtzotjjUeTL9Blp3JdlmF0ra2ERygJwrjjPN/PRqjlcN5nNlaixl5tdBlQFQUXstlhHFYkBpfmjroxWH+8RrOO99X11yKkooBjKhmx+KyTTCvpGPSo+J2DUvqUoidPt4CuHWQ5ONCHhyL6YY2zesrHA62F0XdKoep+OiKudEN9McKuyFpFFC1dkg2lss+zAF3pZhRCm25eMA6zw2gXZ0D7XpDscawFn9FYKiv/c54pme2TeizEK2CxrRKQnn0rKQQRMyZ8Ye42J6C7CgME6PTkVyA5PHsFDS2FkJI2jc5G5Uannu/bcoSex4j0xSncnVk6BOWedJTEnuEmGZXLpDn9D/TDYjaqrB3I/d84+WI0CpDw2HI4ajCGwlU12dMClX/PWtJXee33JMsDv5c9Y83QcDKM1hg4P7CYzg2aZvdxOpGANFU90hhmz5kRzTi6mZcgyWHL+qoVJQEwO5OYCR9dUjg1Ddklexyvt5lNS0WJ0il2bgxszidh1kPvp5q7OISCYHurOCHuop+B/vDBWOR73hJqtnCNG+khZ8Mv0GCX9FfQJu1sT8tckloRuLKQ3h/WhRo61j2YdjLb64XCPGvz4XwW21xFRo6lj4NwL9YUKq7PRQd0WSy2dr4V6ajaWrBd4PX/4KAJjouOKOtb0mTqm16bmUllE+gxQhc2UJybgSTqnlWOpW5GZthXiqZPh+BJkTC/liZqefqYT02U2hihptu/Zhsl1ldjxncHJuXeeKLG9ibQxeg66utOmfQuY6bgdCW8a63zv4TtXikNDg/knh9LcGCmUnTddVO/cDA1M4e3G7CEqJ+zWKH5jK5ZKklyTrG4ovAo9oBnkHjNmOaMKCw8lpNckgv1gZms3rWzoUn7pkKXAmCUopmsvuIQl8Hpc7hfhfusJmqOEsZ+RYUeCdAyZrSRUeSOqGOvNJP3MZwUb+GCOO9KK1MphZWxNBfJO1GYNlkSPrh4eUWCeAAkvVgAAIABJREFULqZoPxSAco0k5BFnZJxah2LWl2IqFUpn5tZhKpSRVVZAP9/CDhQfWvSMbUwrFRUnC9ILl1i02AUe2jHrI7CFTJ9uqiYcLMBGxA36b9rMnCqKpiyRyJWsF9VyhLSKSzFaafU1JEj1K6fPQgZnmxEvTxN4Rb8SCZR938uTAWq92Z/WkR4SNXyiVA419+Aw39eFsHoCTB44Zt2S0+yPkQJAi3kcpvnCnr2r2ML8PU2nBTHaKUPmCXh9hETO4siivkT2kiCjIWBYuAb7oiF4usGAyeZat4tCnKym3fce47HrHhwPiqpBwbrdTkKDKaN9WyMRgr5i73MkQYRkk+ZsqFQpidxMERUMHov16rxsaeVdSkiExx2rAVNehuQCHWPNEKfx9WzbkU6hnZBpbHQm6Wp0dhkCIB9FnFtkixUn77CGGVTNiLBGpSooZBaGdrKP90ULQ6dlLy29j10e1iOfRapnBjJatTx3dUyfGw1gu5QgZY+hEcfqIhg+Y+4gsL2kvS1QKrpI7AdWxraaGDdsYhfCCvZVrhBi2egWTVfrdnPfKuHa3NjNbTvZwjbQG8YtxzVvRTgkdTYb/RszOZWxX4/RqiKiyBosDS4rfdxGoblxD6xyDBmZU+RNVF9TjKmB6Bcz/Q1jsMiKqkBbZi9PAFykUa0y89EsceSFCssthhF93kOzp+TRXtCOQbQCXjpjXj/JXiVe1zEcDrZcRfSKkiqqmLMQFxFZ2IMVKZ8GFtmz0i4HYdhg4gLrMYadObRLjaW3dJsobjym1+w4xdgURDoIQ2laHJNDSu1LhzZwapAlIU/Toy913ZjRyAIrxHOarLFSM8h78Sno4ZwjeTlEzGBkUln2JkY8dNw5V+7sDOmJEvth5EN78RGQWj/LIwxhWhUKO++wtjRZMhKkaiviOoJgWTZrbfyQoedzwJJEo+ZnoD9qW6WIiWTfKJUKnZidWb1foXEPuKL/HCqdNrwu0BI82W6PwQyCgW1jWO9xYHWQFsVOzGoWLqpMlckm1dEBpKcGVCwL0s3GZVVEGIeU1o2Rgm7I6lUxlaEHNdnjx0knxWhRWhQxjDKojAds2W3JCIZZpWNjZcWcf6XuOlW8XJaTk5l4shZBynN04gw9jUD1zlyXecRDVFINE/qWyQdwBAPU4gyP64D+1nKXdnNb5QZoFyvO9TSqWSgZhq8I4LKntmEdxppWKB/g0nEexhHa2a4sO9irUg+hwyP7AdqFTBV7PeIuUY5USAJzH2+uhmRxxASUj/icOfPQMNeSWrgWFFoOhTBWEzk18phWUAgM2Ry9zr5i43zHTvFTLZhRXUiuSu9HVyKHoKwsrYIhD1h0ZlhCUNa0iKHrmoXjtplFI/k2wDGUywV3sHtpjuQ72aF6KYXQHA9oa/b0Lsj40uspMHXKVlFuE+SNp1A2tw3CmLtFBIqjG1Iq+kZF5N7hwHQWIHMU9CpimM6E0jCgo+A83DSuDtiHfEIKzOeYCQNHSzIV8048FMACXKeRy6mIdCzRPcAOflIyrHDdLlSbL2xbCmla9yQG+MrABizMKSkP28tKSegy1oCcBszWTyHM5xpzBlgltXRRuXWZ0Mp05IkXJ9VTBW94Kp5SwjZ75dyvqZehuU7tXPM0cHWpM0AtJjsKC77vKdin7gofnuhErgYncxs22DqJSdricDZHoPSbrdSsbssNOkso2gHULAgTBjA4YC3Dam6YnJSoX3YKGJWmpc0TkcUK4Q0FK3urEWPUNN5NfkwmjXi4g7Ryxg/VFD+LW2UTBBXEiHeuaR6KZkqbNRtwmKV1KLGHA9doTw0D0aM42mk/6UeQWjsuKrTkwXoZDkFZBXPqzPTr/IHBOiu21rGTO35f2OWuNvfYExRE7nf2xrtEnolHFfqzIOhf7rnAWsffx+qZdX5vMreUDVlT6pNk5nFdALJS/IB1kQr8i/xUMifjiN4mxliARYN6wl7sXIowktP8gGaFskqpLdgfAowMBsgkppZj6vwcDLTPd8UNO3Dg6/4MW6+6A4DwpQtvwqYz3ovbd3w3BaU+LFxuFKjNdl74jt/DK9/5O2kb03asWAKI9GctffeNM21VL8kYhy4lLadn6J2KQMVUOwKf+JO34x//1zu8DXw1E9i1nI/0WZtpan/wa7+M3/+VX+6fU0gsrR2UXgMoWajdWXo5k4/ocmS1eFIf5iFVfI/RFBZ4NmHZ/ziICt4hZvlmIo10gqwZyHL6/+iyQgU7czlgOqbodjyhvYFMFz2Whda9WE7Zrmi3pQzoc2QgOc0AIpY1nYBqQRHZ4kvBr5BwLVDthJcPILk/wQ9b98xuz1a2Xn0HNqxMcOJRBwMAzr/idhx56H44/PEbO6Gi4HDbNhFc9pLyuJGhZr4PcYSN4sgwrCGbtrNwHc6L2pZn/lLnfeQt+OqH3oJq3FAifEtm1rpcq09dC59bQMLKLAbICApzgRWfvJGtEztbd6IAFEuZOmxO4XHwVeW+g9pNKBkhuEudJ9WRy5oBjtiyj9LBWgMHHHmtXvAwya2GUkRK6qLs4pKwliBitaLhwVShsZC+RchJb1Ljak6Bk2UeqhI4D+elwzJHQGvUPngOi1MneNIMjkiOqBsnbL3qDjz76EOwYa8pAOKCK2/HqccePjrmeRB0uzGJVOIs0pDzEucqJDxWUuPIE6vF7NkMNVJ+MbBMpUkt8xS7BJcx4bCVmpHhEOuMa3bJSQ5ilpgwF8y7+ISRoGF6qHs924yskWY0hf50K2divKR5iiGDWzhHybJlHIxeGTKvNd1tPfluBGHS4Gqlf7oOkDB4hsrizDhaQmB1931GZrgNhSk8Up8mVs0WZhX0pJHASA8k02N2zhw+db4p60eGYOA84AZPvD7JjSaL9W2Fwtar78RzjzsMFLC6Ovv3qcceauadxAzCkhxmBKI0agJUboKg8axbJSu/PnXml+HaKfI5ZkRmYerLERTST4NTptGaU40XKbcR0S5zDgclhWgVlaZOVtpOJ+gOGDb9RCy54OYR0Z4VxqlwtZzJwhu+tY6luw8RW+KWqxd4pQxYUVqhtejh9UTCOt9vHRtR5zaAz/YAZTgB68PNjV4d0ncKfsi37ZVkQs18sS4AnKT36thgZ3XZWGlmptOURQShyYjIZO4IkzIN1LNMEgliHvTIrj148pve777TVTfeg/d8/JL271//86/h1//8a7PHf+adba899Mhu/P5HtuKjX/kWbrzrO1iZTPDsow/Hz772FLzwxGPcp35o1078yUc+g/MuuQKTyQQvOOlUvPqlZ7iT5rpvX40Ltp2Dm2+9Dnv27MHBBx+Bk07+QRz6xGNdRXHmh94BAHjZG9+Lb170Udx58xbs2vkdnPy6v27f+f7bzsW9t3wZOx/4NrS6E3vtcxAed/gpOOzpb8B0n30BAVv+4d+3z3f2B9/S/v4DP/8+99l373oEF539Idz0zQsxmUxx9IkvxEkv/RGjWZ1dkG9dfAGuvuhcbL/9ZuzevQv77X8gjj7+WTj1Za/Bvhv3NRP88vrfs3s3zj/rLFy27UJsv/MuAMDBhx2C55x2Ek5/1Uuw12SCBx/4Dn7nV34Hq3tW8Xvv+b9wwIGPcw4+jzy8E7/0lt/DXnut4E//7jcw3Xu2Js/6wkX4/Ke24rpv3Yadj+zGIYftj5e8/Bn49z9/OjZv3htWUNgPd2to6DFuue6fwUtTrbXUdh6JRWa/RL3AK6Nkx782A8zzmcc8zjseZ5Sbkkq0YchEGjZDmZJyYuOaZ7DouzoUhhm5U1kdHKAhbazomIuAU7Fwbcm2XvZ9KaOpA6NpSg+qYWSBJ827mWm3pHp0i7G1Y1H4xf91Fv7mi1e5359z6Y0459IbccXf/LpDUP74b/8J5158Rfv3F849G/vtdwCed9KL2jr40Mf+zL3WHXdcj89+9j143Q/9GjYf+JSEKV9+wftw5y3bkoLglkvei/tvP8+91q6H7sSO6z+NB++6GEe/+Dcxne6ziIVz77Pl83+Fm791Yfv3FV//PPbb/0Acd8pL2/o8++Pvx7WXb3Uvc/89d+PCr/4Lbrjmcvz4f/zP2GffjfmtBOzevQf/+73vxY3XXut+d/vNt+IzN9+Kqy+7Em//L/8Bm/bfhBNPfia+ccFF2HLuRXjVD57uzrbzzrkYD333Ebz8jadiZWUKra7ij/773+MrX7rEve5tt9yDD//tubjg3Gvw53/7NmzevLdf20UZUzpfyRzqJhGlmWVs05i+widLFADXGmnJkSA2NgdYC+KjGMZgegZLsqmeYafUfz9x9eIsENJhYoERs76OpqRiSnzpA45slqqUG8bXDCHOUAU0AdUaTc5edZLWpXHkYP2eucpkk1gQHjjfZ8MK7jzz7SCI9378EvzxR7+Bqz/4M9iwsoLf+LOv4uyLb8WWP/lxTIxEQ/Ng+4nzrgMA/OE7XoSfePnxmK7sja3XbMf//PjX0wlw+/Z78Me/+k5s2u9AfPCfP4evbN2CLZdsw2knvah9jWOffiJOfe6rcMABT8S999+Hc879BK6//mJcdskX8cKXvG1e5fbNc9+Oa/HMF/wy9nv892PPZCP27AF23Hw27r/9PKzs/Xg8/uifxoYDnoHpXntjz3dvwN1X/w0evu963HnNJ3HkM9+MU//dX+Pr//BzAIAX//T7sdcUmEyUoI3v3Hc3XvMzv4n9Nh+Ii7/yMXzrkq/i2svOawHwmovOxbWXb8Wmxx2A57/mTTjiqcdgw94bsP2Om3DOp/8Bd95yI75+1hfwkh94Qwl8f/3sL+PGa6/F3vvui9e+6Q049sTjMSFw9SWX4VMf/Sdcd/W1+OoXvoKXve5leN5Ln4dvXHARtp57IV79+tNdh9OXP3s+AODlrzkFAPDFz27DV750CQ46eH+8/V1n4DknPwWbNq3gumtvx5/80adx1RW34gPvOwe/9F9e2YwOcv9v8HVWMfSqLTHfedOwRPJ7rAwfqyzwIixTI1jhumBBpV0cp3XZ29RdnJVOVTsgxxjfOwzDBaqAJ6qmNQqfB6VZEq5c0FrpssyEMo4w6Tbb08hkLqtYU5xdPR8270ejdGv62Zt89Kxv4kdOPwp7TVewumcV/3jOtXjzS5/emMKIXB6yeV8AwJcuuhnv+eRF2HL1bXjW0Yfhvb/yI+HAAX7xR1+HIw8/DBv32Qc/9PJXAADuvPsOB8+96fVvwRMOPQLT6Qo2bz4YL3nxj80ed8e1ZQfGsc/+SRx0+ImYTPdtn2nHTWcBAA5/5jux6dDTMN1rf0ynG7DvAU/HE5/1rlngvG1rYqqdjjOs25Ne/pM44ODvw4YNG3HCC35w9hrbb2/r5ZqLzgUAvOKNb8VRzzgJ+2zaDysbNuAJT3oafuDH3jorjy+7OK2BwWn7sm2zLPa1b/xhPOf5p2G//fbDpv32wykvOg2ve/MbAADfuOBCQMLTjzsahxx2MG664RbcevOdLbjc8K1bcP23bsHTj30SnnjkoQCAf/nMFgDAr/3mm3H6S4/H5gM3Ye99NuD4Zx6J3/pv/25WHv/LlfO5OqEvOgpEh6qFdO0tlHfY6RVWF47SDERQipyP9QxwDN+T6gyv8gJab9BnBLzldFZuGqwpD1XYv9OWwkOgmYNqIg32p9YyRBoXGA6BScDEc8ETLGKv6XUfHGkAkiBOeovTJJfPtMAhfSZIjuCl9JlmI4dopr6hTwk7/If+wn2VS6/bjvd/upeq7/7ABXj3By4AADzwz7+AvVYmLQ1+z7teip/+/c/jU1+/AZ/6+g0ALsBTDj8A7/nPb8BTn3i4u+dHHXE49sw/6+bH7Q8A2LlrV4MjVves4rytZ+GyKy/EjnvuxO7du9pzH37oAeTZl8CBh3x/EzVjdfZtH37gZgDAzVt/d3SZ7fzu3VjQB+NsuQDggIOPaJnvvhs3z3HBna0a2HHnLQCAT37gj0bf8/57thdTXGb/GjC/Y044fuaCPVHTqB73rBMA/D3uvP3Oht08/6Wn4ZMf+RS2nPsNHHHkq+c43+weveRVp7T99+3rZwfMb/zS+0Y/12233ltuz2J5hZZQhgAnPxIBdYW3fIPRuSDNY4EHaj2GBxwPfowiAa+sdNPs5RUJtOBr6hgLrh7MLrWRtWxN7crGC6W+MU0GN67FsG67phHLEUtx3h3SpCaaPlCbveYppOydEjZjLEwI1g8AejD7lc85Ejf87c/hU7/7evynNzwbhx+4CTfcfi9++/1f9LIVAHutrJRlxXDPvnD2mfjSV87EnXfd4oIfAKyu7ik/78remzxcpfWqD/aMzJu2H7v/e2Vlr35oTCJ5tb4/q6urBUOlBWMjOFohnXb6czGdTrHlqxdCEB5+6BGc/5WLsPc+G/C80080a2bta7Fn92rJz7cRB0Y/2JnmPCXQDat36y3lNEulh15JqVkYHlRnd+WEabfR1wySioWr78eN8z+IOKUtHF8x8KmLQAX07M+QIBP1HctgI+VqhjBogqNKD4PrVQ9il1OwBPbgZogP2S842Pb4bn+a5vbJCJ1/+5lvx0TABz5zJX7nry/AdR95C1amE7z7r87Hp86/ARf+2U+AICYTlb3S++69gjNOfQpeftLT8Is//Fw84y1/gUvnmccochL1gQQuunxWrr3u1T+JJz3lBEynG/HgQw/jr/76N+rD0FyMLoOZYJ/9jsBD91+PI5/7bqzsdzRWRUxXhMlEc4xvFSvTvN5WV4XplC7LzrcgsLjzUvGAQ56A7bfdiB9+2/+Bw454MkhhMiEmc1Z1whm2yAKuAYSDDjkEd952G755+RV4zvOe68wbrrrk8hnccPih7ftu2rwJJ55yPL5xwSW49upv47abbscjD+/E6a88Bfvsu3f7jEc+5TBce80t+MM//Q849hlHYDoFJhSmU2A6EaZTYTrprtuy7e2K0JMxhVAcvqTUbig3Tc5IukiH4y4HBkjVGF9ZAi84hdcT+oWgo2bRU2CIEevoYv/noArPGPteTY+npY6eELRJT63aaQ9sBI4XNtsDeUwH54w51efEqhqjyTAN0fWd05knFsilbzKcB8ktV92B5xxzCKaTWdm/5ao7cNqxh6WoZV2Jz/g/P4kvbLsRDz68C995aBc+ff6MFFnVAiH8GuL4DRv2wV4re+OBB7bj7LM/tPDJHqCffceDnjTDF2+9+I9w/61fxq6H7sKePTuh1V145MHbsOPGs/DNc36nn/AbNgEA7r39ckirycrJUelRITh/3HEnvRgA8LkP/ymuvPBcPHDPduzetRN7du/CPXffgcu2nIsPv/cP51rWOPycOOHkGWnx2Y99HBddsAUPfuc7ePA7D2Lr17bgU3//CQDAc047qUUlgnjRy58HADj/Kxfi7C/MyI+XvPK5jq197etPAwD87n/9O3zuzK24/dZ78Mgju7Bz5y7c9O278cl/3IZ3/OxfmtZQ+N55qejaUBoT3tAesx/IAsDW91p6POYwwBFCRwvY3lR3jQypAMqZDe5qdwQ5kR82/XaKdHuacajts/NZwxVp271pyllve09MTN+mba1jwBvRhMe0w+WDcNdiXc02n2zvlQxSGV7PhkoRE9Pnm9GHfkG2XnUH3nD6URCA3avCN755F37sZcckw4tm/kDhc9tuxOe23ZiWxWtPPSYBScR4bysAHP3UY3HZVd/Ax/+5axKf8pQTxzhuU361LwiBOOiIF+PB+76Ne276ArZf/b41D9v9Dz0OO27eiku/1PG71/78XwYDWLbWQRZf4ZhnvwA77rwZV2w5C18583+vgSIoZcOnvuwl+OYVl+Om667Dx/82P/9pxxyF0191elNJCMIxxx+NQw47COedtRW7d+/B4d93CI457knOTv/lrz0Z1197Kz79T+fjf/7BJ8a38ZD6WbcgwQ3p8kFsNSGFRDen8EPJwqS9JRsLt7JmtlbFMxUlc+U4MhJAOdHIOEt4W6hgROANG+mzRNkMq8eLFgxpSxyvpysbRxwDzfxjWy4wlODWqqot6tB01x5qzBLoP5sATIYZrLFTJUpuhvcWA4tCbL//IVx32/045fsPBQVcfsMOfPeR3TjVZID+9s3+9S//44fxnk9eirMuuQXffWQXjjh4f/zI6c/AW8547shMXtZVP4AzXvlGCMS1118FSXja007EC1/4o3jfX/6qQ9yqOb4zfacaDHL4cT+LjQefjHtu/hIeue+b2LPrfpAT7LXxUDzukGfioCe/qL3kU57zUyCE++68Crt3PpiwS0uKsDi0h4PrBa95M57y/c/Eldu+ijtvvg4PPfgAOJli8+MPwpOffhyOP/nUPEVt/vfpygp+5p2/iAvOPguXbd02I0VIHHTowXjOaSfj9Fe/GNOVqT/oJ8QLX34q/ulDnwEAvPgVp6TMQwLe8a4fxPNfdBw+e+bXcdXlN+Leex7EdGWCJ37fgTjtBUfjjB96VkK2iskP1n5z3umziiy98CaSzjecXGBY/Nj9Q7zroOxntIg2Emqs8NFEXRIbpyvYtLKCTdMpNk5XsO90BZumK9i4MsXGyeznw882ray07THga5N5CTmZu9FO5jduwlldP7BRxOxnBDAZdHKczB5jHFUm7fGYv+5k/hyjrxveb/45+mv29xowoSHcTeavDWr2d/aG8fb68+8w/HxivuvEGCJMhtecB0TKfieY7zA83n7/+euaDpPhddx3H67nHM0cnvPIbuKhXRM8vHuCh+f/fWj3FA/vYvvv7HfT/pg9E+xaJXbvIXavErvMv91/3c8m2OmeQ+xanbTHxJ/t0QzvWplghnmtCCsGC5xOhZU5FjYdHjOd4WQr09UZXjbV/D53PG8ymUtHJsMaWMV0/m8SmExmuOxkMhcGTzS7npMZDjeZ/3f4t3vt+Q2I7zU8b8DwJg3Hw/zvwMpkeMzq7OfD9+H8e0wG/A8NA3TXYSqsrAgrE3MNVjB/7dX2s5UpMF0RHlrdB9v3bMaOnQfg7t2bsWP3Adi++wBs37U/tu8+ADt2bcYOfgf37rsV9+69Dffssw337bsN3125aVkwwDWCX2XxPYoNhh+MuH6QDDZ0zK7JNusxrKoS6DqchnJzfhWH2VjzaCcKDaMArb+cn7dYNNHH0ZByMh6aEZC09v9kgXQZzFJ+bAArikAJLs0HdjQcY5RohIHpw0+trTJRYmNikIjTm8c2w04lE5S+rJwdYWfjs2Gq7ByeoHGEnytcvEYaM1pYkUVXbOdk0/qD6U0JaOzJEPuBlVvETI/5bM5x8k7z7G7owGgPHeao0BoRmolwrNdILPkD7tGfm0xUY7upUldTs3sL5sKP/QCotdhalFPmS1ywehHbtR5wurGkVMWbN3++xqR6mQHDVC47tzUF5fBchUUo25liopFsuVsQ4VZDWvuaBm2B24ZjIz6VDLntyM3BnstLfNYmqbziRcGIUUVw9Bsotg9GI4A4BlL0qLpQDOemEUOPEhUpsoWhPcPXqaQiCrOhzfcJIn05UogtEMv4IzZLtjRh0GJkIzIeMTaCFtHOXMNiqJGdfmcPUBXjMd1oFYaZT3ak6ZAcuJEUdHZekjV9RZBiESWQ+phmgUcSOKynnCfGNaexQbqt1zwdLUVY40hB1YO7MhdDxCE6eV6DF1NLIxHdtKv5wdjGRaY4uJsHmz1VRRdknZV/SlRNq6AdlWcGUXMwcAjDkcQx/iibvmZr9IqUSr1Q4dZz0SmYCBm57Lhna24uDKO7T8BbjJmtzyJlZhUP93Zgzf2smOS/TCUvwi6FkwkKjPVHGDoUFAbN0bwg+QqL3ZgBxsZKN12vuRMyaUft+/TvbNea0kQ368vp5iBZcssaidNvBD/vmkseANfb1ZFYXy74ff2wvgDlb2CzeTL8Ytv00XxpOMGVjAhStR6HlxeGhI5QSd/H2ItH40jWPIrozdxkgWT67+yWXph13A9hFrMOg8SnmkskZcH3kM25gTreraEtcTt2EkWGSy8UVxEqFzJu4giG3q9mnAAq5UOgZ+RqnTLOph7DocTE5dVoEL3dYHhAF8KrkCbJxW8Pm6utb8RCS2YQ+3CAhKEpcUAWDZZrkwrNtaeKKotS2UE/Ua9w/KVtUIgbain9ALEg41tEhlQT1Mo5AfFhftpbD2JMenU3lyg4ovbFpJSmJ3rLGaVWVZUpgQsxqzVHiOZZVMgu5ecKe+ww9AUrZiQKQ9DZFlr7FoysXLxq3kJeVl3dcMng5ZbMOAcHbflA7kNk95WbM9BMWGpm+RctKmnstMwi3WgDX64OOyCJTJUFokvKyLoPc+jcQUZTXdSuUMxrZ6gMuCCfENJICAZ8MnYQZi9YmjGldE5wSEYe3X3bF1Ie2lB0XrP7VsvuCB2j+qJ+oEWiV1bBxWZiylPNskeMa1uizRJVvKlj6Zlwl+RtYPwb5T6XsUBXZ2Hdwi/KO9kN7MTKXiOnUsDMsJ3oTv5qKI1GMFeabFBV8DAYnpvDbIkKVbNPqoOSKexUsyX6aUBjLxbuHcddTHv2GVoe3b3uYw/ktKLzo0Nhoh4DPjbgsrLB3ueGVI0I5CAVTS1C8B/ew8yQpksFwqWpQFFmLDddQapAhukxb6N0nmWxcrySzyNUDEQYSu0wHWqpAiAD87UWvodHExST20F3LtHIRKwGOCN745VH/tjoRY0gLXGRyZ+ggy0/gzW5m8DVs5/apqoHUJHusnbMy5o5VMjBPFuWzTfsRi8yWJih1qo3rcJQJYYBcQizU9w1lKOL+sZvbHf8RL62U2HwquAQHpOPxXvKwwddYI4G2MsGT3soytpADaVl/nysPhQC+2W7SejZdJaET3xgNIj2LAYF79gyNlgkVCAqu0f7L1hQ/Cpy30WH73Ia4s+7YEcD16IB6cDiOaAxKDnBtJlsxgVvoIInqfjiNApsBItUcuKvyxX4eR3VTZ/5q400pdOX6EO0ZypT6D8zi88kpfAWbZ46Bx7m3Ka9YmACAAAgAElEQVTrST8Qx2a0Gtmb1lCW2UCsYbOadaYoSFYsu5s8GOWDpw0GGoFlZJvzmeErH5q99qYTUT1j7/3WTHhWGjwly44GEo/DdQisSJh0qBGIk+M1bIZiIHggzmev7j4xm+KqddakqIg8niUP9upXWCiO9GULgBoP2zGtScN21vq6eZSefZrSP4o3j906ox+XCDlSjp6RoZ9noHFWBEWnx3OfqBxsRidHGy0Vq+4XMvy6yxuIPIzLYldWppNGnJddLup4n+tb9l0340Y4hS3Z4D7DAvhte9qmvmxDzhWjmsEnujGJwrg2jcPUSQoU5CTNBg2OzW0uREbb5+AX+/HFXEY6JCNMXxv6bRnWplBYw/mDhtDIvWDzf2xD20cgp2H8QqVfY8Q8C/2vs4/lWN++hSm4Lsrrsc0C83uhhUdep2iVoxPMuVSpaI0zMwfEhEkmeQm8JTylov/HnsbybjGNbSvKpSrkJqIglhPwNkMBJWPavUwZD8KAJBVUpP+oPqTHwd5tylrRCGyxHgQMiBGjDXZg1qfB0qauLbDClqPP4wDwJw7Bgl4auSvmzqrObCtpJv1sATNSNag0DXat6JBrF0DZBMAys5U1AB6GToXdkAe3D62BRkBf9LgxtHH64p4eopxXNl4bHTJ3aXwcBJeyBH4Upe+ib8jxsjOFDZkB3oGOl+LG9NqONAGuLM3hCQfl05EjixDO21Ydc3FAMJMKJTnhqgtn7eLpQ2dYsp3VEHjHdjonGyscGrIIVryIL2tUZElGIiKwOO2LLlljAhFHRNqyrvsk+iDjB4bTfyKaa2xGKdiGEyoce4FvSJaACHN0gULVwLLEtwexJ8aC4sBoO6NYP56QjEdYxBEtfshqhrIXgAsRF3bhNbOd1j+SNgBbqtgDAQ43p4L20cyQXqoAKNQn84j6vDwAxsrh2IJjZmdpXo6IfqOzYLvqTF21lEVmtq+YaXk7NDwBwfKjDlPWSJ/JRhLEpyx+bKZhIKQcxBgEwmRxeSvMVCZfoAphbWAoE67K7gBclMAFsd0FsDLeC5FsRkGqwdwPhoHh8FJCL7WmY7crDLp0247BvnLvtLNoHOiPwOjaz9xFyGJB2KQgEnFrtbg/TCWUKSPhjgyL8cbcPeLNdUbDgriQYYE5rEnbwUMVnUmhzLeZa9Fy+dgPgHaU56OxEmbBHq+ZTUY5Acu5HP6ssVon+j5HuVTAdGAqQIgjoq6xQfBtAQ9ZiNGTmc0s+XIxdnYoAIZs3oUWf+lBShWrW02sM1FRIa8duk6cRo2xzc7TEQm3NCRPvERMM3szMNoDpZysps2bjW1z9rXjcPRAYogBL7XrJdR5Cga4/XpFvSaLgpoo57bA6CbV51JX3V9k+GxmtnDTatoqmgauGAvUPowaLWomIVSoI/JgJGPkat6KsdpASGTcHBkv/l2mMniSpgRpjahXOUCP1kq135SqFEOhN9e284QE1LaSOfdawZ/Ibo37FrNobKCFMKYQfEhhbPwSGZJYm2I4j8oh4JHtDcHLymbEoGfzpgqRt0opvdtM0VDW2m9FVCNovZohhHpJ2kgOjiypIL5smZCXtvug62c6syI9KM+GmiDAik6Ow8nbt/Omuu59nKxoxBk95W9KLKsVZDc9ozyjbVwoG07dcLiYZjsXjc66y5T7DPexry+Ll8ohGu7+GD0uQ80tx2QvWwlMrNPHS4tNUVPZqxF3GcKr/qPAWanf1iNdrBkvqpi34HuQODKzgehaOyiHamm8G4DWVZoBiLH1K5k9DVN7SnIndcPRNTJuoMp8YfpAc9lbpCpmOs5wRxJ6QCUygIwT3Zj4DjuIyNmsM7rjFIPk3QxaS+xatpNzGY7HBINmuuN2Lv1S6pxRMKeQEWs3sTBMH2yKlCwn3dk+Xw0mDAxID+W1qfapA0FCX/1YtxpXFjMCOWFNmPZIhkUi18+ukdzOJxRcula4ymOJCxjdaieNz58BYjuQbGSz05Ztg3U2Myg6v1LfNc1JmEXOTDON4tyPwWPP5gLOdosIYzBHcEopKY5b4GjBPdv0+6JSbY86LI0MIAFj2uAzdPqSj0UrGUszA+bDKdyP6NrjNmwhuGR04aG5P+oMryr8mZ3kKVVntDSKshkErUlFRESZDh33BWUxD2NaCwZ2nE4b6udpwHe7OLfw2lrK4p406afGOp5c84lyW2hVcpgW0lhBsJA0MSY3Zo2LLLHZJSmBQ49pvRdGyI11YIWmfrW3N58n8u4ciar31ktMBjI5ODdeLYDuTBtNrYPCWRmFstcFq4IAYrXOZBv6jPeKncdqMcYhYLNr7wwZ6k9oW065/aBkmtBFv9l1p6oO5QHMbvllR3kaD8fhO7A6H1xvPR3cmgcd54qBhizwqRHLbgZGK6ysiow5VsfThELKA+9c41xi4lHRC0+ZcrR19CSwjkYwXa0U5p7zuAlpD5/oHhMDv1LMz8W7TH8wAjThh6RTkcBathLYTQ1XtZs8EKK1Ah7H1t18pGMwHQjK82RYYIwBhOAbx5BhmDdmNQ7QFgG0zi5mhGby5KMrEWmimlBkYIGsURApKzXJKEWL7mSC1szejEg5ciaphmtdZaZo7s8+v9hwjm6niimT8zI+kyGRZuYKwuQxz18KVq4S5EZhbcT2MOvkI3bjjDbDJajIq6BTCXsV9eRi0ePMNKbFz8QlEPDcKCGxA8d74U3fsmcCcT4eVB++abYJMx8cu3XMfcxGrsGey7bn0e+P5dcBEmundlqQBVayfMX0XtknLSMeSLaZQijZwox2uSUQofR0/vu/0eE9jJtOcu7AbsPSlJIsDA0GzCj2wBqcpxKX2g6L2DHtha9MIHXpKlrMrBE9SG68jMN1y2463V/UzvIckjmawyV08ETyQ5HbLDZ60aYl58vF3mc8L8ecvVq6XkxtRalHnFlAEj/fcEgNRI3GpqPJn+qSF/i4UBkbB8xnVqVqgHXUjp89k0UWP/XTETnyDYfM31ZN6nIuGZs6LlcUnIwGtPX8WcvrryyTfVrfS0m11jRZPV/YMbF1tno7mvKKWlQE2ddS03wxsbbqriIhYATeOm9kO4owsZoGNymGr/vwT29/hJw9DhliutSMoQxOsFwAPOVpZxl4AsFhrwg0oueFQruc+6eyj3Vyio+liLKv2bjBDFPh0oX4JrCwBwgVmRWCVyIMPNE4/nKetCecyBGdYMAOVByUtDl6JbSgL2Lt4dh5PqX0WsleWHnv2BKdpmIQGxTB5WKBuTi7W487dOz8WHNuSDjpk0K+g9YMNS4NPpjSeVPCpObz3NlW2r+F0diucTw/t2jGMkSFt1lXmD5YOHEo5LwMeTGTVtfkRnTu2U1TJuReZwPYOzJhKAuD66gWHmY5wGY/RmeQ3TskYsSK90IoGHt6rWDT5CXXg7I10HX5xMzGtKFhrOWLxbgHqTw2mFQDLCGFDBFYIkPJgMEdiva7JcaeJnB6pp+xmwrGpqyIqnSEJd1oWH8rlo4FVh38tIjRXXC37aJbwyMntbQykwxJKC2fcdSG7vYkN7CmsUmKWI8ZbpE+HENg7kGWgeBAL62UNPou2+vEhhyh0nCk0ElAyhEj9rMqykraexQGrTHTMGYTDh9VikDJmIFchI3E0t07NCNdi5FFVmJZ8bor3PNq4FPOtjqBY7vICwsiBuNAYxpaFesdgAmBiSMzsAMsEhODUXVXEILbPhHK+FPGzFcqAlUurH0bvWG3ZRzORVfC+06WZWGBx2yoUlnLtUvcgvV1mSH9JWdwQam0dv1+M2A7SK4VFhMi6GdfVI32lRbRLDA/woGunCKDwSaCs7ir89Bs+xnlQHFGBvzkPCyYDTGGH3qGV8mBRKWYeIAZeqpG44ySBRWsO2kEjOi8/RB7KWWHYCjvbMLHPIKAhin2pZd86Rx9SR2Oq6ATzAek7c2NX0Yjw6AsI40kvLcZnfKcahWlvYqDuC0j+fUxYM6Ea490zaiqCjPfpihzbdIsEJfMPppBQo85FrhI90pDA62dEa5jylwqMYOJZCvrpIQ9xZjsknyDwVo5iZ/n0MFkv8+C9DavWSdCdmWsj2W+nJXJxmTA6eTiEQytFNyWNHIgmc+lWJSTPlBgmFvBwJjDZU79+qkNAnLltcmIczCee/upKKWi/ROrsQCB0Ag4arLqLB0N5Dp9ekBRSuj8z+jGblLRyYaOvOj69my/C1RKSrpsk86BXa1CUUzVyVDrsDH5xmHM6BRVSmESq0ufQTpk3hwSNC7j0cQYFLBqcdRlZIEXtrP9f/CnbBMy3nsoZtEOpZ4K0NvcINn5u7Z0ZihoaAB7W+UyFNR24aQhPXSjKK2LcGc/w9dmBtQdX0svylaEmwalflNFq8Bn2AIPDc6WNWd0wHpzjWaeCBeFxNaJSkGP5txKJDe3hLGHtY9r65uXlXEC/TSywniid7Bley9//HkoYGyOUx2+bBYfOmkZb788dOF1/mHsF8sZQhH2ZNFT7phjSxoHq3+5A6zQWMhTXAqUluv1tkbGBggf2ibpB4kuWQAcY4K5nqi2zsekLg564NfOcy1SfRTEmgVnWx8wrKlTn3ClgKPHmtDO6GAyt1SJEnSGjclqHtZ8WXLdAqZT040KZYmLMu1Fn1maItfMZSD8TGTZkz85G8sdQkmYI4xkasocOMNckeikIHkuQb78te0vSjWhJxycQavtLFTFHAfzBRUtaGmODMMoBLpuk+gLSKOFixIsbwIc4BHnp2iGZpGuQsnYeG9JTNMrGKAcZ4JbZa7VPTVsPeUPdQT4YMACtZQlMMaJkNHvonXEROUyutAJctBPmRSqZr68ebAVkvWMXWngEF0wNRPsY0kC1YarNozkYXO+OT6qC9jLZdLjTc6BkBxlP2lobQdMMzqoq/fxOjxRyV24Z3pcKIOi6dCP1vO2gShmPUIxIFvjTLGal6Cf/2uzXKSe4+Dvp3oLK4J7g9+hSdjEAuoNA8cd7sc+65lRRZB8BeXGC0SZCmkdgWR8BNUUDQkbDzfDC62Z8dYE6TAM2eo2EDFH8MW7irnKfekzlw9LUgKPBbFHoxFc9FiOM31tMTArzaPFUu4YMF0RTcaRCYTROU8tsATG1Lp3GHsMFSScnF9/kBFbUxH5NkPXt4lMNLlZySNN7XFkqIoSVpbLk2d+2RyAfR8TvZeJ6f5QrgwL4oEGxPQIQthwvj3CdedUBxHLfKUSJGQpN0POrzhxDp7UkDWHYEDzZEYT2AVhsrnQRWjUA2G7Ob+OcFHlWzZsG54Fh12patr0kPrgo0GGH97updaGAEtuTdEAgW4g1vKVwEg48vf+RwHE9WPrC8CUHvNpIHNhgiefzdm5CDSkQGxdio0ogbZzU7JkFP2yhIR1zR1V+YQhJqznc7DMYuxaV59NkmgGWw6yEUAaZAjCqDNHH4EIgy0WGIWJ4ATq7gEDTSY/DFaHTVAAxAyvibNZGCKYnMkbMCd8X3GEqQsIDFrOedueYphl0S7GWg5JS25FoLQofhg5xqJ/WKZCaS40yJgrWEDkdA4tMuUrKcdKd+dsZb/EhKXGN1KYR5wwhSUMgLYOWK/hQYXzVanS2BOGi+/aiGhkD/QdT0DC7RrD6dqmCoPVsJjHLZzkGMLezN5PWsVBXoVbjrSYDPLN9YM0qGeKlcRWTgQbJ7zJBGqFnIY+3bCzVlzya9lOBga/qpN9B0o1l64TGzWnFSxc+yQ6WP1kzgMV8aggMWIqP+TkNIRvbSyhgkJdozSz2M8DVqlD9KWkHX7VBdjmMKdy35SCHVw00Q0mQ+V2pJ962Ds86Vj0LqiHsf/yeUP7Vg3PXDYdYLrR6xnPtxYsyDod5AjDUtYxSvJBJkdcjajP5dKTKKEpFe4Rc2MAhtzwIabnJp0WPJBd73qOjBqlz7BCEPEEDguUIffDuVmwDtD2Nl0NWGfVLS8zLD6zkgTrzhNHWLFvGAeeeXcIGYaWCmuI8t81uMh4D7xsP9XT+YLnoT8fhvZDq4NUWI9C1cdsr3s46BzhQj+e1DKsYnUrPVIn1ChWHD4fiOy43MicjccZLiLTNrZjCpa8BOZ4trbm2EyMb3IHUNkeLhlMZQQ0DItGxvONo3NWbXofECFFqUeU9UbjUNu8ntElJ45FxNwKH7bkba+0IanYCpChAY60FTp8ywTumCG4JFAsB1TJZCK+o4V9GI75WkPblvt+ScVkWM8kNLcatmqdxa6Hwt16xJrev6dho5WZ324F34e8K2aLVVtGcHyIOLDDWS3LPLqHaOCaaGlvsGgqKFjDNQG9XZhrH5SbWey+inlvd5SUmmGOjNxbhgBIjLUEjGdoY72+XIsstpZSxGL0SWmotA2YHsuTA4A5J1ToSI5shc/Sf4S+rBGj0QlCU2ubw9G0be7962xPQYw2gMwy834lM+nNpCj5JZVHaTBubqTOC1TN7kb8mjHTwhhhyOaURxmwixl76VWYM/hsmeNrwiU1XiDvrNESgEhjleWpXgXL+uyLR4ylUcmlPFwrFjiaHwBmbHcZmSUZTLSidkxwVRwf4Nl5O0vFd1nSKI+YKw4y45Mq2iubUxKxvDNB1pvdlVGe4yVwZH/lk/2MT4RFY4TE1j6rwWTVKVl0OZAcmQFbsNGujFYzFeiyNrpkR2Y8oIRkXJpIncIwom0C0QlKFes0p6FjM+SUdXywmgSG7oKRPm2W3992qjCQSp5gGLoRGFrc6Czj4QwNVJAztH2BHIdWCL/BbZ6q4IItZ2irLmEx/nedJGOTLXWtZRgLSYPBKo4VNWWp9dFr5TzdIaSSKRkSBGbr/xpzcWYHcV+lLg5rZhveNsq+GCocxrkpiHIyLKEMpjhlH10qG/3WtHDCHIPgmIUMk24Ga6Hxt7h46WhemGEydztUHzkhOSbwqFg4VHBOUBC8aqxUhpHNGMKiZU1yNkgtEIcMkAoqMfmZtM6Gikplozsk2nWnc7FJGsiUr3PhLCwr0fZjmOQzdX+z1gU3+wZ85t7dRCUEXzuLa5qB8HmucOiiaQdkt5yyjtcxulWWe9WZ1D4Hs/ibscRlFK7EgV/983ksXT6sR+MLKwKHF2T7Qe3Rc20ZSZC1Pu96mz7G+oRVkx7iSDqUnJWN8WjQwvkE0Ey7D3NcSdWfJwH6UTVmGT0TtNm7N9R815T5jHJDWKd3P9BcVQSJg4PYA0XpTEU/NLwHFHn3lYYzMXeghG4MjUz4g3Nh7q9n63y6ofdKAlzvdIfOKBYOCslwuWhvg7nfJSkkJoPUJEF1s0uYBMU0i5jmuqrwgpQTa1rAsrqmAdMsRlqmo6caa2qZ95Gl32DbdmIqeUf3gUg2aVB3LLJC7rHe48d8ABwLXOuZFEfUbATH62YvmchtWp5is/2s0cHC31k3qhR2xCJKO3GvGfOzb2TkJg4jsnMwOJrbGlwq+uIpZKUan70bzbGdHT9KLZjH5Psx4Ufg0JMDVm7ReqC7RMmbUShBmkDc/CHrsgRueLJMwBrKahX8Bsd2FhU67hRE4yyhaATXbCmfImIYsJ4SJfYMEIXUSsW578gJn7fZLg8x28FZVx6X7ZrUzlhBNGImZq2uccmZc9DZZ2kMxkrQl+rrvJQlcIpZa+uixvRtC7S1mbeSm+kVnJ898+l1UPJOJz7FKca2VmIEOSNM7zYd5P6qGc6BaBkjwBkGQsk14dlODLrZrv1krt15rSaN8TAJLokdAujXKUl7GaeqhUmyUZxuO9pcQA4zKWGGKQU9WR7A1DelTZyYUasOwsc0SV7e45yho/edfFnPavKdTetlLKUSLslxiMgoFNzWEI0TuXHeceWNiqREYVjS0JMbqqIxRkmR0lKy5LIsPaMYl94AYdlGYq4dAB8FBLiQAU7SJ6UWgUrNplDiKJkkeIsiOhFX1OjROQXTbjQMTtNxUE6ctkZng5UHShrczI7eFD1nWEqDonrOR04VZH2zUCITk+u0j/QjSO21DAlJIUlSOTHMtiWwjPZp2o6/YuLIe3g37Tybi6kHthrnqOiYHJQEjHKdNZft3C9FUVwezoJgB6QYWKkwtrJYq+iDsOykRjdUihUsQZcpx1nKAyxCxRzG9837SyxH6vRZ0DJONWk01P8PAiA5im8tzigZ5AABuYitkByzGFQqiUUVU9/84zmWsJqT1rX1JMazb07StyWxkndIxobcu2168Fu+rzfaRBGFfRLhHcOUA5tjikzXJhky3DAHQt7hJOGTlQJXBQtI79iTHKADA0CDPXJkqM9MJCyPpTqIrwhnzEPvUeCDduPTavrsrBgWxlkDViYWDH/GgjnCbsuYJrh5MbIaWPrB6cqZPMcWOg3pk4x5c5vgWGtbHJ5FE/ykpe4EWQcJokgfLtDRFFigoh5N1qHY3W2k9vYqmNh5road8gysnaqFuhRxGY0StucwN1eVFEJvVzgaVtdYqNi+V1vukEgFOxOwD2fYUHA53cCVnJfnavimi2FhRKT9rpLPrMgI4BdT7RhAPHFk+cgn7EHMqKpZyLYutLZb2+bmizG6Zu7OGkvZUsrDBkw83LhnSNJkwcqI3MEuX3mowLKrzFhJ5gPHVkvyT7EEFpWxenqD3zgfWn42hRkKptDYFT1iNI7XPuZZYK2D3S0OUAgLHhAjSwCTydKqJ3a7MkpL7ElEuhR92NB24LgU/cwsJKx2w4dB5RQ9uO+U/rEXmC6oyskGDE7CfgLny8YiuaJzIrYGDbHUZ7CasmMzpflQcfrdrDBP2QPvqivfwMSyMmwUi0NLXp8XTRM5Qm7FJKSw0XdXW4X7CvNAdAbnY2+KG7uNenZMMm0NGUKjNA8gC/IFyT1XgWBp2WRyN5KTIcqPtPZ4uE0yjIHIKJOV9pjMBDvl39OP9fQzsJcxA4wpkR5Ffx8XBU+F0WYo3tosRHkhqlhIjGLpEvFEZ+/EgCHFhVrgf+z6uDQfIregmjLW+LjRSHNk/y6PUSs6kthyTk7fjDCRTsE+qydU1ptu/v2k0HZbtHbROHWnLgiWmYpcxW8NB1QEgXiAMlSrzKqRgbJs7sMK891sCAsETUnOhXkDyeWHfriUlwjAtxtmckEBE3ZWLuzSEzeNLrThZ1ebSMqhNmiMBBiZs3Mt6LYh4iy5TAYjujyx2FPLEgA1zhAlW4n1ZIIcTwLbkm0kbe8OSV2tVpEfSp1uTxWHxjCZosfMhcUEOw7zJpg/tEIWF22GKkt2xmsieBEyo6iWSfrAog2NMVC50htew+hObiLaQzHDdT2wjDT3dJmPUttVNGp1bGLF4lbC8Jj5MbivGLxqfBhhl98Mma8COaCIeSiQV7COPMpcjZ2ZgbFpbyYoUW4+tAzezHkLjcpsuMJZo8WakPQFBm+PwTKOIWhzchSTimwu7FKJmLEakm35MsBkX1VgeMVkt7GblLLC5PzhF5lvmjKnC2OW5i3oETzz1NqymMF8KEGY/navpefxWSaLUOm8DtueFRKlCaTBRHGYt0U/RaCafKFIxSbbNm97RFOmaxS3LJxuwmBthoNGI+ypwKopIoVrhEQGJgv0MDN936+rH3ziRztCRRli6elWLgVVzQC2WiZ2UwjL4zCojGUHCtGu3u6UI6E0brHTC+37yATWJKG3SZmiMT+6AYI7YK1AXoF0iWyOmSEzWGQZd+3l6gOJJXAN+hQn2qN4BxXZodWOKctnZYBbKr8pjRloz/rlsqaY2Shgi/YmDk/kCGifdGaInpdMiTKl7ALG+hI7QoDyZQ6ZAzi1Rs+ldcZmGnXsxiCTBRNekaRxArYNJhyDkmqWCXZiWTX3RQWF2rPNqNRN2ZJ1lDYT6jiOwPTknF2ywrKbSchGNDSlq+rGWlg3mSwdkRl67w66rAMK1wIB5oBzNSJr/0Y3X6uPUgwYXjwVKpNLOI2j8G+xFW7dVvdrPB9MAulMIjvE1pAcASInRz6WF7M2I4XErNk2rTjG0fsVZzYy+KKFsiVp0BzrHJlcZhlHmlAW7PQrrz9F49OgX8Q4PKEyfjJUqsbaIBz1UgEtBNLCg+aZ4U/kD6qZwoV7SlqjzKUq/egiV+sF/z8WMFtF9PSWvo7xOaMXxwIz+R3GmDL0k4s0k/3osy6rfWUYljQSm2xW2Ocnx4y7S2OsJEvyK15mjCfDGu7Jx3KJoSfrtr/ngh8ULFRNrBgshYFgUYFxhTkLuXSyaiQGo+NsoS70IUU90FXBLXaRZHqMRHZNLoAbsSqeCwFvApFrF2Q4595OuDiY0RICKtO5UBbn0lyGqRSzmaeVUHBhBxjrbFcByjDBmvSBPXNa8jq4Mi2X4S2izIDe71f18ysKovsm0s2kdqWzEnBQYuq2nVKmR1lRs8jsjTnM6VDoEHHWXGa+MtEnLooFWSj63pYBm7TCIDud1WaZQjHOcFkCoB5N0Ctq26pvdQ1sUN3qtwHSM4uiPueXoy+kvCQln/7H0tH0SaayJLF/KgnsiIcNgLXzy1O25KINQmRuNheS8+5Q+yjhpeF60BwELCj8CsWISSRiqd4HdPfMxLqM2JYpLSgI6sVDVtmHPx8jflFehyBGjgOzHH9dCKM5ZELOG9C3mGFkT1tihhm8MewrQreHr9kZJvINmCLFJGRX0XkSMQl55HUEv4LrD45SJg7VDO3rBl2ukb44wTRYzmlZEgyQa5euWhQlF3SDFKSINdxkaHErmo1GN3fcKfKUsAtprdXNyHHyCV0QEFYJFuZi0/VEGr2YEWoxUqPIIyZz1cjODEYUMNlfcXEwYjdegOI1r1j2IptTnw0xeNWVN4K21M2PkkcNgqDdPlfBUkyhfbGgNKrhRQpJtMlWmkYwOQ8ZFlVx6p6XkYhRtQpnvc9IrATaSBZ+s3NNHHNDrwhgYMtc6DOmBqh9BGlMyTxkaaAfl1TYg6l2daLLepcpA2RguEpcb1Fke/Rj5BqRHt1rU7GYxcEo8CZV4loxLLfOvilhU8TP/fyn8LNv+2e3j3/irZ/Ej7/lE1CKMd0AACAASURBVEmWECYSwiF7GiuelIIcS7zOs3FRVc/CoSeP6FSuTd2UIb8NSWDTGX+CjWe8x9KZxq9wyFZmz3nSm/8Ax/zU/0gbPCf8xtmYOVAOM3ClLL0pPTYYM7tQVbMw0Kk4GFnUJgJoKrJGIvZjD22XVBeMWC++5NsnZgBHppxktkuz4lNFsbFQKm78/VD+/iq5cOM+bS3xO3bZeZauVx3kO5Tvj19OR2h5jO5RG6Kut3xWlqF0qUgMHLMH/94vfwW//UtnIXTetyf/+ju/iF/7xS8GhTociM/Ym89Fn77ugnCgucIAbXnqUKrmgQE/+q4z8aKf+DCu+fa97sUvvupOnPLmv8PJb/4gnvPmD+LG2x6ARbmuvH4HjnvTB/CKX/hYKVQtPVeToLA2KChvUNi4Kp5DFMbvMQtrLWsM6K16t40jqoZ+Whassi2/6cwxVJmwugPKD1G31l8Inib9+yr4+Pmh7a4102JjRJYnJRxWLqwK1fjU2CpoStR5kE4hjot2ZtdHersr42Zts/JkGCwzIc5313N9uNljNAMsj5B1fpfv5fu2E4R5VlBBGOS3KnCjiGlFgTH9KMjRE6tytGUVygq8XGHjF1jmSSccCgD4xhV3OLebbVfeBQBYmc7+veXy290C3HLFbQCA5z3zCSWD6zWFfQqcc1g2Dr5VBuEylcrCTBq//WIwORineBaiyuQcI5bPloeBUcbqKmaAkWjoXnc1RKPgtSUWYmqMscEhfJkEwrGp1nrMOTHTDQSjFqcUbuSCm0kzAr5bZ+uE2ysw1sZyjWFBa9VdYze3OZbBY/53S0WCrCVozinAeAscFyx19lOPRpy5OG4WuF6SKkWvslRzhsFGPjOSNEbowg9eMqWvN3dz2YzrMyVw8vGzAHjh5Xe497jw8jsAAK9/6VGzgHfZHe6CbLls9vvnn3h46TbiWNvW1xvssWTkGhyBbVlxCho9/FKnSHB7GUZaShVskl200lRnVtNUzQD4keyHroncaESNKUaV2bLmNEpGV8bqguHwUDzEA87Tna8LGGNk7Xdo0OrH/EEeYRnlyTTO1n7WcqqiPTBYephs05mNsc+DWUZHwJUycFX/1voqX4eKWsZQwXDRZmPsU+JsVpQ+Dm3tWfnKzSUUZmF9/YJbcN5Xb8HNNz+AXTv34MDH74uTTjoMZ5xxFPbbuCGcXGMD7/rvd+9ZxZmfvRZfPf8m3Hr7dwAA33f44/DSFx6JN77m6VjZa5qticyJcNLxh81L3ruwuipMp8Ce3cLFV98FEnjbG4/HJ758LbbOAyIArK4CW6+Y/ft5JzyhLb6du/fgr868HGeecz2uv/U+AMDTnrgZb3zJUXj7Dz0TG/aatmt+yOv/AgBw88feit/+qwvwT+dcix0PPIL7zvyFsMlnmblE3HDH/fiVPzsHX7zwJuyzYQU/8qKn4zd/9mUpR7eZYbIvY2UZXvf2eGx2UrSXBNTOBTyFaX8y80KUzjE371ar8xdbNa3CCpirWlOkodN86c5cslMKozWtwYfAyfwWTWxwT700c5jAjmlFrapPxPjcCcheea7ChUnTZO4kklZ+GHSO0XbOuugsUxhccZFGa7C3a02PW5iWe02GN1iOxuAq42szELAhxffBNSGpAHzg/Zdi25bb3Gvcfdd38fnPXY9LL70L//U3no+N+24oRbWeZJnhUrt2r+J3/+BruOKa7e7h1990H67/8H3YdvEd+G+/fjomK5N24iswkgcfsBFPeuLjcOOtD+Cab9+DY5/6eFxx3XY8snMPjnnyAXjCIY/DcU99PC6/djuuu/k+HHXEZlzz7R24/8GdOPrIA3DQgftCAHbu2oO3vPvz2HLFHe6zXHnDDvzeDTvw5Qtvxod++wew915T9/v/+Idfxplfu35NTGP7/Q/hpb/6Mdxy94MAgO8+sht/+ZlLcfuOhwqqOc+CiMwsK3DWCOOVCB6ZcbN082No4JN2390IzpFUSnB6026qQNO5M5Tgw85fNWRcMUi19fJW/j5qn5+WcaJvH3TnehNYzwKyMw3u3lezgBkqH+tk0zO61XLmSAzEMNm23NEdSoKk5TYkiJBda5YiAGoNfE9Yn8RlrTw+HVad/aORYtACxebm/da7zlo3Hnn+V2/Bti234YAD9sYbf/RYHHvs47Fh7xXcetP9+PuPXIUbbrgPn/r0tfjRNx1XMTIZtSLwz5+/Fldcsx0bN+6Ft/3EM3Hqsw8HSGy56Db8xQcvwSVX3oWPf/abePMPfn/GttrJKZx8/GG48dYHsO3yO3Dc0w7CtsvvBACccvzhAISTjz8Ul1+7HVsuvx1HHbkZX59ng6edcHj7qH995hXYcsUdeNzGDfi/33oqXvHcI0EQ/7LlRrz7Ly/A1y69DX9x5mV455ue5YLT1qvvwId/6wfwohOeiMdt3ODM+W0J+P989ELccveDeNKhj8MHfvVVePbRh+K8K+/G2//4c6mkst2DlJdjpPOvlcRmJ63aC2XbsegxstZhMWnCYfp6F0gylXycikYUb956Yjf0RE7+Q+Pjx2TiMAugfUi5PNkT+nPUgq+VUtO1TzZmmH1OygzeWQ3D17tawMmEmi9YpKp8RkxYtt/MIp63hqrf0AZVWRiodS1Jflzo0mCAa+F765kZvNbwpIpcYR9w3nXRSrYielSHSFe6n3/uLQCAn/v5Z+GkUw7Hpv02YO8NUzztqMfj7W9/1gx3u/D2LM/QOBT61fNuBgC89SdPwCte8mTsv3kfbN5/A1794qfi7T91IgDgy1+7MbRqefaPIE4+4bA5ETILfBdeOQ+AJxz2/3b3tjG3rdd10Bz7nOvrL2zHaWI7TeraTRR/pbXskC85NCk0RYARgkD5U1EUJKSqQpVoKKoQKaACP/oDghSpqoIEiFaqVBAU/hEljSOgaZLSfDQ0DbSJHROTunZtIju+H4MfZ6/1jDHmfPb7XoPE3Vzp+vqc877v2XvtteYz5xhjjlGFoxBW/eTP/3qhqv7atQB+2+9++/lY/eWP/e9VVfVvfd+31Pf+o99Qb33z6+or3/za+oP/2DfWD3zft1ZV1X/9V37Z0+Gq6j/8Vz9Sf+Bbfke94fXPlYWGx8v97/7nZ13iD/6R31vf9Xu+pt74uufrO7/pa+tPf993R5+HdvhxAtR0XSo92eFdsq3hxa1lGBeC/dYA8WM5KLZ5iGpu17N0xm0IuDnxO1Si2jpaVIJtnjDiOLF2erF1lO65MK4zdJJowyzOMD9XpAQj2nV2qUGkFl7iL7iPIvg0xaGr8A2n6gOs7s3OjNMoQmPjFr4yLzb+u//Jd52uxCjUBVUXoP7EH/mRBbhfcZ9P/trnq6rqP/ozP7l9WZ/+9BfaOhYL7b45xoJPfuoZ5vfNH3y7nLrPnrJv+9A76gd/uOoT/8fnF2DNi13XowP40Pu+uoCq/+UXf6N+60sv1c/+r79RF6A+9L6vqiLqQ+/96npyQf3UL3yqXnzp5frpv/mpulxQ3/L+d5zj0t/55Oeqqur3ffPXyUPy7H+/51veWd9fP1H/26/9g3ZgfeR3v6PI68mH6bx+9mL/zqee/fzv/MDX2CbLd7z/ax1ySBExpnMxdkPYWUnbxuECn565WF+L3ctqT4WNgIFeY8pjkE7bs4rg8GPsHPSKsDEVYnuyxmhK8Wbx2fVN15XroX+RkRntDVyv6UXJXHHilrAog0g5BCed5gxsfNGIcWOQCk39zvk6aXvoJff8fQzAVRcXx+bJHac1HsACcWOE3nWQ7F3hGgH28zMm4S1eGQ/10ktDwpiOAa6XGF4+tm+eRGeHr79+0xufr69/51vqN7/wQv23P/LL9YXferG+8V1fUW983fNVxXrD656r9777rfXZz/9W/fc//nfrc7/5pXrfu95ab3rDa5pmCFU39i36L9/yhtfe+sq6CYgS22FhSghiaIxVXAuz1detH88UVvDd1xbZ1vmo/pHY8XNhexYOxs1oQfAtDXvS8ZgWQsVlNw/f4PDNODcTo+y1owW50cm0m1tZaqGt4+x8D+vhwEk/D6GeWJXOnTqtnZb5xN2YIjw1Wfxs6Hab9Y2sg24CusMKAy4lbDtD3QGnTnJ5xnEctd/+jjfWx3/1c/XH/81vrXe96y3Pqv3ROdaSgoA0y3TW/iH/mre9sX7lE5+rn/4bn6rf95F3nmaWZNVP/vVPVlXV177jHzLV/EkQHOTM9aH48PvfXn/77362/vP/5m9WVdWHr+zw8Ta++f1vr5//5U/Xn/1LP/sM//umtxuG/q6veVP9rV/5TP3oT3+ivve7v+FZV3d94f/DX/t4VVX9rt/+ljY6FYK/zHDk65+/621vqr/1ic/Wx37+k/VPf/u7zz/7H3/hE9EdZU4wrBk8ogYAVL1MW+8yDjHY4hWO3udf8CIkAyr3X5GEBPcElwaA674Qbo0y6KTewSYDMNsrinj5xAivHV6v032Hm8bkirFt2Ouvs0TiKy8ywp8n0MtjQ0FpeBj87ugPil30N+phEuDV1AFOzcMreQ+8gRne/DnRrh8B08oIs6+PWQIEOA5wVVUf+a5no+Gf/aG/Xj/xE5+ov//3vlBf+tLL9cILL9evf+o368c/9vH69/+D/6k5+h6u0BO385Fvfzb+/fB/+XP1oz/xK/W5z3+x/sHnv1Q/8rFfqT/353+uqqq++zt+x8pqRcdtjq7mw1dB9G985gtXAuRtxjR+8/ue/fnHf/3zV/zvHbaq9NHvfFaU/r0f/qv1l370b9dnPv/F+vTnvlh/8Ud+qf6d//SvVlXVP/t7f9eATvi+Jjbd0ke/7V1VVfWv/dCP11/52V+r/+sLX6qP/dzH60/+8I9uGsa1SZruNF6YpFzt7jWUOK5M3DLP71ezWS3taXPf/q4UgovNU8Mz05SVHm/Q/MzdWU1IH98YDxg0RniM2dTmfZmrbAyrEAasGju73eRDdLTSgLhutuOjZu122njfSwf4SvR9txhuPoL82NRBXj881UydOFAbnwU3ypNePtdv/Y7fXp/41c/Vx37s4/UX/otfeMQbarN0YB5V/9T3vLt+5m98qn7xlz5d//Gf+5n2U77pPb+t/pk/8A2O3nP2N//ge76qnlxQL73MenJBffC9X2Vj2Qff+7bzz58+vdSHvvGr7Rr84Y++r37spz9RP/WLn6o//oMfa6/l2z/wjvpXPvqB7Y71GuHQ95xR9a//Cx+qv/Bjv1S/+n9+vr77+/+r88//yW/9+vq1v/f59gAiPHycvFDD0OPhvl6jly+1dSBvpBTbQn53phbiIX3/3AUi2sJVxnE2aTilLMjuB0PhRCoXYFm6bd0UkDW0lfFMzLvT2rCDkSAtP+uU2UB53zBa2OyNIskry+g+CrW/JwvpmkSar24WOBr+x6zCNWv7B752/JmoCWYjg1Fr6n6RR3ByvF03zvf+i++tP/rHPlwf/NDb6s1veb4uT1DPPXepd7zjjfX7f/+76gf+7Y+EX97sZHH86dPnntYP/BvfUX/on39/vfPr3lTPPXep1zx3qd/5dW+qf/kPfqD+9J/4R+q55xAnqZ/Kx09//WtfU+/7+q+sqqr3vPut9frXPWeX6g2ve1rve/ezP/893/Db6rXPPye4KOr5557Uf/anvqe+/w99uN7zzq+o5597Us+/5km9551fUX/yX/qH68//qX+8nnv6ZALB7ERPZ+njcn7lm15XP/Zn/rn66Le9q17//NP6ijc+X3/4ez5QP/TH/onhSrvUBBtn8exy0MLQVxcOexg9o8vpC9b0TAMDRgNOGextIDks2eJdmdmBV3oGhikF5Er4AcOBkCudEBkO0SdK4jzA2MLLeDYSagq8kMW0mEhnGj1fXbhzZkkra13LNgt6MCFsvl7l/6D+6Fu57e6w6/AwBE0/1DHCghle/+RJvf7J0/O/b3jy9Nmvnz6t11+ePPvvkyfX339Sr7k8ORngyxXeeBZ4cw00wqHjQl0udf4e6hlTfLk+FeepXpeFCR7WTdeT3LBCrLWly4EZAHUp1uWwnBd33ovYDa2/9/qaBQ88f43h9Rj4jOvX6Z9dX2etkPP1Pdevv/76cvz5EY0poTiXgl1Te/3X9whc6lJVLxL1xS9d6osvPvv3Cy8+qS++gPriC5f64otP6osvor74wpP6wouX6+89+/dLL6Je5KVeeBH1AqtefOlSL7yMZ/99qerFl1EvvHR59t+XsX79UtULL6NeeAn14suX889efOn4+qoXrj/rgqonT1lPL6wnT1hPLs82bJ48ZT19UtdfH39W9fTJy89+72nV0wvrcv0aXK6fPVhP4L/Gk6onVYULn/3epZ7dN6i6XH/vcnl2jS9XDaF+rf68y/n/5fcu15+FZ6/xcn3Nl0vVkye013m5VD19cv2ay8t1eVL19PyeOq/Bsz+/fr9cn6dPn33d04v8/tP1vU+v/z55UnW5VP39F99cn37pzfXpL73l2f9/8c316RfeUp9+8c3Pfv3CW+qzz/9cfea1P1Wffe3P1Gdf+1P1mdf9TLFeuIcO8IFRVoK8K91P+ACBuCl+u05LCRnserEDn9AUK8uAGGQ5rKZhwjKkOxfmVbm/Y30Z/+unc4WZAAeYq5sGoHKfdzQysk7L43fTGXg4vK6Gs8vHEO2TcOIpNpqtgyvz51PYoBuHw+24qseJwrpRdXeBmL7qeLXHVpbBC0UbuO6d5ZiMkdN3SgV23nNi/OVjP6QoK9IAzRBmNXKeNGf5MsIoHgFfaWJFidozJ3q45MXgOMBIo8IocrDOECKEzlB0FVE7M3I/HeClfaab3N3wz3kcLthQ5NpY68PU+cftYxp68uxWHPdjk6r6WIWI11y6LarekfI1zNsgSa6MtJTCFZ0xXKlqgtfzgT1GGw0yN1xnPdlHl6dKOPtf0X6tksl1o4ZvVldBpjqOtq6Wi2QmQ4kzh9f/6Qv517/rKkFhjkzUrd9BbYTBCVrcjWFmt46xcLAlrYE0UMlLvKuySEhdKTGzXyn2pkmkbJVMQCctka6/inWoQnOAj4OUaXbr5g3r1+ldiTgEYO9BDaGPIk+Rx7AiEwR3UwBxe9tjLFocdoCziG6Ca7jRJjh+W6O9eGOuBkeQeMBVVO3AP1yoG+/XF5r8JmC422mXAGWAD5Zy6kgZGd0YQFMOYWo17cBjjNI8Xk92ehTvJw44bMPnDnNWdmv9vA4eEelFthuZsqYUWWIhgbZMwsDX1CJLQnx4A9M1Hzvt1M46RiHvJVg+D62B2AKnj5D9udAij9VdN+PTUsfx24YdBMzOi7W0iEeLCP3Qbuw4bMzW7c1BiBq0uL370QFemqvKdGFYw7ob2l67fy/3kwo26ofDXdcS7mu0noIgx3oSrQ4fM1GTGQ2W2SvZqdofAHH6wkbRfLyIea3Puzv/2afP2yEJSj86CtgcDxojk/PM4kWk+7KqMwYM+3SNC+3xo2v/lMsVGcufD5v8ZzUtte4YsYVwinY7BMLRftq9m25xbsqGImWCpwTmWvSwEx277BwnEQAJX1fiZTwihOo5vCq77Tgb053khT87mTfT2We6uUFONJsiaAcVnOs/DzfkVHI3BbBqjN8alwvkzuVmqfCmPAbDRL0AOntubASwEugNYtzhVNCqVoegoyhkxD4xkbyrpWjA3QzKzSO1iaVAO67iorBmllFsxpVH7OSyREJq38YNGYgZAVducuJ05RCAZ0HWnI0x4GOe6+ul6vCWQ2a/TEqDwdPPks70v+hs/aiSihN2ypVB7ACu67XhAS0BcPlIHk7mKw8YYxd/utXU3MHDDPVdk0p0vn2olZvODc4YEyumYEZPo+RymLo4Rz4Yc34vi3C3YjF5S7rySAt8PNhcWzhLPYCfUoAmc6kY/4pIFqPjFyuxTULGp9wR67AYnQcibKa6CWvYBC2sxMHqo1tLGRU5FIBhswEhXu2Xf7XHpxIQGE8wDmH0CwMUSYmdnRNZ4QcB2M1bBxSk4Ygn5nmsrZEBRZRv0HHWujDjlGKy4Y37DyXaPLX9hx4JHEmktTPsvVE/RDPBGqckpnQFmW6obwd/6WBDDY1duCt71URDvbs0at2/ue6lg/ul7gkEvDzqq4Y9z0aa3BJC33CGNjsm85WDjaRLAnfDuob9WPQi05lRfWKbGz6zw4KNrYjXtLBwFBJrUYOkeNKRD61aOMG7NXcZoXkJlLHYODEtt6KSk9vGf4Qvn82NDcLSywzd/52cTzA9WsGt54pVrnjJvuyKOUXLNwbKVhuzkHOYJCZXa3+9tGJuyYP2tdhmGcZ681mOwY4xKlpxQgykB5xTEjmY0wCCfONJClFP9lArVGQa90F2iNsE5OF6pXkarxYS5KF/cKMoPlTkWCMWmA5p580o4TIHkItIsK82EHMlWMVncZxWkF3j9aRUtyvirX51U/PbDjOHm2aNIc0+HU5uYnMj7fDQ5HDZQDNUF+OIo4k++JmYxn6+IEfk2JCAni1RQREFwDvy8rwWxL0CN1pdjQzOnWxT3KTNlpfcqHp5yGMkLtbrzIIya/zD+KgyBEe73BYrUBpSj5MIYowdDNONfn4f2yVHFyjFNLsCk1XNBmEGiRy2O4lw3I0MprUjN4rdK/UNrNrogrDZeFI2GDLWdTLBts2OmMID9zIXEkbr3m2TGpiizBwdekbo7jojK3ub+bBxIMfYH2Z1vlY9WROUcIpSlCdO7LpwI63V9h00mmC64XUsP9fGELOXNtlcrHO5Yfip/0P12EjcwFTMJflq2imiNlLJrIFn5hQfOYcTwW4Sh2qQMiAIu9wC+GDXWMEB2H0aO0MMEgjVV/HSrYid71ooyPVo42KYaWP1RAfQ5bWUyeJ60Pi7wZ2RIDsCZCp0NY9H2wLK4a5y9MbTpGLPsZ3AUqSOMQ6T9AiQm807zdOVONk+xV/WTpQEwkCW9N2XbdkoojfGEdjOfNKMMKA7YovmEIwHsjV2zUGzjc/MAsP8c9oyfGiY/TOBkiH73GM7TOjsr2FcyWKmpoy5gyZ4phWm3oEziQVKNb9OBzhwWfhfqa4s8w6xG/ce15fZUWFhmMeLYNXaF7ZsFY5Mr3rwqWUddh3IxubOIBpiOu2NrfYpPXwMs0u8EwnMIkGm2Q5DL58XkrVdXn/o9xD7nI1VUl2TqPgV9AH34805YHFgAdWMUs04MefLHlZdNPU8zVeFk5K31KNwzGT0DoQ5scpJLcV1jU/sLU1bhqXB88ozZqTm5C0IVNuXhlgFq0kThrGYuzGK/SB12IoiK2mAoIEoPYLZ2WVKZKeLGnzMPfFLhooK04KRYrMedENxTVHvBRwu0PA7Bk0xGd8rPoPHe+MU37q5xVSVxpHzrWYxqylvzXtRnRnK1Qrkwwbxr7IRuObub+woHmC4H5sjbFsDbM0iRW2vgS1bA4W49xUYxiChOYsK86263s8bU4RPLNYWR7MJkqULFbKm6FHeBERvw4gknICsGWfJEV9geXGbgPKVsXHAHq2xruNgnIuGelLePwfNoh9qjM7iFO2qKJ7+Gaq/YuJPlSSwkVuCoqJXZMh1OqVSQT7px+cLVDy3dCwzQ/+FCNARgeJwORLICDDH+fsZ57rW++xXV4frGmbo7FTXgYYgarREr1fcC5+GzPOuhNA7Nhcb/I61N0mYRuQdKbKljKOAJFkyGTJi/1IpYmZMtVSsjOg0nWFbbNz10heSsJHuGK+xOW01ZtD74VkA3Pc2afIHhPicgQ+eVw8l3aAnAy1BK7s7VTO9Da86lQtlcfQF27VVo6pp1f5Q+8aN7lH3b0+fPHZmWT+vlSzkUCVDdoKelrYAfx3VMcQdJRMf28P0btu6xVRYDZ3dUUQXMei5KLknbQoqC0Oi7VWvnxV2ZvR1SAruO/GedpjdXQe4LWxfJp0D3iRPMHVNMlBx03KujFPETuPqcBgYINtYz3BJK5t3aKHe/XsrmGMdKo8NCsRirBpOrhSukvxZhMoodP1UphZnFwr2ERDTHA/H6kjG++bNg4vTZ8CZ4HIca0Ow2bXlWUDReP6uz6RKEc3cgT4qTqSbmV74FlA1u6c11vlGjkIgnYl3nWDQ4wcWyDhlMH12vfOA0RL79Xq1PGPDHg4D4g4c1XgrLAb+LP7wNMC6ujKd0p67MkNwIOBhR2hWywm9ifsNX7/GlBRSDoVUbwCsDQrCbMgGf0BdXKfsigoSdm5OSNEjO2NpWE8QCJpZK1SdavbadirFN/DAKhF9ZkvimiEFBCGsCwm9Q0msMMobfRVxwW+zA8pyckFHKUzyQ2tJ6C3RAiq2QU1ayyV4M2Cb894w7O0BQg9+ndoWSeCB/XVxc9bLZpBZ/YhtP/1AUDONPAQh98uZKtdnpxmTEsE/EHjENXgKTMCI5xocEl45HKvphRu4r3D0S7shmtkj5oeG9Qj5y46Ngp8u5/mD1jEshxb4DRUjJTdVmvRxzOQ2qZs7CTHfi7b4RHgGqz884birujoG+6aL5Cy7uTWlDPFZcNB6mbgYC09cIyqlGxARN2BW8g4nbJyTK40h3Mm2pYxyZNDOThbCriPf1y7YSGdps1qBYXgnsgfzUBG8rLo1vuruJCJyEWoaXK4dJtzNpZft289LssDlphsGN6Sc3MTYx3YT1n68QYBs7jVcTh5xaE+bAQzWP7ZxiDtahLu1CQJ0IoQPTcW3WONE2hVo7eIr/bPJGVhFgBSmKs9iGGq3Im9atoLhUWzXQBcaGNbvnjkkoomJuLE8iiUiZbCRHAgiBa4bZ6VuN4bfIAL+zFGw+jZDGbtJ6QQxaPNgZoswfwLIylzrTpEekWwFBYzeGf7JImna4hBIxtk0IT5A7YAdU2Vn8CplKgzsluIOFO8PbCweVX86WJzlZNBYKgz3B9n0CdBxSR304z49ozhPSInDXIzonqXTx730f1kAJ40TbhTIm3PvI2hhw/OWJ96aBvtpQsTF1uUcTAnasAE7DBoAwAAAIABJREFUrelTUpLeeoUc0GlExqZEuWQiNlNY+XP9WdX9Wl+bpc/ldj6hYzixXD/9ZbgxW/s2CE3iwOpLPkeeRtdCaKAF7aFva2gnPrp+hu/bRl07pDicJtw07PI/YcR9JsyLxIZQI6544LoMwaTJuPSGApqRR7uf9HxQQis3VDToCMN9BN84SmI+x2lU7PpCulvADzeZ22xIvpf27yyAuzjMAQuyArPLAkbVOA+1MUMY/aPtTpvA9FAaWGdV0zv1r84Z4uwSb3Sb2DlpQ5HAIG1VqzTN0Rg/GNCfu5j2PpCFcgavToF2Ylg1QRhwYwLtJ7ArQgreuzkrovAtCcT1gW5dJ0vsSqNLCASNeRPW6LwM69494BGpegvM0cir6MracHdYPXGOPVR+g9NHwGXSalBCIsMYGteqYd2tmuWcmVKYiYd8ZtfOArG9xjweyAE3vQJUdK0sVDYEOMxwFwXwlhM0osqkceiUAMXNqJxO4sMq6aSoQbJtA8ZLOhwN6hhVfrMhXFyOWyATaCiaOHVCYd+QhGlU6JIQIKJ1fFvNCAco+sOBZ2IDYz0+l7HlQduzholL0IwHzHSUMCMKe2TRIKta8giaMzYVFLfI01XIVnPEQR+KFfnIdU+c+DDSy3G5xkjUeHfvLsf6phZTmXyEr8qQNxr3s24b9ZVJRgF1F56YDg5d4ANAe5pfbOj8ItLwwkkRQvKcuSkLU3QEHz37vYoKILfP1jB7coMDcsMuNrIsqh2m2XaEDV2gOl11Z7BOmr4iCcw09+yVtNxdxQB+u8npPSRr7ia9OovWjEawWC9KxM/j0DPM8CnTtzGwmkb6ROfB1lXWNqMlrRRPORIzLL3kMFCX75UV7CqE7hTD1pGbKMM6dN1MQa4AhrbPuD/2jv0WRMSEcpjgYmKlOab7PjqqC/eb1jUnEawRHBklILvHOrCC2a3iNLhdgw3dZahh3VXzKha6W/qrewTetGHc4Hu4wX+kNf5EirAmd/ielNOOHR+/zqIR3pM2TtKxMXeGY++tMoAH1d2SFZuj4F7gpkHFuAHhdxI7voPJfCgQfMzQwfLPO4oaetYD/EHmY3YaJ03yeLjUgMzXknbEpj5kw9+DjEIDWQuwP3R0y9y2OitZ82R97AObk3iakUKJLPY2DhluVcvwtjUPfVPGhin4uccGFR12VhHEdaocBuxdPnu2TvbGOCUX+pZzvtd6OMhY97QLfAuxHOXee0x9V+TGwjk4/O5yZu3Mhxt36omJuJtMa8Z4RDmRCK10DX50ZfvDbfV+stNigObKgMsInKbDQBcn94y4yRqLp1h3YamqM/NtCESnj6HxP78XG8LE6irmbgeeB55LimjWahKgjhVkRcHFuMOYS9ylyZDbI4q5uDToiqOMyRxMaanax1wWnk0NzXGlua/Qrwvke9g6XV0fwabPnFP6kNsjKveSYDAObkgOd1Q5NhHGJnfFAvOB8fWxe75Z4DaiajZQpree3MlqzlD0G0UZtHsDtp7G9kEZbAIGS5tPZgD4bGigiErZxsQFHsPkL/2YxYzPoZ/0aH0iQ4YxeNYpk1g1TgIaB4n2kHZG+SYByBtR05USnBPR9euJ9KJDGBNFxxNsusmwggUyAwb0NLu2B32Ovh5/4Kt2fiqOg46iABDRCb3gJEyTBKFup2B7T10JEGxIT7BFnTa2erK3Y8RI3KUj9Bhu9EDntyuWuxU7DKMTet6EN09KLMynXWSetfUwRpUj2P9uOgsIpqaQLYcXovfjY06LJo2QxA8MItoxQGrybYtO+Nzt5KnH087Gvnd8zuWogBd6Dkwloe7Q4Vh9YEiyYUGkt+A8ICx5CDuSn7ggV4sJbeFTmQBsBmaYxCmgtrY9swoaG3bduugJ2mGP48RZ7OVuuo6xjS8bCEn2mhs3kqnzjehIIRfAYWNGhaw84QIiiuDdZILseuVbYmc88HW8YdPDSsvgxuSuMuhrY5aTtr3GcC0dqxtvDU4gBQ1cQE0BbC2pjhMZiM15sIkSpS/Xq3yD8PG1gc7wkV79CBVfU+cRBEY6haqoFQSasycHB2icDi2NIuJEA6AZ6HRjcTfAQBiWcpwYeJ6sDjNyylXqk6p+pkj3ZcEu2roj+mGiGB8lR1CF4UCDSnW8d6EKAsOOWo7ytcOUTFXfvDoPLuSBFBi0daK6RrrJN8H9uCFcZqO0W90eR4HvzeqZe6TMf9l437yxanDvwKZvcZzEU9UsKnA4hY1kEdfcOpyHqb58DYKpCt52rXnV7JZjtk8eNwj2y5fjKUxH2LskHaUYeJSrfnxUJxDCnRgXEXBGVDR9ILA5MRGfG9uvRM6DISR9RF1ELRDxnNM9xY2N/NbTDlFsawhnGrguGvgLW4RB3Nqs0LNS0nN0QQCH9tXa5YmNvIrFY8OIixFGgy4hVm4JcsPuu2cv/FLmdXc3dlhto/w2htMYX9YDcoHqnm2wYLVnBWWwNWEUm8y49aga5XdFiRygCz2u9ebIysAQIaMxJ9pcnXUxknJVbXc2b1w0bjaxsRw7U6SsWjp3t5sfTjvb0nlFMS75jGaLyLBpqV4sNPiKQ0St9SenpT+WN2QjrTzRzQYNqAfioNpQd5bqWbiqydQ85DNdrYnMNShLXpuGYh3ieUlsOw/q3H7SZL3DAAJwgkjFQKxmGssRhMW4rUw5/CYmdLnvhIYi7wvgbojgy2B1W1sdQT+qR3D4wSKqQTucOMNAbGsKak5Xd80KGVaHylPsKbgSZa0qQV99Gbry1K2/3axhFIRD+O6E4tDn9sX+iR0JNNuE5s7RrMshwPYxXLcDK7wPTR7C6LTSUzqws9hSwCY3WtcZwd7cz/aTV0wznFpshZEd2mAGu3O+nS3wyDpJec9A0GdyiJkvYkwl7MEeAMyMoj874VLUTlLWqF0g99lskzG5jNyTD6KSMPtVuuhqeD/rcJcHBdBJXU2t7T6FcF8U8yIziQQfRc6bDWvFm0lQXItE28rlOjEpAJglcZnLd1dKQVxgoB0CNweARhPGAi1rXoNBLJxPtlC0wCfIvmwUmFPK0N07VhcboxOSHEYUQfponYx73viIh776g7VW4jjAuxD8lG38D4R1FcQ2sbPBmDacsNy3r/zQiNVYmQwi14bsmCFcc3CuTzLctvN1K8iaOhmbdiaSrY+gaKRZNGqHqQj0154KqJ0tKqz6m4/eva3C3cIAVag17bvcGoMfSo3zZ6QnkcKxE8cA1dQUfloNAt+dWWNCjRxTlsJ44SAtAIfvNl1PG93IgcmV+ieJXYpvnfq+YbcKo6GDWpTjQZ0WY8R2xc1yQsEYSM9NmBv7RMURdbNsZRun0+d02Chig5thbt2W2XEkD6JcFG3Em8a1wjBj0A0deBbzCCqmF4rJooOp5hd5KM2dBhamNMEAYG0wQMR3QHSg6OmfgAzZyXTHUoId8ndkB12ZCfJg11bDOtuNURe3fq6gPZwsOzs3nyp2VtpqrU4LSO6RFnqU3ejkJgJ6QloyjocZAbSDvBanlemwOtclCZmyLGEg8lr6l5NeHMqA6dxC5K1I93yakLB9POejcePyu8MM5s/a2mWtf1jmCtklTQcRhhsImAaIwkPjiBYPiNTtjFJVhnZa3B3qbXTvwLQfDI9tFYHxIpno3x/s8bLmQuiCcBYwdc9BTaHw1eMShI0+qvkRGUGqP2ey5PCzDGEurHj53WCAt3C+W2PuA9hCb2k2ALRhP7orGiC6OUjD+Y0bXYxLCgZ5Jvvy1OGtx4HSU4NMA6lt2o4uVfNigZpTei1FqcUgm9wB7jY9F5Llkui42QLjqetU40e9AnhOkW0IzMdGT/SUSN+/9GE8ZTqqg1MWLJxdGJOC6eJga2e+JTbYlZ3XIbqvSbJnus293pMIgoHHoUjzL8SGKQblZwB91KXLFdSmf8Lu3bZeulwzBBYs9nR9kc+/LQEvJF7Ne10acG+O0De2Nh4kOvhA1zjRjiG+4rVrOtLWWreHga5EDnzS0dAxnhrQRVbP14UAPVvXi4q9TKzRCZTMEQ74Hh3LtJEBqe+TQsFOB4NNBBbUtbKL8jDGPq5KRmAtplhXxed4i0RfWOnCutyh+Nb+uc+6o1qJM/+cK4ftVt6aR/h4Pm7PDFHMZjVlhqISMKRdIlpMy8Cmc6kMFDaxmM8OTZ1JDmLmS6qw2kdhBnFiRD+G7tnYwqnrkVS8O/rn8jin5023h1dQJEfQL1grLHujhMs9IhHNZorijtDs1GGokmAwas/uA/Nh6cQh+Jngfk06d97SSXcj6nNWWWBzu+e4zV5FPOkMXK0iBkrXAiNHdO7EMRQX9LfoNpIrTpEYTpr11FqXq4H3EF8XluxHw/2/V3GPm1WzntFhGKhRBKuF2KcLnIZJAZivOBxWOK8n3VbfPRaUxIvCHpj6xDnAzBlWx0kdd9n34jg8A/35y65/I3HokoZ7GoEfKHoYxlo+VCSxvRaMNh2tguQ4Q+nb2G8Qv3Ml5rGp6VpqViHA8yY0DjcYwuc9af+5uWOZ7QsXcM4E8JkuxJGTJsv+E/PCyLKEjqS5NlYh5E1mshbDy9gwAFuc8poQ6Z8aJhsgLLYcxXDeSW2hwANNy82RWAGdpWfTf9Lupm5egxFewFQYrMbQN3gaK7iIFc2oXi7OAvNkzSYte2PKoz7vOYTZCHoaFOKmZxfJNvKp2fbTaZ176gEvD3ZtWxaXG2IjWOOapREYnH59tF1ecYqrnfgI0VA9TnT2NNn7vk/fncQCxalJZhYNCcFf2LSJwP5S6QAC1Q0HWN+jAERuEkGyNBlH8IPq44nZZ2dlIZfpz9qGdhf8+e9voAMy5kuUhQhx4KcR0z3i/upGBOjSzMrAo261jCQETj336tRy80X3qnUN0MlpbGZvFGaORlAAN9/lGLnA2D5CH53jvsPgJ7SEgGKCW74O6pkzffTtd8q9miHUA/gebhXMQWXJR1iAyImY6zwZUd6E1OJE4iHckIcrdheH0BYHrtFwS5gbi7g1Z9ZH1vHoKk9G0P4Kegh1dIuQLYH1XEWGcWKuMuh79C2a1s7HJNjrA8PVQpVQiPwT5j5VJtPtJoXVSc0Hx5LfVPgemp8i6FnC6UqS57HAKlNEgO/2e5eFwyxg0yuiuQjoCE8LuLdPnlx2Vep809ZVKYSLWQFv0gQ4Q/HlsIPPGgzPDncpB2dGnnclg9ki2XUDqI4xZjxubxfWlkqG+JAyJDyc+ZeQemO6KhgQiMZuji4kJ/Y/heCUr5u5CX8K9CtjA9VpWdnhWWma740BnfV9WhPWciLheaNz165Fgt0hZA/Qu1HWttP0DJWe8KZdkjkwsUnelqKT2KwYIuIFpCMCt8XOHGIEGuCAU+t9Q/jI55KsrmKAhb+X7fRqWUFt8tBr3mA5OlQd42HDQTQO2Fqb9s9xkzRM9O4RoKC9uLMRGI8R9VXN1bHcR2zAJGrLFnL8Ocwc3hugJPun1uIYM2nrHMcwcDJQA0w/jRliXujNC8rCFKMHwMgZWdSgQQWUTiUPhL794jdhHsWO6vcsY++mRkzpXMm64mUN+JsPOwqOe4a850hm8EI1j0eMiP/UZYtBp3UtYZsFL47a8TvRdP1MMURByrwHNiBADtu098XpUK4bSSkydqQgjTXiekUxTPmkTiXE3qeP4dTjms/qZgmDXrOrHu7GDIE3itSu9sx26TGH9R86uKFwahN5g13F/AWUjsJ1dnIjc4AsI7nNz+TBxADbEu43Kvpr13G9b7ek7AC2qjemvtWSp5wYm+wje1/BDdnP6GjdTol2svsKFMAsJdEs9zw7feAJHxkh7iaY/MYmrdOVgFKZDIExiAikj4vsqYDr8EPEezBvGf9OMogHzJIhusciGwBz7Kkj9pHDRNeYO1R1sKhNPS1WwgiqRNI9KJ2aW8ONe7roiHiXGOAjjEzbNshD3oDYVZ5lad/yzxBMnjzU9ISh2ml42opODQWaCKyuwbsTZl8VJWLqMqvDYWcBYY7UA94KWz2K7GJEZxAGAW742Ue8eVsNa5NFyhXOcZhhZOMuJC1cO28nOtOrYyBj44OSDrddbBAt4wqyw4BcqFEBGjCbRX1Jc/bLglOWMW7s0DMS8SowaTzgju7jtWss9T3mZ13jsFtmRDIkzqzDd4ygFWyejhWq5dd9BqPvPMp30MFj7P9HzLC39GVmlrGDKTcTmPuP8E0sNn7LMLM8cXF0jKwh9OdEbEy31TlQDoOuq+kXox26OohsBZF7nhsSQ1C8SiKgq3RSfdV2n4l7ib/fEi2z74Xy+Lly4meYvWKVwDavIw9HtRc7xPAzCcdNAKFm3055JCiX1nuOsUZ3MvIBWv3lMNFgsvOPN4uB5IJrWCcTJsuc5g2GEhgOfbrHYpjSADtU3jN59NA8CCA1U/AAdtyVDuaygdce988t55ddcZwweG6cPtLufAjXQI91tuJDVjcsGF4Qgrhu7N+xxsZMbo02ZCIFWNt8CRLLfohhHEO28B5f70J0JZy7UBmPkYWC9I6TcAAAHlwPI3+cVvUmxmPYEU4rPeJRvQ3z/MWAO3LIjPQc5Ir+2fzAbHsnEFvFYAVKRWtJ9aZBj3QgB/QEQrRgYIy13kMOiTKp0XHINERZI1XpxIruLKMi8CnT9/TaaVQqlH2uteMdmOH9CqFzkfbmaLvp8ioLx752yhDiCNFoM8W2NeVFAJGZCWfQoF6dkA/Z5y1bbg/ssaeLXXVkYGBqiAk9HjrVwcnD21bgxNlm2vlUEmRGCxH6xTrHKQ3OIQ+PuuPrRPZyvZYthkNagGjUxRIOQoKE8WqsxaUPZ/ortOLJPuVag8fJzMGBZQpXQ5lnmZZaYxj8jBFOJHNWF7O9RR9BaZkjErVJYZOZWDCkKNVp1rEWPGhSIAsIYw3piTmMsCrymJ/992Upqqi7cUKoYxUOj6ls++57HoN588fMLBcHR1kuX7rAImKt3YKv7eRFNzOo2DEhelD1aNVq3nBrbB8LPgfAXgM2QcdaIiCWuXuLHpx+dG4kIh9krWWYhksVzSn+1u62CbqdUTXWmEfHx2rKnSh8Filq2hdO9My6QzhZgM4P62l11fn2sWgx0/PI2Fpk5Tataz5DDzSY+HozQbtrzaKN1IVcz5KGxFY2yCcE9o01ZyNbUlazrORmCYxmm+RJhXOtkFuc+VXKAteNBeeH/rmVJYKJjXiQ1aQUFEKCnYGbWBIjcLr54iJGjSwwnOKMJtFsNS86qnZQj9c0QrWvO5/Ua8eJNjojO9r8qET5DQz8H0Wfdebd8lyaZ67/WYaH7yLT3s8CvrUsMB7OxlFhXMvpf2e7lRZBYxAcYHZfiggSDjm7pKYaY9oDgFT+54wwyWn6rgBsT2zUjvmYPnxdENk2XovKVKw8eiG3hzlMApp1s3PHYWjWDUvGjd1XxDN9N47QE1v7EHj9mK/fGcpt7DmI2PCHtNkWiUk70JgYVG5UmKMVhnYuyIF8RA5nXGyK+1ELuPAS8/K0wuwjhHeb7MFM0jUxHR5igZX04gNzJhfzA8BH3+zUyUq+0rM65LVLQcJk22XFyt9iZrL01zHA0XF9kESZFjzryg79ot4/cL07nFCo3WDUv8yJE0RTi95DuqG/jq2M5DnZDmmXgbLlwyHYGcN1VSOGmcg5cL7Jsxaj+8ZCxFWofn8ymK3/Hx4xAr9y5idvsWM5n+ICspatQys3nHi5lYB6AKO0w4uuzNduAvvVFhVUn4Mt1/7wwS6aALu8aO9wUl9Z9VBw3diYOqWz+8vVqUqDWH3P8/tEW3afgml3yAfMbOZkjsFdHI8VF2YrKYFT6m0DUSuwnEny7QS1ued1S8j7fTU6S51iwiD2kG+iIrCBg9ZbYewiV1nmbmBzfT+zKj0dLdOli6gE8kBzHT8F/RzUhSalgh2a3rje2y4wbtzI4COIEN72BNzWUHYu8wyBVkGmipm7N5/folcyYbK4OgkO1mxKmQVFrN+PcB2oKn5uadByPjJqPKyUFKNMTU84jcgj3K7tiQeCZg92A9XuY3/T+i1hBFJHUf39eTAZNUJXChraPQO0nE5/bRPzzWgHOeEErRoZs+tkWuT0WuxLS2cZ1wu5yThAi3EqcQK6tc5JgSvochf2CFIlidKuDLZLHml19H7O85rDeg4vS1a1wARja3MXHSAfX7W0wLP25ga38MDBD5NxyhRnxi0fivTKO7IMoDGO6DcqdlhfvBeGeBsxcq1AoBm60ht1jl0U/C8ycHCysCJyHRtaL0wQLB5h206gyWzPLmLKVq5IDDPPK5WpTL6GB7fS5Su5w73yNSLq0YFdN29trjkMJjSKVGRKO2kyYGKC5aEto8P3h6YDWjNBCJeWKCHEAQfF2mrRYEPdRS5v8PvfTy90JhEK6Q8xtRVDyPz1OqaNvzwhdU9CwMvNp3/3Z7dCjsYNkdipJJYrSoD/5wMmqv6tM3To15TJpFrnt30OhFfbukkb+msYCW7i9I4c9Aw0KzSEE8QHQTFQMabnmjDIhg0NfjpcbeQM317JGGKQ3A3d4unvCrvWTS4kxQVpqjp4zDVW8pw3kbj/jM1R7Zmw7O7jkG3BV+4a4LJpKt++p/Bat0mcpMloo2nW99Obc/b2vN9ZXWMppNoSrK+1PiOgML3fCX6BO/sgtbtsYy/urAe8jJ3dK2gGm/3XxAo3WzSWisGOMWCFx6w/Xw+w6NQHJ40D02juvxb2zWC2YGtBS6uGMSYAYTuuf1evJwxLK43cxLlyZtl1Eao+J6C50QMSg4UA6KpPQ5i6et9tTC1qZ/CEIXRH4kV30ijDQMcwxzK0jimNkgc7bQXnXEhvrklb31NZ0MjV0YPKq6XFSRc/Rj5EFCl859in9UW0bZY5W/6Mbmy00YMQ+ZO8Di65zcqCSSG1hrcLzIiydU+gd4pon2TdYQHkhtx4iNJ+KAh5p0ZF7Kbms2WCZPp5NxQYC0qiL0GVECpZT7LSsMllyhfdofgfzjyTGh5vxkpSekbYLgCHQ0QlCKHfw2hLztZVIXBND4kKHeP5UDqjfNqss1y4fnWKxk6pxNGJfUBO4HKbwEgR62Ldv9ZFwHYqDWal2KQbGmkFmDmojbX2+Tw09nHBMlzpeHqQYngxdm+gH0eVudYNb9ZxuiciAoGRszv9MJ4/gw1OiAkGnrR1xrspgAFq9jSvXXEbNtdv3RPmicc2VihO7tkQOpm6zoISJ3k8NOoQrJm2hXnCXytp6IF21GSK2i0vOXutI/3VAWRlFsPOjRwd0v14uqG6hZFQrqLD4MhEskZnq63HvXol6gjE7RnXzVQs8rsJNiqLGva4imVA1eSYHOoAlIuyESuPnWI5pTQzGRMVFLPXoR7GbkFrNt1VKcI/tc/MoBVHoFrch68NcpfwSE19wVqVAwQm4TADVKUUrR2zuFdHaFQ9KvR3Wwz5OJyQiEmlp91j82M7TO6xmjCnimpGkKAXM/tAJYZwmaLQTlErULh1YdwhI1X6oLN1drKHsHsEuBuZwgGTwMBktESHcdymjeiIyV4C0LEIJ+6MM1rFc1ySk5cwPB5z7JCb/A5GxDAsxqy4EC0ma73v7K/zdcJeIEdn2dosQ8noiU6sINfi0giVaXo63BHgWqzUvWHWKKg2p6Uzk7puGBv0jGZ3rcYG5riLEXgIrpmoxy+3wOvswIZfV1/jwax5s+4oGV02yUOvVWpDnsUAzS2lqswVxkwJTqaOSxwcD6ha07MyzH3CoXR3Mx5ODJXldGxZA6UiOxwwOsYpk6qSCd5YsJqu9XFjgIkhqyCuW6sUG2aeJTuu8QhDr+eSTfkwoylpx2elmGFncacAv/Tx4/B1LRs4Q7fSl09crxVyGc9XHfHtkqVnUCf1DmoHUaSar7ASTgzv63OfnS1QyeAG3OsIPHVrt+fYRxAnqPGuxrQ8TnFCZs9jiG/mZhzFeD4yXM7lJLbdUFYNkUsaR2nSAbWoJ/oo0qFMG5NvEYk1Fdx2PWkFYmq9MCTvIHBQSFFyF2qOnd3a+9xYX5HxgMq/2MXnYMZBIewsK0E4cbe5GoqaSF0tb9m6qLaBpIv9gZmRyVJ3qSXCWw1T2rpBOQszo4i7WRwttkCfmtzDkdKRsQ8BRDtYOE1tGnYI714d4tERDCsgnf9/KYBbd0UPn2lHyc4hOhbg2Zi8MfRB8A9ER8PZfIKo7qqV+8iTgr0absY2k02fb1tuCicX1kw7bvwkOHQM6PtYpsijmqVC9Hls2eurfITjzNTlHy1WAHc0WynuyqD49TkuxVvyUyMiXEmthWgZJdC2Q56N5Bj8+SANn3ZiNKdldtN5EV5jgHmaC8SIbuaKGcyea5h+JlQKFfIepKn9/EBumDaAHZSt7roDMmJpxaCXF4cf74QIvvw/+3bexj4eSpkLRTqHdv8Mk4GPag2g5qTUR2PZToNU1LhfOdzOxjpzu+OMfZE6/j72BsdkCdomyjXpNgcCH2DY4JgApMFfrNnWD7KZvg6VwP96jZSF+wUhsWnOajfJD5sgBvuDgTn5Qw7Dalc2DKNjasQG/LKDdHGzvH9EXvXROS+DBJirDsN6n52GDblNYGhau8AbwQbIdXIPMsdmsGuel0LctUXvQ2WDLTF3XzLoWyzwY9/VK/19bSA5qYfkJjdjg5D/G6gs7AYxH7XZzbWRsPr6H/rIrfSLbCfJzU8/4Yeg6daDYXWyhO4MZ9ANA9QMf0I1PDi6J7hmkWKnzy7pk5GaIyzs9lZo1AAHl5i5/4mQeqb42dndLFgT6bREv/r9CB8zWjlLhoFtzQariGMuoBMk7rkvtNBziDidgqP6NzruiArDUzugNtKn8UdGWM3husQZfTDzxgw7wkRuYpjq7qEAJiU5PRwziOEn2i0SOY/8cc8YDbPoAqYsl1oEVFi8hy3He2KA1vReOTGk5HK0AAPjYLyXUy4oILf2zCyUfmBzinks1x16CJAX8pY5tMl4wTYZQ8rVFdc5AAAgAElEQVTb1QeOad0V+SmWrIb5VtPrHVFMa8A9zwGOmKPuJPSQ9wELsE6I7kRdKop2jagbuMLyehmVB+cIvbaQVvFCS+XDELmJY/wP7I6bbrq3D+wGp+KqzrV/1zCa5ZhDMT9wMueU02TOy6u6AGLqlupxfl6NleTtFbkBfeVgNT9WIbmh0ytg5WusG78FvOTciRuv9zR+RDRffu5zrJ4uroPgkWrMqmthxODuLg3EWfROFi9XIWIXN2UK2O9VNwghCi/FrYQ3kRCYbMWG0yMJjWtYduG1jK6cnO10uZEm2Tl0nYch5xqT/e/JXQvQxRqQSNZcmvFuGRNhLXM5hiZVjGljYiDWaDsm1zTFdvpeDvBRpX0Flha1NcMi5rtKYKYNpP4Ueh047s3CRnVxFyPwK12D4zzVjCHpTYwJiQecRlsfW2nxjwHimhhUxczJ0MyZthyKGBOph7uw2INSFUE6MxtrecS5Hj1MJyVsIOhpwAsSFcaJ6gDjYzIkdGhVYLdpN4Ul+vXBANlRLEeYokK7rJwT+qZw9eywwqiVe2jrrFrNYDVso9pWjnZMrMD18qwcbLCATqgQs8Y899WJ5qXgDDAlJiKG750oMCRiGEf7au0kJCDMOkBoF0v7XrfAwl1Fo19uJr/dOvYxzCv1iC7SgFm6RCIWxXWkAxgREBpMPoHCR6cE9y2Bd4bEEGEu8NCRGrcegiUQZs55MYY6QsbYiFAyfIly3cU49YwckAeaIw6T9zheN+jjmeoLByCexqJz5hipXS2kSDPs5LVYSNbI1OifpgxDNAGn9lX0lYpbhqdfWwtE7+g3uadnoBPkczhFOUgskAMRFu/BzgAG8mknUqUci/DXdW4vRcB8Y9NrVyPlYIbY+0cioRGAsXbHaA7uyw+wHbubzm8EPON4fOh7AnzNjtFGIwWgxw6k24JAlcZYC/RZ289ikO8F3XGZckIuNpkW89HgdQyXqFq0tuT8+l1rHmwcOiJb1Ifja8qJItjl6ItoLHXIWOBd5lm6NFsCuhGhweBoxWvKtQ8zsPOincqQ0FnO0blshRPoLSYVtzrh2tTZ4bTZP52FBsyt0GPjVaZFPsQeBp4Jrp0bcuyG7bs4pOnEvjOGVCjblkuLOFbb58am688HHI3RvhMG5OwAMSCqfGgM5sO/xS3gVMteh74ShWrM3iF+pYl+IxJSraTOMRCCBznw7T8rPrjISIH8l0KFpFkkboLSycLCcn4p4zyuGyXwhb+tTEg7teXcEWMPMBIH1vui21K3c8GKcCJXV2E0uz2YduFm1FLqcI1z0yAf+6bmHJzIqaMiOn5MA+AwWmstE9TI6oWHP7K8S3Jim33sta7fc3VXFOmULRzXniI3qi669xD1sr1cTKRmQh2c+HeKFDQF2M4UesrjvXSAo1ITMwv8ZcpeBtBFIh1r8MFzo3LNsoA/OeElqJBi7HUeZIQEifeQc/k77DRADL9sGEmHR3UfE56wFvvLa1SdLxwH72vHuhjjj/vnQbpN7k4pOq7KHOUQurw42c6+FlyWZlaHcO4Rc5BunFm2QMzG8M767Fi6gRTUAsuSAieaIKIwazJwMLJ+FfoTv5DfwwRqxucKfx/GhLMMCtFqaGJ6uBlBxf1ATMx4rl2pzVy5WYSG9JHp0h9v0Y1esY+JfBV3gLcY3h3x8Vis8KG6eK4eMc8ce7BAd0XuCWMr/cwZOhijlrrPOS6EZghpyBJqcPYtV5ZSe8cZF0inYjSPww12VW5YSeoTFjbdGlSn60rWbQYwBh3PNEBnMjKF51kYVuahPEi2DBhxsCWz4NA1y9WZtq18084ZzsJMAkz3PfyoOTSVueiUfF+KcIxROX6XsO0+tG512d4Di+AHA+Y5JqjMxmaNoZapz8y2lSI5OMXOkOeqsb4T1J8d6V2wwJhX2G6NwLc2PbQFw66PNx4kk32L7cHoWuCtNrE9kDomwm5KVEtKPUcbDOzjia1w6lS96DDFvGTP86hZlIzWo/RcE80lgRSOFrh0PBQIUQlqHrko+6lhfnpEaqI1jWzxkN0ggN6xskFUC8gHWhB7G6eHnavT/uyYFOgBH+DAQFOS11i+/1yO9fnCBMdKiGBw1eOW8hemh0+pD6JmBzMkU8QwFgd5N7iEd8faafXOXxiu5BkyPdBw5+mAqztahQMfYXj6Cn4iblTHKfsFKFd3sj8cA+69QF+eN4EKRGCBEavwjaxWY2oZD4WspulaLDD4SfUYSRs/GE7WR0GrYAYtiY05ZJauoZj9PzgiF2QH0hPYXh0W/dpJRq2vJapBLUbDILTQHReqW5ASIkM+aB3ycL0RyIPs9x5FnI5kwqUgQUOuZmyQ2dHAV95q2LCZOi2eAehweRIm8wcOXVRMQLY6OTwloTd18ISbKQ/j6UXQDIcT+11Y/b0Z4ucmSOZgfFlZlzeCkZknhmMkRmLgAf2N59/YPucJ9iPPLG+zGqvF7uV+XhKugugPM8bxmYmvVCR1r4i31TGAW3SNDtF1drCZC9JwzvM8AJuslgrtQ4BsxEmUvp8Q98bsvDk06RigZYELLIBctmdoQmIGux7QgxQqRkqc7VCHacVa9VrXIGvEEevJIMMagpwEXamYPZUAXvwsMVjlTKhyPR87naTOPNCtlo1fEg0ktuJ5frKMTULU8oBsyB/uMBaTN9q9XRG6KY7mvnPsNsCRJvaYKXsIpFG7nqru0cbq+CITaytjtxCANcJn9Cbbb8utuYYhRzM4hk6n+/X5zgY/QE5I5mlS2gtNRfKYaRMt7Y19sNEuzR49HS/dfcbPFtgY6m7yYrsFNvy3QretBgFJ1JSRZb1TqQZA6OYLtjHSancF+f9mgBD5Hz4myvtp9wpilzfdjLrtGIevMjiiHfscQBbJkE6SblvHtIunWeube8/ddIDYkBt8hSMxH+gODTu6MoVmuc4bFxy13bKlkwvT2xnSPlanGJmtFIpuvyFIe8BAThHB65RuNh1SkgmjCpnXIbG9zVZCQ6EEPMXG2JbBKmd/Ylf+RgfqYliEC/aSGZ3ynMBybXga/PUPPGrvAXkVrQt7WbEOxnaoc4mJtcAy6U7N4+3ZGO3kZgYbCKGEaWSFRUA3bhuzLUQ/VOVaWT+STwQHu69SC5/NSr/PJ+syR/DXHdnBXB5NajzU+d1iv0czTVmtwSZjgxO2iNC4cug0aG283vTtRBQ2kpmaI+zjIfDV/FgdcVMXjtShqkGDSY06cIwMgj3Y1umjmAgJJzt9NEZGHpYwv3VqCB/Ssw5mTEOiL2xhY43rCMuqVF4pBCEOIyVRBmHG2YB6oKZ4OlCT7SSIKReyaxgj2sujMeSTyUZLombNn48AKkgikLgdyRM/cEqP7vvw0/KD6AmJhmf38HpKljUSgbkzDHBb9xhJRQ98EjOd2e6OEw8Be0d3EBDw7oTDWM5cKhUPs8TQjGwpn9gYGFErwLotcMoQKLv7yX5HuCS2d+zWQee0tLqVQQDHPBn284p5ncROORZLqm6x+oiM+ZnhAH1wGa+H55xic/JAQpL6zPlbCiRpNji5OqcjtrK0NKNW1ThG+8mcDtjuLhv8KMV3GPnPrGQOh1vVGCBABRTOvfd4VrJYycndsONgSFgeFeHbN7nWGWTSCdLrtcR6XuTF4XCFvstQpOE08c73MWV9FwvWO0LrAs/1OAq7WaOzSit85axkK44cOAJ4iPkpbMXckQKRD3t+OdoDu0Sr8ELQLIa6IwomMxwRcJfgTC6mTl2dYDRqea+9BvsQDbDVZPIG2aU/R1+PZNJaWSUGNQD9/Uf852mLhkOPh0bw69hr3nnd3yuCIxPO9z+dWOCFNXP4TslIgZMd5+oka3Acl0MphewqZk+to2bSqw+iWmIRalcbYNCUlCSroYioUL0yoJmF++19bxjgOLry8WQwbpAkuQXCTCxCYB+oyUlv6gCb3GDI6Tg5M67cOF2ZYqIiExTJ6jdXimA550PoupMv5/sX82TDIwMZcdMqWz+YK3gBRWSRYO2JlmsQuenujp3ZzBc+bcOwxibcuDlOfaHKjhgzPGrs0pldHKZsqH4ju7oE4SbkeCMZ5hp0KwtzQAm2OyeHSCIw/b6RIpx12i07TF8baiycqatkrfspbxUMp7y6SqscS5uDybYN5mEICQm7Jwxw2tvlK5jnuR/l8kQ3gfPVTBOD+0kNy9ljNCRWh4OJhcbaPZ6c7c4OQMZmSpHZqfxtST46G5jtkbyGY1wZSBMUG0c3sB7hdkhzGkbKytuKHqMLhk9VVDssF6bn5GiFEUE3aE4Hh34Pw9rW+aHzNCVoXV4Tht9Y60F6EvZDU+NGPG3O8a7dqqP+MASj687aXIebXHBW95FENANLI0kvhKwZ/z4Z9+rZPOd9yTaFaLLKUT2Xuw/EWgsiIN8Ys94NBjjR3XwEo1vj03VjKp5XS1DuuoFwKW7R2yYLUe1fF4QAHBlSc1JiG8SWT2F7ONi3UXbkDyHsq8DbKiqteW3PjPBklMFginDqJ4k22jrOyF72OUAWYJP/tRxeVdJgWdRSc4SxIiuxCV5Pbn9BtJBgdzkIpSijbSyhdyhHxu0gYeJZtDOvSvEw2V4Jey5M2LAK5voatFA40/PimJ5epeOwNP5VU90ttMgDTjAM+xwf+OYw5uYhtq1EM3WCBDpxMpl4VRdA3ih42H1At2bFR7DCegGt5UaHlpgOCmwavv73xok9bCm0qEF0v2dW0xMMPhGcCQ3NeBVPw90BgzYLVzxICpDBrbyG5Xm0AyHNFJjQmahxrie+BSTNzieaG7HcguX7Rg/abpTgzUtqd8pYSWvLsd9qoHZBgbEZbhfOMkfhtFY+3BFPe3vAslBsV7lSfuRyG2bXDxfway8PdhNfZNA8qm2hTNA9QL/H21QzJxfGyvgiRYYFgvsmQcbQjI3JKV9hN7ghQUnvwRhdW0nCW4YVcZN2n07GbEI91GiYjgnrF20b+0i+Apw8GEmXWrjNoxDUj2ql6mWM2/MFKz3PDBxUw8fYXtDucnVMxiqXh5/Pu54cggJW2LiOVBrYpOOutoC2lcKoTQLccoKnh843t0bAKAysTYZ02HPruiJgkwj1TrtqEWGKgZq7vmPUt40RmPGtqx/i7w3rKt1Dpqz+cTQ7xobzvlG82HnrTmXdkx/0rgCm8+OtanZr6H/QV5CrgAXuiLlnNNfhaYslUnGDf+lSUu2kkGJlbrpdVE1bBRBSZ9tBqzVUjlTgKShOJ+Osm+5hzaEQhP8K4I44qURm+vH7nuEU/7KwNZjOT4mRlljL3MCWvtQkOgOTpoVnMomGHxbTRiWzC256Tbfg1UQSjvm8UiDFkbvll+w0ROxEH6ZshJgqGHDLQlo0Skp/nwPJiPLop37IJThk0iGBOarU1fyeO0DcaOOAfZf4Siyy8u/gfBOmOJYT+bH4BbNiYmzqMjpBDsWJupmfluLq+DtgTuokoyOdjeU6+oitChieGmJqOgzIcWYPo6RBiDNMkdZ1Lneg4UwIN+zeYeUci/73okMm5zYrSnwCuyUxYr8HA99RPGz8fW6Y97ITdNz8tmLIRFcaBEN/oLQWdhj6VaoJbxpd3Pr5zeEMpqFVjaJLpGq2l7YHyeeFZvCKcH0+mhZ2A+H7skNVGcyDY/D0/zF3etPPSju4oa4icD6QfepCtfH17Jz4bAG+ZZxiiEGabvwSeyHSyAe07tSddzneOW6RfnQRoKYFwR6GlE+YzT56EWQ4nDbIi052aLFNjZey3yocN/t09FzaPEzW+OxY33o24cccS8gIDhIhGuthYWaZwIY+wKAku2Qak6ObdsmLuPrICWomoTwKZBJ1aBo5KESRICFmv75MPjwxbTbAvMUIwEiS8naA6V3GYhw38/xOJ6FGx5l7KoDTLMdNy2DvlfsucGKVRw1ThvcIlmLzCe1EZRAE+rGlmLcPvxhDKmzwupoV9L2JHQJAs+JXUkKXimArQyxd2zvxRHRqhGm6Gt6GesooKz5t4FjGa7HROZ2n9Z1W7IJ89RE618smj2CW26dPQIav7rVVLnCQ8rF5gFFJFDVWbScjR7MC7UqpMhz6GWbXkXUL9zmLNpMM0oIL326x99Q8Lwe4xfDDxJ5nTD+fAkzwDXyLExrQpK0J72YExsZx8sY4u9MHtus7+R8FdtTwBmcVT882ofDBARujMpY1yziUIbVqKvukWoyJts/b7YtibcwcqBM0Zjn/gAGo96jJDEV/iGECslj39jAL6mJYJy9BRo5Ih0EghFDbh55YG8yFFAP80jNmEd1Qd7mZNHvJ9FKEmm3rA7fw8Fkg23Kom7GCRl9iey1tXA+jARszNZ7AGHgpwqDBGWxqaPQ8IPlradjqckly2Cdt0LCHxF6dBfBWxPxD/2AzR06tX14nOiBOP9jTesmaTHieBE0omsYHSKDLJo8xj/fMQZWVMnJ8mCE42VSc4Ob3doOiicUi92I03exR2LPMbqAaEGNj89Xzbk3X0txP+hYrCBdFD4LZc/snd3DZPFR60V8m3yKlQutk2W5PSYMT/7zc286ru0ikuK/Cf9KniY71+GMxPHCHblSYcfNugUvBXB0mhBrdedulkhiIxtWtZ2ZNU0+wF+709ZzMce+LBHko/GiiAvlA9zhSu2gplIgiSmW+1F5PvoCZvNsaThNWiMnkTiBcoq3DaViq6WDcFH9u3/ZgARIShwipOyU31lWyQsxaIh1Z2AzCFaSZDmNCNyLhzfJn9TObO1w/SFxc1PcqlGjBOFowhdpiEDAp/ioiQ83fkxPoD7GnZzzoqvzuUQh6Lhlxdr4amOB6pxSDsOK2wcNIvhsaiGYkhEb1rfuJ6IMZN00Fs2z67u95XzIDK8ps/S93MwLf8tl5lAfPg1PZZnRmc8U4HdwkD6JJ98iugGlTHrzTOWIiwUE/lotu8gDmA4oagmIRjZqbmZ6n92h/5Yv4CkajzDpww+Sh0g2lueMcmkjUOG96UcSJTWLT/RhWRNrKGcMsApKu5zwjxuhJ5bb7Ax3Rzdr9pHFPX6QZXG2EtcWsOOnGhUE7H5guOFTZCLRCSNJZzfA02XEDDEW2wxsJiw1uqGXlFpctUvWW9VjDw0ZIQYJhT2IPK0wL926GgPp/b59vuy42rgEIo0pLzVL79dyCYnUrpZb6QWFctdhwksLSuQYMYxn62l6WoUQBMv7EwHoOD4CAk6wo4Aj9HmkavAqPPOS1l07RDUiwd0Y4uR5YJ9s/XlgkqMpFEGYGNqJJwNPprszITg5aOT0HFcvQPeaM3QDVLRvpFnDDh08SXzCEC5k7RsRPVrokwKSjdv8y6jADEpGCfzrGjGMxElK0rlU78km6i10ErOCoFOeae3JE3a/CsW4F27+yf7iZcwfLq+PhXPjEkNuW4lbJfVVyU08hd8nwWYZZnM5OD6aFy+/VuE4YI7NOwsImi1cFY8jBNrVha4hskbnBpeaKINJPgV43m3RCIz2bvrLM/240N47XNe9XTFkaYjemDgUC+CPl7KaRixGVnNDJPkYq2TJ02Rw7NLaPcDzYK4wu2ACRkwzKoWLdkB3DbZlG6iKk9nFDDrXnwrPSspeh7Wsrlhy6Yn29uKc9kFt+gLUpjI/qDDHcaB02bM8NOdxI3vVwNKiWlTbANwYqOklMYwzjlC5JWENAo4j7HM3k0gaO82aFsJpw70Jzma628cBdR6uncGW2CtuoaILlAbcFNpjtYGOMwXzCosOibGC4h/qWAUrCbpv0ytdpYzdbzQuYC0xs+SAsD3sBdB0TbesGG+bXlQtyj6Czw7ofzKlrRXn05WQIwhXAPhGuHDzZoFtHFnCFxtw3US7hraUZ2OpBCzkbsc80u6sC+BD+t/UAjAeNE3HM24YAoQVzKAyRRS7+eawxtJmRTcQ5Vckttwb3D04bDoMqnjYvCo7W0rcG7snyacsW4d1PIbJFAkpwZnUaaNiwN6NaORAtaXpaFTKxZc9lOKgWjWMLBkOnQrrp5sbQFNFIW7AR1DxCP3NsxcZ+xNG3jKw26D2pn2M3Gz2NBUiLAOCwfwzVD6bRwwyuNPyvGYrkWZcySQT5k3vulUtRDIAGI/D04ObJq7oA7vA67Lu5kT1OIql5Ak7gUvgWKa4gM8ISrEZ3FWCzLeDTnTcwzImAOFnKnHEupSH8lOCCbP2LgQUE024iB7gRnap5up2+axzGoM6iOpEiOCEmu3es/Wd0fKltgGxG9Ok8PLYUDkFsmm+23DMd6RCbCMMtMwndgXjvEouJKQjq8CvkUGoxMYFo4X4u15pWDmE70dtNqpqcY3xK8W0dmDDcibuhOLKatMbGqSCUbI2Q7GsC8wtdF4P3Mwpfqh5JgHDfzfW7v2pK69u0fAPWQBuJidUJsTdbhiRxAsBGxszHkiz0kHWjcwTiYlwJDhb8NYZrwyDHoGsMF4K5ECOU+wvnkc0XcHOJdXkf1aMDfMLpaWm3P97GYdVQeFXThgmMhe/DCjt7klWU0B0GUCtdpvoHZkzLSWCFE0vGGbCCLamhcBRnCZB9fmgdVa7ddZMRTBY1jUwjSvKYy2U68cCyQlybNDo62QvScUC5uSjP5fJdYDO1GBjFV3EBnDq+WwSIzxQP44JTPqU+fWPWiFpcrYeAxhwurAk1YFohcp+a1HYSC/hO9JHPxp4Bv8vZ+sD7GCaXbchXhxZxCabhTz0lD/6n3eOwFq7VrcxjV3RUe8wMWHYzh6j7wC+nPI7ufl1jaDdi7fGUfWB+XZld1mM2rz+PaHKe8daNNLkm4k48mCGjhjvj5EgI0whOsQaonUHX7ALRx3gK9jeeWkSXk2EYX9MnsUIuzzLBKL5MjvT/exZ46tw2SxxezB5Z5odukoesYzpJQRertodfluZ1my3Aek7sKCul0/P6NjsmuTz7xO0EuwTVMMqUzRfUfM0B3O6UMS24sXcYgmEirO2LqptbIz+tfqjRZ6XkcenA4KM7EDPmVkOX+FEUt3A0sbF7yJahllLFAPOeieK+M+bSLh0GOA6RCOaeIzWmhpw5LJLCbMMQ0wg2uCy5KXxrapmfO5RHug7SKIVCbB0rOvgKKdO5bncpRsDU/YzAfGXFa/PsPYwZTq3ARENoRNhZ/9DxFzF9g3WLeeOxmoEjc3ymFAaMMXKoMKfk4LgMzbvNURKzTCHyiC2TRE/0DPSZrE/k9OLIVuozB9E3hjUUm33EGldx+7NcxUakLExUneI9oocOpWhz61M6WOYICwwLqk/f1cWIYWyuoHif+hxK2Lo7L2PMSUZzolAnatrxmxpNM9mokEsNbufWjG39TNEOsXMn3ILvPOgetlvI2FuuDQJ5byQIvozXjgdygm+ZodKbpzPLFX7jqikjdowzO5OXX5darhnnZKaoVveR6VKdFkBO8fTjrEppDavt3WoYTYnsIdj1HI05sYJDf5MbOcDqYtT3iy3v0smAFlg1ED3tofEjg8otoResFLyvJDk0uvK8auiPYzrMnDInNdXRwgx/rNe0AinSDAejzria67bCF8xbkS1ZDpubFbaBdGgn82CHYYCKB2t2dMvexrP7zMd61xVhBANgvTrvrgA+tM87dnMbF+hbRaY6dtRXK3zksQ20SdQaTh75fGrw92mgYIr6zawvIy51PMyshNC1Ab5/iQSlppyMjF1DxcjuY+yt+AVoYa75s+yZujFmWeHjfiDAMEJpgUBD0qsviET6HsSJGSJ1OUgTxHYYBu5C5RpA09PhFLqv05hMMk3GRsBMavu9uPZk7b1xAFIIix7gprvN9bZBBXrmm3DAUdfnp2arlIBzMQdmNffvXH5Gz5a4SqHYMOL7NETljUq2E0U/BNK2zrKvxLdR7yRCJIc+MLksIBgQ4zV19RUibOQ83DFZjA6UPU1tKf0pMYhqP+Umqgr2Y+yoZRRB4JWMABssURwxjaXdlsnwOnRkiCb/0S6c1qm5QL23/Wc3gSQF2EUDUOdnOFsNfe9szT5ixWyZSHQDXBbHHGrtZ3y6ztD1zWmiRJq9Pma/twpYM4kNGKjy/MjDhH5Nh7hLhGaIk3O5ZNKw+RP2g/GQYS3Z0+TXfS8s8E3wbzPL8gG8cCJVtG2Gpt0P6nlycBEpcxBu48WB76CHOk9tee72tgDowxka4ayLvlSSUUuW0ZBExuSqooytSjrsfl8d1lk07QZGoR0weZiHOyox9NRyU6uLtPr4gVuKnZE0AenWVrgUhizkPq6HXNg3TmyjBA2aYPB53KFjdGhhDMgaOjIY5lvd+l/ZVsSyjKIYp9MzPNKSsMCl5hIzrZPkOAy05xCkrWrqxxmJyAt/nohSuEsTt+Piq50F3uGA2FS0L5fzDsXCuuGmS4dwQ3GW7FhvYqqjUUGepEUSN8AxVgdC1QJxYMQjvgfLSSWTPKCCW65usDehEk0jRgBaVjGcJ5DRkyr3xxTLoVjYfMitFWCeC//QLlN7pEGUnJjpztXJTRsSrqdzJtqFMwq0Yk6R4qZJdCwdS491PqwioZhd07/EoVWCh5rBBSVBMA49ukcCbEyVyUDT3uBSJntmZO0JuhqV8GgaFyL8Js9njZFH0m3+V0e5doO1AbDXcVcY4M4BhJsC1jIBtk/ASI6cIufKAPGg7kyxr1IRdHEnHTO0tR2sUWHVScXVxN6JsZPKjg70UB5PGUvdmr0/GTm9e0TLIR4aRC+GLSEc5tipGb1SG3znNBH5XHWKdshwK2KDiLDz0bdY45qJnBHWgJijgEuWIS0zlcdh8M3Q7MDQkpLVnL9bV4kJjdNw32H4oWGeypJTu1A/Nip1Uy3iUycOsFoay6mlVOzUY2b1angzQltTBBzztqWPAyPFjNK/ukfgZgO0K2iYsb2HJmjc5kpilX/IFnLQWBFwjuN1d+rlgKNwOOEP12WyuoHC8JBmDCQFD+QwPgFjQFqs8UEW5pkJlecmwNpU8Q+EsaR+Xrc6ktO0DqHVPwRSdRRPDUpPvQu3WU0AAATVSURBVNccuib22s1eJEa1vpW1UFJIF0O38Ecu80cR91VDNN7uvBfYQZGMKlA8r7vvyDrlzeGIvfBQ8d7Eud0JhO0A0YwZdBgG7vBCPZCTmzogJ6gWAjODrx22dt28x1CkGoiL8aFn1RD4dbMLbPmWaGdZLv8nlc5t4cwHEIFJo6n8l7mFZ5o2F74EaXboEViWyCBuGWAfLpNk0V3lCiBcjR+iQdn0WzK5VBIuCCJJfZXmwnD0R9RtB9ijUbwBAnPyeBLGEWBz8fbJWvSW0nXg3M9W41w0P0APzpKCyNhuYJBIGStp+BqdXImjQ30S26qgiPPBQWo632Dnf08ogr5ix/w8KdOPWIo1Dm8TcOUHKMN+a1KXUu735Rp+fxhgDe/tVt7vEGi+fx5y7YFzq1axDIToDwf6FmDLObCVSqaVtxtlnroyWzJHZXfcV9jiRhaSII0ZmNcCbS22anjwaMXDT+71GpDBZA7ul4M0sMxhNjxSnxJ1dIaYcppG7HhQAHNNsRAdLkzxjHZkjp4ZgoTNpmas/6n8hEk21akRTOuqXa/S9OkiAj4t+qLInrvi6rDSsYtqqVqT2g8Y3vOyoQeq5eL0kQNxKNArPgJMT0dxPSjA1mUqKWbdafvk7qkDnJTGfITY+bG7w5HbYEVqKKIJC5rsQ4UDwnxqQE7Li2W3nE+0+ByxM+tBHGWAobNhh0fVU6kz1eUnJTV83Q0UkFXUApVCPH7KIPybcWB3lJQ9ZYGMXVlyCN/koGxtRYq3dJHqq2g2VdSHh+NJi8hcGSFosgUPUkPl88Qh5w1P+H0OgYGVtm2fd4ada74yY1sm4EFfd/PYV329EFswCzliHIoV65aV42oEzofRL8+/zz/Xfl6rJNzxXFRGeN6bEHpielEbFuQGtscHmF9Wt9GFWvDQk9N0vGF0SqdxAIOdVICXpiNMRgGcWH1YHOa4laJGqOF6cjBnmU2nVlrNhy1WmNrpSd9YsLxzFbFm6FTDGJXh1vyzvjrIPBjSMwEZecrNCDEwzYTt7Y5aJ7AH2puWCGdHX2LGAHaWvrD2GiCYpmf70jdomoFebF601pQBqVQNsUGlimIkzR2RlZ4PLP8fSbTAIR+OLWz7bPoKnspeVleN6mNxbkzldQd5RwXwlusz8LhRuB7AMYaFAH22TdMn3QdjafwYwxRJ3CJ01GjMUHKHKw2bGaTGXWLfmub2AGmbByvVjo2YGNEBdp/ObhYSfnCoOV4cXg7PG/eU7BxicwR+2EewgzVHYHQQo4Ddckq3z1SzAZf7ECJrGahY22HduJZMG0tQsT03LJy6gAepv+6H2MEmZjURHdJxRo7GUDeRYEw8GPV3OC3iEBgglY1mADjorjR2Tp8H4PEZCFZODge2/v3YQFX3MgKPhzZnVhebA39qEcENSaJmo1wHG/bUB88UMIQ95wLGVDiqNyOwaV2xE80t5wv9gxHBYe9WMe2OCjEB7In2alsN5TIGTuB7PHzhOH3EBaQbNzQgfPqouLKXuXlY9fWZ1ZM2npiIjn7OehCB41gZnVrdvLvxeaUbRTKB9MJU7p2IsDCjaCL1TwxPy8+uxo6LRrxUZ8grYQy0bRtz3uYizsDcAtd0LzpBZMYGjN1lyvqosv9lXaJh98AsIXqV/vN/AwfGJ1FgGOsuAAAAAElFTkSuQmCC'; diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-0.html b/dom/html/reftests/toblob-todataurl/toblob-quality-0.html new file mode 100644 index 0000000000..7e7298cb6b --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/toblob-quality-0.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0; + </script> + <script src='sample.js'></script> + <script src='blob.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-100.html b/dom/html/reftests/toblob-todataurl/toblob-quality-100.html new file mode 100644 index 0000000000..34f318e11f --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/toblob-quality-100.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 1; + </script> + <script src='sample.js'></script> + <script src='blob.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-25.html b/dom/html/reftests/toblob-todataurl/toblob-quality-25.html new file mode 100644 index 0000000000..ed4350e6eb --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/toblob-quality-25.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0.25; + </script> + <script src='sample.js'></script> + <script src='blob.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-50.html b/dom/html/reftests/toblob-todataurl/toblob-quality-50.html new file mode 100644 index 0000000000..47e3b684fa --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/toblob-quality-50.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0.50; + </script> + <script src='sample.js'></script> + <script src='blob.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-75.html b/dom/html/reftests/toblob-todataurl/toblob-quality-75.html new file mode 100644 index 0000000000..45ccfe1fe0 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/toblob-quality-75.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0.75; + </script> + <script src='sample.js'></script> + <script src='blob.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-92.html b/dom/html/reftests/toblob-todataurl/toblob-quality-92.html new file mode 100644 index 0000000000..6a7f8788fb --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/toblob-quality-92.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0.92; + </script> + <script src='sample.js'></script> + <script src='blob.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-default.html b/dom/html/reftests/toblob-todataurl/toblob-quality-default.html new file mode 100644 index 0000000000..c5b404744c --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/toblob-quality-default.html @@ -0,0 +1,7 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script src='sample.js'></script> + <script src='blob.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/toblob-quality-undefined.html b/dom/html/reftests/toblob-todataurl/toblob-quality-undefined.html new file mode 100644 index 0000000000..3252900200 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/toblob-quality-undefined.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = undefined; + </script> + <script src='sample.js'></script> + <script src='blob.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-0.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-0.html new file mode 100644 index 0000000000..1d4eb9b7f1 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-0.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0; + </script> + <script src='sample.js'></script> + <script src='dataurl.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-100.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-100.html new file mode 100644 index 0000000000..66b627c13e --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-100.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 1; + </script> + <script src='sample.js'></script> + <script src='dataurl.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-25.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-25.html new file mode 100644 index 0000000000..15237cea8b --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-25.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0.25; + </script> + <script src='sample.js'></script> + <script src='dataurl.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-50.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-50.html new file mode 100644 index 0000000000..93e820e68b --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-50.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0.50; + </script> + <script src='sample.js'></script> + <script src='dataurl.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-75.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-75.html new file mode 100644 index 0000000000..acdc7416f8 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-75.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0.75; + </script> + <script src='sample.js'></script> + <script src='dataurl.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-92.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-92.html new file mode 100644 index 0000000000..ca3de4ee0b --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-92.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = 0.92; + </script> + <script src='sample.js'></script> + <script src='dataurl.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-default.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-default.html new file mode 100644 index 0000000000..cc7771dbf0 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-default.html @@ -0,0 +1,7 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script src='sample.js'></script> + <script src='dataurl.js'></script> + </body> +</html> diff --git a/dom/html/reftests/toblob-todataurl/todataurl-quality-undefined.html b/dom/html/reftests/toblob-todataurl/todataurl-quality-undefined.html new file mode 100644 index 0000000000..16801e4829 --- /dev/null +++ b/dom/html/reftests/toblob-todataurl/todataurl-quality-undefined.html @@ -0,0 +1,10 @@ +<!doctype html> +<html class='reftest-wait'> + <body> + <script> + var quality = undefined; + </script> + <script src='sample.js'></script> + <script src='dataurl.js'></script> + </body> +</html> diff --git a/dom/html/reftests/video_rotated.mp4 b/dom/html/reftests/video_rotated.mp4 Binary files differnew file mode 100644 index 0000000000..38a1b77f93 --- /dev/null +++ b/dom/html/reftests/video_rotated.mp4 diff --git a/dom/html/reftests/video_rotation_90.mp4 b/dom/html/reftests/video_rotation_90.mp4 Binary files differnew file mode 100644 index 0000000000..85aa055fb9 --- /dev/null +++ b/dom/html/reftests/video_rotation_90.mp4 diff --git a/dom/html/test/347174transform.xsl b/dom/html/test/347174transform.xsl new file mode 100644 index 0000000000..1b201de3f3 --- /dev/null +++ b/dom/html/test/347174transform.xsl @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> + +<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> +<xsl:template match="/"> +<html> +<head> +<script> +window.parent.frameScriptTag(document.readyState); + +function attachCustomEventListener(element, eventName, command) { + if (window.addEventListener && !window.opera) + element.addEventListener(eventName, command, true); + else if (window.attachEvent) + element.attachEvent("on" + eventName, command); +} + +function load() { + window.parent.frameLoad(document.readyState); +} + +function readyStateChange() { + window.parent.frameReadyStateChange(document.readyState); +} + +function DOMContentLoaded() { + window.parent.frameDOMContentLoaded(document.readyState); +} + +window.onload=load; + +attachCustomEventListener(document, "readystatechange", readyStateChange); +attachCustomEventListener(document, "DOMContentLoaded", DOMContentLoaded); + +</script> +</head> +<body> +</body> +</html> +</xsl:template> + +</xsl:stylesheet>
\ No newline at end of file diff --git a/dom/html/test/347174transformable.xml b/dom/html/test/347174transformable.xml new file mode 100644 index 0000000000..68f7bc6dca --- /dev/null +++ b/dom/html/test/347174transformable.xml @@ -0,0 +1,3 @@ +<?xml version='1.0'?> +<?xml-stylesheet type="text/xsl" href="347174transform.xsl"?> +<doc>This is a sample document.</doc> diff --git a/dom/html/test/allowMedia.sjs b/dom/html/test/allowMedia.sjs new file mode 100644 index 0000000000..f29619cd89 --- /dev/null +++ b/dom/html/test/allowMedia.sjs @@ -0,0 +1,12 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +function handleRequest(req, resp) { + resp.setHeader("Cache-Control", "no-cache", false); + resp.setHeader("Content-Type", "text/plain", false); + + let stateKey = "allowMediaState"; + let state = getState(stateKey); + setState(stateKey, req.queryString ? "FAIL" : ""); + resp.write(state || "PASS"); +} diff --git a/dom/html/test/browser.toml b/dom/html/test/browser.toml new file mode 100644 index 0000000000..13775f607b --- /dev/null +++ b/dom/html/test/browser.toml @@ -0,0 +1,46 @@ +[DEFAULT] +support-files = [ + "bug592641_img.jpg", + "dummy_page.html", + "image.png", + "submission_flush.html", + "post_action_page.html", + "form_data_file.bin", + "form_data_file.txt", + "form_submit_server.sjs", + "head.js", +] + +["browser_DOMDocElementInserted.js"] +skip-if = ["bits == 64 && (os == 'mac' || os == 'linux')"] #Bug 1646862 + +["browser_ImageDocument_svg_zoom.js"] + +["browser_bug436200.js"] +support-files = ["bug436200.html"] + +["browser_bug592641.js"] + +["browser_bug1081537.js"] + +["browser_bug1108547.js"] +support-files = [ + "file_bug1108547-1.html", + "file_bug1108547-2.html", + "file_bug1108547-3.html", +] + +["browser_containerLoadingContent.js"] + +["browser_form_post_from_file_to_http.js"] + +["browser_refresh_after_document_write.js"] +support-files = ["file_refresh_after_document_write.html"] + +["browser_submission_flush.js"] + +["browser_targetBlankNoOpener.js"] +support-files = [ + "empty.html", + "image_yellow.png", +] diff --git a/dom/html/test/browser_DOMDocElementInserted.js b/dom/html/test/browser_DOMDocElementInserted.js new file mode 100644 index 0000000000..fb2b2ae63b --- /dev/null +++ b/dom/html/test/browser_DOMDocElementInserted.js @@ -0,0 +1,23 @@ +// Tests that the DOMDocElementInserted event is visible on the frame +add_task(async function () { + let tab = BrowserTestUtils.addTab(gBrowser); + let uri = "data:text/html;charset=utf-8,<html/>"; + + let eventPromise = ContentTask.spawn(tab.linkedBrowser, null, function () { + return new Promise(resolve => { + addEventListener( + "DOMDocElementInserted", + event => resolve(event.target.documentURIObject.spec), + { + once: true, + } + ); + }); + }); + + BrowserTestUtils.startLoadingURIString(tab.linkedBrowser, uri); + let loadedURI = await eventPromise; + is(loadedURI, uri, "Should have seen the event for the right URI"); + + gBrowser.removeTab(tab); +}); diff --git a/dom/html/test/browser_ImageDocument_svg_zoom.js b/dom/html/test/browser_ImageDocument_svg_zoom.js new file mode 100644 index 0000000000..f0df2282a3 --- /dev/null +++ b/dom/html/test/browser_ImageDocument_svg_zoom.js @@ -0,0 +1,39 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const URL = `data:image/svg+xml,<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><rect width="100" height="100" fill="green"/></svg>`; + +function test_once() { + return BrowserTestUtils.withNewTab(URL, async browser => { + return await SpecialPowers.spawn(browser, [], async function () { + const rect = content.document.documentElement.getBoundingClientRect(); + info( + `${rect.width}x${rect.height}, ${content.innerWidth}x${content.innerHeight}` + ); + is( + Math.round(rect.height), + content.innerHeight, + "Should fill the viewport and not overflow" + ); + }); + }); +} + +add_task(async function test_with_no_text_zoom() { + await test_once(); +}); + +add_task(async function test_with_text_zoom() { + let dpi = window.devicePixelRatio; + + await SpecialPowers.pushPrefEnv({ set: [["ui.textScaleFactor", 200]] }); + Assert.greater( + window.devicePixelRatio, + dpi, + "DPI should change as a result of the pref flip" + ); + + return test_once(); +}); diff --git a/dom/html/test/browser_bug1081537.js b/dom/html/test/browser_bug1081537.js new file mode 100644 index 0000000000..2a079be2f7 --- /dev/null +++ b/dom/html/test/browser_bug1081537.js @@ -0,0 +1,11 @@ +// This test is useful because mochitest-browser runs as an addon, so we test +// addon-scope paths here. +var ifr; +function test() { + ifr = document.createXULElement("iframe"); + document.getElementById("main-window").appendChild(ifr); + is(ifr.contentDocument.nodePrincipal.origin, "[System Principal]"); + ifr.contentDocument.open(); + ok(true, "Didn't throw"); +} +registerCleanupFunction(() => ifr.remove()); diff --git a/dom/html/test/browser_bug1108547.js b/dom/html/test/browser_bug1108547.js new file mode 100644 index 0000000000..4949827086 --- /dev/null +++ b/dom/html/test/browser_bug1108547.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +requestLongerTimeout(2); + +function test() { + waitForExplicitFinish(); + + runPass("file_bug1108547-2.html", function () { + runPass("file_bug1108547-3.html", function () { + finish(); + }); + }); +} + +function runPass(getterFile, finishedCallback) { + var rootDir = "http://mochi.test:8888/browser/dom/html/test/"; + var testBrowser; + var privateWin; + + function whenDelayedStartupFinished(win, callback) { + let topic = "browser-delayed-startup-finished"; + Services.obs.addObserver(function onStartup(aSubject) { + if (win != aSubject) { + return; + } + + Services.obs.removeObserver(onStartup, topic); + executeSoon(callback); + }, topic); + } + + // First, set the cookie in a normal window. + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + rootDir + "file_bug1108547-1.html" + ); + BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then( + afterOpenCookieSetter + ); + + function afterOpenCookieSetter() { + gBrowser.removeCurrentTab(); + + // Now, open a private window. + privateWin = OpenBrowserWindow({ private: true }); + whenDelayedStartupFinished(privateWin, afterPrivateWindowOpened); + } + + function afterPrivateWindowOpened() { + // In the private window, open the getter file, and wait for a new tab to be opened. + privateWin.gBrowser.selectedTab = BrowserTestUtils.addTab( + privateWin.gBrowser, + rootDir + getterFile + ); + testBrowser = privateWin.gBrowser.selectedBrowser; + privateWin.gBrowser.tabContainer.addEventListener( + "TabOpen", + onNewTabOpened, + true + ); + } + + function fetchResult() { + return SpecialPowers.spawn(testBrowser, [], function () { + return content.document.getElementById("result").textContent; + }); + } + + function onNewTabOpened() { + // When the new tab is opened, wait for it to load. + privateWin.gBrowser.tabContainer.removeEventListener( + "TabOpen", + onNewTabOpened, + true + ); + BrowserTestUtils.browserLoaded( + privateWin.gBrowser.tabs[privateWin.gBrowser.tabs.length - 1] + .linkedBrowser + ) + .then(fetchResult) + .then(onNewTabLoaded); + } + + function onNewTabLoaded(result) { + // Now, ensure that the private tab doesn't have access to the cookie set in normal mode. + is(result, "", "Shouldn't have access to the cookies"); + + // We're done with the private window, close it. + privateWin.close(); + + // Clear all cookies. + Cc["@mozilla.org/cookiemanager;1"] + .getService(Ci.nsICookieManager) + .removeAll(); + + // Open a new private window, this time to set a cookie inside it. + privateWin = OpenBrowserWindow({ private: true }); + whenDelayedStartupFinished(privateWin, afterPrivateWindowOpened2); + } + + function afterPrivateWindowOpened2() { + // In the private window, open the setter file, and wait for it to load. + privateWin.gBrowser.selectedTab = BrowserTestUtils.addTab( + privateWin.gBrowser, + rootDir + "file_bug1108547-1.html" + ); + BrowserTestUtils.browserLoaded(privateWin.gBrowser.selectedBrowser).then( + afterOpenCookieSetter2 + ); + } + + function afterOpenCookieSetter2() { + // We're done with the private window now, close it. + privateWin.close(); + + // Now try to read the cookie in a normal window, and wait for a new tab to be opened. + gBrowser.selectedTab = BrowserTestUtils.addTab( + gBrowser, + rootDir + getterFile + ); + testBrowser = gBrowser.selectedBrowser; + gBrowser.tabContainer.addEventListener("TabOpen", onNewTabOpened2, true); + } + + function onNewTabOpened2() { + // When the new tab is opened, wait for it to load. + gBrowser.tabContainer.removeEventListener("TabOpen", onNewTabOpened2, true); + BrowserTestUtils.browserLoaded( + gBrowser.tabs[gBrowser.tabs.length - 1].linkedBrowser + ) + .then(fetchResult) + .then(onNewTabLoaded2); + } + + function onNewTabLoaded2(result) { + // Now, ensure that the normal tab doesn't have access to the cookie set in private mode. + is(result, "", "Shouldn't have access to the cookies"); + + // Remove both of the tabs opened here. + gBrowser.removeCurrentTab(); + gBrowser.removeCurrentTab(); + + privateWin = null; + testBrowser = null; + + finishedCallback(); + } +} diff --git a/dom/html/test/browser_bug436200.js b/dom/html/test/browser_bug436200.js new file mode 100644 index 0000000000..7e739c02ad --- /dev/null +++ b/dom/html/test/browser_bug436200.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const kTestPage = "https://example.org/browser/dom/html/test/bug436200.html"; + +async function run_test(shouldShowPrompt, msg) { + let promptShown = false; + + function tabModalObserver(subject) { + promptShown = true; + subject.querySelector(".tabmodalprompt-button0").click(); + } + Services.obs.addObserver(tabModalObserver, "tabmodal-dialog-loaded"); + + function commonDialogObserver(subject) { + let dialog = subject.Dialog; + promptShown = true; + dialog.ui.button0.click(); + } + Services.obs.addObserver(commonDialogObserver, "common-dialog-loaded"); + + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kTestPage); + + await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () { + let form = content.document.getElementById("test_form"); + form.submit(); + }); + Services.obs.removeObserver(tabModalObserver, "tabmodal-dialog-loaded"); + Services.obs.removeObserver(commonDialogObserver, "common-dialog-loaded"); + + is(promptShown, shouldShowPrompt, msg); + BrowserTestUtils.removeTab(tab); +} + +add_task(async function test_prompt() { + await run_test(true, "Should show prompt"); +}); + +add_task(async function test_noprompt() { + await SpecialPowers.pushPrefEnv({ + set: [["security.warn_submit_secure_to_insecure", false]], + }); + await run_test(false, "Should not show prompt"); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_prompt_modal() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ + "prompts.modalType.insecureFormSubmit", + Services.prompt.MODAL_TYPE_WINDOW, + ], + ], + }); + await run_test(true, "Should show prompt"); + await SpecialPowers.popPrefEnv(); +}); diff --git a/dom/html/test/browser_bug592641.js b/dom/html/test/browser_bug592641.js new file mode 100644 index 0000000000..761af6a568 --- /dev/null +++ b/dom/html/test/browser_bug592641.js @@ -0,0 +1,61 @@ +// Test for bug 592641 - Image document doesn't show dimensions of cached images + +// Globals +var testPath = "http://mochi.test:8888/browser/dom/html/test/"; +var ctx = { loadsDone: 0 }; + +// Entry point from Mochikit +function test() { + waitForExplicitFinish(); + + ctx.tab1 = BrowserTestUtils.addTab(gBrowser, testPath + "bug592641_img.jpg"); + ctx.tab1Browser = gBrowser.getBrowserForTab(ctx.tab1); + BrowserTestUtils.browserLoaded(ctx.tab1Browser).then(load1Soon); +} + +function checkTitle(title) { + ctx.loadsDone++; + ok( + /^bug592641_img\.jpg \(JPEG Image, 1500\u00A0\u00D7\u00A01500 pixels\)/.test( + title + ), + "Title should be correct on load #" + ctx.loadsDone + ", was: " + title + ); +} + +function load1Soon() { + // onload is fired in OnStopDecode, so let's use executeSoon() to make sure + // that any other OnStopDecode event handlers get the chance to fire first. + executeSoon(load1Done); +} + +function load1Done() { + // Check the title + var title = ctx.tab1Browser.contentTitle; + checkTitle(title); + + // Try loading the same image in a new tab to make sure things work in + // the cached case. + ctx.tab2 = BrowserTestUtils.addTab(gBrowser, testPath + "bug592641_img.jpg"); + ctx.tab2Browser = gBrowser.getBrowserForTab(ctx.tab2); + BrowserTestUtils.browserLoaded(ctx.tab2Browser).then(load2Soon); +} + +function load2Soon() { + // onload is fired in OnStopDecode, so let's use executeSoon() to make sure + // that any other OnStopDecode event handlers get the chance to fire first. + executeSoon(load2Done); +} + +function load2Done() { + // Check the title + var title = ctx.tab2Browser.contentTitle; + checkTitle(title); + + // Clean up + gBrowser.removeTab(ctx.tab1); + gBrowser.removeTab(ctx.tab2); + + // Test done + finish(); +} diff --git a/dom/html/test/browser_containerLoadingContent.js b/dom/html/test/browser_containerLoadingContent.js new file mode 100644 index 0000000000..4fb10db614 --- /dev/null +++ b/dom/html/test/browser_containerLoadingContent.js @@ -0,0 +1,108 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const DIRPATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "" +); + +const ORIGIN = "https://example.com"; +const CROSSORIGIN = "https://example.org"; + +const TABURL = `${ORIGIN}/${DIRPATH}dummy_page.html`; + +const IMAGEURL = `${ORIGIN}/${DIRPATH}image.png`; +const CROSSIMAGEURL = `${CROSSORIGIN}/${DIRPATH}image.png`; + +const DOCUMENTURL = `${ORIGIN}/${DIRPATH}dummy_page.html`; +const CROSSDOCUMENTURL = `${CROSSORIGIN}/${DIRPATH}dummy_page.html`; + +function getPids(browser) { + return browser.browsingContext.children.map( + child => child.currentWindowContext.osPid + ); +} + +async function runTest(spec, tabUrl, imageurl, crossimageurl, check) { + await BrowserTestUtils.withNewTab(tabUrl, async browser => { + await SpecialPowers.spawn( + browser, + [spec, imageurl, crossimageurl], + async ({ element, attribute }, url1, url2) => { + for (let url of [url1, url2]) { + const object = content.document.createElement(element); + object[attribute] = url; + const onloadPromise = new Promise(res => { + object.onload = res; + }); + content.document.body.appendChild(object); + await onloadPromise; + } + } + ); + + await check(browser); + }); +} + +let iframe = { element: "iframe", attribute: "src" }; +let embed = { element: "embed", attribute: "src" }; +let object = { element: "object", attribute: "data" }; + +async function checkImage(browser) { + let pids = getPids(browser); + is(pids.length, 2, "There should be two browsing contexts"); + ok(pids[0], "The first pid should have a sane value"); + ok(pids[1], "The second pid should have a sane value"); + isnot(pids[0], pids[1], "The two pids should be different"); + + let images = []; + for (let context of browser.browsingContext.children) { + images.push( + await SpecialPowers.spawn(context, [], async () => { + let img = new URL(content.document.querySelector("img").src); + is( + `${img.protocol}//${img.host}`, + `${content.location.protocol}//${content.location.host}`, + "Images should be loaded in the same domain as the document" + ); + return img.href; + }) + ); + } + isnot(images[0], images[1], "The images should have different sources"); +} + +function checkDocument(browser) { + let pids = getPids(browser); + is(pids.length, 2, "There should be two browsing contexts"); + ok(pids[0], "The first pid should have a sane value"); + ok(pids[1], "The second pid should have a sane value"); + isnot(pids[0], pids[1], "The two pids should be different"); +} + +add_task(async function test_iframeImageDocument() { + await runTest(iframe, TABURL, IMAGEURL, CROSSIMAGEURL, checkImage); +}); + +add_task(async function test_embedImageDocument() { + await runTest(embed, TABURL, IMAGEURL, CROSSIMAGEURL, checkImage); +}); + +add_task(async function test_objectImageDocument() { + await runTest(object, TABURL, IMAGEURL, CROSSIMAGEURL, checkImage); +}); + +add_task(async function test_iframeDocument() { + await runTest(iframe, TABURL, DOCUMENTURL, CROSSDOCUMENTURL, checkDocument); +}); + +add_task(async function test_embedDocument() { + await runTest(embed, TABURL, DOCUMENTURL, CROSSDOCUMENTURL, checkDocument); +}); + +add_task(async function test_objectDocument() { + await runTest(object, TABURL, DOCUMENTURL, CROSSDOCUMENTURL, checkDocument); +}); diff --git a/dom/html/test/browser_form_post_from_file_to_http.js b/dom/html/test/browser_form_post_from_file_to_http.js new file mode 100644 index 0000000000..e62912bdcd --- /dev/null +++ b/dom/html/test/browser_form_post_from_file_to_http.js @@ -0,0 +1,181 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ + +const TEST_HTTP_POST = + "http://example.org/browser/dom/html/test/form_submit_server.sjs"; + +// Test for bug 1351358. +async function runTest(doNewTab) { + // Create file URI and test data file paths. + let testFile = getChromeDir(getResolvedURI(gTestPath)); + testFile.append("dummy_page.html"); + const fileUriString = Services.io.newFileURI(testFile).spec; + let filePaths = []; + testFile.leafName = "form_data_file.txt"; + filePaths.push(testFile.path); + testFile.leafName = "form_data_file.bin"; + filePaths.push(testFile.path); + + // Open file:// page tab in which to run the test. + await BrowserTestUtils.withNewTab( + fileUriString, + async function (fileBrowser) { + // Create a form to post to server that writes posted data into body as JSON. + + var promiseLoad; + if (doNewTab) { + promiseLoad = BrowserTestUtils.waitForNewTab( + gBrowser, + TEST_HTTP_POST, + true, + false + ); + } else { + promiseLoad = BrowserTestUtils.browserLoaded( + fileBrowser, + false, + TEST_HTTP_POST + ); + } + + /* eslint-disable no-shadow */ + await SpecialPowers.spawn( + fileBrowser, + [TEST_HTTP_POST, filePaths, doNewTab], + (actionUri, filePaths, doNewTab) => { + // eslint-disable-next-line mozilla/reject-importGlobalProperties + Cu.importGlobalProperties(["File"]); + + let doc = content.document; + let form = doc.body.appendChild(doc.createElement("form")); + form.action = actionUri; + form.method = "POST"; + form.enctype = "multipart/form-data"; + if (doNewTab) { + form.target = "_blank"; + } + + let inputText = form.appendChild(doc.createElement("input")); + inputText.type = "text"; + inputText.name = "text"; + inputText.value = "posted"; + + let inputCheckboxOn = form.appendChild(doc.createElement("input")); + inputCheckboxOn.type = "checkbox"; + inputCheckboxOn.name = "checked"; + inputCheckboxOn.checked = true; + + let inputCheckboxOff = form.appendChild(doc.createElement("input")); + inputCheckboxOff.type = "checkbox"; + inputCheckboxOff.name = "unchecked"; + inputCheckboxOff.checked = false; + + let inputFile = form.appendChild(doc.createElement("input")); + inputFile.type = "file"; + inputFile.name = "file"; + let fileList = []; + let promises = []; + for (let path of filePaths) { + promises.push( + File.createFromFileName(path).then(file => { + fileList.push(file); + }) + ); + } + + Promise.all(promises).then(() => { + inputFile.mozSetFileArray(fileList); + form.submit(); + }); + } + ); + /* eslint-enable no-shadow */ + + var href; + var testBrowser; + var newTab; + if (doNewTab) { + newTab = await promiseLoad; + testBrowser = newTab.linkedBrowser; + href = testBrowser.currentURI.spec; + } else { + testBrowser = fileBrowser; + href = await promiseLoad; + } + is( + href, + TEST_HTTP_POST, + "Check that the loaded page is the one to which we posted." + ); + + let binContentType; + if (AppConstants.platform == "macosx") { + binContentType = "application/macbinary"; + } else { + binContentType = "application/octet-stream"; + } + + /* eslint-disable no-shadow */ + await SpecialPowers.spawn( + testBrowser, + [binContentType], + binContentType => { + let data = JSON.parse(content.document.body.textContent); + is( + data[0].headers["Content-Disposition"], + 'form-data; name="text"', + "Check text input Content-Disposition" + ); + is(data[0].body, "posted", "Check text input body"); + + is( + data[1].headers["Content-Disposition"], + 'form-data; name="checked"', + "Check checkbox input Content-Disposition" + ); + is(data[1].body, "on", "Check checkbox input body"); + + // Note that unchecked checkbox details are not sent. + + is( + data[2].headers["Content-Disposition"], + 'form-data; name="file"; filename="form_data_file.txt"', + "Check text file input Content-Disposition" + ); + is( + data[2].headers["Content-Type"], + "text/plain", + "Check text file input Content-Type" + ); + is(data[2].body, "1234\n", "Check text file input body"); + + is( + data[3].headers["Content-Disposition"], + 'form-data; name="file"; filename="form_data_file.bin"', + "Check binary file input Content-Disposition" + ); + is( + data[3].headers["Content-Type"], + binContentType, + "Check binary file input Content-Type" + ); + is( + data[3].body, + "\u0001\u0002\u0003\u0004\n", + "Check binary file input body" + ); + } + ); + /* eslint-enable no-shadow */ + + if (newTab) { + BrowserTestUtils.removeTab(newTab); + } + } + ); +} + +add_task(async function runWithDocumentChannel() { + await runTest(false); + await runTest(true); +}); diff --git a/dom/html/test/browser_refresh_after_document_write.js b/dom/html/test/browser_refresh_after_document_write.js new file mode 100644 index 0000000000..88e0dbe489 --- /dev/null +++ b/dom/html/test/browser_refresh_after_document_write.js @@ -0,0 +1,52 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* +Test that after using document.write(...), refreshing the document and calling write again, +resulting document.URL is identical to the original URL. + +This testcase is aimed at preventing bug 619092 +*/ +var testURL = + "http://mochi.test:8888/browser/dom/html/test/file_refresh_after_document_write.html"; +let aTab, aBrowser; + +function test() { + waitForExplicitFinish(); + + aTab = BrowserTestUtils.addTab(gBrowser, testURL); + aBrowser = gBrowser.getBrowserForTab(aTab); + BrowserTestUtils.browserLoaded(aBrowser) + .then(() => { + is( + aBrowser.currentURI.spec, + testURL, + "Make sure we start at the correct URL" + ); + + SpecialPowers.spawn(aBrowser, [], () => { + // test_btn calls document.write() then reloads the document + let test_btn = content.document.getElementById("test_btn"); + + docShell.chromeEventHandler.addEventListener( + "load", + () => { + test_btn.click(); + }, + { once: true, capture: true } + ); + + test_btn.click(); + }); + + return BrowserTestUtils.browserLoaded(aBrowser); + }) + .then(() => { + return SpecialPowers.spawn(aBrowser, [], () => content.document.URL); + }) + .then(url => { + is(url, testURL, "Document URL should be identical after reload"); + gBrowser.removeTab(aTab); + finish(); + }); +} diff --git a/dom/html/test/browser_submission_flush.js b/dom/html/test/browser_submission_flush.js new file mode 100644 index 0000000000..add886c6a3 --- /dev/null +++ b/dom/html/test/browser_submission_flush.js @@ -0,0 +1,97 @@ +"use strict"; +// Form submissions triggered by the Javascript 'submit' event listener are +// deferred until the event listener finishes. However, changes to specific +// attributes ("action" and "target" attributes) need to cause an immediate +// flush of any pending submission to prevent the form submission from using the +// wrong action or target. This test ensures that such flushes happen properly. + +const kTestPage = + "https://example.org/browser/dom/html/test/submission_flush.html"; +// This is the page pointed to by the form action in the test HTML page. +const kPostActionPage = + "https://example.org/browser/dom/html/test/post_action_page.html"; + +const kFormId = "test_form"; +const kFrameId = "test_frame"; +const kSubmitButtonId = "submit_button"; + +// Take in a variety of actions (in the form of setting and unsetting form +// attributes). Then submit the form in the submit event listener to cause a +// deferred form submission. Then perform the test actions and ensure that the +// form used the correct attribute values rather than the changed ones. +async function runTest(aTestActions) { + let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, kTestPage); + registerCleanupFunction(() => BrowserTestUtils.removeTab(tab)); + + /* eslint-disable no-shadow */ + let frame_url = await SpecialPowers.spawn( + gBrowser.selectedBrowser, + [{ kFormId, kFrameId, kSubmitButtonId, aTestActions }], + async function ({ kFormId, kFrameId, kSubmitButtonId, aTestActions }) { + let form = content.document.getElementById(kFormId); + + form.addEventListener( + "submit", + event => { + // Need to trigger the deferred submission by submitting in the submit + // event handler. To prevent the form from being submitted twice, the + // <form> tag contains the attribute |onsubmit="return false;"| to cancel + // the original submission. + form.submit(); + + if (aTestActions.setattr) { + for (let { attr, value } of aTestActions.setattr) { + form.setAttribute(attr, value); + } + } + if (aTestActions.unsetattr) { + for (let attr of aTestActions.unsetattr) { + form.removeAttribute(attr); + } + } + }, + { capture: true, once: true } + ); + + // Trigger the above event listener + content.document.getElementById(kSubmitButtonId).click(); + + // Test that the form was submitted to the correct target (the frame) with + // the correct action (kPostActionPage). + let frame = content.document.getElementById(kFrameId); + await new Promise(resolve => { + frame.addEventListener("load", resolve, { once: true }); + }); + return frame.contentWindow.location.href; + } + ); + /* eslint-enable no-shadow */ + is( + frame_url, + kPostActionPage, + "Form should have submitted with correct target and action" + ); +} + +add_task(async function () { + info("Changing action should flush pending submissions"); + await runTest({ setattr: [{ attr: "action", value: "about:blank" }] }); +}); + +add_task(async function () { + info("Changing target should flush pending submissions"); + await runTest({ setattr: [{ attr: "target", value: "_blank" }] }); +}); + +add_task(async function () { + info("Unsetting action should flush pending submissions"); + await runTest({ unsetattr: ["action"] }); +}); + +// On failure, this test will time out rather than failing an assert. When the +// target attribute is not set, the form will submit the active page, navigating +// it away and preventing the wait for iframe load from ever finishing. +add_task(async function () { + info("Unsetting target should flush pending submissions"); + await runTest({ unsetattr: ["target"] }); +}); diff --git a/dom/html/test/browser_targetBlankNoOpener.js b/dom/html/test/browser_targetBlankNoOpener.js new file mode 100644 index 0000000000..1647df0be2 --- /dev/null +++ b/dom/html/test/browser_targetBlankNoOpener.js @@ -0,0 +1,121 @@ +const TEST_URL = "http://mochi.test:8888/browser/dom/html/test/empty.html"; + +async function checkOpener(browser, elm, name, rel) { + let p = BrowserTestUtils.waitForNewTab(gBrowser, null, true, true); + + await SpecialPowers.spawn( + browser, + [{ url: TEST_URL, name, rel, elm }], + async obj => { + let element; + + if (obj.elm == "anchor") { + element = content.document.createElement("a"); + content.document.body.appendChild(element); + element.appendChild(content.document.createTextNode(obj.name)); + } else { + let img = content.document.createElement("img"); + img.src = "image_yellow.png"; + content.document.body.appendChild(img); + + element = content.document.createElement("area"); + img.appendChild(element); + + element.setAttribute("shape", "rect"); + element.setAttribute("coords", "0,0,100,100"); + } + + element.setAttribute("target", "_blank"); + element.setAttribute("href", obj.url); + + if (obj.rel) { + element.setAttribute("rel", obj.rel); + } + + element.click(); + } + ); + + let newTab = await p; + let newBrowser = gBrowser.getBrowserForTab(newTab); + + let hasOpener = await SpecialPowers.spawn( + newTab.linkedBrowser, + [], + _ => !!content.window.opener + ); + + BrowserTestUtils.removeTab(newTab); + return hasOpener; +} + +async function runTests(browser, elm) { + info("Creating an " + elm + " with target=_blank rel=opener"); + ok( + !!(await checkOpener(browser, elm, "rel=opener", "opener")), + "We want the opener with rel=opener" + ); + + info("Creating an " + elm + " with target=_blank rel=noopener"); + ok( + !(await checkOpener(browser, elm, "rel=noopener", "noopener")), + "We don't want the opener with rel=noopener" + ); + + info("Creating an " + elm + " with target=_blank"); + ok( + !(await checkOpener(browser, elm, "no rel", null)), + "We don't want the opener with no rel is passed" + ); + + info("Creating an " + elm + " with target=_blank rel='noopener opener'"); + ok( + !(await checkOpener( + browser, + elm, + "rel=noopener+opener", + "noopener opener" + )), + "noopener wins with rel=noopener+opener" + ); + + info("Creating an " + elm + " with target=_blank rel='noreferrer opener'"); + ok( + !(await checkOpener(browser, elm, "noreferrer wins", "noreferrer opener")), + "We don't want the opener with rel=noreferrer+opener" + ); + + info("Creating an " + elm + " with target=_blank rel='opener noreferrer'"); + ok( + !(await checkOpener( + browser, + elm, + "noreferrer wins again", + "noreferrer opener" + )), + "We don't want the opener with rel=opener+noreferrer" + ); +} + +add_task(async _ => { + await SpecialPowers.flushPrefEnv(); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.block_multiple_popups", false], + ["dom.disable_open_during_load", true], + ["dom.targetBlankNoOpener.enabled", true], + ], + }); + + let tab = BrowserTestUtils.addTab(gBrowser, TEST_URL); + gBrowser.selectedTab = tab; + + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await runTests(browser, "anchor"); + await runTests(browser, "area"); + + info("Removing the tab"); + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/html/test/bug100533_iframe.html b/dom/html/test/bug100533_iframe.html new file mode 100644 index 0000000000..ddf58a15c6 --- /dev/null +++ b/dom/html/test/bug100533_iframe.html @@ -0,0 +1,8 @@ +<html> +<head> +<title></title> +</head> +<body> +<form method='get' action='bug100533_load.html' id='b'><input type="submit"/></form> +</body> +</html> diff --git a/dom/html/test/bug100533_load.html b/dom/html/test/bug100533_load.html new file mode 100644 index 0000000000..99cf26640c --- /dev/null +++ b/dom/html/test/bug100533_load.html @@ -0,0 +1,14 @@ +<html> +<head> +<title></title> +</head> + + +<body onload="parent.submitted();"> + +<span id="foo"></span> + + + +</body> +</html> diff --git a/dom/html/test/bug1260704_iframe.html b/dom/html/test/bug1260704_iframe.html new file mode 100644 index 0000000000..695dc7c1ac --- /dev/null +++ b/dom/html/test/bug1260704_iframe.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript"> + var noDefault = (location.search.includes("noDefault=true")); + var isMap = (location.search.includes("isMap=true")); + + window.addEventListener("load", () => { + let image = document.getElementById("testImage"); + isMap ? image.setAttribute("ismap", "") : image.removeAttribute("ismap"); + image.addEventListener("click", event => { + if (noDefault) { + ok(true, "image element prevents default"); + event.preventDefault(); + } + }); + + window.addEventListener("click", event => { + ok(true, "expected prevent default = " + noDefault); + ok(true, "actual prevent default = " + event.defaultPrevented); + ok(event.defaultPrevented == noDefault, "PreventDefault should work fine"); + if (noDefault) { + window.parent.postMessage("finished", "http://mochi.test:8888"); + } + }); + window.parent.postMessage("started", "http://mochi.test:8888"); + }); + </script> +</head> +<body> +<a href="bug1260704_iframe_empty.html"> + <img id="testImage" src="file_bug1260704.png" width="100" height="100"/> +</a> +</body> +</html> diff --git a/dom/html/test/bug1260704_iframe_empty.html b/dom/html/test/bug1260704_iframe_empty.html new file mode 100644 index 0000000000..e826b1e5e6 --- /dev/null +++ b/dom/html/test/bug1260704_iframe_empty.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript"> + window.addEventListener("load", () => { + window.parent.postMessage("empty_frame_loaded", "http://mochi.test:8888"); + }); + </script> +</head> +<body> +</body> +</html> diff --git a/dom/html/test/bug1292522_iframe.html b/dom/html/test/bug1292522_iframe.html new file mode 100644 index 0000000000..99a3369d00 --- /dev/null +++ b/dom/html/test/bug1292522_iframe.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html><head><title>iframe</title></head> + <body> + <p>var testvar = "testiframe"</p> + <script> + document.domain='example.org'; + var testvar = "testiframe"; + </script> + </body> +</html> diff --git a/dom/html/test/bug1292522_page.html b/dom/html/test/bug1292522_page.html new file mode 100644 index 0000000000..9570f12d2d --- /dev/null +++ b/dom/html/test/bug1292522_page.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test for Bug 1292522</title> + <script> + var check_var = function() { + opener.postMessage(document.getElementsByTagName('iframe')[0].contentWindow.testvar, "http://mochi.test:8888"); + } + </script> + </head> + <body> + <iframe src="http://test2.example.org:80/tests/dom/html/test/bug1292522_iframe.html" onload="document.domain='example.org';check_var();"></iframe> + </body> +</html> diff --git a/dom/html/test/bug1315146-iframe.html b/dom/html/test/bug1315146-iframe.html new file mode 100644 index 0000000000..280db53052 --- /dev/null +++ b/dom/html/test/bug1315146-iframe.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> +document.domain = "example.org"; +</script> diff --git a/dom/html/test/bug1315146-main.html b/dom/html/test/bug1315146-main.html new file mode 100644 index 0000000000..e9f356dda6 --- /dev/null +++ b/dom/html/test/bug1315146-main.html @@ -0,0 +1,15 @@ +<!DOCTYPE HTML> +<iframe src="http://example.org/tests/dom/html/test/bug1315146-iframe.html"></iframe> +<input value="test"> +<script> +document.domain = "example.org"; +onload = function() { + let iframe = document.querySelector("iframe"); + let input = document.querySelector("input"); + input.selectionStart = input.selectionEnd = 2; + document.body.style.overflow = "scroll"; + iframe.contentDocument.body.offsetWidth; + opener.postMessage({start: input.selectionStart, + end: input.selectionEnd}, "*"); +} +</script> diff --git a/dom/html/test/bug196523-subframe.html b/dom/html/test/bug196523-subframe.html new file mode 100644 index 0000000000..ac53572a7a --- /dev/null +++ b/dom/html/test/bug196523-subframe.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<script> + function checkDomain(str, msg) { + window.parent.postMessage((str == document.domain) + ";" +msg, + "http://mochi.test:8888"); + } + + function reportException(msg) { + window.parent.postMessage(false + ";" + msg, "http://mochi.test:8888"); + } + + var win1; + try { + win1 = window.open("", "", "width=100,height=100"); + var otherDomain1 = win1.document.domain; + win1.close(); + checkDomain(otherDomain1, "Opened document should have our domain"); + } catch(e) { + reportException("Exception getting document.domain: " + e); + } finally { + win1.close(); + } + + document.domain = "example.org"; + + var win2; + try { + win2 = window.open("", "", "width=100,height=100"); + var otherDomain2 = win2.document.domain; + checkDomain(otherDomain2, "Opened document should have our domain"); + win2.close(); + } catch(e) { + reportException("Exception getting document.domain after domain set: " + e); + } finally { + win2.close(); + } +</script> diff --git a/dom/html/test/bug199692-nested-d2.html b/dom/html/test/bug199692-nested-d2.html new file mode 100644 index 0000000000..70064efe74 --- /dev/null +++ b/dom/html/test/bug199692-nested-d2.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=199692 +--> +<head> + <title>Nested, nested iframe for bug 199692 tests</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> +</head> +<body> + <div id="nest2div" style="border: 2px dotted blue;">nested, depth 2</div> +</body> +</html> + diff --git a/dom/html/test/bug199692-nested.html b/dom/html/test/bug199692-nested.html new file mode 100644 index 0000000000..27201a953d --- /dev/null +++ b/dom/html/test/bug199692-nested.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=199692 +--> +<head> + <title>Nested iframe for bug 199692 tests</title> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> +</head> +<body> + <div id="nest1div" style="border: 2px dotted green;">nested, depth 1</div> + <iframe src="bug199692-nested-d2.html"></iframe> +</body> +</html> + diff --git a/dom/html/test/bug199692-popup.html b/dom/html/test/bug199692-popup.html new file mode 100644 index 0000000000..de93ca8599 --- /dev/null +++ b/dom/html/test/bug199692-popup.html @@ -0,0 +1,190 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=199692 +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <title>Popup in test for Bug 199692</title> + <style type="text/css"> +#content * { + border: 2px solid black; + margin: 2px; + clear: both; + height: 20px; + overflow: hidden; +} + +#txt, #static, #fixed, #absolute, #relative, #hidden, #float, #empty, #static, #relative { + width: 200px !important; +} + </style> + +</head> +<!-- +Elements are styled in such a way that they don't overlap visually +unless they also overlap structurally. + +This file is designed to be opened from test_bug199692.html in a popup +window, to guarantee that the window in which document.elementFromPoint runs +is large enough to display all the elements being tested. +--> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=199692">Mozilla Bug 199692</a> + +<div id="content" style="width: 500px; background-color: #ccc;"> + +<!-- element containing text --> +<div id="txt" style="height: 30px;">txt</div> + +<!-- element not containing text --> +<div id="empty" style="border: 2px solid blue;"></div> + +<!-- element with only whitespace --> +<p id="whitespace" style="border: 2px solid magenta;"> </p> + +<!-- position: static --> +<span id="static" style="position: static; border-color: green;">static</span> + +<!-- floated element --> +<div id="float" style="border-color: red; float: right;">float</div> + +<!-- position: fixed --> +<span id="fixed" style="position: fixed; top: 500px; left: 100px; border: 3px solid yellow;">fixed</span> + +<!-- position: absolute --> +<span id="absolute" style="position: absolute; top: 550px; left: 150px; border-color: orange;">abs</span> + +<!-- position: relative --> +<div id="relative" style="position: relative; top: 200px; border-color: teal;">rel</div> + +<!-- visibility: hidden --> +<div id="hidden-wrapper" style="border: 1px dashed teal;"> + <div id="hidden" style="opacity: 0.5; background-color: blue; visibility:hidden;">hidden</div> +</div> + +<!-- iframe (within iframe) --> +<iframe id="our-iframe" src="bug199692-nested.html" style="height: 100px; overflow: scroll;"></iframe> + +<input type="textbox" id="textbox" value="textbox"></input> +</div> + +<!-- interaction with scrolling --> +<iframe id="scrolled-iframe" + src="bug199692-scrolled.html#down" + style="position: absolute; top: 345px; left: 325px; height: 200px; width: 200px"></iframe> + +<script type="application/javascript"> + +var SimpleTest = window.opener.SimpleTest; +function ok() { window.opener.ok.apply(window.opener, arguments); } +function is() { window.opener.is.apply(window.opener, arguments); } +function todo() { window.opener.todo.apply(window.opener, arguments); } +function todo_is() { window.opener.todo_is.apply(window.opener, arguments); } +function $(id) { return document.getElementById(id); } + +/** + * Like is, but for tests which don't always succeed or always fail on all + * platforms. + */ +function random_fail(a, b, m) +{ + if (a != b) + todo_is(a, b, m); + else + is(a, b, m); +} + +/* Test for Bug 199692 */ + +function getCoords(elt) +{ + var x = 0, y = 0; + + do + { + x += elt.offsetLeft; + y += elt.offsetTop; + } while ((elt = elt.offsetParent)); + + return { x, y }; +} + +var elts = ["txt", "empty", "whitespace", "static", "fixed", "absolute", + "relative", "float", "textbox"]; + +function testPoints() +{ + ok('elementFromPoint' in document, "document.elementFromPoint must exist"); + ok(typeof document.elementFromPoint === "function", "must be a function"); + + var doc = document; + doc.pt = doc.elementFromPoint; // for shorter lines + is(doc.pt(-1, 0), null, "Negative coordinates (-1, 0) should return null"); + is(doc.pt(0, -1), null, "Negative coordinates (0, -1) should return null"); + is(doc.pt(-1, -1), null, "Negative coordinates (-1, -1) should return null"); + + var pos; + for (var i = 0; i < elts.length; i++) + { + var id = elts[i]; + var elt = $(id); + + // The upper left corner of an element (with a moderate offset) will + // usually contain text, and the lower right corner usually won't. + var pos = getCoords(elt); + var x = pos.x, y = pos.y; + var w = elt.offsetWidth, h = elt.offsetHeight; + + var d = 5; + is(doc.pt(x + d, y + d), elt, + "(" + (x + d) + "," + (y + d) + ") IDs should match (upper left " + + "corner of " + id + ")"); + is(doc.pt(x + w - d, y + h - d), elt, + "(" + (x + w - d) + "," + (y + h - d) + ") IDs should match (lower " + + "right corner of " + id + ")"); + } + + // content + var c = $("content"); + pos = getCoords(c); + x = pos.x + c.offsetWidth / 2; + y = pos.y; + + // This fails on some platforms but not others for unknown reasons + random_fail(doc.pt(x, y), c, "Point to right of #txt should be #content"); + is(doc.pt(x, y + 1), c, "Point to right of #txt should be #content"); + random_fail(doc.pt(x + 1, y), c, "Point to right of #txt should be #content"); + is(doc.pt(x + 1, y + 1), c, "Point to right of #txt should be #content"); + + // hidden + c = $("hidden"); + pos = getCoords(c); + x = pos.x; + y = pos.y; + is(doc.pt(x, y), $("hidden-wrapper"), + "Hit testing should bypass hidden elements."); + + // iframe nested + var iframe = $("our-iframe"); + pos = getCoords(iframe); + x = pos.x; + y = pos.y; + is(doc.pt(x + 20, y + 20), $("our-iframe"), + "Element from nested iframe returned is from calling document"); + // iframe, doubly nested + is(doc.pt(x + 60, y + 60), $("our-iframe"), + "Element from doubly nested iframe returned is from calling document"); + + // scrolled iframe tests + $("scrolled-iframe").contentWindow.runTests(); + + SimpleTest.finish(); + window.close(); +} + +window.onload = testPoints; +</script> +</body> +</html> + diff --git a/dom/html/test/bug199692-scrolled.html b/dom/html/test/bug199692-scrolled.html new file mode 100644 index 0000000000..f13bf7ab12 --- /dev/null +++ b/dom/html/test/bug199692-scrolled.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=199692 +--> +<head> + <title>Scrolled page for bug 199692 tests</title> + <style type="text/css"> +/* Disable default margins/padding/borders so (0, 0) gets a div. */ +* { margin: 0; padding: 0; border: 0; } + </style> + <script type="application/javascript"> +function $(id) { return document.getElementById(id); } + +function runTests() +{ + var is = window.parent.is; + + is(document.elementFromPoint(0, 0), $("down"), + "document.elementFromPoint not respecting scrolling?"); + is(document.elementFromPoint(200, 200), null, + "should have returned null for a not-visible point"); + is(document.elementFromPoint(3, -5), null, + "should have returned null for a not-visible point"); +} + </script> +</head> +<!-- This page is loaded in a 200px-square iframe scrolled to #down. --> +<body> +<div style="height: 150px; background: lightblue;">first</div> +<div id="down" style="height: 250px; background: lightgreen;">second</div> +</body> +</html> + diff --git a/dom/html/test/bug242709_iframe.html b/dom/html/test/bug242709_iframe.html new file mode 100644 index 0000000000..1155299692 --- /dev/null +++ b/dom/html/test/bug242709_iframe.html @@ -0,0 +1,20 @@ +<html> +<head> +<title></title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script type="text/javascript"> +function submitIframeForm () { + document.getElementById('b').submit(); + document.getElementById('thebutton').disabled = true; +} +</script> + +</head> +<body onload="sendMouseEvent({type:'click'}, 'thebutton')"> + +<form method="get" action="bug242709_load.html" id="b"> +<input type="submit" onclick="submitIframeForm()" id="thebutton"> +</form> + +</body> +</html> diff --git a/dom/html/test/bug242709_load.html b/dom/html/test/bug242709_load.html new file mode 100644 index 0000000000..c9be79b241 --- /dev/null +++ b/dom/html/test/bug242709_load.html @@ -0,0 +1,11 @@ +<html> +<head> +<title></title> +</head> + +<body onload="parent.submitted();"> + +<span id="foo"></span> + +</body> +</html> diff --git a/dom/html/test/bug277724_iframe1.html b/dom/html/test/bug277724_iframe1.html new file mode 100644 index 0000000000..d0d881b766 --- /dev/null +++ b/dom/html/test/bug277724_iframe1.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- Use an unload handler to prevent bfcache from messing with us --> +<body onunload="parent.childUnloaded = true;"> + <select id="select"> + <option>aaa</option> + <option>bbbb</option> + </select> + + <textarea id="textarea"> + </textarea> + + <input type="text" id="text"> + <input type="password" id="password"> + <input type="checkbox" id="checkbox"> + <input type="radio" id="radio"> + <input type="image" id="image"> + <input type="submit" id="submit"> + <input type="reset" id="reset"> + <input type="button" id="button input"> + <input type="hidden" id="hidden"> + <input type="file" id="file"> + + <button type="submit" id="submit button"></button> + <button type="reset" id="reset button"></button> + <button type="button" id="button"></button> +</body> +</html> diff --git a/dom/html/test/bug277724_iframe2.xhtml b/dom/html/test/bug277724_iframe2.xhtml new file mode 100644 index 0000000000..14423aa06c --- /dev/null +++ b/dom/html/test/bug277724_iframe2.xhtml @@ -0,0 +1,27 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- Use an unload handler to prevent bfcache from messing with us --> +<body onunload="parent.childUnloaded = true;"> + <select id="select"> + <option>aaa</option> + <option>bbbb</option> + </select> + + <textarea id="textarea"> + </textarea> + + <input type="text" id="text" /> + <input type="password" id="password" /> + <input type="checkbox" id="checkbox" /> + <input type="radio" id="radio" /> + <input type="image" id="image" /> + <input type="submit" id="submit" /> + <input type="reset" id="reset" /> + <input type="button" id="button input" /> + <input type="hidden" id="hidden" /> + <input type="file" id="file" /> + + <button type="submit" id="submit button"></button> + <button type="reset" id="reset button"></button> + <button type="button" id="button"></button> +</body> +</html> diff --git a/dom/html/test/bug277890_iframe.html b/dom/html/test/bug277890_iframe.html new file mode 100644 index 0000000000..c1cb4ff2e1 --- /dev/null +++ b/dom/html/test/bug277890_iframe.html @@ -0,0 +1,20 @@ +<html> +<head> +<title></title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script type="text/javascript"> +function submitIframeForm () { + document.getElementById('b').submit(); + document.getElementById('thebutton').disabled = true; +} +</script> + +</head> +<body onload="sendMouseEvent({type:'click'}, 'thebutton')"> + +<form method="get" action="bug277890_load.html" id="b"> +<button onclick="submitIframeForm()" id="thebutton">Submit</button> +</form> + +</body> +</html> diff --git a/dom/html/test/bug277890_load.html b/dom/html/test/bug277890_load.html new file mode 100644 index 0000000000..c9be79b241 --- /dev/null +++ b/dom/html/test/bug277890_load.html @@ -0,0 +1,11 @@ +<html> +<head> +<title></title> +</head> + +<body onload="parent.submitted();"> + +<span id="foo"></span> + +</body> +</html> diff --git a/dom/html/test/bug340800_iframe.txt b/dom/html/test/bug340800_iframe.txt new file mode 100644 index 0000000000..369dfe7441 --- /dev/null +++ b/dom/html/test/bug340800_iframe.txt @@ -0,0 +1,4 @@ +Line 1. +Line 2. +Line 3. +Line 4. diff --git a/dom/html/test/bug369370-popup.png b/dom/html/test/bug369370-popup.png Binary files differnew file mode 100644 index 0000000000..9063d12648 --- /dev/null +++ b/dom/html/test/bug369370-popup.png diff --git a/dom/html/test/bug372098-link-target.html b/dom/html/test/bug372098-link-target.html new file mode 100644 index 0000000000..b22b8e020e --- /dev/null +++ b/dom/html/test/bug372098-link-target.html @@ -0,0 +1,7 @@ +<html> +<script type="text/javascript"> + +parent.callback(location.search.substr(1)); + +</script> +</html> diff --git a/dom/html/test/bug436200.html b/dom/html/test/bug436200.html new file mode 100644 index 0000000000..1ef7e73b5e --- /dev/null +++ b/dom/html/test/bug436200.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"/> + <title>Secure to Insecure Test</title> + </head> + <body> + <form id="test_form" action="http://example.org/browser/dom/html/test/bug436200.html"> + <button type="submit" id="submit_button">Submit</button> + </form> + </body> +</html> diff --git a/dom/html/test/bug441930_iframe.html b/dom/html/test/bug441930_iframe.html new file mode 100644 index 0000000000..532cd5c36a --- /dev/null +++ b/dom/html/test/bug441930_iframe.html @@ -0,0 +1,27 @@ +<html> +<body> + The content of this <code>textarea</code> should not disappear on page reload:<br /> + <textarea>This text should not disappear on page reload!</textarea> + <script> + var ta = document.getElementsByTagName("textarea").item(0); + if (!parent.reloaded) { + parent.reloaded = true; + ta.disabled = true; + location.reload(); + } else { + // Primary regression test: + parent.isnot(ta.value, "", + "Content of dynamically disabled textarea disappeared on page reload."); + + // Bonus regression test: changing the textarea's defaultValue after + // reloading should also update the textarea's value. + var newDefaultValue = "new default value"; + ta.defaultValue = newDefaultValue; + parent.is(ta.value, newDefaultValue, + "Changing the defaultValue attribute of a textarea fails to update its value attribute."); + + parent.SimpleTest.finish(); + } + </script> +</body> +</html> diff --git a/dom/html/test/bug445004-inner.html b/dom/html/test/bug445004-inner.html new file mode 100644 index 0000000000..b946520ea6 --- /dev/null +++ b/dom/html/test/bug445004-inner.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> + <head> + <base href="http://test1.example.org/tests/dom/html/test/bug445004-inner.html"> + <script src="bug445004-inner.js"></script> + </head> + <body> + <iframe name="w" id="w" width="100" height="100"></iframe> + <iframe name="x" id="x" width="100" height="100"></iframe> + <iframe name="y" id="y" width="100" height="100"></iframe> + <iframe name="z" id="z" width="100" height="100"></iframe> + <img src="test1.example.org.png"> + </body> +</html> diff --git a/dom/html/test/bug445004-inner.js b/dom/html/test/bug445004-inner.js new file mode 100644 index 0000000000..2d751da454 --- /dev/null +++ b/dom/html/test/bug445004-inner.js @@ -0,0 +1,27 @@ +document.domain = "example.org"; +function $(str) { + return document.getElementById(str); +} +function hookLoad(str) { + $(str).onload = function () { + window.parent.parent.postMessage("end", "*"); + }; + window.parent.parent.postMessage("start", "*"); +} +window.onload = function () { + hookLoad("w"); + $("w").contentWindow.location.href = "test1.example.org.png"; + hookLoad("x"); + var doc = $("x").contentDocument; + doc.write('<img src="test1.example.org.png">'); + doc.close(); +}; +function doIt() { + hookLoad("y"); + $("y").contentWindow.location.href = "example.org.png"; + hookLoad("z"); + var doc = $("z").contentDocument; + doc.write('<img src="example.org.png">'); + doc.close(); +} +window.addEventListener("message", doIt); diff --git a/dom/html/test/bug445004-outer-abs.html b/dom/html/test/bug445004-outer-abs.html new file mode 100644 index 0000000000..8a93ef2b73 --- /dev/null +++ b/dom/html/test/bug445004-outer-abs.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <base href="http://example.org/tests/dom/html/test/bug445004-outer.html"> + <script>document.domain = "example.org"</script> + </head> + <body> + <iframe width="500" height="200" src="http://test1.example.org/tests/dom/html/test/bug445004-inner.html" + onload="window.frames[0].doIt()"></iframe> + </body> +</html> diff --git a/dom/html/test/bug445004-outer-rel.html b/dom/html/test/bug445004-outer-rel.html new file mode 100644 index 0000000000..0967338899 --- /dev/null +++ b/dom/html/test/bug445004-outer-rel.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <base href="http://example.org/tests/dom/html/test/bug445004-outer.html"> + <script>document.domain = "example.org"</script> + </head> + <body> + <iframe width="500" height="200" src="bug445004-inner.html" + onload="window.frames[0].doIt()"></iframe> + </body> +</html> diff --git a/dom/html/test/bug445004-outer-write.html b/dom/html/test/bug445004-outer-write.html new file mode 100644 index 0000000000..be6e37b6d7 --- /dev/null +++ b/dom/html/test/bug445004-outer-write.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <base href="http://example.org/tests/dom/html/test/bug445004-outer.html"> + <script>document.domain = "example.org"</script> + </head> + <body> + <iframe width="500" height="200" src="javascript:"<!DOCTYPE html> <html> <script> function $(str) { return document.getElementById(str); } function hookLoad(str) { $(str).onload = function() { window.parent.parent.postMessage('end', '*'); }; window.parent.parent.postMessage('start', '*'); } window.onload = function() { hookLoad(\"w\"); $(\"w\").contentWindow.location.href = \"example.org.png\"; hookLoad(\"x\"); var doc = $(\"x\").contentDocument; doc.write('<img src=\"example.org.png\">'); doc.close(); }; function doIt() { hookLoad(\"y\"); $(\"y\").contentWindow.location.href = \"example.org.png\"; hookLoad(\"z\"); var doc = $(\"z\").contentDocument; doc.write('<img src=\"example.org.png\">'); doc.close(); } </script> <body> <iframe name=\"w\" id=\"w\" width=\"100\" height=\"100\"></iframe> <iframe name=\"x\" id=\"x\" width=\"100\" height=\"100\"></iframe> <iframe name=\"y\" id=\"y\" width=\"100\" height=\"100\"></iframe> <iframe name=\"z\" id=\"z\" width=\"100\" height=\"100\"></iframe><img src=\"example.org.png\"> </body> </html>" " + onload="window.frames[0].doIt();"></iframe> + </body> +</html> diff --git a/dom/html/test/bug446483-iframe.html b/dom/html/test/bug446483-iframe.html new file mode 100644 index 0000000000..fe5a6cf9f7 --- /dev/null +++ b/dom/html/test/bug446483-iframe.html @@ -0,0 +1,10 @@ +<script>
+function doe(){
+window.focus();
+window.getSelection().collapse(document.body, 0);
+}
+setTimeout(doe,50);
+
+setTimeout(function() {window.location.reload()}, 200);
+</script>
+<span contenteditable="true"></span>
diff --git a/dom/html/test/bug448564-echo.sjs b/dom/html/test/bug448564-echo.sjs new file mode 100644 index 0000000000..1eee116fd7 --- /dev/null +++ b/dom/html/test/bug448564-echo.sjs @@ -0,0 +1,6 @@ +function handleRequest(request, response) { + response.setHeader("Cache-Control", "no-cache", false); + response.setStatusLine(request.httpVersion, 200, "OK"); + + response.write(request.queryString); +} diff --git a/dom/html/test/bug448564-iframe-1.html b/dom/html/test/bug448564-iframe-1.html new file mode 100644 index 0000000000..4f3e79e5d2 --- /dev/null +++ b/dom/html/test/bug448564-iframe-1.html @@ -0,0 +1,16 @@ +<html> +<body> + + <table> + <form action="bug448564-echo.sjs" method="GET"> + <tr><td><input name="a" value="aval"></td></tr> + <input type="hidden" name="b" value="bval"> + <input name="c" value="cval"> + <tr><td><input name="d" value="dval" type="submit"></td></tr> + </form> + </table> + + <script src="bug448564-submit.js"></script> + +</body> +</html> diff --git a/dom/html/test/bug448564-iframe-2.html b/dom/html/test/bug448564-iframe-2.html new file mode 100644 index 0000000000..dba19b37e2 --- /dev/null +++ b/dom/html/test/bug448564-iframe-2.html @@ -0,0 +1,16 @@ +<html> +<body> + + <form action="bug448564-echo.sjs" method="GET"> + <table> + <tr><td><input name="a" value="aval"></td></tr> + <input type="hidden" name="b" value="bval"> + <input name="c" value="cval"> + <tr><td><input name="d" value="dval" type="submit"></td></tr> + </table> + </form> + + <script src="bug448564-submit.js"></script> + +</body> +</html> diff --git a/dom/html/test/bug448564-iframe-3.html b/dom/html/test/bug448564-iframe-3.html new file mode 100644 index 0000000000..64288ebb15 --- /dev/null +++ b/dom/html/test/bug448564-iframe-3.html @@ -0,0 +1,16 @@ +<html> +<body> + + <table> + <span><form action="bug448564-echo.sjs" method="GET"> + <tr><td><input name="a" value="aval"></td></tr> + <input type="hidden" name="b" value="bval"> + <input name="c" value="cval"> + <tr><td><input name="d" value="dval" type="submit"></td></tr> + </form></span> + </table> + + <script src="bug448564-submit.js"></script> + +</body> +</html> diff --git a/dom/html/test/bug448564-submit.js b/dom/html/test/bug448564-submit.js new file mode 100644 index 0000000000..a650487d65 --- /dev/null +++ b/dom/html/test/bug448564-submit.js @@ -0,0 +1,6 @@ +var inputs = document.getElementsByTagName("input"); +for (var input, i = 0; (input = inputs[i]); ++i) { + if ("submit" == input.type) { + input.click(); + } +} diff --git a/dom/html/test/bug499092.html b/dom/html/test/bug499092.html new file mode 100644 index 0000000000..0476fa4e76 --- /dev/null +++ b/dom/html/test/bug499092.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> +var title = document.createElementNS("http://www.w3.org/1999/xhtml", "aa:title"); +title.textContent = "HTML OK"; +document.documentElement.firstChild.appendChild(title); +</script> diff --git a/dom/html/test/bug499092.xml b/dom/html/test/bug499092.xml new file mode 100644 index 0000000000..eedd2c77b3 --- /dev/null +++ b/dom/html/test/bug499092.xml @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<doc xmlns:aa="http://www.w3.org/1999/xhtml"> +<aa:title>XML OK</aa:title> +</doc> diff --git a/dom/html/test/bug514856_iframe.html b/dom/html/test/bug514856_iframe.html new file mode 100644 index 0000000000..2abf9e91e2 --- /dev/null +++ b/dom/html/test/bug514856_iframe.html @@ -0,0 +1,21 @@ +<html> + <head> + <style> + html, body, a, img { + padding: 0px; + margin: 0px; + border: 0px; + } + img { + width: 100%; + height: 100%; + } + </style> + </head> + <body> + <a href="bug514856_iframe.html"> + <img ismap="ismap" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0vr4MkhoXe0rZigAAAABJRU5ErkJggg=="> + </a> + </body> +</html> diff --git a/dom/html/test/bug592641_img.jpg b/dom/html/test/bug592641_img.jpg Binary files differnew file mode 100644 index 0000000000..c9103b8b0e --- /dev/null +++ b/dom/html/test/bug592641_img.jpg diff --git a/dom/html/test/bug649134/file_bug649134-1.sjs b/dom/html/test/bug649134/file_bug649134-1.sjs new file mode 100644 index 0000000000..fed0a9d693 --- /dev/null +++ b/dom/html/test/bug649134/file_bug649134-1.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + response.seizePower(); + var r = + "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/html\r\n" + + 'Link: < \014>; rel="stylesheet"\r\n' + + "\r\n" + + "<!-- selector {} body {display:none;} --><body>PASS</body>\r\n"; + response.bodyOutputStream.write(r, r.length); + response.bodyOutputStream.flush(); + response.finish(); +} diff --git a/dom/html/test/bug649134/file_bug649134-2.sjs b/dom/html/test/bug649134/file_bug649134-2.sjs new file mode 100644 index 0000000000..3cbacf7184 --- /dev/null +++ b/dom/html/test/bug649134/file_bug649134-2.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + response.seizePower(); + var r = + "HTTP/1.1 200 OK\r\n" + + "Content-Type: text/html\r\n" + + 'Link: < \014>; rel="stylesheet",\r\n' + + "\r\n" + + "<!-- selector {} body {display:none;} --><body>PASS</body>\r\n"; + response.bodyOutputStream.write(r, r.length); + response.bodyOutputStream.flush(); + response.finish(); +} diff --git a/dom/html/test/bug649134/index.html b/dom/html/test/bug649134/index.html new file mode 100644 index 0000000000..2f3973704e --- /dev/null +++ b/dom/html/test/bug649134/index.html @@ -0,0 +1,3 @@ +body { + display:none; +} diff --git a/dom/html/test/chrome.toml b/dom/html/test/chrome.toml new file mode 100644 index 0000000000..ac226b51c2 --- /dev/null +++ b/dom/html/test/chrome.toml @@ -0,0 +1,12 @@ +[DEFAULT] +support-files = [ + "file_anchor_ping.html", + "image.png", +] + +["test_anchor_ping.html"] +skip-if = ["os == 'android'"] + +["test_bug1414077.html"] + +["test_external_protocol_iframe.html"] diff --git a/dom/html/test/dialog/mochitest.toml b/dom/html/test/dialog/mochitest.toml new file mode 100644 index 0000000000..18f1f551a7 --- /dev/null +++ b/dom/html/test/dialog/mochitest.toml @@ -0,0 +1,4 @@ +[DEFAULT] + +["test_bug1648877_dialog_fullscreen_denied.html"] + diff --git a/dom/html/test/dialog/test_bug1648877_dialog_fullscreen_denied.html b/dom/html/test/dialog/test_bug1648877_dialog_fullscreen_denied.html new file mode 100644 index 0000000000..906c7dd53e --- /dev/null +++ b/dom/html/test/dialog/test_bug1648877_dialog_fullscreen_denied.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1648877 +--> +<head> + <title>Test for Bug 1648877</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1648877">Requesting + fullscreen a dialog element should be denied</a> +<p id="display"></p> +<dialog> +</dialog> +<div style="width: 30px; height:30px" </div> + +<pre id="test"> +<script type="application/javascript"> +SimpleTest.waitForExplicitFinish(); + +function runTest() { + document.addEventListener("fullscreenchange", () => { + ok(false, "Should never receive " + + "a fullscreenchange event in the main window."); + }); + + document.addEventListener('fullscreenerror', (event) => { + ok(!document.fullscreenElement, + "Should not grant request if the element is dialog"); + SimpleTest.finish(); + }); + + const div = document.querySelector("div"); + + div.addEventListener("click", function() { + const dialog = document.querySelector("dialog"); + dialog.requestFullscreen(); + }); + + synthesizeMouseAtCenter(div, {}); +} + +SimpleTest.waitForFocus(runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/dummy_page.html b/dom/html/test/dummy_page.html new file mode 100644 index 0000000000..fd238954c6 --- /dev/null +++ b/dom/html/test/dummy_page.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> +<title>Dummy test page</title> +<meta charset="utf-8"/> +</head> +<body> +<p>Dummy test page</p> +</body> +</html> diff --git a/dom/html/test/empty.html b/dom/html/test/empty.html new file mode 100644 index 0000000000..0dc101b533 --- /dev/null +++ b/dom/html/test/empty.html @@ -0,0 +1 @@ +<html><body></body></html> diff --git a/dom/html/test/file.webm b/dom/html/test/file.webm Binary files differnew file mode 100644 index 0000000000..7bc738b8b4 --- /dev/null +++ b/dom/html/test/file.webm diff --git a/dom/html/test/file_anchor_ping.html b/dom/html/test/file_anchor_ping.html new file mode 100644 index 0000000000..3b9717263f --- /dev/null +++ b/dom/html/test/file_anchor_ping.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title>file_anchor_ping.html</title> + </head> + <body onload="document.body.firstElementChild.click()"> + <a href="/">click me</a> + <script> + document.body.firstElementChild.ping = window.location.search.slice(1); + </script> + </body> +</html> diff --git a/dom/html/test/file_broadcast_load.html b/dom/html/test/file_broadcast_load.html new file mode 100644 index 0000000000..ffae9c6536 --- /dev/null +++ b/dom/html/test/file_broadcast_load.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<h1>file_broadcast_load.html</h1> +<script> +let channel = new BroadcastChannel("test"); +channel.onmessage = function(e) { + console.log("file_broadcast_load.html got message:", e.data); + if (e.data == "close") { + window.close(); + } +}; + +addEventListener("load", function() { + console.log("file_broadcast_load.html loaded"); + channel.postMessage("load"); +}); +</script> diff --git a/dom/html/test/file_bug1108547-1.html b/dom/html/test/file_bug1108547-1.html new file mode 100644 index 0000000000..efc0eae494 --- /dev/null +++ b/dom/html/test/file_bug1108547-1.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> +document.cookie = "foo=bar"; +</script> diff --git a/dom/html/test/file_bug1108547-2.html b/dom/html/test/file_bug1108547-2.html new file mode 100644 index 0000000000..f5d8c5f964 --- /dev/null +++ b/dom/html/test/file_bug1108547-2.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<body onload="document.querySelector('form').submit();"> +<form action="javascript:opener.document.getElementById('result').textContent = document.cookie;" target="_blank" rel="opener"> +</form> +<div id="result">not tested yet</div> +</body> diff --git a/dom/html/test/file_bug1108547-3.html b/dom/html/test/file_bug1108547-3.html new file mode 100644 index 0000000000..e6a8ba3fa2 --- /dev/null +++ b/dom/html/test/file_bug1108547-3.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<body onload="document.querySelector('a').click();"> +<a href="javascript:opener.document.getElementById('result').textContent = document.cookie;" target="_blank" rel="opener">test</a> +<div id="result">not tested yet</div> +</body> diff --git a/dom/html/test/file_bug1166138_1x.png b/dom/html/test/file_bug1166138_1x.png Binary files differnew file mode 100644 index 0000000000..df421453c2 --- /dev/null +++ b/dom/html/test/file_bug1166138_1x.png diff --git a/dom/html/test/file_bug1166138_2x.png b/dom/html/test/file_bug1166138_2x.png Binary files differnew file mode 100644 index 0000000000..6f76d44387 --- /dev/null +++ b/dom/html/test/file_bug1166138_2x.png diff --git a/dom/html/test/file_bug1166138_def.png b/dom/html/test/file_bug1166138_def.png Binary files differnew file mode 100644 index 0000000000..144a2f0b93 --- /dev/null +++ b/dom/html/test/file_bug1166138_def.png diff --git a/dom/html/test/file_bug1260704.png b/dom/html/test/file_bug1260704.png Binary files differnew file mode 100644 index 0000000000..df421453c2 --- /dev/null +++ b/dom/html/test/file_bug1260704.png diff --git a/dom/html/test/file_bug209275_1.html b/dom/html/test/file_bug209275_1.html new file mode 100644 index 0000000000..3f7233876b --- /dev/null +++ b/dom/html/test/file_bug209275_1.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <base href="http://example.org" /> +</head> +<body onload="load();"> +Initial state + +<script> +function load() { + // Nuke and rebuild the page. + document.removeChild(document.documentElement); + var html = document.createElement("html"); + var body = document.createElement("body"); + html.appendChild(body); + var link = document.createElement("a"); + link.href = "#"; + link.id = "link"; + body.appendChild(link); + document.appendChild(html); + + // Tell our parent to have a look at us. + parent.gGen.next(); +} +</script> + +</body> +</html> diff --git a/dom/html/test/file_bug209275_2.html b/dom/html/test/file_bug209275_2.html new file mode 100644 index 0000000000..36e9ff4672 --- /dev/null +++ b/dom/html/test/file_bug209275_2.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <base href="http://example.com" /> +</head> +<body onload="load();"> +Page 2 initial state + +<script> +function load() { + // Nuke and rebuild the page. + document.removeChild(document.documentElement); + html = document.createElement("html"); + html.innerHTML = "<body><a href='/' id='link'>B</a></body>" + document.appendChild(html); + + // Tell our parent to have a look at us + parent.gGen.next(); +} +</script> + +</body> +</html> diff --git a/dom/html/test/file_bug209275_3.html b/dom/html/test/file_bug209275_3.html new file mode 100644 index 0000000000..2544115901 --- /dev/null +++ b/dom/html/test/file_bug209275_3.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <base href="http://example.org" /> +</head> +<body onload="load();"> +Initial state + +<script> +function load() { + // Nuke and rebuild the page. If document.open() clears the <base> properly, + // our new <base> will take precedence and the test will pass. + document.open(); + document.write("<html><base href='http://mochi.test:8888' /><body>" + + "<a id='link' href='/'>A</a></body></html>"); + + // Tell our parent to have a look at us. + parent.gGen.next(); +} +</script> + +</body> +</html> diff --git a/dom/html/test/file_bug297761.html b/dom/html/test/file_bug297761.html new file mode 100644 index 0000000000..5e861a00fd --- /dev/null +++ b/dom/html/test/file_bug297761.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <base href="http://www.mozilla.org/"> + </head> + <body> + <form action=""> + <input type='submit' formaction=""> + <button type='submit' formaction=""></button> + <input id='i' type='image' formaction=""> + </form> + </body> +</html> diff --git a/dom/html/test/file_bug417760.png b/dom/html/test/file_bug417760.png Binary files differnew file mode 100644 index 0000000000..743292dc6f --- /dev/null +++ b/dom/html/test/file_bug417760.png diff --git a/dom/html/test/file_bug871161-1.html b/dom/html/test/file_bug871161-1.html new file mode 100644 index 0000000000..16015f0c4e --- /dev/null +++ b/dom/html/test/file_bug871161-1.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset=windows-1251> +<title>Page with non-default charset</title> +<script> +function run() { + document.forms[0].submit(); +} +</script> +</head> +<body onload="run();"> +<form method=post action="http://example.org/tests/dom/html/test/file_bug871161-2.html"></form> +</body> +</html> + diff --git a/dom/html/test/file_bug871161-2.html b/dom/html/test/file_bug871161-2.html new file mode 100644 index 0000000000..18cf825b2d --- /dev/null +++ b/dom/html/test/file_bug871161-2.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<title>Page without declared charset</title> +<script> +function done() { + window.opener.postMessage(document.characterSet, "*"); +} +</script> +</head> +<body onload="done();"> +</body> +</html> + diff --git a/dom/html/test/file_bug893537.html b/dom/html/test/file_bug893537.html new file mode 100644 index 0000000000..1dcb454ff1 --- /dev/null +++ b/dom/html/test/file_bug893537.html @@ -0,0 +1,9 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=893537 +--> +<body> +<iframe id="iframe" src="data:text/html;charset=US-ASCII,Goodbye World" srcdoc="Hello World"></iframe> +</body> +</html> diff --git a/dom/html/test/file_cookiemanager.js b/dom/html/test/file_cookiemanager.js new file mode 100644 index 0000000000..08c9d72898 --- /dev/null +++ b/dom/html/test/file_cookiemanager.js @@ -0,0 +1,20 @@ +/* eslint-env mozilla/chrome-script */ + +addMessageListener("getCookieFromManager", ({ host, path }) => { + let cm = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager); + let values = []; + path = path.substring(0, path.lastIndexOf("/")); + for (let cookie of cm.cookies) { + if (!cookie) { + break; + } + if (host != cookie.host || path != cookie.path) { + continue; + } + values.push(cookie.name + "=" + cookie.value); + } + + sendAsyncMessage("getCookieFromManager:return", { + cookie: values.join("; "), + }); +}); diff --git a/dom/html/test/file_formSubmission_img.jpg b/dom/html/test/file_formSubmission_img.jpg Binary files differnew file mode 100644 index 0000000000..dcd99b9670 --- /dev/null +++ b/dom/html/test/file_formSubmission_img.jpg diff --git a/dom/html/test/file_formSubmission_text.txt b/dom/html/test/file_formSubmission_text.txt new file mode 100644 index 0000000000..a496efee84 --- /dev/null +++ b/dom/html/test/file_formSubmission_text.txt @@ -0,0 +1 @@ +This is a text file diff --git a/dom/html/test/file_iframe_sandbox_a_if1.html b/dom/html/test/file_iframe_sandbox_a_if1.html new file mode 100644 index 0000000000..b60d52ca00 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if1.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + I am sandboxed without any permissions + <iframe id="if_2a" src="file_iframe_sandbox_a_if2.html" height="10" width="10"></iframe> + <iframe id="if_2b" sandbox="allow-scripts" src="file_iframe_sandbox_a_if2.html" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_a_if10.html b/dom/html/test/file_iframe_sandbox_a_if10.html new file mode 100644 index 0000000000..14306eb613 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if10.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<frameset> + <frame src="file_iframe_sandbox_a_if11.html"> + <frame src="file_iframe_sandbox_a_if16.html"> +</frameset> +</html> diff --git a/dom/html/test/file_iframe_sandbox_a_if11.html b/dom/html/test/file_iframe_sandbox_a_if11.html new file mode 100644 index 0000000000..8eee71df1d --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if11.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script> + function doStuff() { + try { + window.parent.parent.ok_wrapper(false, "a frame inside a sandboxed iframe should NOT be same origin with the iframe's parent"); + } + catch (e) { + window.parent.parent.postMessage({ok: true, desc: "a frame inside a sandboxed iframe is not same origin with the iframe's parent"}, "*"); + } + } + </script> +</head> +<frameset> + <frame onload='doStuff()' src="file_iframe_sandbox_a_if12.html"> +</frameset> +I'm a <frame> inside an iframe which is sandboxed with 'allow-scripts allow-forms' +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if12.html b/dom/html/test/file_iframe_sandbox_a_if12.html new file mode 100644 index 0000000000..d49d4e5625 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if12.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +function doStuff() { + try { + window.parent.parent.parent.ok_wrapper(false, "a frame inside a frame inside a sandboxed iframe should NOT be same origin with the iframe's parent"); + } + catch (e) { + dump("caught some e if12\n"); + window.parent.parent.parent.postMessage({ok: true, desc: "a frame inside a frame inside a sandboxed iframe is not same origin with the iframe's parent"}, "*"); + } +} +</script> +<body onload='doStuff()'> + I'm a <frame> inside a <frame> inside an iframe which is sandboxed with 'allow-scripts allow-forms' +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if13.html b/dom/html/test/file_iframe_sandbox_a_if13.html new file mode 100644 index 0000000000..8737a7682e --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if13.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 886262</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> + <object data="file_iframe_sandbox_a_if14.html"></object> +</body> + +</html> diff --git a/dom/html/test/file_iframe_sandbox_a_if14.html b/dom/html/test/file_iframe_sandbox_a_if14.html new file mode 100644 index 0000000000..b588f7ec50 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if14.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 886262</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + window.addEventListener("message", receiveMessage); + + function receiveMessage(event) + { + window.parent.parent.postMessage({ok: event.data.ok, desc: "objects containing " + event.data.desc}, "*"); + } + + function doStuff() { + try { + window.parent.parent.ok_wrapper(false, "an object inside a sandboxed iframe should NOT be same origin with the iframe's parent"); + } + catch (e) { + window.parent.parent.postMessage({ok: true, desc: "an object inside a sandboxed iframe is not same origin with the iframe's parent"}, "*"); + } + } +</script> + +<body onload='doStuff()'> +I'm a <object> inside an iframe which is sandboxed with 'allow-scripts allow-forms' + + <object data="file_iframe_sandbox_a_if15.html"></object> +</body> + +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if15.html b/dom/html/test/file_iframe_sandbox_a_if15.html new file mode 100644 index 0000000000..9c5a003d7c --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if15.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 886262</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> +function doStuff() { + try { + window.parent.parent.parent.ok_wrapper(false, "an object inside a frame or object inside a sandboxed iframe should NOT be same origin with the iframe's parent"); + } + catch (e) { + window.parent.parent.parent.postMessage({ok: true, desc: "an object inside a frame or object inside a sandboxed iframe is not same origin with the iframe's parent"}, "*"); + } + + // Check that sandboxed forms browsing context flag NOT set by attempting to submit a form. + document.getElementById('a_form').submit(); +} +</script> + +<body onload='doStuff()'> + I'm a <object> inside a <frame> or <object> inside an iframe which is sandboxed with 'allow-scripts allow-forms' + + <form method="get" action="file_iframe_sandbox_form_pass.html" id="a_form"> + First name: <input type="text" name="firstname"> + Last name: <input type="text" name="lastname"> + <input type="submit" id="a_button"> + </form> +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if16.html b/dom/html/test/file_iframe_sandbox_a_if16.html new file mode 100644 index 0000000000..141d3c2b06 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if16.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 886262</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + window.addEventListener("message", receiveMessage); + + function receiveMessage(event) + { + window.parent.parent.postMessage({ok: event.data.ok, desc: "objects containing " + event.data.desc}, "*"); + } +</script> + +<body> +I'm a <frame> inside an iframe which is sandboxed with 'allow-scripts allow-forms' + + <object data="file_iframe_sandbox_a_if15.html"></object> +</body> + +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if17.html b/dom/html/test/file_iframe_sandbox_a_if17.html new file mode 100644 index 0000000000..a736924bf5 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if17.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 886262</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + function doTest() { + var if_18_19 = document.getElementById('if_18_19'); + if_18_19.sandbox = "allow-scripts allow-same-origin"; + if_18_19.contentWindow.postMessage("go", "*"); + } +</script> + +<body onload="doTest()"> + I am sandboxed but with "allow-scripts". I change the sandbox flags on if_18_19 to + "allow-scripts allow-same-origin" then get it to re-navigate itself to + file_iframe_sandbox_a_if18.html, which attemps to call a function in my parent. + This should fail since my sandbox flags should be copied to it when the sandbox + flags are changed. + + <iframe sandbox="allow-scripts" id="if_18_19" src="file_iframe_sandbox_a_if19.html" height="10" width="10"></iframe> +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if18.html b/dom/html/test/file_iframe_sandbox_a_if18.html new file mode 100644 index 0000000000..bbe90970d4 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if18.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 886262</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + function doTest() { + try { + window.parent.parent.ok_wrapper(false, "an iframe in an iframe SHOULD copy its parent's sandbox flags when its sandbox flags are changed"); + } + catch (e) { + window.parent.parent.postMessage({ok: true, desc: "an iframe in an iframe copies its parent's sandbox flags when its sandbox flags are changed"}, "*"); + } + } +</script> + +<body onload="doTest()"> + I'm an iframe whose sandbox flags have been changed to include allow-same-origin. + I should not be able to call a function in my parent's parent because my parent's + iframe does not have allow-same-origin set. +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if19.html b/dom/html/test/file_iframe_sandbox_a_if19.html new file mode 100644 index 0000000000..e4d3d68887 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if19.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 886262</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<script> + window.addEventListener("message", function(e){ + window.open("file_iframe_sandbox_a_if18.html", "_self"); + }); +</script> + +<body> + I'm just here to navigate to file_iframe_sandbox_a_if18.html after my owning + iframe has had allow-same-origin added. +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if2.html b/dom/html/test/file_iframe_sandbox_a_if2.html new file mode 100644 index 0000000000..72bde69e41 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if2.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="text/javascript"> +function doStuff() { + // should NOT be able to execute scripts + window.parent.parent.postMessage({ok: false, desc: "a document within an iframe sandboxed with sandbox='' should NOT be able to execute scripts"}, "*"); +} +</script> + +<body onLoad="doStuff()"> + I am NOT sandboxed or am sandboxed with "allow-scripts" but am contained within an iframe sandboxed with sandbox = "" + or am sandboxed with sandbox='' inside an iframe sandboxed with "allow-scripts" +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if3.html b/dom/html/test/file_iframe_sandbox_a_if3.html new file mode 100644 index 0000000000..899c2f1093 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if3.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<script type="text/javascript"> + function ok_wrapper(condition, msg) { + window.parent.ok_wrapper(condition, msg); + } +</script> + +<body> + I am sandboxed but with "allow-scripts" + + <iframe id='if_4' src='file_iframe_sandbox_a_if4.html' height="10" width="10"></iframe> + <iframe id='if_7' src='file_iframe_sandbox_a_if7.html' height="10" width="10"></iframe> + <iframe id='if_2' sandbox='' src='file_iframe_sandbox_a_if2.html' height="10" width="10"></iframe> +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if4.html b/dom/html/test/file_iframe_sandbox_a_if4.html new file mode 100644 index 0000000000..a216fb572a --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if4.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<script type="text/javascript"> +function doStuff() { + try { + window.parent.ok_wrapper(false, "a document contained within a sandboxed document without 'allow-same-origin' should NOT be same domain with its parent"); + } catch(e) { + window.parent.parent.postMessage({type: "ok", ok: true, desc: "a document contained within a sandboxed document without 'allow-same-origin' should NOT be same domain with its parent"}, "*"); + } + + try { + window.parent.parent.ok_wrapper(false, "a document contained within a sandboxed document without 'allow-same-origin' should NOT be same domain with the top level"); + } catch(e) { + window.parent.parent.postMessage({type: "ok", ok: true, desc: "a document contained within a sandboxed document without 'allow-same-origin' should NOT be same domain with the top level"}, "*"); + } +} +</script> + +<body onLoad="doStuff()"> + I am not sandboxed but contained within a sandboxed document with 'allow-scripts' +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if5.html b/dom/html/test/file_iframe_sandbox_a_if5.html new file mode 100644 index 0000000000..c1081c5039 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if5.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<script type="text/javascript"> + function ok_wrapper(result, desc) { + window.parent.ok_wrapper(result, desc); + } +</script> + +<body> + I am sandboxed but with "allow-scripts allow-same-origin" + + <iframe sandbox='allow-scripts allow-same-origin' id='if_6' src='file_iframe_sandbox_a_if6.html' height="10" width="10"></iframe> +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if6.html b/dom/html/test/file_iframe_sandbox_a_if6.html new file mode 100644 index 0000000000..62a7114316 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if6.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<script type="text/javascript"> +function doStuff() { + window.parent.ok_wrapper(true, "a document sandboxed with 'allow-same-origin' and contained within a sandboxed document with 'allow-same-origin' should be same domain with its parent"); + window.parent.parent.ok_wrapper(true, "a document sandboxed with 'allow-same-origin' contained within a sandboxed document with 'allow-same-origin' should be same domain with the top level"); +} +</script> + +<body onLoad="doStuff()"> + I am sandboxed with 'allow-scripts allow-same-origin' and contained within a sandboxed document with 'allow-scripts allow-same-origin' +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if7.html b/dom/html/test/file_iframe_sandbox_a_if7.html new file mode 100644 index 0000000000..6480eebdba --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if7.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="text/javascript"> +function doStuff() { + // should be able to execute scripts + window.parent.parent.postMessage({ok: true, desc: "a document contained within an iframe contained within an iframe sandboxed with 'allow-scripts' should be able to execute scripts"}, "*"); +} +</script> + +<body onLoad="doStuff()"> + I am NOT sandboxed but am contained within an iframe contained within an iframe sandboxed with sandbox = "allow-scripts" +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if8.html b/dom/html/test/file_iframe_sandbox_a_if8.html new file mode 100644 index 0000000000..87748f542a --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if8.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script> +function doSubload() { + var if_9 = document.getElementById('if_9'); + if_9.src = 'file_iframe_sandbox_a_if9.html'; +} + +window.doSubload = doSubload; + +</script> +<body> + I am sandboxed but with "allow-scripts allow-same-origin". After my initial load, "allow-same-origin" is removed + and then I load file_iframe_sandbox_a_if9.html, which attemps to call a function in window.top. This should + succeed since the new sandbox flags shouldn't have taken affect on me until I'm reloaded. + + <iframe id='if_9' src='about:blank' height="10" width="10"></iframe> +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_a_if9.html b/dom/html/test/file_iframe_sandbox_a_if9.html new file mode 100644 index 0000000000..da2bcf1fa3 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_a_if9.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +function doStuff() { + window.parent.parent.ok_wrapper(true, "a subloaded document should inherit the flags of the document, not of the docshell/sandbox attribute"); +} +</script> +<body onload='doStuff()'> + I'm a subloaded document of file_iframe_sandbox_a_if8.html. I should be able to call a function in window.top + because I should be same-origin with it. +</body> +</html> + diff --git a/dom/html/test/file_iframe_sandbox_b_if1.html b/dom/html/test/file_iframe_sandbox_b_if1.html new file mode 100644 index 0000000000..a65cbec6b9 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_b_if1.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + I am sandboxed without any permissions +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_b_if2.html b/dom/html/test/file_iframe_sandbox_b_if2.html new file mode 100644 index 0000000000..08e7453574 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_b_if2.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> + function ok(condition, msg) { + window.parent.ok_wrapper(condition, msg); + } + + function testXHR() { + var xhr = new XMLHttpRequest(); + + xhr.open("GET", "file_iframe_sandbox_b_if1.html"); + + xhr.onreadystatechange = function (oEvent) { + var result = false; + if (xhr.readyState == 4) { + if (xhr.status == 200) { + result = true; + } + ok(result, "XHR should work normally in an iframe sandboxed with 'allow-same-origin'"); + } + } + + xhr.send(null); + } + + function doStuff() { + ok(true, "documents sandboxed with 'allow-same-origin' should be able to access their parent"); + + // should be able to access document.cookie since we have 'allow-same-origin' + ok(document.cookie == "", "a document sandboxed with allow-same-origin should be able to access document.cookie"); + + // should be able to access localStorage since we have 'allow-same-origin' + ok(window.localStorage, "a document sandboxed with allow-same-origin should be able to access localStorage"); + + // should be able to access sessionStorage since we have 'allow-same-origin' + ok(window.sessionStorage, "a document sandboxed with allow-same-origin should be able to access sessionStorage"); + + testXHR(); + } +</script> +<body onLoad="doStuff()"> + I am sandboxed but with "allow-same-origin" and "allow-scripts" +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_b_if3.html b/dom/html/test/file_iframe_sandbox_b_if3.html new file mode 100644 index 0000000000..350e2ac472 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_b_if3.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> + function ok(result, message) { + window.parent.postMessage({ok: result, desc: message}, "*"); + } + + function testXHR() { + // Standard URL should be blocked as we have a unique origin. + var xhr = new XMLHttpRequest(); + xhr.open("GET", "file_iframe_sandbox_b_if1.html"); + xhr.onreadystatechange = function (oEvent) { + var result = false; + if (xhr.readyState == 4) { + if (xhr.status == 0) { + result = true; + } + ok(result, "XHR should be blocked in an iframe sandboxed WITHOUT 'allow-same-origin'"); + } + } + xhr.send(null); + + // Blob URL should work as it will have our unique origin. + var blobXhr = new XMLHttpRequest(); + var blobUrl = URL.createObjectURL(new Blob(["wibble"], {type: "text/plain"})); + blobXhr.open("GET", blobUrl); + blobXhr.onreadystatechange = function () { + if (this.readyState == 4) { + ok(this.status == 200 && this.response == "wibble", "XHR for a blob URL created in this document should NOT be blocked in an iframe sandboxed WITHOUT 'allow-same-origin'"); + } + } + try { + blobXhr.send(); + } catch(e) { + ok(false, "failed to send XHR for blob URL: error: " + e); + } + + // Data URL should work as it inherits the loader's origin. + var dataXhr = new XMLHttpRequest(); + dataXhr.open("GET", "data:text/html,wibble"); + dataXhr.onreadystatechange = function () { + if (this.readyState == 4) { + ok(this.status == 200 && this.response == "wibble", "XHR for a data URL should NOT be blocked in an iframe sandboxed WITHOUT 'allow-same-origin'"); + } + } + try { + dataXhr.send(); + } catch(e) { + ok(false, "failed to send XHR for data URL: error: " + e); + } + } + + function doStuff() { + try { + window.parent.ok(false, "documents sandboxed without 'allow-same-origin' should NOT be able to access their parent"); + } catch (error) { + ok(true, "documents sandboxed without 'allow-same-origin' should NOT be able to access their parent"); + } + + // should NOT be able to access document.cookie + try { + var foo = document.cookie; + } catch(error) { + ok(true, "a document sandboxed without allow-same-origin should NOT be able to access document.cookie"); + } + + // should NOT be able to access localStorage + try { + var foo = window.localStorage; + } catch(error) { + ok(true, "a document sandboxed without allow-same-origin should NOT be able to access localStorage"); + } + + // should NOT be able to access sessionStorage + try { + var foo = window.sessionStorage; + } catch(error) { + ok(true, "a document sandboxed without allow-same-origin should NOT be able to access sessionStorage"); + } + + testXHR(); + } +</script> +<body onLoad="doStuff()"> + I am sandboxed but with "allow-scripts" +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_c_if1.html b/dom/html/test/file_iframe_sandbox_c_if1.html new file mode 100644 index 0000000000..c2fbf136ae --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_c_if1.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc}, "*"); + } + + function doStuff() { + ok(true, "documents sandboxed with allow-scripts should be able to run inline scripts"); + + document.getElementById('a_form').submit(); + + // trigger the javascript: url test + sendMouseEvent({type:'click'}, 'a_link'); + } +</script> +<script src='file_iframe_sandbox_pass.js'></script> +<body onLoad='ok(true, "documents sandboxed with allow-scripts should be able to run script from event listeners");doStuff();'> + I am sandboxed but with "allow-scripts" + + <form method="get" action="file_iframe_sandbox_form_fail.html" id="a_form"> + First name: <input type="text" name="firstname"> + Last name: <input type="text" name="lastname"> + <input type="submit" onclick="doSubmit()" id="a_button"> + </form> + + <a href = 'javascript:ok(true, "documents sandboxed with allow-scripts should be able to run script from javascript: URLs");' id='a_link'>click me</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_c_if2.html b/dom/html/test/file_iframe_sandbox_c_if2.html new file mode 100644 index 0000000000..1ea8a90ca3 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_c_if2.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc}, "*"); + } + + function doStuff() { + ok(false, "documents sandboxed without allow-scripts should NOT be able to run inline scripts"); + } +</script> +<script src='file_iframe_sandbox_fail.js'></script> +<body onLoad='window.parent.postmessage({ok: false, desc: "documents sandboxed without allow-scripts should NOT be able to run script from event handlers"}, "*");doStuff();'> + I am sandboxed with no permissions + <img src="about:blank" onerror='ok(false, "documents sandboxed without allow-scripts should NOT be able to run script from event handlers");')> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_c_if3.html b/dom/html/test/file_iframe_sandbox_c_if3.html new file mode 100644 index 0000000000..fdf98d93d4 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_c_if3.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +</head> +<script type="text/javascript"> + function doStuff() { + dump("*** c_if3 has loaded\n"); + // try and submit the form - this should succeed + document.getElementById('a_form').submit(); + } +</script> +<body onLoad="doStuff()"> + I am sandboxed but with "allow-scripts allow-forms" + + <form method="get" action="file_iframe_sandbox_form_pass.html" id="a_form"> + First name: <input type="text" name="firstname"> + Last name: <input type="text" name="lastname"> + <input type="submit" onclick="doSubmit()" id="a_button"> + </form> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_c_if4.html b/dom/html/test/file_iframe_sandbox_c_if4.html new file mode 100644 index 0000000000..ee2438f28a --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_c_if4.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.ok_wrapper(result, desc); + } + + function doStuff() { + // try to open a new window via target="_blank", target="BC341604", and window.open() + // the window we try to open closes itself once it opens + sendMouseEvent({type:'click'}, 'target_blank'); + sendMouseEvent({type:'click'}, 'target_BC341604'); + + var threw = false; + try { + window.open("about:blank"); + } catch (error) { + threw = true; + } + + ok(threw, "window.open threw a JS exception and was not allowed"); + } +</script> +<body onLoad="doStuff()"> + I am sandboxed but with "allow-scripts allow-same-origin" + + <a href="file_iframe_sandbox_open_window_fail.html" target="_blank" id="target_blank" rel="opener">open window</a> + <a href="file_iframe_sandbox_open_window_fail.html" target="BC341604" id="target_BC341604">open window</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_c_if5.html b/dom/html/test/file_iframe_sandbox_c_if5.html new file mode 100644 index 0000000000..bd368de425 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_c_if5.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.ok_wrapper(result, desc); + } +</script> +<body onLoad="doStuff()"> + I am sandboxed but with "allow-same-origin" + + <a href = 'javascript:ok(false, "documents sandboxed without allow-scripts should not be able to run script with javascript: URLs");' id='a_link'>click me</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_c_if6.html b/dom/html/test/file_iframe_sandbox_c_if6.html new file mode 100644 index 0000000000..e5ecf3051e --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_c_if6.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.ok_wrapper(result, desc);
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ ok(true, "a document sandboxed with allow-same-origin and allow-scripts should be same origin with its parent and able to run scripts " +
+ "regardless of what kind of whitespace was used in its sandbox attribute");
+ }
+</script>
+<body onLoad="doStuff()">
+ I am sandboxed but with "allow-same-origin" and "allow-scripts"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if7.html b/dom/html/test/file_iframe_sandbox_c_if7.html new file mode 100644 index 0000000000..b9a55def6f --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_c_if7.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ try {
+ var thing = indexedDB.open("sandbox");
+ ok(false, "documents sandboxed without allow-same-origin should NOT be able to access indexedDB");
+ }
+
+ catch(e) {
+ ok(true, "documents sandboxed without allow-same-origin should NOT be able to access indexedDB");
+ }
+ }
+</script>
+<body onLoad='doStuff();'>
+ I am sandboxed but with "allow-scripts"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if8.html b/dom/html/test/file_iframe_sandbox_c_if8.html new file mode 100644 index 0000000000..d8b8948466 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_c_if8.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="text/javascript">
+ function ok(result, desc) {
+ window.parent.postMessage({ok: result, desc}, "*");
+ }
+
+ function doStuff() {
+ var thing = indexedDB.open("sandbox");
+
+ thing.onerror = function(event) {
+ ok(false, "documents sandboxed with allow-same-origin SHOULD be able to access indexedDB");
+ };
+ thing.onsuccess = function(event) {
+ ok(true, "documents sandboxed with allow-same-origin SHOULD be able to access indexedDB");
+ };
+ }
+</script>
+<body onLoad='doStuff();'>
+ I am sandboxed but with "allow-scripts allow-same-origin"
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_c_if9.html b/dom/html/test/file_iframe_sandbox_c_if9.html new file mode 100644 index 0000000000..0c88a677cb --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_c_if9.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 671389</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + I am + <ul> + <li>sandboxed but with "allow-forms", "allow-pointer-lock", "allow-popups", "allow-same-origin", "allow-scripts", and "allow-top-navigation", </li> + <li>sandboxed but with "allow-same-origin", "allow-scripts", </li> + <li>sandboxed, or </li> + <li>not sandboxed.</li> + </ul> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_close.html b/dom/html/test/file_iframe_sandbox_close.html new file mode 100644 index 0000000000..3b87534978 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_close.html @@ -0,0 +1,3 @@ +<script> + self.close(); +</script> diff --git a/dom/html/test/file_iframe_sandbox_d_if1.html b/dom/html/test/file_iframe_sandbox_d_if1.html new file mode 100644 index 0000000000..744594e813 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if1.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> +function doTest() { + sendMouseEvent({type:'click'}, 'anchor'); +} +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' + + <a href="file_iframe_sandbox_navigation_pass.html?Test 1:%20" target="_self" id='anchor'> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if10.html b/dom/html/test/file_iframe_sandbox_d_if10.html new file mode 100644 index 0000000000..41fb46b586 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if10.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> +function doTest() { + window.parent.postMessage({type: "if_10"}, "*"); +} +</script> +<body onload='doTest()'> + I am sandboxed with 'allow-scripts' +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if11.html b/dom/html/test/file_iframe_sandbox_d_if11.html new file mode 100644 index 0000000000..63880587f5 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if11.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> + +function navigateAway() { + document.getElementById("anchor").click(); +} + +function doTest() { + try { + // this should fail the first time, but work the second + window.parent.ok_wrapper(true, "a document that was loaded, navigated to another document, had 'allow-same-origin' added and then was" + + " navigated back should be same-origin with its parent"); + } catch (e) { + navigateAway(); + } +} + +</script> +<body onload='doTest()'> + I am sandboxed with 'allow-scripts' + <a href='file_iframe_sandbox_d_if12.html' id='anchor'>CLICK ME</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if12.html b/dom/html/test/file_iframe_sandbox_d_if12.html new file mode 100644 index 0000000000..0d7936512e --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if12.html @@ -0,0 +1,16 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> +function doTest() { + window.parent.postMessage({test:'if_11'}, "*"); +} +</script> +<body onload='doTest()'> + I am sandboxed with 'allow-scripts' +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if13.html b/dom/html/test/file_iframe_sandbox_d_if13.html new file mode 100644 index 0000000000..aad330c33c --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if13.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> +window.addEventListener("message", receiveMessage); + +function receiveMessage(event) { + // this message is part of if_11's test + if (event.data.test == 'if_11') { + doIf11TestPart2(); + } +} + +function ok_wrapper(result, msg) { + window.opener.postMessage({ok: result, desc: msg}, "*"); + window.close(); +} + +function doIf11TestPart2() { + var if_11 = document.getElementById('if_11'); + if_11.sandbox = 'allow-scripts allow-same-origin'; + // window.history is no longer cross-origin accessible in gecko. + SpecialPowers.wrap(if_11).contentWindow.history.back(); +} +</script> +<body> + <iframe sandbox='allow-scripts' id="if_11" src="file_iframe_sandbox_d_if11.html" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if14.html b/dom/html/test/file_iframe_sandbox_d_if14.html new file mode 100644 index 0000000000..237a9d704f --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if14.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 838692</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="text/javascript"> + var test20Context = "Test 20: Navigate another window (not opened by us): "; + + function doTest() { + // Try to navigate auxiliary browsing context (window) not opened by us. + // We should not be able to do this as we are sandboxed. + sendMouseEvent({type:'click'}, 'navigate_window'); + window.parent.postMessage({type: "attempted"}, "*"); + + // Try to navigate auxiliary browsing context (window) not opened by us, using window.open(). + // We should not be able to do this as we are sandboxed. + try { + window.open("file_iframe_sandbox_window_navigation_fail.html?" + escape(test20Context), "window_to_navigate2"); + window.parent.postMessage({type: "attempted"}, "*"); + } catch(error) { + window.parent.postMessage({ok: true, desc: test20Context + "as expected, error thrown during window.open(..., \"window_to_navigate2\")"}, "*"); + } + } +</script> + +<body onload="doTest()"> + I am sandboxed but with "allow-scripts allow-same-origin allow-top-navigation". + + <a href="file_iframe_sandbox_window_navigation_fail.html?Test 14: Navigate another window (not opened by us):%20" target="window_to_navigate" id="navigate_window">navigate window</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if15.html b/dom/html/test/file_iframe_sandbox_d_if15.html new file mode 100644 index 0000000000..6c969c8fe1 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if15.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> + I am an unsandboxed iframe. + + <iframe sandbox="allow-same-origin allow-scripts" id="if_16" src="file_iframe_sandbox_d_if16.html" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if16.html b/dom/html/test/file_iframe_sandbox_d_if16.html new file mode 100644 index 0000000000..e50dd97ea0 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if16.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<script type="application/javascript"> +function doTest() { + window.parent.parent.postMessage({type: "attempted"}, "*"); + sendMouseEvent({type:'click'}, 'anchor'); +} +</script> + +<body onload="doTest()"> + I am sandboxed with 'allow-same-origin allow-scripts' + + <a href="file_iframe_sandbox_navigation_fail.html?Test 16: Navigate parent/ancestor by name:%20" target='if_parent' id='anchor'> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if17.html b/dom/html/test/file_iframe_sandbox_d_if17.html new file mode 100644 index 0000000000..047a08137d --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if17.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="application/javascript"> + var testContext = "Test 17: navigate _self with window.open(): "; + + function doTest() { + try { + window.open("file_iframe_sandbox_navigation_pass.html?" + escape(testContext), "_self"); + } catch(error) { + window.parent.postMessage({ok: false, desc: testContext + "error thrown during window.open(..., \"_self\")"}, "*"); + } + } +</script> + +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if18.html b/dom/html/test/file_iframe_sandbox_d_if18.html new file mode 100644 index 0000000000..fdcb4198f4 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if18.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<script type="application/javascript"> + window.addEventListener("message", receiveMessage); + + function receiveMessage(event) { + window.parent.postMessage(event.data, "*"); + } + + var testContext = "Test 18: navigate child with window.open(): "; + + function doTest() { + try { + window.open("file_iframe_sandbox_navigation_pass.html?" + escape(testContext), "foo"); + } catch(error) { + window.parent.postMessage({ok: false, desc: testContext + " error thrown during window.open(..., \"foo\")"}, "*"); + } + } +</script> + +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' + + <iframe name="foo" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if19.html b/dom/html/test/file_iframe_sandbox_d_if19.html new file mode 100644 index 0000000000..d766d26492 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if19.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + I am sandboxed with 'allow-scripts' + + <iframe sandbox="allow-scripts" id="if_20" src="file_iframe_sandbox_d_if20.html" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if2.html b/dom/html/test/file_iframe_sandbox_d_if2.html new file mode 100644 index 0000000000..b45cb975ca --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if2.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> +// needed to forward the message to the main test page +window.addEventListener("message", receiveMessage); + +function receiveMessage(event) { + window.parent.postMessage(event.data, "*"); +} + +function doTest() { + sendMouseEvent({type:'click'}, 'anchor'); +} +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' + + <iframe name="foo" src="file_iframe_sandbox_navigation_start.html" height="10" width="10"></iframe> + + <a href="file_iframe_sandbox_navigation_pass.html?Test 2:%20" target='foo' id='anchor'> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if20.html b/dom/html/test/file_iframe_sandbox_d_if20.html new file mode 100644 index 0000000000..005c4bc823 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if20.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="application/javascript"> + var testContext = "Test 19: navigate _parent with window.open(): "; + + function doTest() { + try { + window.open("file_iframe_sandbox_navigation_fail.html?" + escape(testContext), "_parent"); + window.parent.parent.postMessage({type: "attempted"}, "*"); + } catch(error) { + window.parent.parent.postMessage({ok: true, desc: testContext + "as expected, error thrown during window.open(..., \"_parent\")"}, "*"); + } + } +</script> + +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if21.html b/dom/html/test/file_iframe_sandbox_d_if21.html new file mode 100644 index 0000000000..6d0ab232e0 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if21.html @@ -0,0 +1,14 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> + I am an unsandboxed iframe. + + <iframe sandbox="allow-same-origin allow-scripts" id="if_22" src="file_iframe_sandbox_d_if22.html" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if22.html b/dom/html/test/file_iframe_sandbox_d_if22.html new file mode 100644 index 0000000000..bd27157926 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if22.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="application/javascript"> + var testContext = "Test 21: navigate parent by name with window.open(): "; + + function doTest() { + try { + window.open("file_iframe_sandbox_navigation_fail.html?" + escape(testContext), "if_parent2"); + window.parent.parent.postMessage({type: "attempted"}, "*"); + } catch(error) { + window.parent.parent.postMessage({ok: true, desc: testContext + "as expected, error thrown during window.open(..., \"if_parent2\")"}, "*"); + } + } +</script> + +<body onload="doTest()"> + I am sandboxed with 'allow-same-origin allow-scripts' +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if23.html b/dom/html/test/file_iframe_sandbox_d_if23.html new file mode 100644 index 0000000000..e755511e37 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if23.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="application/javascript"> + var test27Context = "Test 27: navigate opened window by name with anchor: "; + var test28Context = "Test 28: navigate opened window by name with window.open(): "; + + var windowsToClose = new Array(); + + function closeWindows() { + for (var i = 0; i < windowsToClose.length; i++) { + windowsToClose[i].close(); + } + } + + // Add message listener to forward messages on to parent + window.addEventListener("message", receiveMessage); + + function receiveMessage(event) { + switch (event.data.type) { + case "closeWindows": + closeWindows(); + break; + default: + window.parent.postMessage(event.data, "*"); + } + } + + function doTest() { + try { + windowsToClose.push(window.open("about:blank", "test27window")); + var test27Anchor = document.getElementById("test27Anchor"); + test27Anchor.href = "file_iframe_sandbox_window_navigation_pass.html?" + escape(test27Context); + sendMouseEvent({type:"click"}, "test27Anchor"); + window.parent.postMessage({type: "attempted"}, "*"); + } catch(error) { + window.parent.postMessage({ok: false, desc: test27Context + "error thrown during window.open(): " + error}, "*"); + } + + try { + windowsToClose.push(window.open("about:blank", "test28window")); + window.open("file_iframe_sandbox_window_navigation_pass.html?" + escape(test28Context), "test28window"); + window.parent.postMessage({type: "attempted"}, "*"); + } catch(error) { + window.parent.postMessage({ok: false, desc: test28Context + "error thrown during window.open(): " + error}, "*"); + } + } +</script> + +<body onload="doTest()"> + I am sandboxed with 'allow-scripts allow-popups' + + <a id="test27Anchor" target="test27window">Test 27 anchor</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if3.html b/dom/html/test/file_iframe_sandbox_d_if3.html new file mode 100644 index 0000000000..cd2d53bce9 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if3.html @@ -0,0 +1,13 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + I am sandboxed with 'allow-scripts' + + <iframe sandbox="allow-scripts" id="if_4" src="file_iframe_sandbox_d_if4.html" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if4.html b/dom/html/test/file_iframe_sandbox_d_if4.html new file mode 100644 index 0000000000..c11a414551 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if4.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> +function doTest() { + window.parent.parent.postMessage({type: "attempted"}, "*"); + sendMouseEvent({type:'click'}, 'anchor'); +} +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' + + <a href="file_iframe_sandbox_navigation_fail.html" target='_parent' id='anchor'> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if5.html b/dom/html/test/file_iframe_sandbox_d_if5.html new file mode 100644 index 0000000000..d8fe4289af --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if5.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> +function doTest() { + window.parent.postMessage({type: "attempted"}, "*"); + sendMouseEvent({type:'click'}, 'anchor'); +} +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts allow-same-origin' + + <a href="file_iframe_sandbox_navigation_fail.html?Test 4: Navigate sibling iframe by name:%20" target='if_sibling' id='anchor'> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if6.html b/dom/html/test/file_iframe_sandbox_d_if6.html new file mode 100644 index 0000000000..9bb48cbb20 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if6.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> +function doTest() { + sendMouseEvent({type:'click'}, 'anchor'); +} +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' + + <a href="file_iframe_sandbox_d_if7.html" target='_self' id='anchor'> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if7.html b/dom/html/test/file_iframe_sandbox_d_if7.html new file mode 100644 index 0000000000..5023ee0294 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if7.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> +function doTest() { + try { + window.parent.ok_wrapper(false, "a sandboxed document when navigated should still NOT be same-origin with its parent"); + } catch(error) { + window.parent.postMessage({ok: true, desc: "sandboxed document's attempt to access parent after navigation blocked, as not same-origin."}, "*"); + } +} +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if8.html b/dom/html/test/file_iframe_sandbox_d_if8.html new file mode 100644 index 0000000000..2b4398ef00 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if8.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="application/javascript"> + function doTest() { + window.parent.modify_if_8(); + } +</script> + +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' and 'allow-same-origin' the first time I am loaded, and with 'allow-scripts' the second time +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_d_if9.html b/dom/html/test/file_iframe_sandbox_d_if9.html new file mode 100644 index 0000000000..ee641904fc --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_d_if9.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+function doTest() {
+ window.parent.modify_if_9();
+}
+</script>
+<body onload="doTest()">
+ I am sandboxed with 'allow-scripts' and 'allow-same-origin' the first time I am loaded, and with 'allow-same-origin' the second time
+</body>
+</html>
+
diff --git a/dom/html/test/file_iframe_sandbox_e_if1.html b/dom/html/test/file_iframe_sandbox_e_if1.html new file mode 100644 index 0000000000..e3882dfb28 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if1.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> + +<script> + function doTest() { + var testContext = location.search == "" ? "?Test 10: Navigate _top:%20" : location.search; + document.getElementById("if_6").src = "file_iframe_sandbox_e_if6.html" + testContext; + } +</script> + +<body onload="doTest()"> + <iframe sandbox='allow-scripts' id='if_6' height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if10.html b/dom/html/test/file_iframe_sandbox_e_if10.html new file mode 100644 index 0000000000..2484b8f342 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if10.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + function doTest() { + var testContext = "?Test 23: Nested navigate _top with window.open():%20"; + document.getElementById("if_9").src = "file_iframe_sandbox_e_if9.html" + testContext; + } +</script> + +<body onload="doTest()"> + <iframe sandbox='allow-scripts allow-top-navigation' id='if_9' height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if11.html b/dom/html/test/file_iframe_sandbox_e_if11.html new file mode 100644 index 0000000000..106c4c629b --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if11.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> + function doTest() { + var testContext = location.search.substring(1); + try { + window.open("file_iframe_sandbox_top_navigation_pass.html?" + testContext, "_top"); + } catch(error) { + window.top.opener.postMessage({ok: false, desc: unescape(testContext) + "error thrown during window.open(..., \"_top\")"}, "*"); + window.top.close(); + } + } +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts and allow-top-navigation' +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if12.html b/dom/html/test/file_iframe_sandbox_e_if12.html new file mode 100644 index 0000000000..0b1b87e09b --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if12.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + function doTest() { + var testContext = location.search == "" ? "?Test 24: Navigate _top with window.open():%20" : location.search; + document.getElementById("if_14").src = "file_iframe_sandbox_e_if14.html" + testContext; + } +</script> + +<body onload="doTest()"> + <iframe sandbox='allow-scripts' id='if_14' height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if13.html b/dom/html/test/file_iframe_sandbox_e_if13.html new file mode 100644 index 0000000000..f5cf912f67 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if13.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + function doTest() { + var testContext = "?Test 25: Nested navigate _top with window.open():%20"; + document.getElementById("if_12").src = "file_iframe_sandbox_e_if12.html" + testContext; + } +</script> + +<body onload="doTest()"> + <iframe sandbox='allow-scripts allow-top-navigation' id='if_12' height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if14.html b/dom/html/test/file_iframe_sandbox_e_if14.html new file mode 100644 index 0000000000..76d9787020 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if14.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> + function doTest() { + var testContext = location.search.substring(1); + try { + var topsOpener = window.top.opener; + window.open("file_iframe_sandbox_top_navigation_fail.html?" + testContext, "_top"); + topsOpener.postMessage({ok: false, desc: unescape(testContext) + "top navigation should NOT be allowed by a document sandboxed without 'allow-top-navigation.'"}, "*"); + } catch(error) { + window.top.opener.postMessage({ok: true, desc: unescape(testContext) + "as expected error thrown during window.open(..., \"_top\")"}, "*"); + window.top.close(); + } + } +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if15.html b/dom/html/test/file_iframe_sandbox_e_if15.html new file mode 100644 index 0000000000..bf4138e1d6 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if15.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + // Set our name, to allow an attempt to navigate us by name. + window.name = "e_if15"; +</script> + +<body> + <iframe sandbox='allow-scripts' id='if_16' src="file_iframe_sandbox_e_if16.html" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if16.html b/dom/html/test/file_iframe_sandbox_e_if16.html new file mode 100644 index 0000000000..06c8bf8714 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if16.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + var testContext = "Test 26: navigate top by name with window.open(): "; + + function doTest() { + try { + var topsOpener = window.top.opener; + window.open("file_iframe_sandbox_top_navigation_fail.html?" + escape(testContext), "e_if15"); + topsOpener.postMessage({ok: false, desc: unescape(testContext) + "top navigation should NOT be allowed by a document sandboxed without 'allow-top-navigation.'"}, "*"); + } catch(error) { + window.top.opener.postMessage({ok: true, desc: testContext + "as expected, error thrown during window.open(..., \"e_if15\")"}, "*"); + window.top.close(); + } + } +</script> + +<body onload="doTest()"> + I am sandboxed but with "allow-scripts" +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if2.html b/dom/html/test/file_iframe_sandbox_e_if2.html new file mode 100644 index 0000000000..739dbacbd5 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if2.html @@ -0,0 +1,12 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> + <iframe sandbox='allow-scripts allow-top-navigation allow-same-origin' id='if_1' src="file_iframe_sandbox_e_if1.html?Test 11: Nested navigate _top:%20" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if3.html b/dom/html/test/file_iframe_sandbox_e_if3.html new file mode 100644 index 0000000000..ce010e6893 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if3.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <iframe sandbox='allow-scripts allow-top-navigation' id='if_5' src="file_iframe_sandbox_e_if5.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if4.html b/dom/html/test/file_iframe_sandbox_e_if4.html new file mode 100644 index 0000000000..740a33a94d --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if4.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <iframe sandbox='allow-scripts allow-top-navigation' id='if_3' src="file_iframe_sandbox_e_if3.html" height="10" width="10"></iframe>
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_e_if5.html b/dom/html/test/file_iframe_sandbox_e_if5.html new file mode 100644 index 0000000000..e550df45e5 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if5.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> +function navigateAway() { + document.getElementById("anchor").click(); +} +</script> +<body onload="navigateAway()"> + I am sandboxed with 'allow-scripts and allow-top-navigation' + + <a href="file_iframe_sandbox_top_navigation_pass.html" target='_top' id='anchor'>Click me</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if6.html b/dom/html/test/file_iframe_sandbox_e_if6.html new file mode 100644 index 0000000000..399c3c202b --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if6.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> +function doTest() { + document.getElementById('anchor').href = "file_iframe_sandbox_top_navigation_fail.html" + location.search; + window.top.opener.postMessage({type: "attempted"}, "*"); + sendMouseEvent({type:'click'}, 'anchor'); +} +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts' + + <a target='_top' id='anchor'> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if7.html b/dom/html/test/file_iframe_sandbox_e_if7.html new file mode 100644 index 0000000000..9d60ed2dbc --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if7.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + // Set our name, to allow an attempt to navigate us by name. + window.name = "e_if7"; +</script> + +<body> + <iframe sandbox='allow-scripts' id='if_8' src="file_iframe_sandbox_e_if8.html" height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if8.html b/dom/html/test/file_iframe_sandbox_e_if8.html new file mode 100644 index 0000000000..97699abba9 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if8.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<script> + function doTest() { + // Try to navigate top using its name (e_if7). We should not be able to do this as allow-top-navigation is not specified. + window.top.opener.postMessage({type: "attempted"}, "*"); + sendMouseEvent({type:'click'}, 'navigate_top'); + } +</script> + +<body onload="doTest()"> + I am sandboxed but with "allow-scripts" + + <a href="file_iframe_sandbox_top_navigation_fail.html?Test 15: Navigate top by name:%20" target="e_if7" id="navigate_top">navigate top</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_e_if9.html b/dom/html/test/file_iframe_sandbox_e_if9.html new file mode 100644 index 0000000000..f18a16dba6 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_e_if9.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + function doTest() { + var testContext = location.search == "" ? "?Test 22: Navigate _top with window.open():%20" : location.search; + document.getElementById("if_11").src = "file_iframe_sandbox_e_if11.html" + testContext; + } +</script> + +<body onload="doTest()"> + <iframe sandbox='allow-scripts allow-top-navigation' id='if_11' height="10" width="10"></iframe> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_fail.js b/dom/html/test/file_iframe_sandbox_fail.js new file mode 100644 index 0000000000..1f1290d046 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_fail.js @@ -0,0 +1,4 @@ +ok( + false, + "documents sandboxed with allow-scripts should NOT be able to run <script src=...>" +); diff --git a/dom/html/test/file_iframe_sandbox_form_fail.html b/dom/html/test/file_iframe_sandbox_form_fail.html new file mode 100644 index 0000000000..6976ced8ad --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_form_fail.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body onLoad="doStuff()"> + I should NOT be loaded by a form submit from a sandbox without 'allow-forms' +</body> +</html> + +<script> + function doStuff() { + window.parent.postMessage({ok: false, desc: "documents sandboxed without allow-forms should NOT be able to submit forms"}, "*"); + } +</script>
\ No newline at end of file diff --git a/dom/html/test/file_iframe_sandbox_form_pass.html b/dom/html/test/file_iframe_sandbox_form_pass.html new file mode 100644 index 0000000000..1ba8853fa5 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_form_pass.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> +</head> + +<body onLoad="doStuff()"> + I should be loaded by a form submit from a sandbox with 'allow-forms' +</body> +</html> + +<script> + function doStuff() { + window.parent.postMessage({ok: true, desc: "documents sandboxed with allow-forms should be able to submit forms"}, "*"); + } +</script>
\ No newline at end of file diff --git a/dom/html/test/file_iframe_sandbox_g_if1.html b/dom/html/test/file_iframe_sandbox_g_if1.html new file mode 100644 index 0000000000..67604f1f64 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_g_if1.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.postMessage({ok: result, desc}, "*"); + } + + function doStuff() { + // test data URI + + // self.onmessage = function(event) { + // self.postMessage('make it so'); + // }; + var data_url = "data:text/plain;charset=utf-8;base64,c2VsZi5vbm1lc3NhZ2UgPSBmdW5jdGlvbihldmVudCkgeyAgDQogICAgc2VsZi5wb3N0TWVzc2FnZSgnbWFrZSBpdCBzbycpOyAgDQp9Ow=="; + var worker_data = new Worker(data_url); + worker_data.addEventListener('message', function(event) { + ok(true, "a worker in a sandboxed document should be able to be loaded from a data: URI"); + }); + + worker_data.postMessage("engage!"); + + // test a blob URI we created (will have the same null principal + // as us + var b = new Blob(["onmessage = function(event) { self.postMessage('make it so');};"]); + + var blobURL = URL.createObjectURL(b); + + var worker_blob = new Worker(blobURL); + + worker_blob.addEventListener('message', function(event) { + ok(true, "a worker in a sandboxed document should be able to be loaded from a blob URI " + + "created by that sandboxed document"); + }); + + worker_blob.postMessage("engage!"); + + // test loading with relative url - this should fail since we are + // sandboxed and have a null principal + var worker_js = new Worker('file_iframe_sandbox_worker.js'); + worker_js.onerror = function(error) { + ok(true, "a worker in a sandboxed document should tell the load error via error event"); + } + + worker_js.addEventListener('message', function(event) { + ok(false, "a worker in a sandboxed document should not be able to load from a relative URI"); + }); + + worker_js.postMessage('engage'); + } +</script> +<body onload='doStuff();'> + I am sandboxed but with "allow-scripts" +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_h_if1.html b/dom/html/test/file_iframe_sandbox_h_if1.html new file mode 100644 index 0000000000..7c5cada2dc --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_h_if1.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 766282</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +</head> +<script type="text/javascript"> + function ok(result, desc) { + window.parent.ok_wrapper(result, desc); + } + + function doStuff() { + // Try to open a new window via target="_blank", target="BC766282" and window.open(). + // The window we try to open closes itself once it opens. + sendMouseEvent({type:'click'}, 'target_blank'); + sendMouseEvent({type:'click'}, 'target_BC766282'); + + try { + window.open("file_iframe_sandbox_open_window_pass.html"); + } catch(e) { + ok(false, "Test 3: iframes sandboxed with allow-popups, should be able to open windows"); + } + } +</script> +<body onLoad="doStuff()"> + I am sandboxed but with "allow-popups allow-scripts allow-same-origin" + + <a href="file_iframe_sandbox_open_window_pass.html" target="_blank" rel="opener" id="target_blank">open window</a> + <a href="file_iframe_sandbox_open_window_pass.html?BC766282" target="BC766282" id="target_BC766282">open window</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_k_if1.html b/dom/html/test/file_iframe_sandbox_k_if1.html new file mode 100644 index 0000000000..f6f1238085 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_k_if1.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="text/javascript"> + var windowsToClose = new Array(); + + function closeWindows() { + for (var i = 0; i < windowsToClose.length; i++) { + windowsToClose[i].close(); + } + window.open("file_iframe_sandbox_close.html", "blank_if2"); + window.open("file_iframe_sandbox_close.html", "BC766282_if2"); + } + + // Add message listener to forward messages on to parent + window.addEventListener("message", receiveMessage); + + function receiveMessage(event) { + switch (event.data.type) { + case "closeWindows": + closeWindows(); + break; + } + } + + function doStuff() { + // Open a new window via target="_blank", target="BC766282_if2" and window.open(). + sendMouseEvent({type:'click'}, 'target_blank_if2'); + sendMouseEvent({type:'click'}, 'target_BC766282_if2'); + + windowsToClose.push(window.open("file_iframe_sandbox_k_if2.html")); + } +</script> +<body onLoad="doStuff()"> + I am navigated to from file_iframe_sandbox_k_if8.html. + This was opened in an iframe with "allow-scripts allow-popups allow-same-origin". + However allow-same-origin was removed from the iframe before navigating to me, + so I should only have "allow-scripts allow-popups" in force. + <a href="file_iframe_sandbox_k_if2.html" target="_blank" id="target_blank_if2" rel="opener">open window</a> + <a href="file_iframe_sandbox_k_if2.html" target="BC766282_if2" id="target_BC766282_if2">open window</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_k_if2.html b/dom/html/test/file_iframe_sandbox_k_if2.html new file mode 100644 index 0000000000..dce42aef54 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_k_if2.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="text/javascript"> + if (window.name == "") { + window.name = "blank_if2"; + } + + function ok(result, message) { + window.opener.parent.postMessage({type: "ok", ok: result, desc: message}, "*"); + } + + function doStuff() { + // Check that sandboxed forms browsing context flag copied by attempting to submit a form. + document.getElementById('a_form').submit(); + window.opener.parent.postMessage({type: "attempted"}, "*"); + + // Check that sandboxed origin browsing context flag copied by attempting to access cookies. + try { + var foo = document.cookie; + ok(false, "Sandboxed origin browsing context flag NOT copied to new auxiliary browsing context."); + } catch(error) { + ok(true, "Sandboxed origin browsing context flag copied to new auxiliary browsing context."); + } + + // Check that sandboxed top-level navigation browsing context flag copied. + // if_3 tries to navigate this document. + var if_3 = document.getElementById('if_3'); + if_3.src = "file_iframe_sandbox_k_if3.html"; + } +</script> + +<body onLoad="doStuff()"> + I am not sandboxed directly, but opened from a sandboxed document with 'allow-scripts allow-popups' + + <form method="get" action="file_iframe_sandbox_window_form_fail.html" id="a_form"> + First name: <input type="text" name="firstname"> + Last name: <input type="text" name="lastname"> + <input type="submit" id="a_button"> + </form> + + <iframe id="if_3" src="about:blank" height="10" width="10"></iframe> + +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_k_if3.html b/dom/html/test/file_iframe_sandbox_k_if3.html new file mode 100644 index 0000000000..a2619dd006 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_k_if3.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> +<script type="application/javascript"> + function doTest() { + sendMouseEvent({type:'click'}, 'anchor'); + window.parent.opener.parent.postMessage({type: "attempted"}, "*"); + } +</script> +<body onload="doTest()"> + I am sandboxed with 'allow-scripts allow-popups' + + <a href="file_iframe_sandbox_window_top_navigation_fail.html" target='_top' id='anchor'> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_k_if4.html b/dom/html/test/file_iframe_sandbox_k_if4.html new file mode 100644 index 0000000000..3d030158dc --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_k_if4.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="text/javascript"> + function doStuff() { + // Open a new window via target="_blank", target="BC766282_if5" and window.open(). + sendMouseEvent({type:'click'}, 'target_blank_if5'); + sendMouseEvent({type:'click'}, 'target_BC766282_if5'); + + window.open("file_iframe_sandbox_k_if5.html"); + + // Open a new window via target="_blank", target="BC766282_if7" and window.open(). + sendMouseEvent({type:'click'}, 'target_blank_if7'); + sendMouseEvent({type:'click'}, 'target_BC766282_if7'); + + window.open("file_iframe_sandbox_k_if7.html"); + } +</script> + +<body onLoad="doStuff()"> + I am sandboxed with "allow-scripts allow-popups allow-same-origin allow-forms allow-top-navigation". + <a href="file_iframe_sandbox_k_if5.html" target="_blank" id="target_blank_if5" rel="opener">open window</a> + <a href="file_iframe_sandbox_k_if5.html" target="BC766282_if5" id="target_BC766282_if5">open window</a> + + <a href="file_iframe_sandbox_k_if7.html" target="_blank" id="target_blank_if7" rel="opener">open window</a> + <a href="file_iframe_sandbox_k_if7.html" target="BC766282_if7" id="target_BC766282_if7">open window</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_k_if5.html b/dom/html/test/file_iframe_sandbox_k_if5.html new file mode 100644 index 0000000000..8deb65852f --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_k_if5.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="text/javascript"> + function doStuff() { + // Check that sandboxed origin browsing context flag NOT set by attempting to access cookies. + try { + var foo = document.cookie; + window.opener.parent.ok_wrapper(true, "Sandboxed origin browsing context flag NOT set on new auxiliary browsing context."); + } catch(error) { + window.opener.parent.ok_wrapper(false, "Sandboxed origin browsing context flag set on new auxiliary browsing context."); + } + + // Check that sandboxed top-level navigation browsing context flag NOT set. + // if_6 tries to navigate this document. + var if_6 = document.getElementById('if_6'); + if_6.src = "file_iframe_sandbox_k_if6.html"; + } +</script> + +<body onLoad="doStuff()"> + I am not sandboxed directly, but opened from a sandboxed document with at least + 'allow-scripts allow-popups allow-same-origin allow-top-navigation' + + <iframe id="if_6" src="about:blank" height="10" width="10"></iframe> + +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_k_if6.html b/dom/html/test/file_iframe_sandbox_k_if6.html new file mode 100644 index 0000000000..53ed080e3e --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_k_if6.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/EventUtils.js"></script> +</head> + +<script type="application/javascript"> + function doTest() { + sendMouseEvent({type:'click'}, 'anchor'); + } +</script> + +<body onload="doTest()"> + I am sandboxed with at least 'allow-scripts allow-popups allow-top-navigation' + + <a href="file_iframe_sandbox_window_top_navigation_pass.html" target='_top' id='anchor'> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_k_if7.html b/dom/html/test/file_iframe_sandbox_k_if7.html new file mode 100644 index 0000000000..269e31eb5b --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_k_if7.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="text/javascript"> + function doStuff() { + // Check that sandboxed forms browsing context flag NOT set by attempting to submit a form. + document.getElementById('a_form').submit(); + } +</script> + +<body onLoad="doStuff()"> + I am not sandboxed directly, but opened from a sandboxed document with at least + 'allow-scripts allow-popups allow-forms allow-same-origin' + + <form method="get" action="file_iframe_sandbox_window_form_pass.html" id="a_form"> + First name: <input type="text" name="firstname"> + Last name: <input type="text" name="lastname"> + <input type="submit" id="a_button"> + </form> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_k_if8.html b/dom/html/test/file_iframe_sandbox_k_if8.html new file mode 100644 index 0000000000..e4aad97f3b --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_k_if8.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="text/javascript"> + function doSubOpens() { + // Open a new window via target="_blank", target="BC766282_if9" and window.open(). + sendMouseEvent({type:'click'}, 'target_blank_if9'); + sendMouseEvent({type:'click'}, 'target_BC766282_if9'); + + window.open("file_iframe_sandbox_k_if9.html"); + + sendMouseEvent({type:'click'}, 'target_if1'); + } + + window.doSubOpens = doSubOpens; +</script> + +<body> + I am sandboxed but with "allow-scripts allow-popups allow-same-origin". + After my initial load, "allow-same-origin" is removed and then I open file_iframe_sandbox_k_if9.html + in 3 different ways, which attemps to call a function in my parent. + This should succeed since the new sandbox flags shouldn't have taken affect on me until I'm reloaded. + <a href="file_iframe_sandbox_k_if9.html" target="_blank" id="target_blank_if9" rel="opener">open window</a> + <a href="file_iframe_sandbox_k_if9.html" target="BC766282_if9" id="target_BC766282_if9">open window</a> + + Now navigate to file_iframe_sandbox_k_if1.html to do tests for a sandbox opening a window + when only "allow-scripts allow-popups" are specified. + <a href="file_iframe_sandbox_k_if1.html" id="target_if1">navigate to if1</a> +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_k_if9.html b/dom/html/test/file_iframe_sandbox_k_if9.html new file mode 100644 index 0000000000..56e8db3f9a --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_k_if9.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + function doStuff() { + window.opener.parent.ok_wrapper(true, "A window opened from within a sandboxed document should inherit the flags of the document, not of the docshell/sandbox attribute."); + self.close(); + } +</script> + +<body onload='doStuff()'> + I'm a window opened from the sandboxed document of file_iframe_sandbox_k_if8.html. + I should be able to call ok_wrapper in main test page directly because I should be same-origin with it. +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_navigation_fail.html b/dom/html/test/file_iframe_sandbox_navigation_fail.html new file mode 100644 index 0000000000..bae5276bd1 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_navigation_fail.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onLoad="doStuff()"> +FAIL +</body> +<script> + function doStuff() { + var testContext = unescape(location.search.substring(1)); + window.parent.postMessage({ok: false, desc: testContext + "this navigation should NOT be allowed by a sandboxed document", addToAttempted: false}, "*"); + } +</script> +</html> diff --git a/dom/html/test/file_iframe_sandbox_navigation_pass.html b/dom/html/test/file_iframe_sandbox_navigation_pass.html new file mode 100644 index 0000000000..e07248247b --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_navigation_pass.html @@ -0,0 +1,17 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +function doStuff() { + var testContext = unescape(location.search.substring(1)); + window.parent.postMessage({ok: true, desc: testContext + "this navigation should be allowed by a sandboxed document"}, "*"); +} +</script> +<body onLoad="doStuff()"> +PASS +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_navigation_start.html b/dom/html/test/file_iframe_sandbox_navigation_start.html new file mode 100644 index 0000000000..fa56425177 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_navigation_start.html @@ -0,0 +1,11 @@ +<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+I am just a normal HTML document, probably contained in a sandboxed iframe
+</body>
+</html>
diff --git a/dom/html/test/file_iframe_sandbox_open_window_fail.html b/dom/html/test/file_iframe_sandbox_open_window_fail.html new file mode 100644 index 0000000000..64e0d36180 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_open_window_fail.html @@ -0,0 +1,19 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body onLoad="doStuff()"> + I should NOT be opened by a sandboxed iframe via any method +</body> +</html> + +<script> + function doStuff() { + window.opener.ok(false, "sandboxed documents should NOT be able to open windows"); + self.close(); + } +</script> diff --git a/dom/html/test/file_iframe_sandbox_open_window_pass.html b/dom/html/test/file_iframe_sandbox_open_window_pass.html new file mode 100644 index 0000000000..ac45c7fd32 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_open_window_pass.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body onLoad="doStuff()"> + I should be opened by a sandboxed iframe via any method when "allow-popups" is specified. +</body> +</html> + +<script> + function doStuff() { + // Check that the browsing context's (window's) name is as expected. + var expectedName = location.search.substring(1); + if (expectedName == window.name) { + window.opener.ok(true, "sandboxed documents should be able to open windows when \"allow-popups\" is specified"); + } else { + window.opener.ok(false, "window opened with \"allow-popups\", but expected name was " + expectedName + " and actual was " + window.name); + } + self.close(); + } +</script> diff --git a/dom/html/test/file_iframe_sandbox_pass.js b/dom/html/test/file_iframe_sandbox_pass.js new file mode 100644 index 0000000000..15b3e7d3ff --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_pass.js @@ -0,0 +1,4 @@ +ok( + true, + "documents sandboxed with allow-scripts should be able to run <script src=...>" +); diff --git a/dom/html/test/file_iframe_sandbox_redirect.html b/dom/html/test/file_iframe_sandbox_redirect.html new file mode 100644 index 0000000000..62419d7f46 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_redirect.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<body>redirect</body> diff --git a/dom/html/test/file_iframe_sandbox_redirect.html^headers^ b/dom/html/test/file_iframe_sandbox_redirect.html^headers^ new file mode 100644 index 0000000000..71b739c42a --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_redirect.html^headers^ @@ -0,0 +1,2 @@ +HTTP 301 Moved Permanently +Location: file_iframe_sandbox_redirect_target.html diff --git a/dom/html/test/file_iframe_sandbox_redirect_target.html b/dom/html/test/file_iframe_sandbox_redirect_target.html new file mode 100644 index 0000000000..c134ac0ffd --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_redirect_target.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<head> + <script> + onmessage = function(event) { + parent.postMessage(event.data + " redirect target", "*"); + } + </script> +</head> +<body>I have been redirected</body> diff --git a/dom/html/test/file_iframe_sandbox_refresh.html b/dom/html/test/file_iframe_sandbox_refresh.html new file mode 100644 index 0000000000..1fad80c428 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_refresh.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<body>refresh</body> diff --git a/dom/html/test/file_iframe_sandbox_refresh.html^headers^ b/dom/html/test/file_iframe_sandbox_refresh.html^headers^ new file mode 100644 index 0000000000..a7cc383b4f --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_refresh.html^headers^ @@ -0,0 +1 @@ +Refresh: 0 url=data:text/html,Refreshed diff --git a/dom/html/test/file_iframe_sandbox_srcdoc_allow_scripts.html b/dom/html/test/file_iframe_sandbox_srcdoc_allow_scripts.html new file mode 100644 index 0000000000..7d585be04f --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_srcdoc_allow_scripts.html @@ -0,0 +1 @@ +<script>parent.parent.ok_wrapper(true, "an object inside an iframe sandboxed with allow-scripts allow-same-origin should be able to run scripts and call functions in the parent of the iframe")</script> diff --git a/dom/html/test/file_iframe_sandbox_srcdoc_no_allow_scripts.html b/dom/html/test/file_iframe_sandbox_srcdoc_no_allow_scripts.html new file mode 100644 index 0000000000..b6faf83cc9 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_srcdoc_no_allow_scripts.html @@ -0,0 +1 @@ +<script>parent.parent.ok_wrapper(false, 'an object inside an iframe sandboxed with only allow-same-origin should not be able to run scripts')</script> diff --git a/dom/html/test/file_iframe_sandbox_top_navigation_fail.html b/dom/html/test/file_iframe_sandbox_top_navigation_fail.html new file mode 100644 index 0000000000..dad6b2c006 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_top_navigation_fail.html @@ -0,0 +1,18 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +function doStuff() { + var testContext = unescape(location.search.substring(1)); + window.opener.postMessage({ok: false, desc: testContext + "top navigation should NOT be allowed by a document sandboxed without 'allow-top-navigation'", addToAttempted: false}, "*"); + window.close(); +} +</script> +<body onLoad="doStuff()"> +FAIL\ +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_top_navigation_pass.html b/dom/html/test/file_iframe_sandbox_top_navigation_pass.html new file mode 100644 index 0000000000..712240ecb2 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_top_navigation_pass.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> +function doStuff() { + var testContext = unescape(location.search.substring(1)); + var bc = new BroadcastChannel("test_iframe_sandbox_navigation"); + bc.postMessage({ok: true, desc: testContext + "top navigation should be allowed by a document sandboxed with 'allow-top-navigation'"}); + bc.close(); + window.close(); +} +</script> +<body onLoad="doStuff()"> +PASS +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_window_form_fail.html b/dom/html/test/file_iframe_sandbox_window_form_fail.html new file mode 100644 index 0000000000..2d678b3ac9 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_window_form_fail.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body onLoad="doStuff()"> + I should NOT be loaded by a form submit from a window opened from a sandbox without 'allow-forms'. +</body> +</html> + +<script> + function doStuff() { + window.opener.parent.postMessage({ok: false, desc: "documents sandboxed without allow-forms should NOT be able to submit forms"}, "*"); + + self.close(); + } +</script> diff --git a/dom/html/test/file_iframe_sandbox_window_form_pass.html b/dom/html/test/file_iframe_sandbox_window_form_pass.html new file mode 100644 index 0000000000..dd2656c1ec --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_window_form_pass.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + function doStuff() { + window.opener.parent.ok_wrapper(true, "Sandboxed forms browsing context flag NOT set on new auxiliary browsing context."); + + self.close(); + } +</script> + +<body onLoad="doStuff()"> + I should be loaded by a form submit from a window opened from a sandbox with 'allow-forms allow-same-origin'. +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_window_navigation_fail.html b/dom/html/test/file_iframe_sandbox_window_navigation_fail.html new file mode 100644 index 0000000000..f8e3c83ce8 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_window_navigation_fail.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 838692</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> +function doStuff() { + var testContext = unescape(location.search.substring(1)); + window.opener.postMessage({ok: false, desc: testContext + "a sandboxed document should not be able to navigate a window it hasn't opened.", addToAttempted: false}, "*"); + window.close(); +} +</script> + +<body onLoad="doStuff()"> +FAIL +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_window_navigation_pass.html b/dom/html/test/file_iframe_sandbox_window_navigation_pass.html new file mode 100644 index 0000000000..a1bff9eb83 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_window_navigation_pass.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> +function doStuff() { + var testContext = unescape(location.search.substring(1)); + window.opener.postMessage({type: "ok", ok: true, desc: testContext + "a permitted sandboxed document should be able to navigate a window it has opened.", addToAttempted: false}, "*"); + window.close(); +} +</script> + +<body onLoad="doStuff()"> +PASS +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_window_top_navigation_fail.html b/dom/html/test/file_iframe_sandbox_window_top_navigation_fail.html new file mode 100644 index 0000000000..af50476045 --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_window_top_navigation_fail.html @@ -0,0 +1,24 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script> + function doStuff() { + window.opener.parent.postMessage({ok: false, desc: "Sandboxed top-level navigation browsing context flag NOT copied to new auxiliary browsing context."}, "*"); + + // Check that when no browsing context returned by "target='_top'", a new browsing context isn't opened by mistake. + try { + window.opener.parent.opener.parent.postMessage({ok: false, desc: "An attempt at top navigation without 'allow-top-navigation' should not have opened a new browsing context."}, "*"); + } catch (error) { + } + + self.close(); + } +</script> +<body onLoad="doStuff()"> +FAIL +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_window_top_navigation_pass.html b/dom/html/test/file_iframe_sandbox_window_top_navigation_pass.html new file mode 100644 index 0000000000..d3637fb04e --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_window_top_navigation_pass.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test for Bug 766282</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script> + function doStuff() { + window.opener.parent.ok_wrapper(true, "Sandboxed top-level navigation browsing context flag NOT copied to new auxiliary browsing context."); + + self.close(); + } +</script> + +<body onLoad="doStuff()"> + I am navigated to from a window opened from a sandbox with allow-top-navigation. +</body> +</html> diff --git a/dom/html/test/file_iframe_sandbox_worker.js b/dom/html/test/file_iframe_sandbox_worker.js new file mode 100644 index 0000000000..3cb9f650dc --- /dev/null +++ b/dom/html/test/file_iframe_sandbox_worker.js @@ -0,0 +1,3 @@ +self.onmessage = function (event) { + self.postMessage("make it so"); +}; diff --git a/dom/html/test/file_refresh_after_document_write.html b/dom/html/test/file_refresh_after_document_write.html new file mode 100644 index 0000000000..ebf3272e08 --- /dev/null +++ b/dom/html/test/file_refresh_after_document_write.html @@ -0,0 +1,15 @@ +<html> +<head> +<title></title> +</head> +<script> +function write_and_refresh(){ + document.write("This could be anything"); + location.reload(); +} +</script> +<body> +<button id='test_btn' onclick='write_and_refresh()'> +</body> + +</html> diff --git a/dom/html/test/file_script_module.html b/dom/html/test/file_script_module.html new file mode 100644 index 0000000000..78c4992654 --- /dev/null +++ b/dom/html/test/file_script_module.html @@ -0,0 +1,42 @@ +<html> +<body> + <script> +// Helper methods. +function ok(a, msg) { + parent.postMessage({ check: !!a, msg }, "*") +} + +function is(a, b, msg) { + ok(a === b, msg); +} + +function finish() { + parent.postMessage({ done: true }, "*"); +} + </script> + + <script id="a" nomodule>42</script> + <script id="b">42</script> + <script> +// Let's test the behavior of nomodule attribute and noModule getter/setter. +var a = document.getElementById("a"); +is(a.noModule, true, "HTMLScriptElement with nomodule attribute has noModule set to true"); +a.removeAttribute("nomodule"); +is(a.noModule, false, "HTMLScriptElement without nomodule attribute has noModule set to false"); +a.noModule = true; +ok(a.hasAttribute('nomodule'), "HTMLScriptElement.noModule = true add the nomodule attribute"); + +var b = document.getElementById("b"); +is(b.noModule, false, "HTMLScriptElement without nomodule attribute has noModule set to false"); +b.noModule = true; +ok(b.hasAttribute('nomodule'), "HTMLScriptElement.noModule = true add the nomodule attribute"); + </script> + + <script>var foo = 42;</script> + <script nomodule>foo = 43;</script> + <script> +is(foo, 42, "nomodule HTMLScriptElements should not be executed in modern browsers"); +finish(); + </script> +</body> +</html> diff --git a/dom/html/test/file_srcdoc-2.html b/dom/html/test/file_srcdoc-2.html new file mode 100644 index 0000000000..bd75f5e059 --- /dev/null +++ b/dom/html/test/file_srcdoc-2.html @@ -0,0 +1,10 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=802895 +--> +<body> +<iframe id="iframe" srcdoc="Hello World"></iframe> +</body> + +</html> diff --git a/dom/html/test/file_srcdoc.html b/dom/html/test/file_srcdoc.html new file mode 100644 index 0000000000..7f084bc74b --- /dev/null +++ b/dom/html/test/file_srcdoc.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=802895 +--> +<body> +<iframe id="iframe" srcdoc="Hello World"></iframe> + +<iframe id="iframe1" src="about:mozilla" + srcdoc="Goodbye World"></iframe> +<iframe id="iframe2" srcdoc="Peeking test" sandbox=""></iframe> +<iframe id="iframe3" src="file_srcdoc_iframe3.html" + srcdoc="Going"></iframe> +</body> + +</html> diff --git a/dom/html/test/file_srcdoc_iframe3.html b/dom/html/test/file_srcdoc_iframe3.html new file mode 100644 index 0000000000..233692734f --- /dev/null +++ b/dom/html/test/file_srcdoc_iframe3.html @@ -0,0 +1 @@ +Gone diff --git a/dom/html/test/file_window_close_and_open.html b/dom/html/test/file_window_close_and_open.html new file mode 100644 index 0000000000..ad96e50aac --- /dev/null +++ b/dom/html/test/file_window_close_and_open.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<html> +<script> + console.log("loading file_window_close_and_open.html"); + addEventListener("load", function() { + console.log("got load event!"); + let link = document.querySelector("a"); + if (window.location.hash === "#noopener") { + link.setAttribute("rel", "noopener"); + } else if (window.location.hash === "#opener") { + link.setAttribute("rel", "opener"); + } + link.click(); + }); +</script> +<body> + <h1>close and re-open popup</h1> + <a href="file_broadcast_load.html" target="_blank" onclick="window.close()">close and open</a> +</body> +</html> diff --git a/dom/html/test/file_window_open_close_inner.html b/dom/html/test/file_window_open_close_inner.html new file mode 100644 index 0000000000..dbc7e3aba8 --- /dev/null +++ b/dom/html/test/file_window_open_close_inner.html @@ -0,0 +1,7 @@ +<html> +<body> +<script> +window.close(); +</script> +</html> +</body> diff --git a/dom/html/test/file_window_open_close_outer.html b/dom/html/test/file_window_open_close_outer.html new file mode 100644 index 0000000000..682b399e75 --- /dev/null +++ b/dom/html/test/file_window_open_close_outer.html @@ -0,0 +1,5 @@ +<html> +<body> +<a id="link" href="file_window_open_close_inner.html" target="_blank" rel="opener" onclick="setTimeout(function () { window.close() }, 0)">link</a> +</html> +</body> diff --git a/dom/html/test/formData_test.js b/dom/html/test/formData_test.js new file mode 100644 index 0000000000..3997aff4d1 --- /dev/null +++ b/dom/html/test/formData_test.js @@ -0,0 +1,289 @@ +function testHas() { + var f = new FormData(); + f.append("foo", "bar"); + f.append("another", "value"); + ok(f.has("foo"), "has() on existing name should be true."); + ok(f.has("another"), "has() on existing name should be true."); + ok(!f.has("nonexistent"), "has() on non-existent name should be false."); +} + +function testGet() { + var f = new FormData(); + f.append("foo", "bar"); + f.append("foo", "bar2"); + f.append("blob", new Blob(["hey"], { type: "text/plain" })); + f.append("file", new File(["hey"], "testname", { type: "text/plain" })); + + is(f.get("foo"), "bar", "get() on existing name should return first value"); + ok( + f.get("blob") instanceof Blob, + "get() on existing name should return first value" + ); + is( + f.get("blob").type, + "text/plain", + "get() on existing name should return first value" + ); + ok( + f.get("file") instanceof File, + "get() on existing name should return first value" + ); + is( + f.get("file").name, + "testname", + "get() on existing name should return first value" + ); + + is( + f.get("nonexistent"), + null, + "get() on non-existent name should return null." + ); +} + +function testGetAll() { + var f = new FormData(); + f.append("other", "value"); + f.append("foo", "bar"); + f.append("foo", "bar2"); + f.append("foo", new Blob(["hey"], { type: "text/plain" })); + + var arr = f.getAll("foo"); + is(arr.length, 3, "getAll() should retrieve all matching entries."); + is(arr[0], "bar", "values should match and be in order"); + is(arr[1], "bar2", "values should match and be in order"); + ok(arr[2] instanceof Blob, "values should match and be in order"); + + is( + f.get("nonexistent"), + null, + "get() on non-existent name should return null." + ); +} + +function testDelete() { + var f = new FormData(); + f.append("other", "value"); + f.append("foo", "bar"); + f.append("foo", "bar2"); + f.append("foo", new Blob(["hey"], { type: "text/plain" })); + + ok(f.has("foo"), "has() on existing name should be true."); + f.delete("foo"); + ok(!f.has("foo"), "has() on deleted name should be false."); + is(f.getAll("foo").length, 0, "all entries should be deleted."); + + is(f.getAll("other").length, 1, "other names should still be there."); + f.delete("other"); + is(f.getAll("other").length, 0, "all entries should be deleted."); +} + +function testSet() { + var f = new FormData(); + + f.set("other", "value"); + ok(f.has("other"), "set() on new name should be similar to append()"); + is( + f.getAll("other").length, + 1, + "set() on new name should be similar to append()" + ); + + f.append("other", "value2"); + is( + f.getAll("other").length, + 2, + "append() should not replace existing entries." + ); + + f.append("foo", "bar"); + f.append("other", "value3"); + f.append("other", "value3"); + f.append("other", "value3"); + is( + f.getAll("other").length, + 5, + "append() should not replace existing entries." + ); + + f.set("other", "value4"); + is(f.getAll("other").length, 1, "set() should replace existing entries."); + is(f.getAll("other")[0], "value4", "set() should replace existing entries."); +} + +function testFilename() { + var f = new FormData(); + f.append("blob", new Blob(["hi"])); + ok(f.get("blob") instanceof Blob, "We should have a blob back."); + + // If a filename is passed, that should replace the original. + f.append("blob2", new Blob(["hi"]), "blob2.txt"); + is( + f.get("blob2").name, + "blob2.txt", + 'Explicit filename should override "blob".' + ); + + var file = new File(["hi"], "file1.txt"); + f.append("file1", file); + // If a file is passed, the "create entry" algorithm should not create a new File, but reuse the existing one. + is( + f.get("file1"), + file, + "Retrieved File object should be original File object and not a copy." + ); + is( + f.get("file1").name, + "file1.txt", + "File's filename should be original's name if no filename is explicitly passed." + ); + + file = new File(["hi"], "file2.txt"); + f.append("file2", file, "fakename.txt"); + ok( + f.get("file2") !== file, + "Retrieved File object should be new File object if explicit filename is passed." + ); + is( + f.get("file2").name, + "fakename.txt", + "File's filename should be explicitly passed name." + ); + f.append("file3", new File(["hi"], "")); + is(f.get("file3").name, "", "File's filename is returned even if empty."); +} + +function testIterable() { + var fd = new FormData(); + fd.set("1", "2"); + fd.set("2", "4"); + fd.set("3", "6"); + fd.set("4", "8"); + fd.set("5", "10"); + + var key_iter = fd.keys(); + var value_iter = fd.values(); + var entries_iter = fd.entries(); + for (var i = 0; i < 5; ++i) { + var v = i + 1; + var key = key_iter.next(); + var value = value_iter.next(); + var entry = entries_iter.next(); + is(key.value, v.toString(), "Correct Key iterator: " + v.toString()); + ok(!key.done, "key.done is false"); + is( + value.value, + (v * 2).toString(), + "Correct Value iterator: " + (v * 2).toString() + ); + ok(!value.done, "value.done is false"); + is( + entry.value[0], + v.toString(), + "Correct Entry 0 iterator: " + v.toString() + ); + is( + entry.value[1], + (v * 2).toString(), + "Correct Entry 1 iterator: " + (v * 2).toString() + ); + ok(!entry.done, "entry.done is false"); + } + + var last = key_iter.next(); + ok(last.done, "Nothing more to read."); + is(last.value, undefined, "Undefined is the last key"); + + last = value_iter.next(); + ok(last.done, "Nothing more to read."); + is(last.value, undefined, "Undefined is the last value"); + + last = entries_iter.next(); + ok(last.done, "Nothing more to read."); + + key_iter = fd.keys(); + key_iter.next(); + key_iter.next(); + fd.delete("1"); + fd.delete("2"); + fd.delete("3"); + fd.delete("4"); + fd.delete("5"); + + last = key_iter.next(); + ok(last.done, "Nothing more to read."); + is(last.value, undefined, "Undefined is the last key"); +} + +function testSend(doneCb) { + var xhr = new XMLHttpRequest(); + xhr.open("POST", "form_submit_server.sjs"); + xhr.onload = function () { + var response = xhr.response; + + for (var entry of response) { + is(entry.body, "hey"); + is(entry.headers["Content-Type"], "text/plain"); + } + + is( + response[0].headers["Content-Disposition"], + 'form-data; name="empty"; filename="blob"' + ); + + is( + response[1].headers["Content-Disposition"], + 'form-data; name="explicit"; filename="explicit-file-name"' + ); + + is( + response[2].headers["Content-Disposition"], + 'form-data; name="explicit-empty"; filename=""' + ); + + is( + response[3].headers["Content-Disposition"], + 'form-data; name="file-name"; filename="testname"' + ); + + is( + response[4].headers["Content-Disposition"], + 'form-data; name="empty-file-name"; filename=""' + ); + + is( + response[5].headers["Content-Disposition"], + 'form-data; name="file-name-overwrite"; filename="overwrite"' + ); + + doneCb(); + }; + + var file, + blob = new Blob(["hey"], { type: "text/plain" }); + + var fd = new FormData(); + fd.append("empty", blob); + fd.append("explicit", blob, "explicit-file-name"); + fd.append("explicit-empty", blob, ""); + file = new File([blob], "testname", { type: "text/plain" }); + fd.append("file-name", file); + file = new File([blob], "", { type: "text/plain" }); + fd.append("empty-file-name", file); + file = new File([blob], "testname", { type: "text/plain" }); + fd.append("file-name-overwrite", file, "overwrite"); + xhr.responseType = "json"; + xhr.send(fd); +} + +function runTest(doneCb) { + testHas(); + testGet(); + testGetAll(); + testDelete(); + testSet(); + testFilename(); + testIterable(); + // Finally, send an XHR and verify the response matches. + testSend(doneCb); +} diff --git a/dom/html/test/formData_worker.js b/dom/html/test/formData_worker.js new file mode 100644 index 0000000000..750522fbfa --- /dev/null +++ b/dom/html/test/formData_worker.js @@ -0,0 +1,23 @@ +function ok(a, msg) { + postMessage({ type: "status", status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + postMessage({ + type: "status", + status: a === b, + msg: a + " === " + b + ": " + msg, + }); +} + +function todo(a, msg) { + postMessage({ type: "todo", status: !!a, msg: a + ": " + msg }); +} + +importScripts("formData_test.js"); + +onmessage = function () { + runTest(function () { + postMessage({ type: "finish" }); + }); +}; diff --git a/dom/html/test/formSubmission_chrome.js b/dom/html/test/formSubmission_chrome.js new file mode 100644 index 0000000000..da1224d107 --- /dev/null +++ b/dom/html/test/formSubmission_chrome.js @@ -0,0 +1,20 @@ +/* eslint-env mozilla/chrome-script */ + +// eslint-disable-next-line mozilla/reject-importGlobalProperties +Cu.importGlobalProperties(["File"]); + +addMessageListener("files.open", function (message) { + let list = []; + let promises = []; + for (let path of message) { + promises.push( + File.createFromFileName(path).then(file => { + list.push(file); + }) + ); + } + + Promise.all(promises).then(() => { + sendAsyncMessage("files.opened", list); + }); +}); diff --git a/dom/html/test/form_data_file.bin b/dom/html/test/form_data_file.bin new file mode 100644 index 0000000000..744bde3558 --- /dev/null +++ b/dom/html/test/form_data_file.bin @@ -0,0 +1 @@ + diff --git a/dom/html/test/form_data_file.txt b/dom/html/test/form_data_file.txt new file mode 100644 index 0000000000..81c545efeb --- /dev/null +++ b/dom/html/test/form_data_file.txt @@ -0,0 +1 @@ +1234 diff --git a/dom/html/test/form_submit_server.sjs b/dom/html/test/form_submit_server.sjs new file mode 100644 index 0000000000..553809c01f --- /dev/null +++ b/dom/html/test/form_submit_server.sjs @@ -0,0 +1,86 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function utf8decode(s) { + return decodeURIComponent(escape(s)); +} + +function utf8encode(s) { + return unescape(encodeURIComponent(s)); +} + +function handleRequest(request, response) { + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var result = []; + var requestBody = ""; + while ((bodyAvail = bodyStream.available()) > 0) { + requestBody += bodyStream.readBytes(bodyAvail); + } + + if (request.method == "POST") { + var contentTypeParams = {}; + request + .getHeader("Content-Type") + .split(/\s*\;\s*/) + .forEach(function (s) { + if (s.indexOf("=") >= 0) { + let [name, value] = s.split("="); + contentTypeParams[name] = value; + } else { + contentTypeParams[""] = s; + } + }); + + if ( + contentTypeParams[""] == "multipart/form-data" && + request.queryString == "" + ) { + requestBody + .split("--" + contentTypeParams.boundary) + .slice(1, -1) + .forEach(function (s) { + let headers = {}; + let headerEnd = s.indexOf("\r\n\r\n"); + s.substr(2, headerEnd - 2) + .split("\r\n") + .forEach(function (str) { + // We're assuming UTF8 for now + let [name, value] = str.split(": "); + headers[name] = utf8decode(value); + }); + + let body = s.substring(headerEnd + 4, s.length - 2); + if ( + !headers["Content-Type"] || + headers["Content-Type"] == "text/plain" + ) { + // We're assuming UTF8 for now + body = utf8decode(body); + } + result.push({ headers, body }); + }); + } + if ( + contentTypeParams[""] == "text/plain" && + request.queryString == "plain" + ) { + result = utf8decode(requestBody); + } + if ( + contentTypeParams[""] == "application/x-www-form-urlencoded" && + request.queryString == "url" + ) { + result = requestBody; + } + } else if (request.method == "GET") { + result = request.queryString; + } + + // Send response body + response.setHeader("Content-Type", "text/plain; charset=utf-8", false); + response.write(utf8encode(JSON.stringify(result))); +} diff --git a/dom/html/test/forms/FAIL.html b/dom/html/test/forms/FAIL.html new file mode 100644 index 0000000000..94e1707e85 --- /dev/null +++ b/dom/html/test/forms/FAIL.html @@ -0,0 +1 @@ +FAIL diff --git a/dom/html/test/forms/PASS.html b/dom/html/test/forms/PASS.html new file mode 100644 index 0000000000..7ef22e9a43 --- /dev/null +++ b/dom/html/test/forms/PASS.html @@ -0,0 +1 @@ +PASS diff --git a/dom/html/test/forms/chrome.toml b/dom/html/test/forms/chrome.toml new file mode 100644 index 0000000000..0f49518b9b --- /dev/null +++ b/dom/html/test/forms/chrome.toml @@ -0,0 +1,6 @@ +[DEFAULT] +support-files = ["submit_invalid_file.sjs"] + +["test_autocompleteinfo.html"] + +["test_submit_invalid_file.html"] diff --git a/dom/html/test/forms/file_double_submit.html b/dom/html/test/forms/file_double_submit.html new file mode 100644 index 0000000000..44889f86bc --- /dev/null +++ b/dom/html/test/forms/file_double_submit.html @@ -0,0 +1,11 @@ +<form action="PASS.html" method="POST"><input name="foo"></form> +<button>clicky</button> + +<script> +document.querySelector("button") + .addEventListener("click", () => { + let f = document.querySelector("form"); + f.dispatchEvent(new Event("submit")); + f.submit(); + }); +</script> diff --git a/dom/html/test/forms/file_login_fields.html b/dom/html/test/forms/file_login_fields.html new file mode 100644 index 0000000000..f23ee0ad6a --- /dev/null +++ b/dom/html/test/forms/file_login_fields.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> + <head> + <script> + // Add an unload listener to bypass bfcache. + window.addEventListner("unload", _ => _); + </script> + </head> + <body> + <input id="un" /> + <input id="pw1" type="password" /> + <input id="pw2" /> + <a id="navigate" href="?navigated">Navigate</a> + <a id="back" href="javascript:history.back()">Back</a> + </body> +</html> diff --git a/dom/html/test/forms/mochitest.toml b/dom/html/test/forms/mochitest.toml new file mode 100644 index 0000000000..80d6d3530f --- /dev/null +++ b/dom/html/test/forms/mochitest.toml @@ -0,0 +1,229 @@ +[DEFAULT] +support-files = [ + "save_restore_radio_groups.sjs", + "test_input_number_data.js", + "!/dom/html/test/reflect.js", + "FAIL.html", + "PASS.html", +] +prefs = ["formhelper.autozoom.force-disable.test-only=true"] + +["test_MozEditableElement_setUserInput.html"] + +["test_autocomplete.html"] + +["test_bug1039548.html"] + +["test_bug1283915.html"] + +["test_bug1286509.html"] + +["test_button_attributes_reflection.html"] + +["test_change_event.html"] + +["test_datalist_element.html"] + +["test_double_submit.html"] +support-files = ["file_double_submit.html"] + +["test_form_attribute-1.html"] + +["test_form_attribute-2.html"] + +["test_form_attribute-3.html"] + +["test_form_attribute-4.html"] + +["test_form_attributes_reflection.html"] + +["test_form_named_getter_dynamic.html"] + +["test_formaction_attribute.html"] + +["test_formnovalidate_attribute.html"] + +["test_input_attributes_reflection.html"] + +["test_input_color_input_change_events.html"] + +["test_input_color_picker_datalist.html"] + +["test_input_color_picker_initial.html"] + +["test_input_color_picker_popup.html"] + +["test_input_color_picker_update.html"] + +["test_input_date_bad_input.html"] + +["test_input_date_key_events.html"] + +["test_input_datetime_calendar_button.html"] + +["test_input_datetime_disabled_focus.html"] + +["test_input_datetime_focus_blur.html"] + +["test_input_datetime_focus_blur_events.html"] + +["test_input_datetime_focus_state.html"] + +["test_input_datetime_hidden.html"] + +["test_input_datetime_input_change_events.html"] + +["test_input_datetime_readonly.html"] + +["test_input_datetime_reset_default_value_input_change_event.html"] + +["test_input_datetime_tabindex.html"] + +["test_input_defaultValue.html"] + +["test_input_email.html"] + +["test_input_event.html"] + +["test_input_file_picker.html"] + +["test_input_hasBeenTypePassword.html"] + +["test_input_hasBeenTypePassword_navigation.html"] +support-files = ["file_login_fields.html"] + +["test_input_list_attribute.html"] + +["test_input_number_focus.html"] + +["test_input_number_key_events.html"] + +["test_input_number_l10n.html"] + +["test_input_number_mouse_events.html"] +# Not run on Firefox for Android where the spin buttons are hidden: +skip-if = [ + "os == 'android'", + "os == 'mac' && debug", # Bug 1484442 +] + +["test_input_number_placeholder_shown.html"] + +["test_input_number_rounding.html"] + +["test_input_number_validation.html"] + +["test_input_password_click_show_password_button.html"] + +["test_input_password_show_password_button.html"] + +["test_input_radio_indeterminate.html"] + +["test_input_radio_radiogroup.html"] + +["test_input_radio_required.html"] + +["test_input_range_attr_order.html"] + +["test_input_range_key_events.html"] + +["test_input_range_mouse_and_touch_events.html"] + +["test_input_range_rounding.html"] + +["test_input_sanitization.html"] + +["test_input_setting_value.html"] + +["test_input_textarea_set_value_no_scroll.html"] + +["test_input_time_key_events.html"] + +["test_input_time_sec_millisec_field.html"] + +["test_input_types_pref.html"] + +["test_input_typing_sanitization.html"] + +["test_input_untrusted_key_events.html"] + +["test_input_url.html"] + +["test_interactive_content_in_label.html"] + +["test_interactive_content_in_summary.html"] + +["test_label_control_attribute.html"] + +["test_label_input_controls.html"] + +["test_max_attribute.html"] + +["test_maxlength_attribute.html"] + +["test_meter_element.html"] + +["test_meter_pseudo-classes.html"] + +["test_min_attribute.html"] + +["test_minlength_attribute.html"] + +["test_mozistextfield.html"] + +["test_novalidate_attribute.html"] + +["test_option_disabled.html"] + +["test_option_index_attribute.html"] + +["test_option_text.html"] + +["test_output_element.html"] + +["test_pattern_attribute.html"] + +["test_preserving_metadata_between_reloads.html"] + +["test_progress_element.html"] + +["test_radio_in_label.html"] + +["test_radio_radionodelist.html"] + +["test_reportValidation_preventDefault.html"] + +["test_required_attribute.html"] + +["test_restore_form_elements.html"] + +["test_save_restore_custom_elements.html"] +support-files = ["save_restore_custom_elements_sample.html"] + +["test_save_restore_radio_groups.html"] + +["test_select_change_event.html"] +skip-if = ["os == 'mac'"] + +["test_select_input_change_event.html"] +skip-if = ["os == 'mac'"] + +["test_select_selectedOptions.html"] + +["test_select_validation.html"] + +["test_set_range_text.html"] + +["test_step_attribute.html"] + +["test_stepup_stepdown.html"] + +["test_textarea_attributes_reflection.html"] + +["test_validation.html"] + +["test_validation_not_in_doc.html"] + +["test_valueasdate_attribute.html"] + +["test_valueasnumber_attribute.html"] diff --git a/dom/html/test/forms/save_restore_custom_elements_sample.html b/dom/html/test/forms/save_restore_custom_elements_sample.html new file mode 100644 index 0000000000..75dc4c388d --- /dev/null +++ b/dom/html/test/forms/save_restore_custom_elements_sample.html @@ -0,0 +1,43 @@ +<script> + class CEBase extends HTMLElement { + static formAssociated = true; + constructor() { + super(); + this.internals = this.attachInternals(); + this.state_ = undefined; + } + formStateRestoreCallback(state, reason) { + if (reason == "restore") { + this.state_ = state; + } + } + set(state, value) { + this.state_ = state; + this.value_ = value; + this.internals.setFormValue(value, state); + } + get state() { + return this.state_; + } + get value() { + return this.value_; + } + } + + customElements.define("c-e", class extends CEBase {}); +</script> +<form> + <c-e id="custom0"></c-e> + <c-e id="custom1"></c-e> + <c-e id="custom2"></c-e> + <c-e id="custom3"></c-e> + <c-e id="custom4"></c-e> + <upgraded-ce id="upgraded0"></upgraded-ce> + <upgraded-ce id="upgraded1"></upgraded-ce> + <upgraded-ce id="upgraded2"></upgraded-ce> + <upgraded-ce id="upgraded3"></upgraded-ce> + <upgraded-ce id="upgraded4"></upgraded-ce> +</form> +<script> + customElements.define("upgraded-ce", class extends CEBase {}); +</script> diff --git a/dom/html/test/forms/save_restore_radio_groups.sjs b/dom/html/test/forms/save_restore_radio_groups.sjs new file mode 100644 index 0000000000..b4c9c4401a --- /dev/null +++ b/dom/html/test/forms/save_restore_radio_groups.sjs @@ -0,0 +1,48 @@ +var pages = [ + "<!DOCTYPE html>" + + "<html><body>" + + "<form>" + + "<input name='a' type='radio' checked><input name='a' type='radio'><input name='a' type='radio'>" + + "</form>" + + "</body></html>", + "<!DOCTYPE html>" + + "<html><body>" + + "<form>" + + "<input name='a' type='radio'><input name='a' type='radio' checked><input name='a' type='radio'>" + + "</form>" + + "</body></html>", +]; + +/** + * This SJS is going to send the same page the two first times it will be called + * and another page the two following times. After that, the response will have + * no content. + * The use case is to have two iframes using this SJS and both being reloaded + * once. + */ + +function handleRequest(request, response) { + var counter = +getState("counter"); // convert to number; +"" === 0 + + response.setStatusLine(request.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/html"); + response.setHeader("Cache-Control", "no-cache"); + + switch (counter) { + case 0: + case 1: + response.write(pages[0]); + break; + case 2: + case 3: + response.write(pages[1]); + break; + } + + // When we finish the test case we need to reset the counter + if (counter == 3) { + setState("counter", "0"); + } else { + setState("counter", "" + ++counter); + } +} diff --git a/dom/html/test/forms/submit_invalid_file.sjs b/dom/html/test/forms/submit_invalid_file.sjs new file mode 100644 index 0000000000..3b4b576ec6 --- /dev/null +++ b/dom/html/test/forms/submit_invalid_file.sjs @@ -0,0 +1,13 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 200, "Ok"); + response.setHeader("Content-Type", "text/html"); + response.setHeader("Cache-Control", "no-cache"); + + var result = {}; + request.bodyInputStream.search("testfile", true, result, {}); + if (result.value) { + response.write("SUCCESS"); + } else { + response.write("FAIL"); + } +} diff --git a/dom/html/test/forms/test_MozEditableElement_setUserInput.html b/dom/html/test/forms/test_MozEditableElement_setUserInput.html new file mode 100644 index 0000000000..06380776f6 --- /dev/null +++ b/dom/html/test/forms/test_MozEditableElement_setUserInput.html @@ -0,0 +1,581 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for MozEditableElement.setUserInput()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content"></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +// eslint-disable-next-line complexity +SimpleTest.waitForFocus(async () => { + const kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"); + + let content = document.getElementById("content"); + /** + * Test structure: + * element: the tag name to create. + * type: the type attribute value for the element. If unnecessary omit it. + * input: the values calling setUserInput() with. + * before: used when calling setUserInput() before the element gets focus. + * after: used when calling setUserInput() after the element gets focus. + * result: the results of calling setUserInput(). + * before: the element's expected value of calling setUserInput() before the element gets focus. + * after: the element's expected value of calling setUserInput() after the element gets focus. + * fireBeforeInputEvent: true if "beforeinput" event should be fired. Otherwise, false. + * fireInputEvent: true if "input" event should be fired. Otherwise, false. + */ + for (let test of [{element: "input", type: "hidden", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}}, + {element: "input", type: "text", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}, + {element: "input", type: "search", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}, + {element: "input", type: "tel", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}, + {element: "input", type: "url", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}, + {element: "input", type: "email", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}, + {element: "input", type: "password", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}, + // "date" does not support setUserInput, but dispatches "input" event... + {element: "input", type: "date", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}}, + // "month" does not support setUserInput, but dispatches "input" event... + {element: "input", type: "month", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}}, + // "week" does not support setUserInput, but dispatches "input" event... + {element: "input", type: "week", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}}, + // "time" does not support setUserInput, but dispatches "input" event... + {element: "input", type: "time", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}}, + // "datetime-local" does not support setUserInput, but dispatches "input" event... + {element: "input", type: "datetime-local", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}}, + {element: "input", type: "number", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}, + {element: "input", type: "range", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}}, + // "color" does not support setUserInput, but dispatches "input" event... + {element: "input", type: "color", + input: {before: "#5C5C5C", after: "#FFFFFF"}, + result: {before: "#5c5c5c", after:"#ffffff", fireBeforeInputEvent: false, fireInputEvent: true}}, + {element: "input", type: "checkbox", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}}, + {element: "input", type: "radio", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: true}}, + // "file" is not supported by setUserInput? But there is a path... + {element: "input", type: "file", + input: {before: "3", after: "6"}, + result: {before: "", after:"", fireBeforeInputEvent: false, fireInputEvent: true}}, + {element: "input", type: "submit", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}}, + {element: "input", type: "image", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}}, + {element: "input", type: "reset", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}}, + {element: "input", type: "button", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: false, fireInputEvent: false}}, + {element: "textarea", + input: {before: "3", after: "6"}, + result: {before: "3", after:"6", fireBeforeInputEvent: true, fireInputEvent: true}}]) { + let tag = + test.type !== undefined ? `<${test.element} type="${test.type}">` : + `<${test.element}>`; + content.innerHTML = + test.element !== "input" ? tag : `${tag}</${test.element}>`; + content.scrollTop; // Flush pending layout. + let target = content.firstChild; + + let inputEvents = [], beforeInputEvents = []; + function onBeforeInput(aEvent) { + beforeInputEvents.push(aEvent); + } + function onInput(aEvent) { + inputEvents.push(aEvent); + } + target.addEventListener("beforeinput", onBeforeInput); + target.addEventListener("input", onInput); + + // Before setting focus, editor of the element may have not been created yet. + let previousValue = target.value; + SpecialPowers.wrap(target).setUserInput(test.input.before); + if (target.value == previousValue && test.result.before != previousValue) { + todo_is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`); + } else { + is(target.value, test.result.before, `setUserInput("${test.input.before}") before ${tag} gets focus should set its value to "${test.result.before}"`); + } + if (target.value == previousValue) { + if (test.type === "date" || test.type === "time" || test.type === "datetime-local") { + todo_is(inputEvents.length, 0, + `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } else { + is(inputEvents.length, 0, + `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } + } else { + if (!test.result.fireBeforeInputEvent) { + is(beforeInputEvents.length, 0, + `No "beforeinput" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } else { + is(beforeInputEvents.length, 1, + `Only one "beforeinput" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } + if (!test.result.fireInputEvent) { + // HTML spec defines that "input" elements whose type are "hidden", + // "submit", "image", "reset" and "button" shouldn't fire input event + // when its value is changed. + // XXX Perhaps, we shouldn't support setUserInput() with such types. + if (test.type === "hidden" || + test.type === "submit" || + test.type === "image" || + test.type === "reset" || + test.type === "button") { + todo_is(inputEvents.length, 0, + `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } else { + is(inputEvents.length, 0, + `No "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } + } else { + is(inputEvents.length, 1, + `Only one "input" event should be dispatched when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } + } + if (inputEvents.length) { + if (SpecialPowers.wrap(target).isInputEventTarget) { + if (test.type === "time") { + todo(inputEvents[0] instanceof InputEvent, + `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } else { + if (beforeInputEvents.length && test.result.fireBeforeInputEvent) { + is(beforeInputEvents[0].cancelable, kSetUserInputCancelable, + `"beforeinput" event for "insertReplacementText" should be cancelable when setUserInput("${test.input.before}") is called before ${tag} gets focus unless it's suppressed by the pref`); + is(beforeInputEvents[0].inputType, "insertReplacementText", + `inputType of "beforeinput"event should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + is(beforeInputEvents[0].data, test.input.before, + `data of "beforeinput" event should be "${test.input.before}" when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + is(beforeInputEvents[0].dataTransfer, null, + `dataTransfer of "beforeinput" event should be null when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + is(beforeInputEvents[0].getTargetRanges().length, 0, + `getTargetRanges() of "beforeinput" event should return empty array when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } + ok(inputEvents[0] instanceof InputEvent, + `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + is(inputEvents[0].inputType, "insertReplacementText", + `inputType of "input" event should be "insertReplacementText" when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + is(inputEvents[0].data, test.input.before, + `data of "input" event should be "${test.input.before}" when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + is(inputEvents[0].dataTransfer, null, + `dataTransfer of "input" event should be null when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + is(inputEvents[0].getTargetRanges().length, 0, + `getTargetRanges() of "input" event should return empty array when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } + } else { + ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent), + `"input" event should be dispatched with Event interface when setUserInput("${test.input.before}") is called before ${tag} gets focus`); + } + is(inputEvents[0].cancelable, false, + `"input" event should be never cancelable (${tag}, before getting focus)`); + is(inputEvents[0].bubbles, true, + `"input" event should always bubble (${tag}, before getting focus)`); + } + + beforeInputEvents = []; + inputEvents = []; + target.focus(); + previousValue = target.value; + SpecialPowers.wrap(target).setUserInput(test.input.after); + if (target.value == previousValue && test.result.after != previousValue) { + todo_is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`); + } else { + is(target.value, test.result.after, `setUserInput("${test.input.after}") after ${tag} gets focus should set its value to "${test.result.after}"`); + } + if (target.value == previousValue) { + if (test.type === "date" || test.type === "time" || test.type === "datetime-local") { + todo_is(inputEvents.length, 0, + `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } else { + is(inputEvents.length, 0, + `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } + } else { + if (!test.result.fireBeforeInputEvent) { + is(beforeInputEvents.length, 0, + `No "beforeinput" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } else { + is(beforeInputEvents.length, 1, + `Only one "beforeinput" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } + if (!test.result.fireInputEvent) { + // HTML spec defines that "input" elements whose type are "hidden", + // "submit", "image", "reset" and "button" shouldn't fire input event + // when its value is changed. + // XXX Perhaps, we shouldn't support setUserInput() with such types. + if (test.type === "hidden" || + test.type === "submit" || + test.type === "image" || + test.type === "reset" || + test.type === "button") { + todo_is(inputEvents.length, 0, + `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } else { + is(inputEvents.length, 0, + `No "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } + } else { + is(inputEvents.length, 1, + `Only one "input" event should be dispatched when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } + } + if (inputEvents.length) { + if (SpecialPowers.wrap(target).isInputEventTarget) { + if (test.type === "time") { + todo(inputEvents[0] instanceof InputEvent, + `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } else { + if (beforeInputEvents.length && test.result.fireBeforeInputEvent) { + is(beforeInputEvents[0].cancelable, kSetUserInputCancelable, + `"beforeinput" event should be cancelable when setUserInput("${test.input.after}") is called after ${tag} gets focus unless it's suppressed by the pref`); + is(beforeInputEvents[0].inputType, "insertReplacementText", + `inputType of "beforeinput" event should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + is(beforeInputEvents[0].data, test.input.after, + `data of "beforeinput" should be "${test.input.after}" when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + is(beforeInputEvents[0].dataTransfer, null, + `dataTransfer of "beforeinput" should be null when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + is(beforeInputEvents[0].getTargetRanges().length, 0, + `getTargetRanges() of "beforeinput" should return empty array when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } + ok(inputEvents[0] instanceof InputEvent, + `"input" event should be dispatched with InputEvent interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + is(inputEvents[0].inputType, "insertReplacementText", + `inputType of "input" event should be "insertReplacementText" when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + is(inputEvents[0].data, test.input.after, + `data of "input" event should be "${test.input.after}" when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + is(inputEvents[0].dataTransfer, null, + `dataTransfer of "input" event should be null when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + is(inputEvents[0].getTargetRanges().length, 0, + `getTargetRanges() of "input" event should return empty array when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } + } else { + ok(inputEvents[0] instanceof Event && !(inputEvents[0] instanceof UIEvent), + `"input" event should be dispatched with Event interface when setUserInput("${test.input.after}") is called after ${tag} gets focus`); + } + is(inputEvents[0].cancelable, false, + `"input" event should be never cancelable (${tag}, after getting focus)`); + is(inputEvents[0].bubbles, true, + `"input" event should always bubble (${tag}, after getting focus)`); + } + + target.removeEventListener("input", onInput); + } + + function testValidationMessage(aType, aInvalidValue, aValidValue) { + let tag = `<input type="${aType}">` + content.innerHTML = tag; + content.scrollTop; // Flush pending layout. + let target = content.firstChild; + + let inputEvents = []; + let validationMessage = ""; + + function reset() { + inputEvents = []; + validationMessage = ""; + } + + function onInput(aEvent) { + inputEvents.push(aEvent); + validationMessage = aEvent.target.validationMessage; + } + target.addEventListener("input", onInput); + + reset(); + SpecialPowers.wrap(target).setUserInput(aInvalidValue); + is(inputEvents.length, 1, + `Only one "input" event should be dispatched when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`); + isnot(validationMessage, "", + `${tag}.validationMessage should not be empty when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`); + ok(target.matches(":invalid"), + `The target should have invalid pseudo class when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`); + + reset(); + SpecialPowers.wrap(target).setUserInput(aValidValue); + is(inputEvents.length, 1, + `Only one "input" event should be dispatched when setUserInput("${aValidValue}") is called before ${tag} gets focus`); + is(validationMessage, "", + `${tag}.validationMessage should be empty when setUserInput("${aValidValue}") is called before ${tag} gets focus`); + ok(!target.matches(":invalid"), + `The target shouldn't have invalid pseudo class when setUserInput("${aValidValue}") is called before ${tag} gets focus`); + + reset(); + SpecialPowers.wrap(target).setUserInput(aInvalidValue); + is(inputEvents.length, 1, + `Only one "input" event should be dispatched again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`); + isnot(validationMessage, "", + `${tag}.validationMessage should not be empty again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`); + ok(target.matches(":invalid"), + `The target should have invalid pseudo class again when setUserInput("${aInvalidValue}") is called before ${tag} gets focus`); + + target.value = ""; + target.focus(); + + reset(); + SpecialPowers.wrap(target).setUserInput(aInvalidValue); + is(inputEvents.length, 1, + `Only one "input" event should be dispatched when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`); + isnot(validationMessage, "", + `${tag}.validationMessage should not be empty when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`); + ok(target.matches(":invalid"), + `The target should have invalid pseudo class when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`); + + reset(); + SpecialPowers.wrap(target).setUserInput(aValidValue); + is(inputEvents.length, 1, + `Only one "input" event should be dispatched when setUserInput("${aValidValue}") is called after ${tag} gets focus`); + is(validationMessage, "", + `${tag}.validationMessage should be empty when setUserInput("${aValidValue}") is called after ${tag} gets focus`); + ok(!target.matches(":invalid"), + `The target shouldn't have invalid pseudo class when setUserInput("${aValidValue}") is called after ${tag} gets focus`); + + reset(); + SpecialPowers.wrap(target).setUserInput(aInvalidValue); + is(inputEvents.length, 1, + `Only one "input" event should be dispatched again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`); + isnot(validationMessage, "", + `${tag}.validationMessage should not be empty again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`); + ok(target.matches(":invalid"), + `The target should have invalid pseudo class again when setUserInput("${aInvalidValue}") is called after ${tag} gets focus`); + + target.removeEventListener("input", onInput); + } + testValidationMessage("email", "f", "foo@example.com"); + + function testValueMissing(aType, aValidValue) { + let tag = aType === "textarea" ? "<textarea required>" : `<input type="${aType}" required>`; + content.innerHTML = `${tag}${aType === "textarea" ? "</textarea>" : ""}`; + content.scrollTop; // Flush pending layout. + let target = content.firstChild; + + let inputEvents = [], beforeInputEvents = []; + function reset() { + beforeInputEvents = []; + inputEvents = []; + } + + function onBeforeInput(aEvent) { + aEvent.validity = aEvent.target.checkValidity(); + beforeInputEvents.push(aEvent); + } + function onInput(aEvent) { + aEvent.validity = aEvent.target.checkValidity(); + inputEvents.push(aEvent); + } + target.addEventListener("beforeinput", onBeforeInput); + target.addEventListener("input", onInput); + + reset(); + SpecialPowers.wrap(target).setUserInput(aValidValue); + is(beforeInputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "beforeinput" event (before gets focus)`); + if (beforeInputEvents.length) { + is(beforeInputEvents[0].validity, false, + `The ${tag} should be invalid at "beforeinput" event (before gets focus)`); + } + is(inputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "input" event (before gets focus)`); + if (inputEvents.length) { + is(inputEvents[0].validity, true, + `The ${tag} should be valid at "input" event (before gets focus)`); + } + + target.removeEventListener("beforeinput", onBeforeInput); + target.removeEventListener("input", onInput); + + content.innerHTML = ""; + content.scrollTop; // Flush pending layout. + content.innerHTML = `${tag}${aType === "textarea" ? "</textarea>" : ""}`; + content.scrollTop; // Flush pending layout. + target = content.firstChild; + + target.focus(); + target.addEventListener("beforeinput", onBeforeInput); + target.addEventListener("input", onInput); + + reset(); + SpecialPowers.wrap(target).setUserInput(aValidValue); + is(beforeInputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "beforeinput" event (after gets focus)`); + if (beforeInputEvents.length) { + is(beforeInputEvents[0].validity, false, + `The ${tag} should be invalid at "beforeinput" event (after gets focus)`); + } + is(inputEvents.length, 1, `Calling ${tag}.setUserInput(${aValidValue}) should cause a "input" event (after gets focus)`); + if (inputEvents.length) { + is(inputEvents[0].validity, true, + `The ${tag} should be valid at "input" event (after gets focus)`); + } + + target.removeEventListener("beforeinput", onBeforeInput); + target.removeEventListener("input", onInput); + } + testValueMissing("text", "abc"); + testValueMissing("password", "abc"); + testValueMissing("textarea", "abc"); + testValueMissing("email", "foo@example.com"); + testValueMissing("url", "https://example.com/"); + + function testEditorValueAtEachEvent(aType) { + let tag = aType === "textarea" ? "<textarea>" : `<input type="${aType}">` + let closeTag = aType === "textarea" ? "</textarea>" : ""; + content.innerHTML = `${tag}${closeTag}`; + content.scrollTop; // Flush pending layout. + let target = content.firstChild; + target.value = "Old Value"; + let description = `Setting new value of ${tag} before setting focus: `; + let onBeforeInput = (aEvent) => { + is(target.value, "Old Value", + `${description}The value should not have been modified at "beforeinput" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`); + }; + let onInput = (aEvent) => { + is(target.value, "New Value", + `${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`); + }; + target.addEventListener("beforeinput", onBeforeInput); + target.addEventListener("input", onInput); + SpecialPowers.wrap(target).setUserInput("New Value"); + + description = `Setting new value of ${tag} after setting focus: `; + target.value = "Old Value"; + target.focus(); + SpecialPowers.wrap(target).setUserInput("New Value"); + + target.removeEventListener("beforeinput", onBeforeInput); + target.removeEventListener("input", onInput); + + // FYI: This is not realistic situation because we should do nothing + // while user composing IME. + // TODO: TextControlState should stop returning setting value as the value + // while committing composition. + description = `Setting new value of ${tag} during composition: `; + target.value = ""; + target.focus(); + synthesizeCompositionChange({ + composition: { + string: "composition string", + clauses: [{length: 18, attr: COMPOSITION_ATTR_RAW_CLAUSE}], + }, + caret: {start: 18, length: 0}, + }); + let onCompositionUpdate = (aEvent) => { + todo_is(target.value, "composition string", + `${description}The value should not have been modified at "compositionupdate" event yet (data: "${aEvent.data}")`); + }; + let onCompositionEnd = (aEvent) => { + todo_is(target.value, "composition string", + `${description}The value should not have been modified at "compositionupdate" event yet (data: "${aEvent.data}")`); + }; + onBeforeInput = (aEvent) => { + if (aEvent.inputType === "insertCompositionText") { + todo_is(target.value, "composition string", + `${description}The value should not have been modified at "beforeinput" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`); + } else { + is(target.value, "composition string", + `${description}The value should not have been modified at "beforeinput" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`); + } + }; + onInput = (aEvent) => { + if (aEvent.inputType === "insertCompositionText") { + todo_is(target.value, "composition string", + `${description}The value should not have been modified at "input" event yet (inputType: "${aEvent.inputType}", data: "${aEvent.data}")`); + } else { + is(target.value, "New Value", + `${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`); + } + }; + target.addEventListener("compositionupdate", onCompositionUpdate); + target.addEventListener("compositionend", onCompositionEnd); + target.addEventListener("beforeinput", onBeforeInput); + target.addEventListener("input", onInput); + SpecialPowers.wrap(target).setUserInput("New Value"); + target.removeEventListener("compositionupdate", onCompositionUpdate); + target.removeEventListener("compositionend", onCompositionEnd); + target.removeEventListener("beforeinput", onBeforeInput); + target.removeEventListener("input", onInput); + } + testEditorValueAtEachEvent("text"); + testEditorValueAtEachEvent("textarea"); + + async function testBeforeInputCancelable(aType) { + let tag = aType === "textarea" ? "<textarea>" : `<input type="${aType}">` + let closeTag = aType === "textarea" ? "</textarea>" : ""; + for (const kShouldBeCancelable of [true, false]) { + await SpecialPowers.pushPrefEnv({ + set: [["dom.input_event.allow_to_cancel_set_user_input", kShouldBeCancelable]], + }); + + content.innerHTML = `${tag}${closeTag}`; + content.scrollTop; // Flush pending layout. + let target = content.firstChild; + target.value = "Old Value"; + let description = `Setting new value of ${tag} before setting focus (the pref ${kShouldBeCancelable ? "allows" : "disallows"} to cancel beforeinput): `; + let onBeforeInput = (aEvent) => { + is(aEvent.cancelable, kShouldBeCancelable, + `${description}The "beforeinput" event should be ${kShouldBeCancelable ? "cancelable" : "not be cancelable due to suppressed by the pref"}`); + }; + let onInput = (aEvent) => { + is(aEvent.cancelable, false, + `${description}The value should have been modified at "input" event (inputType: "${aEvent.inputType}", data: "${aEvent.data}"`); + }; + target.addEventListener("beforeinput", onBeforeInput); + target.addEventListener("input", onInput); + SpecialPowers.wrap(target).setUserInput("New Value"); + + description = `Setting new value of ${tag} after setting focus (the pref ${kShouldBeCancelable ? "allows" : "disallows"} to cancel beforeinput): `; + target.value = "Old Value"; + target.focus(); + SpecialPowers.wrap(target).setUserInput("New Value"); + + target.removeEventListener("beforeinput", onBeforeInput); + target.removeEventListener("input", onInput); + } + + await SpecialPowers.clearUserPref({ + clear: [["dom.input_event.allow_to_cancel_set_user_input"]], + }); + } + await testBeforeInputCancelable("text"); + await testBeforeInputCancelable("textarea"); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/dom/html/test/forms/test_autocomplete.html b/dom/html/test/forms/test_autocomplete.html new file mode 100644 index 0000000000..c98be94eea --- /dev/null +++ b/dom/html/test/forms/test_autocomplete.html @@ -0,0 +1,164 @@ +<!DOCTYPE html> +<html> +<!-- +Test @autocomplete on <input>/<select>/<textarea> +--> +<head> + <title>Test for @autocomplete</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<script> +"use strict"; + +var values = [ + // @autocomplete content attribute, expected IDL attribute value + + // Missing or empty attribute + [undefined, ""], + ["", ""], + + // One token + ["on", "on"], + ["On", "on"], + ["off", "off"], + ["OFF", "off"], + ["name", "name"], + [" name ", "name"], + ["username", "username"], + [" username ", "username"], + ["cc-csc", ""], + ["one-time-code", ""], + ["language", ""], + [" language ", ""], + ["tel-extension", ""], + ["foobar", ""], + ["section-blue", ""], + [" WEBAUTHN ", "webauthn"], + + // One token + WebAuthn credential type + ["on webauthn", ""], + ["off webauthn", ""], + ["webauthn webauthn", ""], + ["username WebAuthn", "username webauthn"], + ["current-PASSWORD webauthn", "current-password webauthn"], + + // Two tokens + ["on off", ""], + ["off on", ""], + ["username tel", ""], + ["tel username ", ""], + [" username tel ", ""], + ["tel mobile", ""], + ["tel shipping", ""], + ["shipping tel", "shipping tel"], + ["shipPING tel", "shipping tel"], + ["mobile tel", "mobile tel"], + [" MoBiLe TeL ", "mobile tel"], + ["pager impp", ""], + ["fax tel-extension", ""], + ["XXX tel", ""], + ["XXX username", ""], + ["name section-blue", ""], + ["scetion-blue cc-name", ""], + ["pager language", ""], + ["fax url", ""], + ["section-blue name", "section-blue name"], + ["section-blue tel", "section-blue tel"], + ["webauthn username", ""], + + // Two tokens + WebAuthn credential type + ["fax url webauthn", ""], + ["shipping tel webauthn", "shipping tel webauthn"], + + // Three tokens + ["billing invalid tel", ""], + ["___ mobile tel", ""], + ["mobile foo tel", ""], + ["mobile tel foo", ""], + ["tel mobile billing", ""], + ["billing mobile tel", "billing mobile tel"], + [" BILLing MoBiLE tEl ", "billing mobile tel"], + ["billing home tel", "billing home tel"], + ["home section-blue tel", ""], + ["setion-blue work email", ""], + ["section-blue home address-level2", ""], + ["section-blue shipping name", "section-blue shipping name"], + ["section-blue mobile tel", "section-blue mobile tel"], + ["shipping webauthn tel", ""], + + // Three tokens + WebAuthn credential type + ["invalid mobile tel webauthn", ""], + ["section-blue shipping name webauthn", "section-blue shipping name webauthn"], + + // Four tokens + ["billing billing mobile tel", ""], + ["name section-blue shipping home", ""], + ["secti shipping work address-line1", ""], + ["section-blue shipping home name", ""], + ["section-blue shipping mobile tel", "section-blue shipping mobile tel"], + ["section-blue webauthn mobile tel", ""], + + // Four tokens + WebAuthn credential type + ["section-blue shipping home name webauthn", ""], + ["section-blue shipping mobile tel webauthn", "section-blue shipping mobile tel webauthn"], + + // Five tokens (invalid) + ["billing billing billing mobile tel", ""], + ["section-blue section-blue billing mobile tel", ""], + ["section-blue section-blue billing webauthn tel", ""], + + // Five tokens + WebAuthn credential type (invalid) + ["billing billing billing mobile tel webauthn", ""], +]; + +var types = [undefined, "hidden", "text", "search"]; // Valid types for all non-multiline hints. + +function checkAutocompleteValues(field, type) { + for (var test of values) { + if (typeof(test[0]) === "undefined") + field.removeAttribute("autocomplete"); + else + field.setAttribute("autocomplete", test[0]); + is(field.autocomplete, test[1], "Checking @autocomplete for @type=" + type + " of: " + test[0]); + is(field.autocomplete, test[1], "Checking cached @autocomplete for @type=" + type + " of: " + test[0]); + } +} + +function start() { + var inputField = document.getElementById("input-field"); + for (var type of types) { + // Switch the input type + if (typeof(type) === "undefined") + inputField.removeAttribute("type"); + else + inputField.type = type; + checkAutocompleteValues(inputField, type || ""); + } + + var selectField = document.getElementById("select-field"); + checkAutocompleteValues(selectField, "select"); + + var textarea = document.getElementById("textarea"); + checkAutocompleteValues(textarea, "textarea"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [["dom.forms.autocomplete.formautofill", true]]}, start); +</script> +</head> + +<body> +<p id="display"></p> +<div id="content" style="display: none"> + <form> + <input id="input-field" /> + <select id="select-field" /> + <textarea id="textarea"></textarea> + </form> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_autocompleteinfo.html b/dom/html/test/forms/test_autocompleteinfo.html new file mode 100644 index 0000000000..a3357ac8de --- /dev/null +++ b/dom/html/test/forms/test_autocompleteinfo.html @@ -0,0 +1,206 @@ +<!DOCTYPE html> +<html> +<!-- +Test getAutocompleteInfo() on <input> and <select> +--> +<head> + <title>Test for getAutocompleteInfo()</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> + +<body> +<p id="display"></p> +<div id="content" style="display: none"> + <form> + <input id="input"/> + <select id="select" /> + </form> +</div> +<pre id="test"> +<script> +"use strict"; + +var values = [ + // Missing or empty attribute + [undefined, {}, ""], + ["", {}, ""], + + // One token + ["on", {fieldName: "on" }, "on"], + ["On", {fieldName: "on" }, "on"], + ["off", {fieldName: "off", canAutomaticallyPersist: false}, "off" ], + ["name", {fieldName: "name" }, "name"], + [" name ", {fieldName: "name" }, "name"], + ["username", {fieldName: "username"}, "username"], + [" username ", {fieldName: "username"}, "username"], + ["current-password", {fieldName: "current-password", canAutomaticallyPersist: false}, "current-password"], + ["new-password", {fieldName: "new-password", canAutomaticallyPersist: false}, "new-password"], + ["cc-number", {fieldName: "cc-number", canAutomaticallyPersist: false}, "cc-number"], + ["cc-csc", {fieldName: "cc-csc", canAutomaticallyPersist: false}, ""], + ["one-time-code", {fieldName: "one-time-code", canAutomaticallyPersist: false}, ""], + ["language", {fieldName: "language"}, ""], + [" language ", {fieldName: "language"}, ""], + ["tel-extension", {fieldName: "tel-extension"}, ""], + ["foobar", {}, ""], + ["section-blue", {}, ""], + [" WEBAUTHN ", {fieldName: "webauthn", credentialType: "webauthn"}, "webauthn"], + + // One token + WebAuthn credential type + ["on webauthn", {}, ""], + ["off webauthn", {}, ""], + ["webauthn webauthn", {}, ""], + ["username WebAuthn", {fieldName: "username", credentialType: "webauthn"}, "username webauthn"], + ["current-PASSWORD webauthn", {fieldName: "current-password", credentialType: "webauthn", canAutomaticallyPersist: false}, "current-password webauthn"], + + // Two tokens + ["on off", {}, ""], + ["off on", {}, ""], + ["username tel", {}, ""], + ["tel username ", {}, ""], + [" username tel ", {}, ""], + ["tel mobile", {}, ""], + ["tel shipping", {}, ""], + ["shipping tel", {addressType: "shipping", fieldName: "tel"}, "shipping tel"], + ["shipPING tel", {addressType: "shipping", fieldName: "tel"}, "shipping tel"], + ["mobile tel", {contactType: "mobile", fieldName: "tel"}, "mobile tel"], + [" MoBiLe TeL ", {contactType: "mobile", fieldName: "tel"}, "mobile tel"], + ["pager impp", {contactType: "pager", fieldName: "impp"}, ""], + ["fax tel-extension", {contactType: "fax", fieldName: "tel-extension"}, ""], + ["XXX tel", {}, ""], + ["XXX username", {}, ""], + ["name section-blue", {}, ""], + ["scetion-blue cc-name", {}, ""], + ["pager language", {}, ""], + ["fax url", {}, ""], + ["section-blue name", {section: "section-blue", fieldName: "name"}, "section-blue name"], + ["section-blue tel", {section: "section-blue", fieldName: "tel"}, "section-blue tel"], + ["webauthn username", {}, ""], + + // Two tokens + WebAuthn credential type + ["fax url webauthn", {}, ""], + ["shipping tel webauthn", {addressType: "shipping", fieldName: "tel", credentialType: "webauthn"}, "shipping tel webauthn"], + + // Three tokens + ["billing invalid tel", {}, ""], + ["___ mobile tel", {}, ""], + ["mobile foo tel", {}, ""], + ["mobile tel foo", {}, ""], + ["tel mobile billing", {}, ""], + ["billing mobile tel", {addressType: "billing", contactType: "mobile", fieldName: "tel"}, "billing mobile tel"], + [" BILLing MoBiLE tEl ", {addressType: "billing", contactType: "mobile", fieldName: "tel"}, "billing mobile tel"], + ["billing home tel", {addressType: "billing", contactType: "home", fieldName: "tel"}, "billing home tel"], + ["home section-blue tel", {}, ""], + ["setion-blue work email", {}, ""], + ["section-blue home address-level2", {}, ""], + ["section-blue shipping name", {section: "section-blue", addressType: "shipping", fieldName: "name"}, "section-blue shipping name"], + ["section-blue mobile tel", {section: "section-blue", contactType: "mobile", fieldName: "tel"}, "section-blue mobile tel"], + ["shipping webauthn tel", {}, ""], + + // Three tokens + WebAuthn credential type + ["invalid mobile tel webauthn", {}, ""], + ["section-blue shipping name webauthn", {section: "section-blue", addressType: "shipping", fieldName: "name", credentialType: "webauthn"}, "section-blue shipping name webauthn"], + + // Four tokens + ["billing billing mobile tel", {}, ""], + ["name section-blue shipping home", {}, ""], + ["secti shipping work address-line1", {}, ""], + ["section-blue shipping home name", {}, ""], + ["section-blue shipping mobile tel", {section: "section-blue", addressType: "shipping", contactType: "mobile", fieldName: "tel"}, "section-blue shipping mobile tel"], + ["section-blue webauthn mobile tel", {}, ""], + + // Four tokens + WebAuthn credential type + ["section-blue shipping home name webauthn", {}, ""], + ["section-blue shipping mobile tel webauthn", {section: "section-blue", addressType: "shipping", contactType: "mobile", fieldName: "tel", credentialType: "webauthn"}, "section-blue shipping mobile tel webauthn"], + + // Five tokens (invalid) + ["billing billing billing mobile tel", {}, ""], + ["section-blue section-blue billing mobile tel", {}, ""], + ["section-blue section-blue billing webauthn tel", {}, ""], + + // Five tokens + WebAuthn credential type (invalid) + ["billing billing billing mobile tel webauthn", {}, ""], +]; + +var autocompleteInfoFieldIds = ["input", "select"]; +var autocompleteEnabledTypes = ["hidden", "text", "search", "url", "tel", + "email", "password", "date", "time", "number", + "range", "color"]; +var autocompleteDisabledTypes = ["reset", "submit", "image", "button", "radio", + "checkbox", "file"]; + +function testInputTypes() { + let field = document.getElementById("input"); + + for (var type of autocompleteEnabledTypes) { + testAutocomplete(field, type, true); + } + + for (var type of autocompleteDisabledTypes) { + testAutocomplete(field, type, false); + } + + // Clear input type attribute. + field.removeAttribute("type"); +} + +function testAutocompleteInfoValue(aEnabled) { + for (var fieldId of autocompleteInfoFieldIds) { + let field = document.getElementById(fieldId); + + for (var test of values) { + if (typeof(test[0]) === "undefined") + field.removeAttribute("autocomplete"); + else + field.setAttribute("autocomplete", test[0]); + + var info = field.getAutocompleteInfo(); + if (aEnabled) { + // We need to consider if getAutocompleteInfo() is valid, + // but @autocomplete is invalid case, because @autocomplete + // has smaller set of values. + is(field.autocomplete, test[2], "Checking @autocomplete of: " + test[0]); + } + + is(info.section, "section" in test[1] ? test[1].section : "", + "Checking autocompleteInfo.section for " + field + ": " + test[0]); + is(info.addressType, "addressType" in test[1] ? test[1].addressType : "", + "Checking autocompleteInfo.addressType for " + field + ": " + test[0]); + is(info.contactType, "contactType" in test[1] ? test[1].contactType : "", + "Checking autocompleteInfo.contactType for " + field + ": " + test[0]); + is(info.fieldName, "fieldName" in test[1] ? test[1].fieldName : "", + "Checking autocompleteInfo.fieldName for " + field + ": " + test[0]); + is(info.credentialType, "credentialType" in test[1] ? test[1].credentialType: "", + "Checking autocompleteInfo.credentialType for " + field + ": " + test[0]); + is(info.canAutomaticallyPersist, "canAutomaticallyPersist" in test[1] ? test[1].canAutomaticallyPersist : true, + "Checking autocompleteInfo.canAutomaticallyPersist for " + field + ": " + test[0]); + } + } +} + +function testAutocomplete(aField, aType, aEnabled) { + aField.type = aType; + if (aEnabled) { + ok(aField.getAutocompleteInfo() !== null, "getAutocompleteInfo shouldn't return null"); + } else { + is(aField.getAutocompleteInfo(), null, "getAutocompleteInfo should return null"); + } +} + +// getAutocompleteInfo() should be able to parse all tokens as defined +// in the spec regardless of whether dom.forms.autocomplete.formautofill pref +// is on or off. +add_task(async function testAutocompletePreferenceEnabled() { + await SpecialPowers.pushPrefEnv({"set": [["dom.forms.autocomplete.formautofill", true]]}, testInputTypes); + testAutocompleteInfoValue(true); +}); + +add_task(async function testAutocompletePreferenceDisabled() { + await SpecialPowers.pushPrefEnv({"set": [["dom.forms.autocomplete.formautofill", false]]}, testInputTypes); + testAutocompleteInfoValue(false); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_bug1039548.html b/dom/html/test/forms/test_bug1039548.html new file mode 100644 index 0000000000..cea3cd67ef --- /dev/null +++ b/dom/html/test/forms/test_bug1039548.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1039548 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1039548</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1039548 **/ + + SimpleTest.waitForExplicitFinish(); + + SimpleTest.waitForFocus(test); + + var didTryToSubmit; + function test() { + var r = document.getElementById("radio"); + r.focus(); + didTryToSubmit = false; + sendKey("return"); + ok(!didTryToSubmit, "Shouldn't have tried to submit!"); + + var t = document.getElementById("text"); + t.focus(); + didTryToSubmit = false; + sendKey("return"); + ok(didTryToSubmit, "Should have tried to submit!"); + SimpleTest.finish(); + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1039548">Mozilla Bug 1039548</a> +<p id="display"></p> +<div id="content"> + + <form onsubmit="didTryToSubmit = true; event.preventDefault();"> + <input type="radio" id="radio"> + </form> + + <form onsubmit="didTryToSubmit = true; event.preventDefault();"> + <input type="text" id="text"> + </form> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_bug1283915.html b/dom/html/test/forms/test_bug1283915.html new file mode 100644 index 0000000000..90bffd4b20 --- /dev/null +++ b/dom/html/test/forms/test_bug1283915.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1283915 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1283915</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1283915 **/ + + SimpleTest.waitForExplicitFinish(); + + function isCursorAtEnd(field){ + is(field.selectionStart, field.value.length); + is(field.selectionEnd, field.value.length); + } + + function test() { + var tField = document.getElementById("textField"); + tField.focus(); + + sendString("a"); + is(tField.value, "a"); + isCursorAtEnd(tField); + document.body.offsetWidth; // frame must be created after type change + + sendString("b"); + is(tField.value, "ab"); + isCursorAtEnd(tField); + + sendString("c"); + is(tField.value, "abc"); + isCursorAtEnd(tField); + + var nField = document.getElementById("numField"); + nField.focus(); + + sendString("1"); + is(nField.value, "1"); + document.body.offsetWidth; + + sendString("2"); + is(nField.value, "12"); + + sendString("3"); + is(nField.value, "123"); + + SimpleTest.finish(); + } + + SimpleTest.waitForFocus(test); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1283915">Mozilla Bug 1283915</a> +<p id="display"></p> +<input id="textField" type="text" oninput="if (this.type !='password') this.type = 'password';"> +<input id="numField" type="text" oninput="if (this.type !='number') this.type = 'number';"> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_bug1286509.html b/dom/html/test/forms/test_bug1286509.html new file mode 100644 index 0000000000..638e7fe85c --- /dev/null +++ b/dom/html/test/forms/test_bug1286509.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1286509 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1286509</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1286509">Mozilla Bug 1286509</a> +<p id="display"></p> +<div id="content"> + <input type="range" id="test_input" min="0" max="10" value="5"> +</div> +<pre id="test"> + <script type="application/javascript"> + /** Test for Bug 1286509 **/ + SimpleTest.waitForExplicitFinish(); + var expectedEventSequence = ['keydown', 'change', 'keyup']; + var eventCounts = {}; + var expectedEventIdx = 0; + + function test() { + var range = document.getElementById("test_input"); + range.focus(); + expectedEventSequence.forEach((eventName) => { + eventCounts[eventName] = 0; + range.addEventListener(eventName, (e) => { + ++eventCounts[eventName]; + is(expectedEventSequence[expectedEventIdx], e.type, "Events sequence should be keydown, change, keyup"); + expectedEventIdx = (expectedEventIdx + 1) % 3; + }); + }); + synthesizeKey("KEY_ArrowUp"); + synthesizeKey("KEY_ArrowDown"); + synthesizeKey("KEY_ArrowLeft"); + synthesizeKey("KEY_ArrowRight"); + is(eventCounts.change, 4, "Expect key up/down/left/right should trigger range input to fire change events"); + SimpleTest.finish(); + } + addLoadEvent(test); + </script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_button_attributes_reflection.html b/dom/html/test/forms/test_button_attributes_reflection.html new file mode 100644 index 0000000000..de2097cb4c --- /dev/null +++ b/dom/html/test/forms/test_button_attributes_reflection.html @@ -0,0 +1,144 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLButtonElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="../reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLButtonElement attributes reflection **/ + +// .autofocus +reflectBoolean({ + element: document.createElement("button"), + attribute: "autofocus", +}); + +// .disabled +reflectBoolean({ + element: document.createElement("button"), + attribute: "disabled", +}); + +// .formAction +reflectURL({ + element: document.createElement("button"), + attribute: "formAction", +}); + +// .formEnctype +reflectLimitedEnumerated({ + element: document.createElement("button"), + attribute: "formEnctype", + validValues: [ + "application/x-www-form-urlencoded", + "multipart/form-data", + "text/plain", + ], + invalidValues: [ "text/html", "", "tulip" ], + defaultValue: { + invalid: "application/x-www-form-urlencoded", + missing: "", + } +}); + +// .formMethod +add_task(async function() { + reflectLimitedEnumerated({ + element: document.createElement("button"), + attribute: "formMethod", + validValues: [ "get", "post", "dialog"], + invalidValues: [ "put", "", "tulip" ], + defaultValue: { + invalid: "get", + missing: "", + } + }); +}); + +// .formNoValidate +reflectBoolean({ + element: document.createElement("button"), + attribute: "formNoValidate", +}); + +// .formTarget +reflectString({ + element: document.createElement("button"), + attribute: "formTarget", + otherValues: [ "_blank", "_self", "_parent", "_top" ], +}); + +// .name +reflectString({ + element: document.createElement("button"), + attribute: "name", + otherValues: [ "isindex", "_charset_" ] +}); + +// .type +reflectLimitedEnumerated({ + element: document.createElement("button"), + attribute: "type", + validValues: [ "submit", "reset", "button" ], + invalidValues: [ "this-is-probably-a-wrong-type", "", "tulip" ], + unsupportedValues: [ "menu" ], + defaultValue: "submit", +}); + +// .value +reflectString({ + element: document.createElement("button"), + attribute: "value", +}); + +// .willValidate +ok("willValidate" in document.createElement("button"), + "willValidate should be an IDL attribute of the button element"); +is(typeof(document.createElement("button").willValidate), "boolean", + "button.willValidate should be a boolean"); + +// .validity +ok("validity" in document.createElement("button"), + "validity should be an IDL attribute of the button element"); +is(typeof(document.createElement("button").validity), "object", + "button.validity should be an object"); +ok(document.createElement("button").validity instanceof ValidityState, + "button.validity sohuld be an instance of ValidityState"); + +// .validationMessage +ok("validationMessage" in document.createElement("button"), + "validationMessage should be an IDL attribute of the button element"); +is(typeof(document.createElement("button").validationMessage), "string", + "button.validationMessage should be a string"); + +// .checkValidity() +ok("checkValidity" in document.createElement("button"), + "checkValidity() should be a method of the button element"); +is(typeof(document.createElement("button").checkValidity), "function", + "button.checkValidity should be a function"); + +// .setCustomValidity() +ok("setCustomValidity" in document.createElement("button"), + "setCustomValidity() should be a method of the button element"); +is(typeof(document.createElement("button").setCustomValidity), "function", + "button.setCustomValidity should be a function"); + +// .labels +ok("labels" in document.createElement("button"), + "button.labels should be an IDL attribute of the button element"); +is(typeof(document.createElement("button").labels), "object", + "button.labels should be an object"); +ok(document.createElement("button").labels instanceof NodeList, + "button.labels sohuld be an instance of NodeList"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_change_event.html b/dom/html/test/forms/test_change_event.html new file mode 100644 index 0000000000..8be4554c58 --- /dev/null +++ b/dom/html/test/forms/test_change_event.html @@ -0,0 +1,286 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=722599 +--> +<head> +<title>Test for Bug 722599</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=722599">Mozilla Bug 722599</a> +<p id="display"></p> +<div id="content"> +<input type="file" id="fileInput"></input> +<textarea id="textarea" onchange="++textareaChange;"></textarea> +<input type="text" id="input_text" onchange="++textInputChange[0];"></input> +<input type="email" id="input_email" onchange="++textInputChange[1];"></input> +<input type="search" id="input_search" onchange="++textInputChange[2];"></input> +<input type="tel" id="input_tel" onchange="++textInputChange[3];"></input> +<input type="url" id="input_url" onchange="++textInputChange[4];"></input> +<input type="password" id="input_password" onchange="++textInputChange[5];"></input> + +<!-- "Non-text" inputs--> +<input type="button" id="input_button" onchange="++NonTextInputChange[0];"></input> +<input type="submit" id="input_submit" onchange="++NonTextInputChange[1];"></input> +<input type="image" id="input_image" onchange="++NonTextInputChange[2];"></input> +<input type="reset" id="input_reset" onchange="++NonTextInputChange[3];"></input> +<input type="radio" id="input_radio" onchange="++NonTextInputChange[4];"></input> +<input type="checkbox" id="input_checkbox" onchange="++NonTextInputChange[5];"></input> +<input type="number" id="input_number" onchange="++numberChange;"></input> +<input type="range" id="input_range" onchange="++rangeChange;"></input> + +<!-- Input text with default value and blurs on focus--> +<input type="text" id="input_text_value" onchange="++textInputValueChange" + onfocus="this.blur();" value="foo"></input> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + /** Test for Bug 722599 **/ + + const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent); + + var textareaChange = 0; + var fileInputChange = 0; + var textInputValueChange = 0; + + var textInputTypes = ["text", "email", "search", "tel", "url", "password"]; + var textInputChange = [0, 0, 0, 0, 0, 0]; + + var NonTextInputTypes = ["button", "submit", "image", "reset", "radio", "checkbox"]; + var NonTextInputChange = [0, 0, 0, 0, 0, 0]; + + var numberChange = 0; + var rangeChange = 0; + + var blurTestCalled = false; //Sentinel to prevent infinite loop. + + SimpleTest.waitForExplicitFinish(); + var MockFilePicker = SpecialPowers.MockFilePicker; + MockFilePicker.init(window); + + function fileInputBlurTest() { + var btn = document.getElementById('fileInput'); + btn.focus() + btn.blur(); + is(fileInputChange, 1, "change event shouldn't be dispatched on blur for file input element(1)"); + } + + function testUserInput() { + //Simulating an OK click and with a file name return. + MockFilePicker.useBlobFile(); + MockFilePicker.returnValue = MockFilePicker.returnOK; + var input = document.getElementById('fileInput'); + input.focus(); + + input.addEventListener("change", function (aEvent) { + ++fileInputChange; + if (!blurTestCalled) { + is(fileInputChange, 1, "change event should have been dispatched on file input."); + blurTestCalled = true; + fileInputBlurTest(); + } + else { + is(fileInputChange, 1, "change event shouldn't be dispatched on blur for file input element (2)"); + } + }); + input.click(); + // blur the file input, we can't use blur() because of bug 760283 + document.getElementById('input_text').focus(); + setTimeout(testUserInput2, 0); + } + + function testUserInput2() { + var input = document.getElementById('fileInput'); + // remove it, otherwise cleanup() opens a native file picker! + input.remove(); + MockFilePicker.cleanup(); + + //text, email, search, telephone, url & password input tests + for (var i = 0; i < textInputTypes.length; ++i) { + input = document.getElementById("input_" + textInputTypes[i]); + input.focus(); + synthesizeKey("KEY_Enter"); + is(textInputChange[i], 0, "Change event shouldn't be dispatched on " + textInputTypes[i] + " input element"); + + sendString("m"); + synthesizeKey("KEY_Enter"); + is(textInputChange[i], 1, textInputTypes[i] + " input element should have dispatched change event."); + } + + //focus and blur text input + input = document.getElementById("input_text"); + input.focus(); + sendString("f"); + input.blur(); + is(textInputChange[0], 2, "text input element should have dispatched change event (2)."); + + // value being set while focused + input.focus(); + input.value = 'foo'; + input.blur(); + is(textInputChange[0], 2, "text input element should not have dispatched change event (2)."); + + // value being set while focused after being modified manually + input.focus(); + sendString("f"); + input.value = 'bar'; + input.blur(); + is(textInputChange[0], 3, "text input element should have dispatched change event (3)."); + + //focus and blur textarea + var textarea = document.getElementById("textarea"); + textarea.focus(); + sendString("f"); + textarea.blur(); + is(textareaChange, 1, "Textarea element should have dispatched change event."); + + // value being set while focused + textarea.focus(); + textarea.value = 'foo'; + textarea.blur(); + is(textareaChange, 1, "textarea should not have dispatched change event (1)."); + + // value being set while focused after being modified manually + textarea.focus(); + sendString("f"); + textarea.value = 'bar'; + textarea.blur(); + is(textareaChange, 2, "textearea should have dispatched change event (2)."); + + //Non-text input tests: + for (var i = 0; i < NonTextInputTypes.length; ++i) { + //button, submit, image and reset input type tests. + if (i < 4) { + input = document.getElementById("input_" + NonTextInputTypes[i]); + input.focus(); + input.click(); + is(NonTextInputChange[i], 0, "Change event shouldn't be dispatched on " + NonTextInputTypes[i] + " input element"); + input.blur(); + is(NonTextInputChange[i], 0, "Change event shouldn't be dispatched on " + NonTextInputTypes[i] + " input element(2)"); + } + //for radio and and checkboxes, we require that change event should ONLY be dispatched on setting the value. + else { + input = document.getElementById("input_" + NonTextInputTypes[i]); + input.focus(); + input.click(); + is(NonTextInputChange[i], 1, NonTextInputTypes[i] + " input element should have dispatched change event."); + input.blur(); + is(NonTextInputChange[i], 1, "Change event shouldn't be dispatched on " + NonTextInputTypes[i] + " input element"); + + // Test that change event is not dispatched if click event is cancelled. + function preventDefault(e) { + e.preventDefault(); + } + input.addEventListener("click", preventDefault); + input.click(); + is(NonTextInputChange[i], 1, "Change event shouldn't be dispatched if click event is cancelled"); + input.removeEventListener("click", preventDefault); + } + } + + // Special case type=number + var number = document.getElementById("input_number"); + number.focus(); + sendString("a"); + number.blur(); + is(numberChange, 0, "Change event shouldn't be dispatched on number input element for key changes that don't change its value"); + number.value = ""; + number.focus(); + sendString("12"); + is(numberChange, 0, "Change event shouldn't be dispatched on number input element for keyboard input until it loses focus"); + number.blur(); + is(numberChange, 1, "Change event should be dispatched on number input element on blur"); + is(number.value, "12", "Sanity check that number keys were actually handled"); + if (isDesktop) { // up/down arrow keys not supported on android/b2g + number.value = ""; + number.focus(); + synthesizeKey("KEY_ArrowUp"); + synthesizeKey("KEY_ArrowUp"); + synthesizeKey("KEY_ArrowDown"); + is(numberChange, 4, "Change event should be dispatched on number input element for up/down arrow keys (a special case)"); + is(number.value, "1", "Sanity check that number and arrow keys were actually handled"); + } + + // Special case type=range + var range = document.getElementById("input_range"); + range.focus(); + sendString("a"); + range.blur(); + is(rangeChange, 0, "Change event shouldn't be dispatched on range input element for key changes that don't change its value"); + range.focus(); + synthesizeKey("VK_HOME"); + is(rangeChange, 1, "Change event should be dispatched on range input element for key changes"); + range.blur(); + is(rangeChange, 1, "Change event shouldn't be dispatched on range input element on blur"); + range.focus(); + var bcr = range.getBoundingClientRect(); + var centerOfRangeX = bcr.width / 2; + var centerOfRangeY = bcr.height / 2; + synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, { type: "mousedown" }); + is(rangeChange, 1, "Change event shouldn't be dispatched on range input element for mousedown"); + synthesizeMouse(range, centerOfRangeX - 5, centerOfRangeY, { type: "mousemove" }); + is(rangeChange, 1, "Change event shouldn't be dispatched on range input element during drag of thumb"); + synthesizeMouse(range, centerOfRangeX, centerOfRangeY, { type: "mouseup" }); + is(rangeChange, 2, "Change event should be dispatched on range input element at end of drag"); + range.blur(); + is(rangeChange, 2, "Change event shouldn't be dispatched on range input element when range loses focus after a drag"); + synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, {}); + is(rangeChange, 3, "Change event should be dispatched on range input element for a click that gives the range focus"); + + if (isDesktop) { // up/down arrow keys not supported on android/b2g + synthesizeKey("KEY_ArrowUp"); + is(rangeChange, 4, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowUp)"); + synthesizeKey("KEY_ArrowDown"); + is(rangeChange, 5, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowDown)"); + synthesizeKey("KEY_ArrowRight"); + is(rangeChange, 6, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowRight)"); + synthesizeKey("KEY_ArrowLeft"); + is(rangeChange, 7, "Change event should be dispatched on range input element for key changes that change its value (KEY_ArrowLeft)"); + synthesizeKey("KEY_ArrowUp", {shiftKey: true}); + is(rangeChange, 8, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowUp)"); + synthesizeKey("KEY_ArrowDown", {shiftKey: true}); + is(rangeChange, 9, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowDown)"); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}); + is(rangeChange, 10, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowRight)"); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + is(rangeChange, 11, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_ArrowLeft)"); + synthesizeKey("KEY_PageUp"); + is(rangeChange, 12, "Change event should be dispatched on range input element for key changes that change its value (KEY_PageUp)"); + synthesizeKey("KEY_PageDown"); + is(rangeChange, 13, "Change event should be dispatched on range input element for key changes that change its value (KEY_PageDown"); + synthesizeKey("KEY_ArrowRight", {shiftKey: true}); + is(rangeChange, 14, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_PageUp)"); + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + is(rangeChange, 15, "Change event should be dispatched on range input element for key changes that change its value (Shift+KEY_PageDown)"); + } + //Input type change test. + input = document.getElementById("input_checkbox"); + input.type = "text"; + input.focus(); + input.click(); + input.blur(); + is(NonTextInputChange[5], 1, "Change event shouldn't be dispatched for checkbox ---> text input type change"); + + setTimeout(testInputWithDefaultValue, 0); + } + + function testInputWithDefaultValue() { + // focus and blur an input text should not trigger change event if content hasn't changed. + var input = document.getElementById('input_text_value'); + input.focus(); + is(textInputValueChange, 0, "change event shouldn't be dispatched on input text with default value"); + + SimpleTest.finish(); + } + + addLoadEvent(testUserInput); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_datalist_element.html b/dom/html/test/forms/test_datalist_element.html new file mode 100644 index 0000000000..5f05634018 --- /dev/null +++ b/dom/html/test/forms/test_datalist_element.html @@ -0,0 +1,118 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for the datalist element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + <datalist> + </datalist> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 555840 **/ + +function checkClassesAndAttributes() +{ + var d = document.getElementsByTagName('datalist'); + is(d.length, 1, "One datalist has been found"); + + d = d[0]; + ok(d instanceof HTMLDataListElement, + "The datalist should be instance of HTMLDataListElement"); + + ok('options' in d, "datalist has an options IDL attribute"); + + ok(d.options, "options IDL attribute is not null"); + ok(!d.getAttribute('options'), "datalist has no options content attribute"); + + ok(d.options instanceof HTMLCollection, + "options IDL attribute should be instance of HTMLCollection"); +} + +function checkOptions() +{ + var testData = [ + /* [ Child list, Function modifying children, Recognized options ] */ + [['option'], null, 1], + [['option', 'option', 'option', 'option'], null, 4], + /* Disabled options are not valid. */ + [['option'], function(d) { d.childNodes[0].disabled = true; }, 0], + [['option', 'option'], function(d) { d.childNodes[0].disabled = true; }, 1], + /* Non-option elements are not recognized. */ + [['input'], null, 0], + [['input', 'option'], null, 1], + [['input', 'textarea'], null, 0], + /* .value and .label are not needed to be valid options. */ + [['option', 'option'], function(d) { d.childNodes[0].value = 'value'; }, 2], + [['option', 'option'], function(d) { d.childNodes[0].label = 'label'; }, 2], + [['option', 'option'], function(d) { d.childNodes[0].value = 'value'; d.childNodes[0].label = 'label'; }, 2], + [['select'], + function(d) { + var s = d.childNodes[0]; + s.appendChild(new Option("foo")); + s.appendChild(new Option("bar")); + }, + 2], + [['select'], + function(d) { + var s = d.childNodes[0]; + s.appendChild(new Option("foo")); + s.appendChild(new Option("bar")); + var label = document.createElement("label"); + d.appendChild(label); + label.appendChild(new Option("foobar")); + }, + 3], + [['select'], + function(d) { + var s = d.childNodes[0]; + s.appendChild(new Option("foo")); + s.appendChild(new Option("bar")); + var label = document.createElement("label"); + d.appendChild(label); + label.appendChild(new Option("foobar")); + s.appendChild(new Option()) + }, + 4], + [[], function(d) { d.appendChild(document.createElementNS("foo", "option")); }, 0] + ]; + + var d = document.getElementsByTagName('datalist')[0]; + var cachedOptions = d.options; + + testData.forEach(function(data) { + data[0].forEach(function(e) { + d.appendChild(document.createElement(e)); + }) + + /* Modify children. */ + if (data[1]) { + data[1](d); + } + + is(d.options, cachedOptions, "Should get the same object") + is(d.options.length, data[2], + "The number of recognized options should be " + data[2]) + + for (var i = 0; i < d.options.length; ++i) { + is(d.options[i].localName, "option", + "Should get an option for d.options[" + i + "]") + } + + /* Cleaning-up. */ + d.textContent = ""; + }) +} + +checkClassesAndAttributes(); +checkOptions(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_double_submit.html b/dom/html/test/forms/test_double_submit.html new file mode 100644 index 0000000000..d27fb290a4 --- /dev/null +++ b/dom/html/test/forms/test_double_submit.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for multiple submissions in straightline code</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script> + +add_task(async function double_submit() { + dump("test start\n"); + let popup = window.open("file_double_submit.html"); + await new Promise(resolve => { + popup.addEventListener("load", resolve, {once: true}) + }); + + let numCalls = 0; + popup.addEventListener("beforeunload", () => { + numCalls++; + info("beforeunload called " + numCalls + " times"); + }); + + info("clicking button"); + popup.document.querySelector("button").click(); + + is(numCalls, 1, "beforeunload should only fire once"); + popup.close(); +}); + +</script> +</body> +</html> diff --git a/dom/html/test/forms/test_form_attribute-1.html b/dom/html/test/forms/test_form_attribute-1.html new file mode 100644 index 0000000000..6735f514ae --- /dev/null +++ b/dom/html/test/forms/test_form_attribute-1.html @@ -0,0 +1,473 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=588683 +--> +<head> + <title>Test for form attributes 1</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for form attributes 1 **/ + +/** + * All functions take an array of forms in first argument and an array of + * elements in second argument. + * Then, it returns an array containing an array of form and an array of array + * of elements. The array represent the form association with elements like this: + * [ [ form1, form2 ], [ [ elmt1ofForm1, elmt2ofForm2 ], [ elmtofForm2 ] ] ] + */ + +/** + * test0a and test0b are testing the regular behavior of form ownership. + */ +function test0a(aForms, aElements) +{ + // <form><element></form> + // <form><element></form> + aForms[0].appendChild(aElements[0]); + aForms[1].appendChild(aElements[1]); + + return [[aForms[0],aForms[1]],[[aElements[0]],[aElements[1]]]]; +} + +function test0b(aForms, aElements) +{ + // <form><element><form><element></form></form> + aForms[0].appendChild(aElements[0]); + aForms[0].appendChild(aForms[1]); + aForms[1].appendChild(aElements[1]); + + return [[aForms[0],aForms[1]],[[aElements[0]],[aElements[1]]]]; +} + +/** + * This function test that, when an element is not a descendant of a form + * element and has @form set to a valid form id, it's form owner is the form + * which has the id. + */ +function test1(aForms, aElements) +{ + // <form id='f'></form><element id='f'> + aForms[0].id = 'f'; + aElements[0].setAttribute('form', 'f'); + + return [[aForms[0]], [[aElements[0]]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a valid form id (not it's descendant), it's form + * owner is the form which has the id. + */ +function test2(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + + return [[aForms[0], aForms[1]], [[aElements[0]],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a valid form id (not it's descendant), then the + * form attribute is removed, it does not have a form owner. + */ +function test3(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + aElements[0].removeAttribute('form'); + + return [[aForms[0], aForms[1]], [[],[aElements[0]]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a valid form id (not it's descendant), then the + * form's id attribute is removed, it does not have a form owner. + */ +function test4(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + aForms[0].removeAttribute('id'); + + return [[aForms[0], aForms[1]], [[],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to an invalid form id, then it does not have a form + * owner. + */ +function test5(aForms, aElements) +{ + // <form id='f'></form><form><element form='foo'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'foo'); + + return [[aForms[0], aForms[1]], [[],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a valid form id (not it's descendant), then the + * form id attribute is changed to an invalid id, it does not have a form owner. + */ +function test6(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + aElements[0].setAttribute('form', 'foo'); + + return [[aForms[0], aForms[1]], [[],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to an invalid form id, then the form id attribute + * is changed to a valid form id, it's form owner is the form which has this id. + */ +function test7(aForms, aElements) +{ + // <form id='f'></form><form><element form='foo'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'foo'); + aElements[0].setAttribute('form', 'f'); + + return [[aForms[0], aForms[1]], [[aElements[0]],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a list of ids containing one valid form, then + * it does not have a form owner. + */ +function test8(aForms, aElements) +{ + // <form id='f'></form><form><element form='f foo'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f foo'); + + return [[aForms[0], aForms[1]], [[],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a form id which is valid in a case insensitive + * way, then it does not have a form owner. + */ +function test9(aForms, aElements) +{ + // <form id='f'></form><form><element form='F'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'F'); + + return [[aForms[0], aForms[1]], [[],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a form id which is not a valid id, then it's + * form owner is it does not have a form owner. + */ +function test10(aForms, aElements) +{ + // <form id='F'></form><form><element form='f'></form> + aForms[0].id = 'F'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + + return [[aForms[0], aForms[1]], [[],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a form id which is not a valid id, then it's + * form owner is it does not have a form owner. + */ +function test11(aForms, aElements) +{ + // <form id='foo bar'></form><form><element form='foo bar'></form> + aForms[0].id = 'foo bar'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'foo bar'); + + return [[aForms[0], aForms[1]], [[aElements[0]],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a valid form id and the form id change, then + * it does not have a form owner. + */ +function test12(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + aForms[0].id = 'foo'; + + return [[aForms[0], aForms[1]], [[],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to an invalid form id and the form id change to a + * valid one, then it's form owner is the form which has the id. + */ +function test13(aForms, aElements) +{ + // <form id='foo'></form><form><element form='f'></form> + aForms[0].id = 'foo'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + aForms[0].id = 'f'; + + return [[aForms[0], aForms[1]], [[aElements[0]],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a valid form id and a form with the same id is + * inserted before in the tree, then it's form owner is the form which has the + * id. + */ +function test14(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + aForms[2].id = 'f'; + + document.getElementById('content').insertBefore(aForms[2], aForms[0]); + + return [[aForms[0], aForms[1], aForms[2]], [[],[],[aElements[0]]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a valid form id and an element with the same id is + * inserted before in the tree, then it does not have a form owner. + */ +function test15(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + aElements[1].id = 'f'; + + document.getElementById('content').insertBefore(aElements[1], aForms[0]); + + return [[aForms[0], aForms[1]], [[],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form + * element and has @form set to a valid form id and the form is removed from + * the tree, then it does not have a form owner. + */ +function test16(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + aElements[1].id = 'f'; + + document.getElementById('content').removeChild(aForms[0]); + + return [[aForms[0], aForms[1]], [[],[]]]; +} + +/** + * This function test that, when an element is a descendant of a form element + * and has @form set to the empty string, it does not have a form owner. + */ +function test17(aForms, aElements) +{ + // <form><element form=''></form> + aForms[0].appendChild(aElements[0]); + aElements[0].setAttribute('form', ''); + + return [[aForms[0]], [[]]]; +} + +/** + * This function test that, when an element is a descendant of a form element + * and has @form set to the empty string, it does not have a form owner even if + * it's parent has its id equals to the empty string. + */ +function test18(aForms, aElements) +{ + // <form id=''><element form=''></form> + aForms[0].id = ''; + aForms[0].appendChild(aElements[0]); + aElements[0].setAttribute('form', ''); + + return [[aForms[0]], [[]]]; +} + +/** + * This function test that, when an element is a descendant of a form element + * and has @form set to a valid form id and the element is being moving inside + * it's parent, it's form owner will remain the form with the id. + */ +function test19(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'><element></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aForms[1].appendChild(aElements[1]); + aElements[0].setAttribute('form', 'f'); + aForms[1].appendChild(aElements[0]); + + return [[aForms[0],aForms[1]],[[aElements[0]],[aElements[1]]]]; +} + +/** + * This function test that, when an element is a descendant of a form element + * and has @form set to a valid form id and the element is being moving inside + * another form, it's form owner will remain the form with the id. + */ +function test20(aForms, aElements) +{ + // <form id='f'></form><form><element form='f'><element></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aForms[1].appendChild(aElements[1]); + aElements[0].setAttribute('form', 'f'); + aForms[2].appendChild(aElements[0]); + + return [[aForms[0],aForms[1],aForms[2]],[[aElements[0]],[aElements[1]],[]]]; +} + +/** + * This function test that when removing a form, the elements with a @form set + * will be correctly removed from there form owner. + */ +function test21(aForms, aElements) +{ + // <form id='f'><form><form><element form='f'></form> + aForms[0].id = 'f'; + aForms[1].appendChild(aElements[0]); + aElements[0].setAttribute('form', 'f'); + document.getElementById('content').removeChild(aForms[1]); + + return [[aForms[0]],[[]]]; +} + +var functions = [ + test0a, test0b, + test1, test2, test3, test4, test5, test6, test7, test8, test9, + test10, test11, test12, test13, test14, test15, test16, test17, test18, test19, + test20, test21, +]; + +// Global variable to have an easy access to <div id='content'>. +var content = document.getElementById('content'); + +// Initializing the needed elements. +var forms = [ + document.createElement('form'), + document.createElement('form'), + document.createElement('form'), +]; + +var elementNames = [ + 'button', 'fieldset', 'input', 'label', 'object', 'output', 'select', + 'textarea' +]; + +var todoElements = [ + ['keygen', 'Keygen'], +]; + +for (var e of todoElements) { + var node = document.createElement(e[0]); + var nodeString = HTMLElement.prototype.toString.apply(node); + nodeString = nodeString.replace(/Element[\] ].*/, "Element"); + todo_is(nodeString, "[object HTML" + e[1] + "Element", + e[0] + " should not be implemented"); +} + +for (var name of elementNames) { + var elements = [ + document.createElement(name), + document.createElement(name), + ]; + + for (var func of functions) { + // Clean-up. + while (content.firstChild) { + content.firstChild.remove(); + } + for (form of forms) { + content.appendChild(form); + form.removeAttribute('id'); + } + for (e of elements) { + content.appendChild(e); + e.removeAttribute('form'); + is(e.form, null, "The element should not have a form owner"); + } + + // Calling the test. + var results = func(forms, elements); + + // Checking the results. + var formsList = results[0]; + for (var i=0; i<formsList.length; ++i) { + var elementsList = results[1][i]; + if (name != 'label' && name != 'meter' && name != 'progress') { + is(formsList[i].elements.length, elementsList.length, + "The form should contain " + elementsList.length + " elements"); + } + for (var j=0; j<elementsList.length; ++j) { + if (name != 'label' && name != 'meter' && name != 'progress') { + is(formsList[i].elements[j], elementsList[j], + "The form should contain " + elementsList[j]); + } + if (name != 'label') { + is(elementsList[j].form, formsList[i], + "The form owner should be the form associated to the list"); + } + } + } + } + + // Cleaning-up. + for (e of elements) { + e.remove(); + e = null; + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_form_attribute-2.html b/dom/html/test/forms/test_form_attribute-2.html new file mode 100644 index 0000000000..b7fe5daa87 --- /dev/null +++ b/dom/html/test/forms/test_form_attribute-2.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=588683 +--> +<head> + <title>Test for form attributes 2</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a> +<p id="display"></p> +<div id="content" style="display: none"> + <form id='a'> + <form id='b'> + <input id='i' form='b'> + <script> + is(document.getElementById('i').form, document.getElementById('b'), + "While parsing, the form property should work."); + </script> + </form> + </form> + <form id='c'> + <form id='d'> + <input id='i2' form='c'> + <script> + is(document.getElementById('i2').form, document.getElementById('c'), + "While parsing, the form property should work."); + </script> + </form> + </form> + <!-- Let's tests without @form --> + <form id='e'> + <form id='f'> + <input id='i3'> + <script> + // bug 589073 + todo_is(document.getElementById('i3').form, document.getElementById('f'), + "While parsing, the form property should work."); + </script> + </form> + </form> + <form id='g'> + <input id='i4'> + <script> + is(document.getElementById('i4').form, document.getElementById('g'), + "While parsing, the form property should work."); + </script> + </form> +</div> +</body> +</html> diff --git a/dom/html/test/forms/test_form_attribute-3.html b/dom/html/test/forms/test_form_attribute-3.html new file mode 100644 index 0000000000..9ceed86716 --- /dev/null +++ b/dom/html/test/forms/test_form_attribute-3.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=588683 +--> +<head> + <title>Test for form attributes 3</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a> +<p id="display"></p> +<div id="content"> + <form id='f'> + <input name='e1'> + </form> + <form id='f2'> + <input name='e2'> + <input id='i3' form='f' + onfocus="var catched=false; + try { e1; } catch(e) { catched=true; } + ok(!catched, 'e1 should be in the scope of i3'); + catched = false; + try { e2; } catch(e) { catched=true; } + ok(catched, 'e2 should not be in the scope of i3'); + document.getElementById('i4').focus();" + > + <input id='i4' form='f2' + onfocus="var catched=false; + try { e2; } catch(e) { catched=true; } + ok(!catched, 'e2 should be in the scope of i4'); + document.getElementById('i5').focus();" + > + <input id='i5' + onfocus="var catched=false; + try { e2; } catch(e) { catched=true; } + ok(!catched, 'e2 should be in the scope of i5'); + document.getElementById('i6').focus();" + > + </form> + <input id='i6' form='f' + onfocus="var catched=false; + try { e1; } catch(e) { catched=true; } + ok(!catched, 'e1 should be in the scope of i6'); + document.getElementById('i7').focus();" + > + <input id='i7' form='f2' + onfocus="var catched=false; + try { e2; } catch(e) { catched=true; } + ok(!catched, 'e2 should be in the scope of i7'); + this.blur(); + SimpleTest.finish();" + > +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for form attributes 3 **/ + +SimpleTest.waitForExplicitFinish(); + +document.getElementById('i3').focus(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_form_attribute-4.html b/dom/html/test/forms/test_form_attribute-4.html new file mode 100644 index 0000000000..f2228cec45 --- /dev/null +++ b/dom/html/test/forms/test_form_attribute-4.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=588683 +--> +<head> + <title>Test for form attributes 4</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=588683">Mozilla Bug 588683</a> +<p id="display"></p> +<div id="content" style='display:none;'> + <form id='f'> + </form> + <table id='t'> + <form id='f2'> + <tr><td><input id='i1'></td></tr> + <tr><td><input id='i2' form='f'></td></tr> + </form> + </table> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for form attributes 4 **/ + +var table = document.getElementById('t'); +var i1 = document.getElementById('i1'); +var i2 = document.getElementById('i2'); + +is(i1.form, document.getElementById('f2'), + "i1 form should be it's parent"); +is(i2.form, document.getElementById('f'), + "i1 form should be the form with the id in @form"); + +table.removeChild(document.getElementById('f2')); +is(i1, document.getElementById('i1'), + "i1 should still be in the document"); +is(i1.form, null, "i1 should not have any form owner"); +is(i2.form, document.getElementById('f'), + "i1 form should be the form with the id in @form"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_form_attributes_reflection.html b/dom/html/test/forms/test_form_attributes_reflection.html new file mode 100644 index 0000000000..0d0ef6b870 --- /dev/null +++ b/dom/html/test/forms/test_form_attributes_reflection.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLFormElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="../reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLFormElement attributes reflection **/ + +// .acceptCharset +reflectString({ + element: document.createElement("form"), + attribute: { idl: "acceptCharset", content: "accept-charset" }, + otherValues: [ "ISO-8859-1", "UTF-8" ], +}); + +reflectURL({ + element: document.createElement("form"), + attribute: "action", +}); + +// .autocomplete +reflectLimitedEnumerated({ + element: document.createElement("form"), + attribute: "autocomplete", + validValues: [ "on", "off" ], + invalidValues: [ "", "foo", "tulip", "default" ], + defaultValue: "on", +}); + +// .enctype +reflectLimitedEnumerated({ + element: document.createElement("form"), + attribute: "enctype", + validValues: [ "application/x-www-form-urlencoded", "multipart/form-data", + "text/plain" ], + invalidValues: [ "", "foo", "tulip", "multipart/foo" ], + defaultValue: "application/x-www-form-urlencoded" +}); + +// .encoding +reflectLimitedEnumerated({ + element: document.createElement("form"), + attribute: { idl: "encoding", content: "enctype" }, + validValues: [ "application/x-www-form-urlencoded", "multipart/form-data", + "text/plain" ], + invalidValues: [ "", "foo", "tulip", "multipart/foo" ], + defaultValue: "application/x-www-form-urlencoded" +}); + +// .method +reflectLimitedEnumerated({ + element: document.createElement("form"), + attribute: "method", + validValues: [ "get", "post" ], + invalidValues: [ "", "foo", "tulip" ], + defaultValue: "get" +}); + +// .name +reflectString({ + element: document.createElement("form"), + attribute: "name", +}); + +// .noValidate +reflectBoolean({ + element: document.createElement("form"), + attribute: "noValidate", +}); + +// .target +reflectString({ + element: document.createElement("form"), + attribute: "target", + otherValues: [ "_blank", "_self", "_parent", "_top" ], +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_form_named_getter_dynamic.html b/dom/html/test/forms/test_form_named_getter_dynamic.html new file mode 100644 index 0000000000..4a19768453 --- /dev/null +++ b/dom/html/test/forms/test_form_named_getter_dynamic.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=377413
+-->
+<head>
+ <title>Test for Bug 377413</title>
+ <script type="text/javascript" src="/resources/testharness.js"></script>
+ <link rel='stylesheet' href='/resources/testharness.css'>
+ <script type="text/javascript" src="/resources/testharnessreport.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377413">Mozilla Bug 377413</a>
+<p id="log"></p>
+<div id="content">
+ <form>
+ <table>
+ <tbody>
+ </tbody>
+ </table>
+ </form>
+</div>
+
+<script type="text/javascript">
+
+/** Tests for Bug 377413 **/
+var tb = document.getElementsByTagName('tbody')[0];
+
+test(function(){
+ tb.innerHTML = '<tr><td><input name="fooboo"></td></tr>';
+ document.forms[0].fooboo.value = 'testme';
+ document.getElementsByTagName('table')[0].deleteRow(0);
+ assert_equals(document.forms[0].fooboo, undefined);
+}, "no element reference after deleting it with deleteRow()");
+
+test(function(){
+ var b = tb.appendChild(document.createElement('tr')).appendChild(document.createElement('td')).appendChild(document.createElement('button'));
+ b.name = b.value = 'boofoo';
+ assert_equals(document.forms[0].elements[0].value, 'boofoo');
+}, 'element value set correctly');
+
+test(function(){
+ assert_true('boofoo' in document.forms[0]);
+}, 'element name has created property on form');
+
+test(function(){
+ tb.innerHTML = '';
+ assert_false('boofoo' in document.forms[0]);
+}, "no element reference after deleting it by setting innerHTML");
+
+
+</script>
+</body>
+</html>
diff --git a/dom/html/test/forms/test_formaction_attribute.html b/dom/html/test/forms/test_formaction_attribute.html new file mode 100644 index 0000000000..0dee2f172d --- /dev/null +++ b/dom/html/test/forms/test_formaction_attribute.html @@ -0,0 +1,169 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=566160 +--> +<head> + <title>Test for Bug 566160</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566160">Mozilla Bug 566160</a> +<p id="display"></p> +<style> + iframe { width: 130px; height: 100px;} +</style> +<iframe name='frame1' id='frame1'></iframe> +<iframe name='frame2' id='frame2'></iframe> +<iframe name='frame3' id='frame3'></iframe> +<iframe name='frame3bis' id='frame3bis'></iframe> +<iframe name='frame4' id='frame4'></iframe> +<iframe name='frame5' id='frame5'></iframe> +<iframe name='frame6' id='frame6'></iframe> +<iframe name='frame7' id='frame7'></iframe> +<div id="content"> + <!-- submit controls with formaction that are validated with a CLICK --> + <form target="frame1" action="FAIL.html" method="GET"> + <input name='foo' value='foo'> + <input type='submit' id='is' formaction="PASS.html"> + </form> + <form target="frame2" action="FAIL.html" method="GET"> + <input name='bar' value='bar'> + <input type='image' id='ii' formaction="PASS.html"> + </form> + <form target="frame3" action="FAIL.html" method="GET"> + <input name='tulip' value='tulip'> + <button type='submit' id='bs' formaction="PASS.html">submit</button> + </form> + <form target="frame3bis" action="FAIL.html" method="GET"> + <input name='tulipbis' value='tulipbis'> + <button type='submit' id='bsbis' formaction="PASS.html">submit</button> + </form> + + <!-- submit controls with formaction that are validated with ENTER --> + <form target="frame4" action="FAIL.html" method="GET"> + <input name='footulip' value='footulip'> + <input type='submit' id='is2' formaction="PASS.html"> + </form> + <form target="frame5" action="FAIL.html" method="GET"> + <input name='foobar' value='foobar'> + <input type='image' id='ii2' formaction="PASS.html"> + </form> + <form target="frame6" action="FAIL.html" method="GET"> + <input name='tulip2' value='tulip2'> + <button type='submit' id='bs2' formaction="PASS.html">submit</button> + </form> + + <!-- check that when submitting a from from an element + which is not a submit control, @formaction isn't used --> + <form target='frame7' action="PASS.html" method="GET"> + <input id='enter' name='input' value='enter' formaction="FAIL.html"> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 566160 **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +const BASE_URI = `${location.origin}/tests/dom/html/test/forms/PASS.html`; +var gTestResults = { + frame1: BASE_URI + "?foo=foo", + frame2: BASE_URI + "?bar=bar&x=0&y=0", + frame3: BASE_URI + "?tulip=tulip", + frame3bis: BASE_URI + "?tulipbis=tulipbis", + frame4: BASE_URI + "?footulip=footulip", + frame5: BASE_URI + "?foobar=foobar&x=0&y=0", + frame6: BASE_URI + "?tulip2=tulip2", + frame7: BASE_URI + "?input=enter", +}; + +var gPendingLoad = 0; // Has to be set after depending on the frames number. + +function runTests() +{ + // We add a load event for the frames which will be called when the forms + // will be submitted. + var frames = [ document.getElementById('frame1'), + document.getElementById('frame2'), + document.getElementById('frame3'), + document.getElementById('frame3bis'), + document.getElementById('frame4'), + document.getElementById('frame5'), + document.getElementById('frame6'), + document.getElementById('frame7'), + ]; + gPendingLoad = frames.length; + + for (var i=0; i<frames.length; i++) { + frames[i].setAttribute('onload', "frameLoaded(this);"); + } + + /** + * We are going to focus each element before interacting with either for + * simulating the ENTER key (synthesizeKey) or a click (synthesizeMouse) or + * using .click(). This because it may be needed (ENTER) and because we want + * to have the element visible in the iframe. + * + * Focusing the first element (id='is') is launching the tests. + */ + document.getElementById('is').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('is'), 5, 5, {}); + document.getElementById('ii').focus(); + }, {once: true}); + + document.getElementById('ii').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('ii'), 5, 5, {}); + document.getElementById('bs').focus(); + }, {once: true}); + + document.getElementById('bs').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('bs'), 5, 5, {}); + document.getElementById('bsbis').focus(); + }, {once: true}); + + document.getElementById('bsbis').addEventListener('focus', function(aEvent) { + document.getElementById('bsbis').click(); + document.getElementById('is2').focus(); + }, {once: true}); + + document.getElementById('is2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('ii2').focus(); + }, {once: true}); + + document.getElementById('ii2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('bs2').focus(); + }, {once: true}); + + document.getElementById('bs2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('enter').focus(); + }, {once: true}); + + document.getElementById('enter').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + }, {once: true}); + + document.getElementById('is').focus(); +} + +function frameLoaded(aFrame) { + // Check if formaction/action has the correct behavior. + is(aFrame.contentWindow.location.href, gTestResults[aFrame.name], + "the action attribute doesn't have the correct behavior"); + + if (--gPendingLoad == 0) { + SimpleTest.finish(); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_formnovalidate_attribute.html b/dom/html/test/forms/test_formnovalidate_attribute.html new file mode 100644 index 0000000000..2e3714d2fe --- /dev/null +++ b/dom/html/test/forms/test_formnovalidate_attribute.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=589696 +--> +<head> + <title>Test for Bug 589696</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=589696">Mozilla Bug 589696</a> +<p id="display"></p> +<iframe style='width:50px; height: 50px;' name='t'></iframe> +<div id="content"> + <!-- Next forms should not submit because formnovalidate isn't set on the + element used for the submission. --> + <form target='t' action='data:text/html,'> + <input id='av' required> + <input type='submit' formnovalidate> + <input id='a' type='submit'> + </form> + <form target='t' action='data:text/html,'> + <input id='bv' type='checkbox' required> + <button type='submit' formnovalidate></button> + <button id='b' type='submit'></button> + </form> + <!-- Next form should not submit because formnovalidate only applies for + submit controls. --> + <form target='t' action='data:text/html,'> + <input id='c' required formnovalidate> + </form> + <!--- Next forms should submit without any validation check. --> + <form target='t' action='data:text/html,'> + <input id='dv' required> + <input id='d' type='submit' formnovalidate> + </form> + <form target='t' action='data:text/html,'> + <input id='ev' type='checkbox' required> + <button id='e' type='submit' formnovalidate></button> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 589696 **/ + +document.getElementById('av').addEventListener("invalid", function(aEvent) { + aEvent.target.removeAttribute("invalid", arguments.callee, false); + ok(true, "formnovalidate should not apply on if not set on the submit " + + "control used for the submission"); + document.getElementById('b').click(); +}); + +document.getElementById('bv').addEventListener("invalid", function(aEvent) { + aEvent.target.removeAttribute("invalid", arguments.callee, false); + ok(true, "formnovalidate should not apply on if not set on the submit " + + "control used for the submission"); + var c = document.getElementById('c'); + c.focus(); + synthesizeKey("KEY_Enter"); +}); + +document.getElementById('c').addEventListener("invalid", function(aEvent) { + aEvent.target.removeAttribute("invalid", arguments.callee, false); + ok(true, "formnovalidate should only apply on submit controls"); + document.getElementById('d').click(); +}); + +document.forms[3].addEventListener("submit", function(aEvent) { + aEvent.target.removeAttribute("submit", arguments.callee, false); + ok(true, "formnovalidate applies if set on the submit control used for the submission"); + document.getElementById('e').click(); +}); + +document.forms[4].addEventListener("submit", function(aEvent) { + aEvent.target.removeAttribute("submit", arguments.callee, false); + ok(true, "formnovalidate applies if set on the submit control used for the submission"); + SimpleTest.executeSoon(SimpleTest.finish); +}); + +/** + * We have to be sure invalid events behave as expected. + * They should be sent before the submit event so we can just create a test + * failure if we got one when unexpected. All of them should be caught if + * sent. + * At worst, we got random green which isn't harmful. + * If expected, they will be part of the chain reaction. + */ +function unexpectedInvalid(aEvent) +{ + aEvent.target.removeAttribute("invalid", unexpectedInvalid, false); + ok(false, "invalid event should not be sent"); +} + +document.getElementById('dv').addEventListener("invalid", unexpectedInvalid); +document.getElementById('ev').addEventListener("invalid", unexpectedInvalid); + +/** + * Some submission have to be canceled. In that case, the submit events should + * not be sent. + * Same behavior as unexpected invalid events. + */ +function unexpectedSubmit(aEvent) +{ + aEvent.target.removeAttribute("submit", unexpectedSubmit, false); + ok(false, "submit event should not be sent"); +} + +document.forms[0].addEventListener("submit", unexpectedSubmit); +document.forms[1].addEventListener("submit", unexpectedSubmit); +document.forms[2].addEventListener("submit", unexpectedSubmit); + +SimpleTest.waitForExplicitFinish(); + +// This is going to call all the tests (with a chain reaction). +SimpleTest.waitForFocus(function() { + document.getElementById('a').click(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_attributes_reflection.html b/dom/html/test/forms/test_input_attributes_reflection.html new file mode 100644 index 0000000000..348ea0f80d --- /dev/null +++ b/dom/html/test/forms/test_input_attributes_reflection.html @@ -0,0 +1,271 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLInputElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="../reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLInputElement attributes reflection **/ + +// TODO: maybe make those reflections be tested against all input types. + +function testWidthHeight(attr) { + var element = document.createElement('input'); + is(element[attr], 0, attr + ' always returns 0 if not type=image'); + element.setAttribute(attr, '42'); + is(element[attr], 0, attr + ' always returns 0 if not type=image'); + is(element.getAttribute(attr), '42'); + element[attr] = 0; + is(element.getAttribute(attr), '0', 'setting ' + attr + ' changes the content attribute'); + element[attr] = 12; + is(element.getAttribute(attr), '12', 'setting ' + attr + ' changes the content attribute'); + + element.removeAttribute(attr); + is(element.getAttribute(attr), null); + + element = document.createElement('input'); + element.type = 'image'; + element.style.display = "inline"; + document.getElementById('content').appendChild(element); + isnot(element[attr], 0, attr + ' represents the dimension of the element if type=image'); + + element.setAttribute(attr, '42'); + isnot(element[attr], 0, attr + ' represents the dimension of the element if type=image'); + isnot(element[attr], 42, attr + ' represents the dimension of the element if type=image'); + is(element.getAttribute(attr), '42'); + element[attr] = 0; + is(element.getAttribute(attr), '0', 'setting ' + attr + ' changes the content attribute'); + element[attr] = 12; + is(element.getAttribute(attr), '12', 'setting ' + attr + ' changes the content attribute'); + + element.removeAttribute(attr); + is(element.getAttribute(attr), null); +} + +// .accept +reflectString({ + element: document.createElement("input"), + attribute: "accept", + otherValues: [ "audio/*", "video/*", "image/*", "image/png", + "application/msword", "appplication/pdf" ], +}); + +// .alt +reflectString({ + element: document.createElement("input"), + attribute: "alt", +}); + +// .autocomplete +reflectLimitedEnumerated({ + element: document.createElement("input"), + attribute: "autocomplete", + validValues: [ "on", "off" ], + invalidValues: [ "", "default", "foo", "tulip" ], +}); + +// .autofocus +reflectBoolean({ + element: document.createElement("input"), + attribute: "autofocus", +}); + +// .defaultChecked +reflectBoolean({ + element: document.createElement("input"), + attribute: { idl: "defaultChecked", content: "checked" }, +}); + +// .checked doesn't reflect a content attribute. + +// .dirName +reflectString({ + element: document.createElement("input"), + attribute: "dirName" +}); + +// .disabled +reflectBoolean({ + element: document.createElement("input"), + attribute: "disabled", +}); + +// TODO: form (HTMLFormElement) +// TODO: files (FileList) + +// .formAction +reflectURL({ + element: document.createElement("button"), + attribute: "formAction", +}); + +// .formEnctype +reflectLimitedEnumerated({ + element: document.createElement("input"), + attribute: "formEnctype", + validValues: [ "application/x-www-form-urlencoded", "multipart/form-data", + "text/plain" ], + invalidValues: [ "", "foo", "tulip", "multipart/foo" ], + defaultValue: { invalid: "application/x-www-form-urlencoded", missing: "" } +}); + +// .formMethod +reflectLimitedEnumerated({ + element: document.createElement("input"), + attribute: "formMethod", + validValues: [ "get", "post" ], + invalidValues: [ "", "foo", "tulip" ], + defaultValue: { invalid: "get", missing: "" } +}); + +// .formNoValidate +reflectBoolean({ + element: document.createElement("input"), + attribute: "formNoValidate", +}); + +// .formTarget +reflectString({ + element: document.createElement("input"), + attribute: "formTarget", + otherValues: [ "_blank", "_self", "_parent", "_top" ], +}); + +// .height +testWidthHeight('height'); + +// .indeterminate doesn't reflect a content attribute. + +// TODO: list (HTMLElement) + +// .max +reflectString({ + element: document.createElement('input'), + attribute: 'max', +}); + +// .maxLength +reflectInt({ + element: document.createElement("input"), + attribute: "maxLength", + nonNegative: true, +}); + +// .min +reflectString({ + element: document.createElement('input'), + attribute: 'min', +}); + +// .multiple +reflectBoolean({ + element: document.createElement("input"), + attribute: "multiple", +}); + +// .name +reflectString({ + element: document.createElement("input"), + attribute: "name", + otherValues: [ "isindex", "_charset_" ], +}); + +// .pattern +reflectString({ + element: document.createElement("input"), + attribute: "pattern", + otherValues: [ "[0-9][A-Z]{3}" ], +}); + +// .placeholder +reflectString({ + element: document.createElement("input"), + attribute: "placeholder", + otherValues: [ "foo\nbar", "foo\rbar", "foo\r\nbar" ], +}); + +// .readOnly +reflectBoolean({ + element: document.createElement("input"), + attribute: "readOnly", +}); + +// .required +reflectBoolean({ + element: document.createElement("input"), + attribute: "required", +}); + +// .size +reflectUnsignedInt({ + element: document.createElement("input"), + attribute: "size", + nonZero: true, + defaultValue: 20, +}); + +// .src (URL) +reflectURL({ + element: document.createElement('input'), + attribute: 'src', +}); + +// .step +reflectString({ + element: document.createElement('input'), + attribute: 'step', +}); + +// .type +reflectLimitedEnumerated({ + element: document.createElement("input"), + attribute: "type", + validValues: [ "hidden", "text", "search", "tel", "url", "email", "password", + "checkbox", "radio", "file", "submit", "image", "reset", + "button", "date", "time", "number", "range", "color", "month", + "week", "datetime-local" ], + invalidValues: [ "this-is-probably-a-wrong-type", "", "tulip" ], + defaultValue: "text" +}); + +// .defaultValue +reflectString({ + element: document.createElement("input"), + attribute: { idl: "defaultValue", content: "value" }, + otherValues: [ "foo\nbar", "foo\rbar", "foo\r\nbar" ], +}); + +// .value doesn't reflect a content attribute. + +// .valueAsDate +is("valueAsDate" in document.createElement("input"), true, + "valueAsDate should be available"); + +// Deeper check will be done with bug 763305. +is('valueAsNumber' in document.createElement("input"), true, + "valueAsNumber should be available"); + +// .selectedOption +todo("selectedOption" in document.createElement("input"), + "selectedOption isn't implemented yet"); + +// .width +testWidthHeight('width'); + +// .willValidate doesn't reflect a content attribute. +// .validity doesn't reflect a content attribute. +// .validationMessage doesn't reflect a content attribute. +// .labels doesn't reflect a content attribute. + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_color_input_change_events.html b/dom/html/test/forms/test_input_color_input_change_events.html new file mode 100644 index 0000000000..f97d54f66e --- /dev/null +++ b/dom/html/test/forms/test_input_color_input_change_events.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=885996 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1234567</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test that update() modifies the element value such as done() when it is + * not called as a concellation. + */ + + SimpleTest.waitForExplicitFinish(); + + var MockColorPicker = SpecialPowers.MockColorPicker; + + var test = runTest(); + + SimpleTest.waitForFocus(function() { + test.next(); + }); + + function* runTest() { + MockColorPicker.init(window); + var element = null; + + MockColorPicker.showCallback = function(picker, update) { + is(picker.initialColor, element.value); + + var inputEvent = false; + var changeEvent = false; + element.oninput = function() { + inputEvent = true; + }; + element.onchange = function() { + changeEvent = true; + }; + + if (element.dataset.type == 'update') { + update('#f00ba4'); + + is(inputEvent, true, 'input event should have been received'); + is(changeEvent, false, 'change event should not have been received'); + + inputEvent = changeEvent = false; + + is(element.value, '#f00ba4'); + + MockColorPicker.returnColor = '#f00ba7'; + isnot(element.value, MockColorPicker.returnColor); + } else if (element.dataset.type == 'cancel') { + MockColorPicker.returnColor = '#bababa'; + isnot(element.value, MockColorPicker.returnColor); + } else if (element.dataset.type == 'done') { + MockColorPicker.returnColor = '#098766'; + isnot(element.value, MockColorPicker.returnColor); + } else if (element.dataset.type == 'noop-done') { + MockColorPicker.returnColor = element.value; + is(element.value, MockColorPicker.returnColor); + } + + SimpleTest.executeSoon(function() { + if (element.dataset.type == 'cancel') { + isnot(element.value, MockColorPicker.returnColor); + is(inputEvent, false, 'no input event should have been sent'); + is(changeEvent, false, 'no change event should have been sent'); + } else if (element.dataset.type == 'noop-done') { + is(element.value, MockColorPicker.returnColor); + is(inputEvent, false, 'no input event should have been sent'); + is(changeEvent, false, 'no change event should have been sent'); + } else { + is(element.value, MockColorPicker.returnColor); + is(inputEvent, true, 'input event should have been sent'); + is(changeEvent, true, 'change event should have been sent'); + } + + changeEvent = false; + element.blur(); + + setTimeout(function() { + is(changeEvent, false, "change event should not be fired on blur"); + test.next(); + }); + }); + + return element.dataset.type == 'cancel' ? "" : MockColorPicker.returnColor; + }; + + for (var i = 0; i < document.getElementsByTagName('input').length; ++i) { + element = document.getElementsByTagName('input')[i]; + element.focus(); + synthesizeMouseAtCenter(element, {}); + yield undefined; + }; + + MockColorPicker.cleanup(); + SimpleTest.finish(); + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a> +<p id="display"></p> +<div id="content"> + <input type='color' data-type='update'> + <input type='color' data-type='cancel'> + <input type='color' data-type='done'> + <input type='color' data-type='noop-done'> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_color_picker_datalist.html b/dom/html/test/forms/test_input_color_picker_datalist.html new file mode 100644 index 0000000000..1a268c0701 --- /dev/null +++ b/dom/html/test/forms/test_input_color_picker_datalist.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> +<meta charset="utf-8"> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<script> +SimpleTest.waitForExplicitFinish(); + +function runTest() { + let MockColorPicker = SpecialPowers.MockColorPicker; + + MockColorPicker.init(window); + + MockColorPicker.showCallback = (picker) => { + is(picker.defaultColors.length, 2); + is(picker.defaultColors[0], "#112233"); + is(picker.defaultColors[1], "#00ffaa"); + + MockColorPicker.cleanup(); + SimpleTest.finish(); + } + + let input = document.querySelector("input"); + synthesizeMouseAtCenter(input, {}); +} + +SimpleTest.waitForFocus(runTest); +</script> +</head> +<body> +<input type="color" list="color-list"> +<datalist id="color-list"> + <option value="#112233"></option> + <option value="black"></option> <!-- invalid --> + <option value="#000000" disabled></option> + <option value="#00FFAA"></option> + <option></option> +</datalist> +</body> +</html> diff --git a/dom/html/test/forms/test_input_color_picker_initial.html b/dom/html/test/forms/test_input_color_picker_initial.html new file mode 100644 index 0000000000..c7467c7520 --- /dev/null +++ b/dom/html/test/forms/test_input_color_picker_initial.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=885996 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1234567</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test that the initial value of the nsIColorPicker is the current value of + the <input type='color'> element. **/ + + SimpleTest.waitForExplicitFinish(); + + var MockColorPicker = SpecialPowers.MockColorPicker; + + var test = runTest(); + + SimpleTest.waitForFocus(function() { + test.next(); + }); + + function* runTest() { + MockColorPicker.init(window); + var element = null; + + MockColorPicker.showCallback = function(picker) { + is(picker.initialColor, element.value); + SimpleTest.executeSoon(function() { + test.next(); + }); + return ""; + }; + + for (var i = 0; i < document.getElementsByTagName('input').length; ++i) { + element = document.getElementsByTagName('input')[i]; + if (element.parentElement.id === 'dynamic-values') { + element.value = '#deadbe'; + } + synthesizeMouseAtCenter(element, {}); + yield undefined; + }; + + MockColorPicker.cleanup(); + SimpleTest.finish(); + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a> +<p id="display"></p> +<div id="content"> + <div id='valid-values'> + <input type='color' value='#ff00ff'> + <input type='color' value='#ab3275'> + <input type='color' value='#abcdef'> + <input type='color' value='#ABCDEF'> + </div> + <div id='invalid-values'> + <input type='color' value='ffffff'> + <input type='color' value='#abcdez'> + <input type='color' value='#0123456'> + </div> + <div id='dynamic-values'> + <input type='color' value='#ab4594'> + <input type='color' value='#984534'> + <input type='color' value='#f8b9a0'> + </div> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_color_picker_popup.html b/dom/html/test/forms/test_input_color_picker_popup.html new file mode 100644 index 0000000000..9fbebf15bc --- /dev/null +++ b/dom/html/test/forms/test_input_color_picker_popup.html @@ -0,0 +1,144 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=885996 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1234567</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> body { font-family: serif } </style> + <script type="application/javascript"> + + /** Test the behaviour of the <input type='color'> when clicking on it from + different ways. **/ + + SimpleTest.waitForExplicitFinish(); + + var MockColorPicker = SpecialPowers.MockColorPicker; + + var test = runTest(); + var testData = [ + { id: 'normal', result: true }, + { id: 'hidden', result: false }, + { id: 'normal', type: 'untrusted', result: true }, + { id: 'normal', type: 'prevent-default-1', result: false }, + { id: 'normal', type: 'prevent-default-2', result: false }, + { id: 'normal', type: 'click-method', result: true }, + { id: 'normal', type: 'show-picker', result: true }, + { id: 'normal', type: 'right-click', result: false }, + { id: 'normal', type: 'middle-click', result: false }, + { id: 'label-1', result: true }, + { id: 'label-2', result: true }, + { id: 'label-3', result: true }, + { id: 'label-4', result: true }, + { id: 'button-click', result: true }, + { id: 'button-down', result: true }, + { id: 'button-up', result: true }, + { id: 'div-click', result: true }, + { id: 'div-click-on-demand', result: true }, + ]; + + SimpleTest.waitForFocus(function() { + test.next(); + }); + + function* runTest() { + let currentTest = null; + MockColorPicker.init(window); + var element = null; + + MockColorPicker.showCallback = function(picker) { + ok(currentTest.result); + SimpleTest.executeSoon(function() { + test.next(); + }); + return ""; + }; + + while (testData.length) { + currentTest = testData.shift(); + element = document.getElementById(currentTest.id); + + // To make sure we can actually click on the element. + element.focus(); + + switch (currentTest.type) { + case 'untrusted': + var e = document.createEvent('MouseEvents'); + e.initEvent('click', true, false); + document.getElementById(element.dispatchEvent(e)); + break; + case 'prevent-default-1': + element.onclick = function() { + return false; + }; + element.click(); + element.onclick = function() {}; + break; + case 'prevent-default-2': + element.onclick = function(event) { + event.preventDefault(); + }; + element.click(); + element.onclick = function() {}; + break; + case 'click-method': + element.click(); + break; + case 'show-picker': + SpecialPowers.wrap(document).notifyUserGestureActivation(); + element.showPicker(); + break; + case 'right-click': + synthesizeMouseAtCenter(element, { button: 2 }); + break; + case 'middle-click': + synthesizeMouseAtCenter(element, { button: 1 }); + break; + default: + synthesizeMouseAtCenter(element, {}); + } + + if (!currentTest.result) { + setTimeout(function() { + setTimeout(function() { + ok(true); + SimpleTest.executeSoon(function() { + test.next(); + }); + }); + }); + } + yield undefined; + }; + + MockColorPicker.cleanup(); + SimpleTest.finish(); + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a> +<p id="display"></p> +<div id="content"> + <input type='color' id='normal'> + <input type='color' id='hidden' hidden> + <label id='label-1'>foo<input type='color'></label> + <label id='label-2' for='labeled-2'>foo</label><input id='labeled-2' type='color'></label> + <label id='label-3'>foo<input type='color'></label> + <label id='label-4' for='labeled-4'>foo</label><input id='labeled-4' type='color'></label> + <input id='by-button' type='color'> + <button id='button-click' onclick="document.getElementById('by-button').click();">click</button> + <button id='button-down' onclick="document.getElementById('by-button').click();">click</button> + <button id='button-up' onclick="document.getElementById('by-button').click();">click</button> + <div id='div-click' onclick="document.getElementById('by-button').click();">click</div> + <div id='div-click-on-demand' onclick="var i=document.createElement('input'); i.type='color'; i.click();">click</div> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_color_picker_update.html b/dom/html/test/forms/test_input_color_picker_update.html new file mode 100644 index 0000000000..5c22b667e1 --- /dev/null +++ b/dom/html/test/forms/test_input_color_picker_update.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=885996 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1234567</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> body { font-family: serif } </style> + <script type="application/javascript"> + + /** Test that update() modifies the element value such as done() when it is + * not called as a concellation. + */ + + SimpleTest.waitForExplicitFinish(); + + var MockColorPicker = SpecialPowers.MockColorPicker; + + var test = runTest(); + + SimpleTest.waitForFocus(function() { + test.next(); + }); + + function* runTest() { + MockColorPicker.init(window); + var element = null; + + MockColorPicker.showCallback = function(picker, update) { + is(picker.initialColor, element.value); + + if (element.dataset.type == 'update') { + update('#f00ba4'); + is(element.value, '#f00ba4'); + + MockColorPicker.returnColor = '#f00ba7'; + isnot(element.value, MockColorPicker.returnColor); + } else if (element.dataset.type == 'cancel') { + MockColorPicker.returnColor = '#bababa'; + isnot(element.value, MockColorPicker.returnColor); + } else if (element.dataset.type == 'done') { + MockColorPicker.returnColor = '#098766'; + isnot(element.value, MockColorPicker.returnColor); + } + + SimpleTest.executeSoon(function() { + if (element.dataset.type == 'cancel') { + isnot(element.value, MockColorPicker.returnColor); + } else { + is(element.value, MockColorPicker.returnColor); + } + + test.next(); + }); + + return element.dataset.type == 'cancel' ? "" : MockColorPicker.returnColor; + }; + + for (var i = 0; i < document.getElementsByTagName('input').length; ++i) { + element = document.getElementsByTagName('input')[i]; + synthesizeMouseAtCenter(element, {}); + yield undefined; + }; + + MockColorPicker.cleanup(); + SimpleTest.finish(); + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885996">Mozilla Bug 885996</a> +<p id="display"></p> +<div id="content"> + <input type='color' data-type='update'> + <input type='color' data-type='cancel'> + <input type='color' data-type='done'> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_date_bad_input.html b/dom/html/test/forms/test_input_date_bad_input.html new file mode 100644 index 0000000000..516d48263f --- /dev/null +++ b/dom/html/test/forms/test_input_date_bad_input.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1372369 +--> +<head> + <title>Test for <input type='date'> bad input validity state</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input { background-color: rgb(0,0,0) !important; } + :valid { background-color: rgb(0,255,0) !important; } + :invalid { background-color: rgb(255,0,0) !important; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1372369">Mozilla Bug 1372369</a> +<p id="display"></p> +<div id="content"> + <form> + <input type="date" id="input"> + <form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for <input type='date'> bad input validity state **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +const DATE_BAD_INPUT_MSG = "Please enter a valid date."; +const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent); + +function checkValidity(aElement, aIsBadInput) { + is(aElement.validity.valid, !aIsBadInput, + "validity.valid should be " + (aIsBadInput ? "false" : "true")); + is(aElement.validity.badInput, !!aIsBadInput, + "validity.badInput should be " + (aIsBadInput ? "true" : "false")); + is(aElement.validationMessage, aIsBadInput ? DATE_BAD_INPUT_MSG : "", + "validationMessage should be: " + (aIsBadInput ? DATE_BAD_INPUT_MSG : "")); + + is(window.getComputedStyle(aElement).getPropertyValue('background-color'), + aIsBadInput ? "rgb(255, 0, 0)" : "rgb(0, 255, 0)", + (aIsBadInput ? ":invalid" : "valid") + " pseudo-class should apply"); +} + +function sendKeys(aKey) { + if (aKey.startsWith("KEY_")) { + synthesizeKey(aKey); + } else { + sendString(aKey); + } +} + +function test() { + var elem = document.getElementById("input"); + + elem.focus(); + sendKeys("02312017"); + elem.blur(); + checkValidity(elem, true); + + elem.focus(); + sendKeys("02292016"); + elem.blur(); + checkValidity(elem, false); + + elem.focus(); + sendKeys("06312000"); + elem.blur(); + checkValidity(elem, true); + + // Removing some of the fields keeps the input as invalid. + elem.focus(); + sendKeys("KEY_Backspace"); + elem.blur(); + checkValidity(elem, true); + + // Removing all of the fields manually makes the input valid (but empty) again. + elem.focus(); + sendKeys("KEY_ArrowRight"); + sendKeys("KEY_Backspace"); + sendKeys("KEY_ArrowRight"); + sendKeys("KEY_Delete"); + elem.blur(); + checkValidity(elem, false); + + elem.focus(); + sendKeys("02292017"); + elem.blur(); + checkValidity(elem, true); + + // Clearing all fields should clear bad input validity state as well. + elem.focus(); + synthesizeKey("KEY_Backspace", { accelKey: true }); + checkValidity(elem, false); + + sendKeys("22334444"); + elem.blur(); + elem.focus(); + synthesizeKey("KEY_Delete", { accelKey: true }); + checkValidity(elem, false); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_date_key_events.html b/dom/html/test/forms/test_input_date_key_events.html new file mode 100644 index 0000000000..387cb37af7 --- /dev/null +++ b/dom/html/test/forms/test_input_date_key_events.html @@ -0,0 +1,270 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1286182 +--> +<head> + <title>Test key events for date control</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1286182">Mozilla Bug 1286182</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1804669">Mozilla Bug 1804669</a> +<p id="display"></p> +<div id="content"> + <input id="input" type="date"> + <div id="host"></div> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +var testData = [ + /** + * keys: keys to send to the input element. + * initialVal: initial value set to the input element. + * expectedVal: expected value of the input element after sending the keys. + */ + { + // Type 11222016, default order is month, day, year. + keys: ["11222016"], + initialVal: "", + expectedVal: "2016-11-22" + }, + { + // Type 3 in the month field will automatically advance to the day field, + // then type 5 in the day field will automatically advance to the year + // field. + keys: ["352016"], + initialVal: "", + expectedVal: "2016-03-05" + }, + { + // Type 13 in the month field will set it to the maximum month, which is + // 12. + keys: ["13012016"], + initialVal: "", + expectedVal: "2016-12-01" + }, + { + // Type 00 in the month field will set it to the minimum month, which is 1. + keys: ["00012016"], + initialVal: "", + expectedVal: "2016-01-01" + }, + { + // Type 33 in the day field will set it to the maximum day, which is 31. + keys: ["12332016"], + initialVal: "", + expectedVal: "2016-12-31" + }, + { + // Type 00 in the day field will set it to the minimum day, which is 1. + keys: ["12002016"], + initialVal: "", + expectedVal: "2016-12-01" + }, + { + // Type 275769 in the year field will set it to 0069, because the + // 5th digit will erase the previous 4 digits. + keys: ["0101275769"], + initialVal: "", + expectedVal: "0069-01-01" + }, + { + // Type 0000 in the year field will set it to the minimum year, which is + // 0001. + keys: ["01010000"], + initialVal: "", + expectedVal: "0001-01-01" + }, + { + // Advance to year field and decrement. + keys: ["KEY_Tab", "KEY_Tab", "KEY_ArrowDown"], + initialVal: "2016-11-25", + expectedVal: "2015-11-25" + }, + { + // Right key should do the same thing as TAB key. + keys: ["KEY_ArrowRight", "KEY_ArrowRight", "KEY_ArrowDown"], + initialVal: "2016-11-25", + expectedVal: "2015-11-25" + }, + { + // Advance to day field then back to month field and decrement. + keys: ["KEY_ArrowRight", "KEY_ArrowLeft", "KEY_ArrowDown"], + initialVal: "2000-05-01", + expectedVal: "2000-04-01" + }, + { + // Focus starts on the first field, month in this case, and increment. + keys: ["KEY_ArrowUp"], + initialVal: "2000-03-01", + expectedVal: "2000-04-01" + }, + { + // Advance to day field and decrement. + keys: ["KEY_Tab", "KEY_ArrowDown"], + initialVal: "1234-01-01", + expectedVal: "1234-01-31" + }, + { + // Advance to day field and increment. + keys: ["KEY_Tab", "KEY_ArrowUp"], + initialVal: "1234-01-01", + expectedVal: "1234-01-02" + }, + { + // PageUp on month field increments month by 3. + keys: ["KEY_PageUp"], + initialVal: "1999-01-01", + expectedVal: "1999-04-01" + }, + { + // PageDown on month field decrements month by 3. + keys: ["KEY_PageDown"], + initialVal: "1999-01-01", + expectedVal: "1999-10-01" + }, + { + // PageUp on day field increments day by 7. + keys: ["KEY_Tab", "KEY_PageUp"], + initialVal: "1999-01-01", + expectedVal: "1999-01-08" + }, + { + // PageDown on day field decrements day by 7. + keys: ["KEY_Tab", "KEY_PageDown"], + initialVal: "1999-01-01", + expectedVal: "1999-01-25" + }, + { + // PageUp on year field increments year by 10. + keys: ["KEY_Tab", "KEY_Tab", "KEY_PageUp"], + initialVal: "1999-01-01", + expectedVal: "2009-01-01" + }, + { + // PageDown on year field decrements year by 10. + keys: ["KEY_Tab", "KEY_Tab", "KEY_PageDown"], + initialVal: "1999-01-01", + expectedVal: "1989-01-01" + }, + { + // Home key on month field sets it to the minimum month, which is 01. + keys: ["KEY_Home"], + initialVal: "2016-06-01", + expectedVal: "2016-01-01" + }, + { + // End key on month field sets it to the maximum month, which is 12. + keys: ["KEY_End"], + initialVal: "2016-06-01", + expectedVal: "2016-12-01" + }, + { + // Home key on day field sets it to the minimum day, which is 01. + keys: ["KEY_Tab", "KEY_Home"], + initialVal: "2016-01-10", + expectedVal: "2016-01-01" + }, + { + // End key on day field sets it to the maximum day, which is 31. + keys: ["KEY_Tab", "KEY_End"], + initialVal: "2016-01-10", + expectedVal: "2016-01-31" + }, + { + // Home key should have no effect on year field. + keys: ["KEY_Tab", "KEY_Tab", "KEY_Home"], + initialVal: "2016-01-01", + expectedVal: "2016-01-01" + }, + { + // End key should have no effect on year field. + keys: ["KEY_Tab", "KEY_Tab", "KEY_End"], + initialVal: "2016-01-01", + expectedVal: "2016-01-01" + }, + { + // Incomplete value maps to empty .value. + keys: ["1111"], + initialVal: "", + expectedVal: "" + }, + { + // Backspace key should clean a month field and map to empty .value. + keys: ["KEY_Backspace"], + initialVal: "2016-01-01", + expectedVal: "" + }, + { + // Backspace key should clean a day field and map to empty .value. + keys: ["KEY_Tab", "KEY_Backspace"], + initialVal: "2016-01-01", + expectedVal: "" + }, + { + // Backspace key should clean a year field and map to empty .value. + keys: ["KEY_Tab", "KEY_Tab", "KEY_Backspace"], + initialVal: "2016-01-01", + expectedVal: "" + }, + { + // Backspace key on Calendar button should not change a value. + keys: ["KEY_Tab", "KEY_Tab", "KEY_Tab", "KEY_Backspace"], + initialVal: "2016-01-01", + expectedVal: "2016-01-01" + }, +]; + +function sendKeys(aKeys) { + for (let i = 0; i < aKeys.length; i++) { + let key = aKeys[i]; + if (key.startsWith("KEY_")) { + synthesizeKey(key); + } else { + sendString(key); + } + } +} + +function test() { + document.querySelector("#host").attachShadow({ mode: "open" }).innerHTML = ` + <input type="date"> + `; + + function chromeListener(e) { + ok(false, "Picker should not be opened when dispatching untrusted click."); + } + + for (const elem of [document.getElementById("input"), document.getElementById("host").shadowRoot.querySelector("input")]) { + for (let { keys, initialVal, expectedVal } of testData) { + elem.focus(); + elem.value = initialVal; + sendKeys(keys); + is(elem.value, expectedVal, + "Test with " + keys + ", result should be " + expectedVal); + elem.value = ""; + elem.blur(); + } + SpecialPowers.addChromeEventListener("MozOpenDateTimePicker", + chromeListener); + elem.click(); + SpecialPowers.removeChromeEventListener("MozOpenDateTimePicker", + chromeListener); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_calendar_button.html b/dom/html/test/forms/test_input_datetime_calendar_button.html new file mode 100644 index 0000000000..970eee9027 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_calendar_button.html @@ -0,0 +1,179 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1479708 +--> +<head> +<title>Test required date/datetime-local input's Calendar button</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Created for <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1479708">Mozilla Bug 1479708</a> and updated by <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1676068">Mozilla Bug 1676068</a> and <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1865885">Mozilla Bug 1865885</a> +<p id="display"></p> +<div id="content"> +<input type="date" id="id_date" value="2017-06-08"> +<input type="time" id="id_time" value="10:30"> +<input type="datetime-local" id="id_datetime-local" value="2017-06-08T10:30"> +<input type="date" id="id_date_required" value="2017-06-08" required> +<input type="time" id="id_time_required" value="10:30" required> +<input type="datetime-local" id="id_datetime-local_required" value="2017-06-08T10:30" required> +<input type="date" id="id_date_readonly" value="2017-06-08" readonly> +<input type="time" id="id_time_readonly" value="10:30" readonly> +<input type="datetime-local" id="id_datetime-local_readonly" value="2017-06-08T10:30" readonly> +<input type="date" id="id_date_disabled" value="2017-06-08" disabled> +<input type="time" id="id_time_disabled" value="10:30" disabled> +<input type="datetime-local" id="id_datetime-local_disabled" value="2017-06-08T10:30" disabled> +</div> +<pre id="test"> +<script class="testbody"> + +const kTypes = ["date", "time", "datetime-local"]; + +function id_for_type(type, kind) { + return "id_" + type + (kind ? "_" + kind : ""); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + // Initial load. + assert_calendar_visible_all(""); + assert_calendar_visible_all("required"); + assert_calendar_hidden_all("readonly"); + assert_calendar_hidden_all("disabled"); + + // Dynamic toggling. + test_make_readonly(""); + test_make_editable("readonly"); + test_disabled_field_disabled(); + + // Now toggle the inputs to the initial state, but while being + // display: none. This tests for bug 1567191. + for (const input of document.querySelectorAll("input")) { + input.style.display = "none"; + is(input.getBoundingClientRect().width, 0, "Should be undisplayed"); + } + + test_make_readonly("readonly"); + test_make_editable(""); + + // And test other toggling as well. + test_readonly_field_disabled(); + test_disabled_field_disabled(); + + SimpleTest.finish(); +}); + +function test_disabled_field_disabled() { + for (let type of kTypes) { + const id = id_for_type(type, "disabled"); + const input = document.getElementById(id); + + ok(input.disabled, `#${id} Should be disabled`); + ok( + is_calendar_button_hidden(id), + `disabled's Calendar button is hidden (${id})` + ); + + input.disabled = false; + ok(!input.disabled, `#${id} Should not be disabled anymore`); + if (type === "time") { + assert_calendar_hidden(id); + } else { + ok( + !is_calendar_button_hidden(id), + `enabled field's Calendar button is not hidden (${id})` + ); + } + + input.disabled = true; // reset to the original state. + } +} + +function test_readonly_field_disabled() { + for (let type of kTypes) { + const id = id_for_type(type, "readonly"); + const input = document.getElementById(id); + + ok(input.readOnly, `#${id} Should be read-only`); + ok(is_calendar_button_hidden(id), `readonly field's Calendar button is hidden (${id})`); + + input.readOnly = false; + ok(!input.readOnly, `#${id} Should not be read-only anymore`); + if (type === "time") { + assert_calendar_hidden(id); + } else { + ok( + !is_calendar_button_hidden(id), + `non-readonly field's Calendar button is not hidden (${id})` + ); + } + + input.readOnly = true; // reset to the original state. + } +} + +function test_make_readonly(kind) { + for (let type of kTypes) { + const id = id_for_type(type, kind); + const input = document.getElementById(id); + is(input.readOnly, false, `Precondition: input #${id} is editable`); + + input.readOnly = true; + assert_calendar_hidden(id); + } +} + +function test_make_editable(kind) { + for (let type of kTypes) { + const id = id_for_type(type, kind); + const input = document.getElementById(id); + is(input.readOnly, true, `Precondition: input #${id} is read-only`); + + input.readOnly = false; + if (type === "time") { + assert_calendar_hidden(id); + } else { + assert_calendar_visible(id); + } + } +} + +function assert_calendar_visible_all(kind) { + for (let type of kTypes) { + if (type === "time") { + assert_calendar_hidden(id_for_type(type, kind)); + } else { + assert_calendar_visible(id_for_type(type, kind)); + } + } +} +function assert_calendar_visible(id) { + const isCalendarButtonHidden = is_calendar_button_hidden(id); + ok(!isCalendarButtonHidden, `Calendar button is not hidden on #${id}`); +} + +function assert_calendar_hidden_all(kind) { + for (let type of kTypes) { + assert_calendar_hidden(id_for_type(type, kind)); + } +} + +function assert_calendar_hidden(id) { + const isCalendarButtonHidden = is_calendar_button_hidden(id); + ok(isCalendarButtonHidden, `Calendar button is hidden on #${id}`); +} + +function is_calendar_button_hidden(id) { + const input = document.getElementById(id); + const shadowRoot = SpecialPowers.wrap(input).openOrClosedShadowRoot; + const calendarButton = shadowRoot.getElementById("calendar-button"); + const calendarButtonDisplay = SpecialPowers.wrap(window).getComputedStyle(calendarButton).display; + return calendarButtonDisplay === "none"; +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_disabled_focus.html b/dom/html/test/forms/test_input_datetime_disabled_focus.html new file mode 100644 index 0000000000..68a89b1780 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_disabled_focus.html @@ -0,0 +1,82 @@ +<!DOCTYPE html>
+<title>Test for bugs 1772841 and 1865885</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="/tests/SimpleTest/EventUtils.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1772841">Mozilla Bug 1772841</a> and <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1865885">Mozilla Bug 1865885</a>
+<div id="content">
+ <!-- Disabled -->
+ <input type="date" id="date" disabled>
+ <input type="time" id="time" disabled>
+ <input type="datetime-local" id="datetime-local" disabled>
+ <fieldset id="fieldset" disabled>
+ <input type="date" id="fieldset-date">
+ <input type="time" id="fieldset-time">
+ <input type="datetime-local" id="fieldset-datetime-local">
+ </fieldset>
+
+ <!-- Dynamically disabled -->
+ <input type="date" id="date1">
+ <input type="time" id="time1">
+ <input type="datetime-local" id="datetime-local1">
+ <fieldset id="fieldset1">
+ <input type="date" id="fieldset-date1">
+ <input type="time" id="fieldset-time1">
+ <input type="datetime-local" id="fieldset-datetime-local1">
+ </fieldset>
+
+ <!-- Dynamically enabled -->
+ <input type="date" id="date2" disabled>
+ <input type="time" id="time2" disabled>
+ <input type="datetime-local" id="datetime-local2" disabled>
+ <fieldset id="fieldset2" disabled>
+ <input type="date" id="fieldset-date2">
+ <input type="time" id="fieldset-time2">
+ <input type="datetime-local" id="fieldset-datetime-local2">
+ </fieldset>
+</div>
+<script>
+ /*
+ * Test for bugs 1772841 and 1865885
+ * This test checks that when a datetime input element is disabled by itself
+ * or from its containing fieldset, it should not be focusable by click.
+ **/
+
+ add_task(async function() {
+ await SimpleTest.promiseFocus(window);
+ for (let inputId of ["time", "date", "datetime-local", "fieldset-time", "fieldset-date", "fieldset-datetime-local"]) {
+ testFocusState(inputId, /* isDisabled = */ true);
+ testDynamicChange(inputId, "1", /* isDisabling = */ true);
+ testDynamicChange(inputId, "2", /* isDisabling = */ false);
+ }
+ })
+ function testFocusState(inputId, isDisabled) {
+ let input = document.getElementById(inputId);
+
+ document.getElementById("content").click();
+ input.click();
+ if (isDisabled) {
+ isnot(document.activeElement, input, `This disabled ${inputId} input should not be focusable by click`);
+ } else {
+ // The click method won't set the focus on clicked input, thus we
+ // only check that the state is changed to enabled here
+ ok(!input.disabled, `This ${inputId} input is not disabled`);
+ }
+
+ document.getElementById("content").click();
+ synthesizeMouseAtCenter(input, {});
+ if (isDisabled) {
+ isnot(document.activeElement, input, `This disabled ${inputId} input should not be focusable by click`);
+ } else {
+ is(document.activeElement, input, `This enabled ${inputId} input should be focusable by click`);
+ }
+ }
+ function testDynamicChange(inputId, index, isDisabling) {
+ if (inputId.split("-")[0] === "fieldset") {
+ document.getElementById("fieldset" + index).disabled = isDisabling;
+ } else {
+ document.getElementById(inputId + index).disabled = isDisabling;
+ }
+ testFocusState(inputId + index, /* isDisabled = */ isDisabling);
+ }
+</script>
diff --git a/dom/html/test/forms/test_input_datetime_focus_blur.html b/dom/html/test/forms/test_input_datetime_focus_blur.html new file mode 100644 index 0000000000..bff7b2ceb8 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_focus_blur.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 +--> +<head> + <title>Test focus/blur behaviour for date/time input types</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a> +<p id="display"></p> +<div id="content"> + <input id="input_time" type="time"> + <input id="input_date" type="date"> + <input id="input_datetime-local" type="datetime-local"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 1288591. + * This test checks whether date/time input types' .focus()/.blur() works + * correctly. This test also checks when focusing on an date/time input element, + * the focus is redirected to the anonymous text control, but the + * document.activeElement still returns date/time input element. + **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function testFocusBlur(type) { + let input = document.getElementById("input_" + type); + input.focus(); + + // The active element returns the date/time input element. + let activeElement = document.activeElement; + is(activeElement, input, "activeElement should be the date/time input element"); + is(activeElement.localName, "input", "activeElement should be an input element"); + is(activeElement.type, type, "activeElement should be of type " + type); + + // Use FocusManager to check that the actual focus is on the anonymous + // text control. + let fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"] + .getService(SpecialPowers.Ci.nsIFocusManager); + let focusedElement = fm.focusedElement; + is(focusedElement.localName, "span", "focusedElement should be an span element"); + + input.blur(); + isnot(document.activeElement, input, "activeElement should no longer be the datetime input element"); +} + +function test() { + for (let inputType of ["time", "date", "datetime-local"]) { + testFocusBlur(inputType); + } +} +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_focus_blur_events.html b/dom/html/test/forms/test_input_datetime_focus_blur_events.html new file mode 100644 index 0000000000..2e4e918119 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_focus_blur_events.html @@ -0,0 +1,93 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1301306 +--> +<head> +<title>Test for Bug 1301306</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1301306">Mozilla Bug 722599</a> +<p id="display"></p> +<div id="content"> +<input type="time" id="input_time" onfocus="++focusEvents[0]" + onblur="++blurEvents[0]" onfocusin="++focusInEvents[0]" + onfocusout="++focusOutEvents[0]"> +<input type="date" id="input_date" onfocus="++focusEvents[1]" + onblur="++blurEvents[1]" onfocusin="++focusInEvents[1]" + onfocusout="++focusOutEvents[1]"> +<input type="datetime-local" id="input_datetime-local" onfocus="++focusEvents[2]" + onblur="++blurEvents[2]" onfocusin="++focusInEvents[2]" + onfocusout="++focusOutEvents[2]"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** + * Test for Bug 1301306. + * This test checks that when moving inside the time input element, e.g. jumping + * through the inner text boxes, does not fire extra focus/blur events. + **/ + +var inputTypes = ["time", "date", "datetime-local"]; +var focusEvents = [0, 0, 0]; +var focusInEvents = [0, 0, 0]; +var focusOutEvents = [0, 0, 0]; +var blurEvents = [0, 0, 0]; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function test() { + for (var i = 0; i < inputTypes.length; i++) { + var input = document.getElementById("input_" + inputTypes[i]); + + input.focus(); + is(focusEvents[i], 1, inputTypes[i] + " input element should have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + // Move around inside the input element's input box. + synthesizeKey("KEY_Tab"); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " time input element should not have dispatched blur event."); + + synthesizeKey("KEY_ArrowRight"); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + synthesizeKey("KEY_ArrowLeft"); + is(focusEvents[i], 1,inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + synthesizeKey("KEY_ArrowRight"); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 0, inputTypes[i] + " input element should not have dispatched focusout event."); + is(blurEvents[i], 0, inputTypes[i] + " input element should not have dispatched blur event."); + + input.blur(); + is(focusEvents[i], 1, inputTypes[i] + " input element should not have dispatched focus event."); + is(focusInEvents[i], 1, inputTypes[i] + " input element should not have dispatched focusin event."); + is(focusOutEvents[i], 1, inputTypes[i] + " input element should have dispatched focusout event."); + is(blurEvents[i], 1, inputTypes[i] + " input element should have dispatched blur event."); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_focus_state.html b/dom/html/test/forms/test_input_datetime_focus_state.html new file mode 100644 index 0000000000..3b771f2394 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_focus_state.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1346085 +--> +<head> + <title>Test moving focus in onfocus/onblur handler</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1346085">Mozilla Bug 1346085</a> +<p id="display"></p> +<div id="content"> + <input id="input_time" type="time"> + <input id="input_date" type="date"> + <input id="input_dummy" type="text"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 1346085. + * This test checks whether date/time input types' focus state are set + * correctly, event when moving focus in onfocus/onblur handler. + **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function testFocusState(type) { + let input = document.getElementById("input_" + type); + + input.focus(); + let focus = document.querySelector(":focus"); + let focusRing = document.querySelector(":-moz-focusring"); + is(focus, input, "input should have :focus state after focus"); + is(focusRing, input, "input should have :-moz-focusring state after focus"); + + input.blur(); + focus = document.querySelector(":focus"); + focusRing = document.querySelector(":-moz-focusring"); + isnot(focus, input, "input should not have :focus state after blur"); + isnot(focusRing, input, "input should not have :-moz-focusring state after blur"); + + input.addEventListener("focus", function() { + document.getElementById("input_dummy").focus(); + }, { once: true }); + + input.focus(); + focus = document.querySelector(":focus"); + focusRing = document.querySelector(":-moz-focusring"); + isnot(focus, input, "input should not have :focus state when moving focus in onfocus handler"); + isnot(focusRing, input, "input should not have :-moz-focusring state when moving focus in onfocus handler"); + + input.addEventListener("blur", function() { + document.getElementById("input_dummy").focus(); + }, { once: true }); + + input.blur(); + focus = document.querySelector(":focus"); + focusRing = document.querySelector(":-moz-focusring"); + isnot(focus, input, "input should not have :focus state when moving focus in onblur handler"); + isnot(focusRing, input, "input should not have :-moz-focusring state when moving focus in onblur handler"); +} + +function test() { + let inputTypes = ["time", "date"]; + + for (let i = 0; i < inputTypes.length; i++) { + testFocusState(inputTypes[i]); + } +} +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_hidden.html b/dom/html/test/forms/test_input_datetime_hidden.html new file mode 100644 index 0000000000..7d8a6766a9 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_hidden.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1514040 +--> +<head> + <title>Test construction of hidden date input type</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1514040">Mozilla Bug 1514040</a> +<p id="display"></p> +<div id="content"> + <input id="date" type="date" hidden value="1947-02-28"> +</div> +<pre id="test"> +<script type="application/javascript"> + +let el = document.getElementById("date"); +ok(el.hidden, "element is hidden"); +is(el.value, "1947-02-28", ".value is set correctly"); +let fieldElements = Array.from(SpecialPowers.wrap(el).openOrClosedShadowRoot.querySelectorAll(".datetime-edit-field")); +is(fieldElements[0].textContent, "02", "month is set"); +is(fieldElements[1].textContent, "28", "day is set"); +is(fieldElements[2].textContent, "1947", "year is set"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_input_change_events.html b/dom/html/test/forms/test_input_datetime_input_change_events.html new file mode 100644 index 0000000000..63c8012252 --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_input_change_events.html @@ -0,0 +1,143 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1370858 +--> +<head> +<title>Test for Bugs 1370858 and 1804881</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1370858">Mozilla Bug 1370858</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1804881">Mozilla Bug 1804881</a> +<p id="display"></p> +<div id="content"> +<input type="time" id="input_time" onchange="++changeEvents[0]" + oninput="++inputEvents[0]"> +<input type="date" id="input_date" onchange="++changeEvents[1]" + oninput="++inputEvents[1]"> +<input type="datetime-local" id="input_datetime-local" onchange="++changeEvents[2]" + oninput="++inputEvents[2]"> +</div> +<pre id="test"> +<script class="testbody"> + +/** + * Test for Bug 1370858. + * Test that change and input events are (not) fired for date/time inputs. + **/ + +const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent); + +var inputTypes = ["time", "date", "datetime-local"]; +var changeEvents = [0, 0, 0]; +var inputEvents = [0, 0, 0]; +var values = ["10:30", "2017-06-08", "2017-06-08T10:30"]; +var expectedValues = [ + ["09:30", "01:30", "01:25", "", "01:59", "13:59", ""], + ["2017-05-08", "2017-01-08", "2017-01-25", "", "2017-01-31", "2017-01-31", ""], + ["2017-05-08T10:30", "2017-01-08T10:30", "2017-01-25T10:30", "", "2017-01-31T10:30", "2017-01-31T10:30", ""] +]; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function test() { + for (var i = 0; i < inputTypes.length; i++) { + var input = document.getElementById("input_" + inputTypes[i]); + + is(changeEvents[i], 0, "Number of change events should be 0 at start."); + is(inputEvents[i], 0, "Number of input events should be 0 at start."); + + // Test that change and input events are not dispatched setting .value by + // script. + input.value = values[i]; + is(input.value, values[i], "Check that value was set correctly (0)."); + is(changeEvents[i], 0, "Change event should not have dispatched (0)."); + is(inputEvents[i], 0, "Input event should not have dispatched (0)."); + + // Test that change and input events are fired when changing the value using + // up/down keys. + input.focus(); + synthesizeKey("KEY_ArrowDown"); + is(input.value, expectedValues[i][0], "Check that value was set correctly (1)."); + is(changeEvents[i], 1, "Change event should be dispatched (1)."); + is(inputEvents[i], 1, "Input event should be dispatched (1)."); + + // Test that change and input events are fired when changing the value with + // the keyboard. + sendString("01"); + // We get event per character. + is(input.value, expectedValues[i][1], "Check that value was set correctly (2)."); + is(changeEvents[i], 3, "Change event should be dispatched (2)."); + is(inputEvents[i], 3, "Input event should be dispatched (2)."); + + // Test that change and input events are fired when changing the value with + // both the numeric keyboard and digit keys. + synthesizeKey("2", { code: "Numpad2" }); + synthesizeKey("5"); + // We get event per character. + is(input.value, expectedValues[i][2], "Check that value was set correctly (3)."); + is(changeEvents[i], 5, "Change event should be dispatched (3)."); + is(inputEvents[i], 5, "Input event should be dispatched (3)."); + + // Test that change and input events are not fired when navigating with Tab. + // Return to the previously focused field (minutes, day, day). + synthesizeKey("KEY_Tab", { shiftKey: true }); + is(input.value, expectedValues[i][2], "Check that value was not changed (4)."); + is(changeEvents[i], 5, "Change event should not be dispatched (4)."); + is(inputEvents[i], 5, "Input event should not be dispatched (4)."); + + // Test that change and input events are fired when using Backspace. + synthesizeKey("KEY_Backspace"); + // We get event per character. + is(input.value, expectedValues[i][3], "Check that value was set correctly (5)."); + is(changeEvents[i], 6, "Change event should be dispatched (5)."); + is(inputEvents[i], 6, "Input event should be dispatched (5)."); + + // Test that change and input events are fired when using Home key. + synthesizeKey("KEY_End"); + // We get event per character. + is(input.value, expectedValues[i][4], "Check that value was set correctly (6)."); + is(changeEvents[i], 7, "Change event should be dispatched (6)."); + is(inputEvents[i], 7, "Input event should be dispatched (6)."); + + // Test that change and input events are fired for time and not fired + // for others when changing the value with a letter key. + // Navigate to the next field (time of the day, year, year). + synthesizeKey("KEY_Tab"); + synthesizeKey("P"); + // We get event per character. + is(input.value, expectedValues[i][5], "Check that value was set correctly (7)."); + if (i === 0) { + // For the time input, the time of the day should be focused and it, + // as an AM/PM toggle, should change to "PM" when the "p" key is pressed + is(changeEvents[i], 8, "Change event should be dispatched (7)."); + is(inputEvents[i], 8, "Input event should be dispatched (7)."); + } else { + // For the date and datetime inputs, the year should be focused and it, + // as a numeric value, should not change when the "p" key is pressed + is(changeEvents[i], 7, "Change event should not be dispatched (7)."); + is(inputEvents[i], 7, "Input event should not be dispatched (7)."); + } + + // Test that change and input events are fired when clearing the value + // using a Ctrl/Cmd+Delete/Backspace key combination + let events = (i === 0) ? 9 : 8; + synthesizeKey("KEY_Backspace", { accelKey: true }); + // We get one event + is(input.value, expectedValues[i][6], "Check that value was cleared out correctly (8)."); + is(changeEvents[i], events, "Change event should be dispatched (8)."); + is(inputEvents[i], events, "Input event should be dispatched (8)."); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_readonly.html b/dom/html/test/forms/test_input_datetime_readonly.html new file mode 100644 index 0000000000..aa7b40753b --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_readonly.html @@ -0,0 +1,20 @@ +<!doctype html> +<title>Test for bug 1461509</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<input id="i" type="date" value="1995-11-20" readonly required> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + let input = document.getElementById("i"); + let value = input.value; + + isnot(value, "", "should have a value"); + + input.focus(); + synthesizeKey("KEY_Backspace"); + is(input.value, value, "Value shouldn't change"); + SimpleTest.finish(); +}); +</script> diff --git a/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html b/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html new file mode 100644 index 0000000000..393de9fdee --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_reset_default_value_input_change_event.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<!-- + https://bugzilla.mozilla.org/show_bug.cgi?id=1446722 +--> +<head> +<title>Test for bug 1446722</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script type="application/javascript" src="utils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1446722">Mozilla bug 1446722</a> +<p id="display"></p> +<div id="content"> +<form> +<input type="time" id="input_time" value="10:30" onchange="++numberChangeEvents" + oninput="++numberInputEvents"> +<input type="date" id="input_date" value="2012-05-06" onchange="++numberChangeEvents" + oninput="++numberInputEvents"> +<input type="time" id="input_time2" value="11:30" onchange="++numberChangeEvents" + oninput="++numberInputEvents"> +<input type="date" id="input_date2" value="2014-07-08" + onchange="++numberChangeEvents" + oninput="++numberInputEvents"> +<input type="time" id="input_time3" value="12:30" onchange="++numberChangeEvents" + oninput="++numberInputEvents"> +<input type="date" id="input_date3" value="2014-08-09" + onchange="++numberChangeEvents" + oninput="++numberInputEvents"> +<input type="reset" id="input_reset"> +</form> +</div> +<pre id="test"> +<script class="testbody" type="application/javascript"> + +/** + * Test for bug 1446722. + * + * Test change and input events are fired for date and time inputs when the + * default value is reset from the date UI and the time UI. + * Test they are not fired when the value is changed via a script. + * Test clicking the reset button of a form does not fire these events. + **/ + +const INPUT_FIELD_ID_PREFIX = "input_"; + +var numberChangeEvents = 0; +var numberInputEvents = 0; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test_reset_in_script_does_not_trigger_change_and_input_event( + "time2", numberChangeEvents, numberInputEvents); + test_reset_in_script_does_not_trigger_change_and_input_event( + "date2", numberChangeEvents, numberInputEvents); + + test_reset_form_does_not_trigger_change_and_input_events("time3", "14:00", + numberChangeEvents, numberInputEvents); + test_reset_form_does_not_trigger_change_and_input_events("date3", "2016-01-01", + numberChangeEvents, numberInputEvents); + + SimpleTest.finish(); +}); + +function test_reset_in_script_does_not_trigger_change_and_input_event( + inputFieldIdSuffix, oldNumberChangeEvents, oldNumberInputEvents) { + const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix; + var input = document.getElementById(inputFieldName); + + is(input.value, input.defaultValue, + "Check " + inputFieldName + "'s default value is initialized correctly."); + is(numberChangeEvents, oldNumberChangeEvents, + "Check numberChangeEvents is initialized correctly for " + inputFieldName + + "."); + is(numberInputEvents, oldNumberInputEvents, + "Check numberInputEvents is initialized correctly for " + inputFieldName + + "."); + + input.value = ""; + + is(numberChangeEvents, oldNumberChangeEvents, + "Change event should not be dispatched for " + inputFieldName + "."); + is(numberInputEvents, oldNumberInputEvents, + "Input event should not be dispatched for " + inputFieldName + "."); +} + +function test_reset_form_does_not_trigger_change_and_input_events( + inputFieldIdSuffix, newValue, oldNumberChangeEvents, oldNumberInputEvents) { + const inputFieldName = INPUT_FIELD_ID_PREFIX + inputFieldIdSuffix; + const inputFieldResetButtonName = "input_reset"; + var input = document.getElementById(inputFieldName); + + is(input.value, input.defaultValue, + "Check " + inputFieldName + "'s default value is initialized correctly."); + isnot(input.defaultValue, newValue, "Check default value differs from newValue for " + + inputFieldName + "."); + is(numberChangeEvents, oldNumberChangeEvents, + "Check numberChangeEvents is initialized correctly for " + inputFieldName + + "."); + is(numberInputEvents, oldNumberInputEvents, + "Check numberInputEvents is initialized correctly for " + inputFieldName + + "."); + + input.value = newValue; + + var resetButton = document.getElementById(inputFieldResetButtonName); + synthesizeMouseAtCenter(resetButton, {}); + + is(input.value, input.defaultValue, "Check value is reset to default for " + + inputFieldName + "."); + is(numberChangeEvents, oldNumberChangeEvents, + "Change event should not be dispatched for " + inputFieldResetButtonName + "."); + is(numberInputEvents, oldNumberInputEvents, + "Input event should not be dispatched for " + inputFieldResetButtonName + "."); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_datetime_tabindex.html b/dom/html/test/forms/test_input_datetime_tabindex.html new file mode 100644 index 0000000000..207a7a8a8e --- /dev/null +++ b/dom/html/test/forms/test_input_datetime_tabindex.html @@ -0,0 +1,113 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 +--> +<head> + <title>Test tabindex attribute for date/time input types</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a> +<p id="display"></p> +<div id="content"> + <input id="time1" type="time" tabindex="0"> + <input id="time2" type="time" tabindex="-1"> + <input id="time3" type="time" tabindex="0"> + <input id="time4" type="time" disabled> + <input id="date1" type="date" tabindex="0"> + <input id="date2" type="date" tabindex="-1"> + <input id="date3" type="date" tabindex="0"> + <input id="date4" type="date" disabled> + <input id="datetime-local1" type="datetime-local" tabindex="0"> + <input id="datetime-local2" type="datetime-local" tabindex="-1"> + <input id="datetime-local3" type="datetime-local" tabindex="0"> + <input id="datetime-local4" type="datetime-local" disabled> +</div> +<pre id="test"> +<script> +/** + * Test for Bug 1288591. + * This test checks whether date/time input types tabindex attribute works + * correctly. + **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function checkInnerTextboxTabindex(input, tabindex) { + let fields = SpecialPowers.wrap(input).openOrClosedShadowRoot.querySelectorAll(".datetime-edit-field"); + + for (let field of fields) { + is(field.tabIndex, tabindex, "tabIndex in the inner textbox should be correct"); + } + +} + +function testTabindex(type) { + let input1 = document.getElementById(type + "1"); + let input2 = document.getElementById(type + "2"); + let input3 = document.getElementById(type + "3"); + let input4 = document.getElementById(type + "4"); + + input1.focus(); + is(document.activeElement, input1, + "input element with tabindex=0 is focusable"); + + // Time input does not include a Calendar button + let fieldCount; + if (type == "datetime-local") { + fieldCount = 7; + } else if (type == "date") { + fieldCount = 4; + } else { + fieldCount = 3; + }; + + // Advance through inner fields. + for (let i = 0; i < fieldCount - 1; ++i) { + synthesizeKey("KEY_Tab"); + is(document.activeElement, input1, + "input element with tabindex=0 is tabbable"); + } + + // Advance to next element + synthesizeKey("KEY_Tab"); + is(document.activeElement, input3, + "input element with tabindex=-1 is not tabbable"); + + input2.focus(); + is(document.activeElement, input2, + "input element with tabindex=-1 is still focusable"); + + checkInnerTextboxTabindex(input1, 0); + checkInnerTextboxTabindex(input2, -1); + checkInnerTextboxTabindex(input3, 0); + + // Changing the tabindex attribute dynamically. + input3.setAttribute("tabindex", "-1"); + + synthesizeKey("KEY_Tab"); // need only one TAB since input2 is not tabbable + + isnot(document.activeElement, input3, + "element with tabindex changed to -1 should not be tabbable"); + isnot(document.activeElement, input4, + "disabled element should not be tabbable"); + + checkInnerTextboxTabindex(input3, -1); +} + +function test() { + for (let inputType of ["time", "date", "datetime-local"]) { + testTabindex(inputType); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_defaultValue.html b/dom/html/test/forms/test_input_defaultValue.html new file mode 100644 index 0000000000..03849d7f54 --- /dev/null +++ b/dom/html/test/forms/test_input_defaultValue.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=977029 +--> +<head> + <title>Test for Bug 977029</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<div id="content"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=977029">Bug 977029</a> + <p> + Goal of this test is to check that modifying defaultValue and value attribute + of input types is working as expected. + </p> + <form> + <input id='a' type="color" value="#00ff00"> + <input id='b' type="text" value="foo"> + <input id='c' type="email" value="foo"> + <input id='d' type="date" value="2010-09-20"> + <input id='e' type="search" value="foo"> + <input id='f' type="tel" value="foo"> + <input id='g' type="url" value="foo"> + <input id='h' type="number" value="42"> + <input id='i' type="range" value="42" min="0" max="100"> + <input id='j' type="time" value="17:00:25.54"> + </form> +</div> +<script type="application/javascript"> + +// [ element id | original defaultValue | another value | another default value] +// Preferably use only valid values: the goal of this test isn't to test the +// value sanitization algorithm (for input types which have one) as this is +// already part of another test) +var testData = [["a", "#00ff00", "#00aaaa", "#00ccaa"], + ["b", "foo", "bar", "tulip"], + ["c", "foo", "foo@bar.org", "tulip"], + ["d", "2010-09-20", "2012-09-21", ""], + ["e", "foo", "bar", "tulip"], + ["f", "foo", "bar", "tulip"], + ["g", "foo", "bar", "tulip"], + ["h", "42", "1337", "3"], + ["i", "42", "17", "3"], + ["j", "17:00:25.54", "07:00:25", "03:00:03"], + ]; + +for (var data of testData) { + id = data[0]; + input = document.getElementById(id); + originalDefaultValue = data[1]; + is(originalDefaultValue, input.defaultValue, + "Default value isn't the expected one"); + is(originalDefaultValue, input.value, + "input.value original value is different from defaultValue"); + input.defaultValue = data[2] + is(input.defaultValue, input.value, + "Changing default value before value was changed should change value too"); + input.value = data[3]; + input.defaultValue = originalDefaultValue; + is(input.value, data[3], + "Changing default value after value was changed should not change value"); + input.value = data[2]; + is(originalDefaultValue, input.defaultValue, + "defaultValue shouldn't change when changing value"); + input.defaultValue = data[3]; + is(input.defaultValue, data[3], + "defaultValue should have changed"); + // Change the value... + input.value = data[2]; + is(input.value, data[2], + "value should have changed"); + // ...then reset the form + input.form.reset(); + is(input.defaultValue, input.value, + "reset form should bring back the default value"); +} +</script> +</body> +</html> + diff --git a/dom/html/test/forms/test_input_email.html b/dom/html/test/forms/test_input_email.html new file mode 100644 index 0000000000..96ff939215 --- /dev/null +++ b/dom/html/test/forms/test_input_email.html @@ -0,0 +1,237 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=555559 +https://bugzilla.mozilla.org/show_bug.cgi?id=668817 +https://bugzilla.mozilla.org/show_bug.cgi?id=854812 +--> +<head> + <title>Test for <input type='email'> validity</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=555559">Mozilla Bug 555559</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=668817">Mozilla Bug 668817</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=854812">Mozilla Bug 854812</a> +<p id="display"></p> +<div id="content" style="display: none"> + <form> + <input type='email' name='email' id='i' oninvalid="invalidEventHandler(event);"> + <form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for <input type='email'> validity **/ + +var gInvalid = false; + +function invalidEventHandler(e) +{ + is(e.type, "invalid", "Invalid event type should be invalid"); + gInvalid = true; +} + +function checkValidEmailAddress(element) +{ + gInvalid = false; + ok(!element.validity.typeMismatch && !element.validity.badInput, + "Element should not suffer from type mismatch or bad input (with value='"+element.value+"')"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "Element should be valid"); + ok(!gInvalid, "The invalid event should not have been thrown"); + is(element.validationMessage, '', + "Validation message should be the empty string"); + ok(element.matches(":valid"), ":valid pseudo-class should apply"); +} + +const VALID = 0; +const TYPE_MISMATCH = 1 << 0; +const BAD_INPUT = 1 << 1; + +function checkInvalidEmailAddress(element, failedValidityStates) +{ + info("Checking " + element.value); + gInvalid = false; + var expectTypeMismatch = !!(failedValidityStates & TYPE_MISMATCH); + var expectBadInput = !!(failedValidityStates & BAD_INPUT); + ok(element.validity.typeMismatch == expectTypeMismatch, + "Element should " + (expectTypeMismatch ? "" : "not ") + "suffer from type mismatch (with value='"+element.value+"')"); + ok(element.validity.badInput == expectBadInput, + "Element should " + (expectBadInput ? "" : "not ") + "suffer from bad input (with value='"+element.value+"')"); + ok(!element.validity.valid, "Element should not be valid"); + ok(!element.checkValidity(), "Element should not be valid"); + ok(gInvalid, "The invalid event should have been thrown"); + is(element.validationMessage, "Please enter an email address.", + "Validation message is not valid"); + ok(element.matches(":invalid"), ":invalid pseudo-class should apply"); +} + +function testEmailAddress(aElement, aValue, aMultiple, aValidityFailures) +{ + aElement.multiple = aMultiple; + aElement.value = aValue; + + if (!aValidityFailures) { + checkValidEmailAddress(aElement); + } else { + checkInvalidEmailAddress(aElement, aValidityFailures); + } +} + +var email = document.forms[0].elements[0]; + +// Simple values, checking the e-mail syntax validity. +var values = [ + [ '' ], // The empty string shouldn't be considered as invalid. + [ 'foo@bar.com', VALID ], + [ ' foo@bar.com', VALID ], + [ 'foo@bar.com ', VALID ], + [ '\r\n foo@bar.com', VALID ], + [ 'foo@bar.com \n\r', VALID ], + [ '\n\n \r\rfoo@bar.com\n\n \r\r', VALID ], + [ '\n\r \n\rfoo@bar.com\n\r \n\r', VALID ], + [ 'tulip', TYPE_MISMATCH ], + // Some checks on the user part of the address. + [ '@bar.com', TYPE_MISMATCH ], + [ 'f\noo@bar.com', VALID ], + [ 'f\roo@bar.com', VALID ], + [ 'f\r\noo@bar.com', VALID ], + [ 'fü@foo.com', TYPE_MISMATCH ], + // Some checks for the domain part. + [ 'foo@bar', VALID ], + [ 'foo@b', VALID ], + [ 'foo@', TYPE_MISMATCH ], + [ 'foo@bar.', TYPE_MISMATCH ], + [ 'foo@foo.bar', VALID ], + [ 'foo@foo..bar', TYPE_MISMATCH ], + [ 'foo@.bar', TYPE_MISMATCH ], + [ 'foo@tulip.foo.bar', VALID ], + [ 'foo@tulip.foo-bar', VALID ], + [ 'foo@1.2', VALID ], + [ 'foo@127.0.0.1', VALID ], + [ 'foo@1.2.3', VALID ], + [ 'foo@b\nar.com', VALID ], + [ 'foo@b\rar.com', VALID ], + [ 'foo@b\r\nar.com', VALID ], + [ 'foo@.', TYPE_MISMATCH ], + [ 'foo@fü.com', VALID ], + [ 'foo@fu.cüm', VALID ], + [ 'thisUsernameIsLongerThanSixtyThreeCharactersInLengthRightAboutNow@mozilla.tld', VALID ], + // Long strings with UTF-8 in username. + [ 'this.is.email.should.be.longer.than.sixty.four.characters.föö@mözillä.tld', TYPE_MISMATCH ], + [ 'this-is-email-should-be-longer-than-sixty-four-characters-föö@mözillä.tld', TYPE_MISMATCH, true ], + // Long labels (labels greater than 63 chars long are not allowed). + [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss', VALID ], + [ 'foo@thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', VALID ], + [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss.com', VALID ], + [ 'foo@foo.thislabelisexactly63characterssssssssssssssssssssssssssssssssss', VALID ], + [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ], + [ 'foo@thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ], + [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ], + [ 'foo@foo.thislabelisexactly64charactersssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ], + // Long labels with UTF-8 (punycode encoding will increase the label to more than 63 chars). + [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ], + [ 'foo@thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ], + [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss.com', TYPE_MISMATCH | BAD_INPUT ], + [ 'foo@foo.thisläbelisexäctly63charäcterssssssssssssssssssssssssssssssssss', TYPE_MISMATCH | BAD_INPUT ], + // The domains labels (sub-domains or tld) can't start or finish with a '-' + [ 'foo@foo-bar', VALID ], + [ 'foo@-foo', TYPE_MISMATCH ], + [ 'foo@foo-.bar', TYPE_MISMATCH ], + [ 'foo@-.-', TYPE_MISMATCH ], + [ 'foo@fo-o.bar', VALID ], + [ 'foo@fo-o.-bar', TYPE_MISMATCH ], + [ 'foo@fo-o.bar-', TYPE_MISMATCH ], + [ 'foo@fo-o.-', TYPE_MISMATCH ], + [ 'foo@fo--o', VALID ], +]; + +// Multiple values, we don't check e-mail validity, only multiple stuff. +var multipleValues = [ + [ 'foo@bar.com, foo@bar.com', VALID ], + [ 'foo@bar.com,foo@bar.com', VALID ], + [ 'foo@bar.com,foo@bar.com,foo@bar.com', VALID ], + [ ' foo@bar.com , foo@bar.com ', VALID ], + [ '\tfoo@bar.com\t,\tfoo@bar.com\t', VALID ], + [ '\rfoo@bar.com\r,\rfoo@bar.com\r', VALID ], + [ '\nfoo@bar.com\n,\nfoo@bar.com\n', VALID ], + [ '\ffoo@bar.com\f,\ffoo@bar.com\f', VALID ], + [ '\t foo@bar.com\r,\nfoo@bar.com\f', VALID ], + [ 'foo@b,ar.com,foo@bar.com', TYPE_MISMATCH ], + [ 'foo@bar.com,foo@bar.com,', TYPE_MISMATCH ], + [ ' foo@bar.com , foo@bar.com , ', TYPE_MISMATCH ], + [ ',foo@bar.com,foo@bar.com', TYPE_MISMATCH ], + [ ',foo@bar.com,foo@bar.com', TYPE_MISMATCH ], + [ 'foo@bar.com,,,foo@bar.com', TYPE_MISMATCH ], + [ 'foo@bar.com;foo@bar.com', TYPE_MISMATCH ], + [ '<foo@bar.com>, <foo@bar.com>', TYPE_MISMATCH ], + [ 'foo@bar, foo@bar.com', VALID ], + [ 'foo@bar.com, foo', TYPE_MISMATCH ], + [ 'foo, foo@bar.com', TYPE_MISMATCH ], +]; + +/* Additional username checks. */ + +var legalCharacters = "abcdefghijklmnopqrstuvwxyz"; +legalCharacters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +legalCharacters += "0123456789"; +legalCharacters += "!#$%&'*+-/=?^_`{|}~."; + +// Add all username legal characters individually to the list. +for (c of legalCharacters) { + values.push([c + "@bar.com", VALID]); +} +// Add the concatenation of all legal characters too. +values.push([legalCharacters + "@bar.com", VALID]); + +// Add username illegal characters, the same way. +var illegalCharacters = "()<>[]:;@\\, \t"; +for (c of illegalCharacters) { + values.push([illegalCharacters + "@bar.com", TYPE_MISMATCH]); +} + +/* Additional domain checks. */ + +legalCharacters = "abcdefghijklmnopqrstuvwxyz"; +legalCharacters += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +legalCharacters += "0123456789"; + +// Add domain legal characters (except '.' and '-' because they are special). +for (c of legalCharacters) { + values.push(["foo@foo.bar" + c, VALID]); +} +// Add the concatenation of all legal characters too. +values.push(["foo@bar." + legalCharacters, VALID]); + +// Add domain illegal characters. +illegalCharacters = "()<>[]:;@\\,!#$%&'*+/=?^_`{|}~ \t"; +for (c of illegalCharacters) { + values.push(['foo@foo.ba' + c + 'r', TYPE_MISMATCH]); +} + +values.forEach(function([value, valid, todo]) { + if (todo === true) { + email.value = value; + todo_is(email.validity.valid, true, "value should be valid"); + } else { + testEmailAddress(email, value, false, valid); + } +}); + +multipleValues.forEach(function([value, valid]) { + testEmailAddress(email, value, true, valid); +}); + +// Make sure setting multiple changes the value. +email.multiple = false; +email.value = "foo@bar.com, foo@bar.com"; +checkInvalidEmailAddress(email, TYPE_MISMATCH); +email.multiple = true; +checkValidEmailAddress(email); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_event.html b/dom/html/test/forms/test_input_event.html new file mode 100644 index 0000000000..72863ca335 --- /dev/null +++ b/dom/html/test/forms/test_input_event.html @@ -0,0 +1,409 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=851780 +--> +<head> +<title>Test for input event</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=851780">Mozilla Bug 851780</a> +<p id="display"></p> +<div id="content"></div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + /** Test for input event. This is highly based on test_change_event.html **/ + + const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent); + + let expectedInputType = ""; + let expectedData = null; + let expectedBeforeInputCancelable = false; + function checkBeforeInputEvent(aEvent, aDescription) { + ok(aEvent instanceof InputEvent, + `"beforeinput" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.inputType, expectedInputType, + `inputType of "beforeinput" event should be "${expectedInputType}" ${aDescription}`); + is(aEvent.data, expectedData, + `data of "beforeinput" event should be ${expectedData} ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer of "beforeinput" event should be null ${aDescription}`); + is(aEvent.getTargetRanges().length, 0, + `getTargetRanges() of "beforeinput" event should return empty array ${aDescription}`); + is(aEvent.cancelable, expectedBeforeInputCancelable, + `"beforeinput" event for "${expectedInputType}" should ${expectedBeforeInputCancelable ? "be" : "not be"} cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"beforeinput" event should always bubble ${aDescription}`); + } + + let skipExpectedDataCheck = false; + function checkIfInputIsInputEvent(aEvent, aDescription) { + ok(aEvent instanceof InputEvent, + `"input" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.inputType, expectedInputType, + `inputType should be "${expectedInputType}" ${aDescription}`); + if (!skipExpectedDataCheck) + is(aEvent.data, expectedData, `data should be ${expectedData} ${aDescription}`); + else + info(`data is ${aEvent.data} ${aDescription}`); + is(aEvent.dataTransfer, null, + `dataTransfer should be null ${aDescription}`); + is(aEvent.cancelable, false, + `"input" event should be never cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"input" event should always bubble ${aDescription}`); + } + + function checkIfInputIsEvent(aEvent, aDescription) { + ok(aEvent instanceof Event && !(aEvent instanceof UIEvent), + `"input" event should be dispatched with InputEvent interface ${aDescription}`); + is(aEvent.cancelable, false, + `"input" event should be never cancelable ${aDescription}`); + is(aEvent.bubbles, true, + `"input" event should always bubble ${aDescription}`); + } + + let textareaInput = 0, textareaBeforeInput = 0; + let textTypes = ["text", "email", "search", "tel", "url", "password"]; + let textBeforeInput = [0, 0, 0, 0, 0, 0]; + let textInput = [0, 0, 0, 0, 0, 0]; + let nonTextTypes = ["button", "submit", "image", "reset", "radio", "checkbox"]; + let nonTextBeforeInput = [0, 0, 0, 0, 0, 0]; + let nonTextInput = [0, 0, 0, 0, 0, 0]; + let rangeInput = 0, rangeBeforeInput = 0; + let numberInput = 0, numberBeforeInput = 0; + + // Don't create elements whose event listener attributes are required before enabling `beforeinput` event. + function init() { + document.getElementById("content").innerHTML = + `<input type="file" id="fileInput"> + <textarea id="textarea"></textarea> + <input type="text" id="input_text"> + <input type="email" id="input_email"> + <input type="search" id="input_search"> + <input type="tel" id="input_tel"> + <input type="url" id="input_url"> + <input type="password" id="input_password"> + + <!-- "Non-text" inputs--> + <input type="button" id="input_button"> + <input type="submit" id="input_submit"> + <input type="image" id="input_image"> + <input type="reset" id="input_reset"> + <input type="radio" id="input_radio"> + <input type="checkbox" id="input_checkbox"> + <input type="range" id="input_range"> + <input type="number" id="input_number">`; + + document.getElementById("textarea").addEventListener("beforeinput", (aEvent) => { + ++textareaBeforeInput; + checkBeforeInputEvent(aEvent, "on textarea element"); + }); + document.getElementById("textarea").addEventListener("input", (aEvent) => { + ++textareaInput; + checkIfInputIsInputEvent(aEvent, "on textarea element"); + }); + + // These are the type were the input event apply. + for (let id of ["input_text", "input_email", "input_search", "input_tel", "input_url", "input_password"]) { + document.getElementById(id).addEventListener("beforeinput", (aEvent) => { + ++textBeforeInput[textTypes.indexOf(aEvent.target.type)]; + checkBeforeInputEvent(aEvent, `on input element whose type is ${aEvent.target.type}`); + }); + document.getElementById(id).addEventListener("input", (aEvent) => { + ++textInput[textTypes.indexOf(aEvent.target.type)]; + checkIfInputIsInputEvent(aEvent, `on input element whose type is ${aEvent.target.type}`); + }); + } + + // These are the type were the input event does not apply. + for (let id of ["input_button", "input_submit", "input_image", "input_reset", "input_radio", "input_checkbox"]) { + document.getElementById(id).addEventListener("beforeinput", (aEvent) => { + ++nonTextBeforeInput[nonTextTypes.indexOf(aEvent.target.type)]; + }); + document.getElementById(id).addEventListener("input", (aEvent) => { + ++nonTextInput[nonTextTypes.indexOf(aEvent.target.type)]; + checkIfInputIsEvent(aEvent, `on input element whose type is ${aEvent.target.type}`); + }); + } + + document.getElementById("input_range").addEventListener("beforeinput", (aEvent) => { + ++rangeBeforeInput; + }); + document.getElementById("input_range").addEventListener("input", (aEvent) => { + ++rangeInput; + checkIfInputIsEvent(aEvent, "on input element whose type is range"); + }); + + document.getElementById("input_number").addEventListener("beforeinput", (aEvent) => { + ++numberBeforeInput; + }); + document.getElementById("input_number").addEventListener("input", (aEvent) => { + ++numberInput; + checkIfInputIsInputEvent(aEvent, "on input element whose type is number"); + }); + } + + var MockFilePicker = SpecialPowers.MockFilePicker; + MockFilePicker.init(window); + + function testUserInput() { + // Simulating an OK click and with a file name return. + MockFilePicker.useBlobFile(); + MockFilePicker.returnValue = MockFilePicker.returnOK; + var input = document.getElementById('fileInput'); + input.focus(); + + input.addEventListener("beforeinput", function (aEvent) { + ok(false, "beforeinput event shouldn't be dispatched on file input."); + }); + input.addEventListener("input", function (aEvent) { + ok(true, "input event should've been dispatched on file input."); + checkIfInputIsEvent(aEvent, "on file input"); + }); + + input.click(); + SimpleTest.executeSoon(testUserInput2); + } + + function testUserInput2() { + // Some generic checks for types that support the input event. + for (var i = 0; i < textTypes.length; ++i) { + input = document.getElementById("input_" + textTypes[i]); + input.focus(); + expectedInputType = "insertLineBreak"; + expectedData = null; + expectedBeforeInputCancelable = true; + synthesizeKey("KEY_Enter"); + is(textBeforeInput[i], 1, "beforeinput event should've been dispatched on " + textTypes[i] + " input element"); + is(textInput[i], 0, "input event shouldn't be dispatched on " + textTypes[i] + " input element"); + + expectedInputType = "insertText"; + expectedData = "m"; + expectedBeforeInputCancelable = true; + sendString("m"); + is(textBeforeInput[i], 2, textTypes[i] + " input element should've been dispatched beforeinput event."); + is(textInput[i], 1, textTypes[i] + " input element should've been dispatched input event."); + expectedInputType = "insertLineBreak"; + expectedData = null; + expectedBeforeInputCancelable = true; + synthesizeKey("KEY_Enter", {shiftKey: true}); + is(textBeforeInput[i], 3, "input event should've been dispatched on " + textTypes[i] + " input element"); + is(textInput[i], 1, "input event shouldn't be dispatched on " + textTypes[i] + " input element"); + + expectedInputType = "deleteContentBackward"; + expectedData = null; + expectedBeforeInputCancelable = true; + synthesizeKey("KEY_Backspace"); + is(textBeforeInput[i], 4, textTypes[i] + " input element should've been dispatched beforeinput event."); + is(textInput[i], 2, textTypes[i] + " input element should've been dispatched input event."); + } + + // Some scenarios of value changing from script and from user input. + input = document.getElementById("input_text"); + input.focus(); + expectedInputType = "insertText"; + expectedData = "f"; + expectedBeforeInputCancelable = true; + sendString("f"); + is(textBeforeInput[0], 5, "beforeinput event should've been dispatched"); + is(textInput[0], 3, "input event should've been dispatched"); + input.blur(); + is(textBeforeInput[0], 5, "input event should not have been dispatched"); + is(textInput[0], 3, "input event should not have been dispatched"); + + input.focus(); + input.value = 'foo'; + is(textBeforeInput[0], 5, "beforeinput event should not have been dispatched"); + is(textInput[0], 3, "input event should not have been dispatched"); + input.blur(); + is(textBeforeInput[0], 5, "beforeinput event should not have been dispatched"); + is(textInput[0], 3, "input event should not have been dispatched"); + + input.focus(); + expectedInputType = "insertText"; + expectedData = "f"; + expectedBeforeInputCancelable = true; + sendString("f"); + is(textBeforeInput[0], 6, "beforeinput event should've been dispatched"); + is(textInput[0], 4, "input event should've been dispatched"); + input.value = 'bar'; + is(textBeforeInput[0], 6, "beforeinput event should not have been dispatched"); + is(textInput[0], 4, "input event should not have been dispatched"); + input.blur(); + is(textBeforeInput[0], 6, "beforeinput event should not have been dispatched"); + is(textInput[0], 4, "input event should not have been dispatched"); + + // Same for textarea. + var textarea = document.getElementById("textarea"); + textarea.focus(); + expectedInputType = "insertText"; + expectedData = "f"; + expectedBeforeInputCancelable = true; + sendString("f"); + is(textareaBeforeInput, 1, "beforeinput event should've been dispatched"); + is(textareaInput, 1, "input event should've been dispatched"); + textarea.blur(); + is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched"); + is(textareaInput, 1, "input event should not have been dispatched"); + + textarea.focus(); + textarea.value = 'foo'; + is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched"); + is(textareaInput, 1, "input event should not have been dispatched"); + textarea.blur(); + is(textareaBeforeInput, 1, "beforeinput event should not have been dispatched"); + is(textareaInput, 1, "input event should not have been dispatched"); + + textarea.focus(); + expectedInputType = "insertText"; + expectedData = "f"; + expectedBeforeInputCancelable = true; + sendString("f"); + is(textareaBeforeInput, 2, "beforeinput event should've been dispatched"); + is(textareaInput, 2, "input event should've been dispatched"); + textarea.value = 'bar'; + is(textareaBeforeInput, 2, "beforeinput event should not have been dispatched"); + is(textareaInput, 2, "input event should not have been dispatched"); + expectedInputType = "deleteContentBackward"; + expectedData = null; + expectedBeforeInputCancelable = true; + synthesizeKey("KEY_Backspace"); + is(textareaBeforeInput, 3, "beforeinput event should've been dispatched"); + is(textareaInput, 3, "input event should've been dispatched"); + textarea.blur(); + is(textareaBeforeInput, 3, "beforeinput event should not have been dispatched"); + is(textareaInput, 3, "input event should not have been dispatched"); + + // Non-text input tests: + for (var i = 0; i < nonTextTypes.length; ++i) { + // Button, submit, image and reset input type tests. + if (i < 4) { + input = document.getElementById("input_" + nonTextTypes[i]); + input.focus(); + input.click(); + is(nonTextBeforeInput[i], 0, "beforeinput event doesn't apply"); + is(nonTextInput[i], 0, "input event doesn't apply"); + input.blur(); + is(nonTextBeforeInput[i], 0, "beforeinput event doesn't apply"); + is(nonTextInput[i], 0, "input event doesn't apply"); + } + // For radio and checkboxes, input event should be dispatched. + else { + input = document.getElementById("input_" + nonTextTypes[i]); + input.focus(); + input.click(); + is(nonTextBeforeInput[i], 0, "beforeinput event should not have been dispatched"); + is(nonTextInput[i], 1, "input event should've been dispatched"); + input.blur(); + is(nonTextBeforeInput[i], 0, "beforeinput event should not have been dispatched"); + is(nonTextInput[i], 1, "input event should not have been dispatched"); + + // Test that input event is not dispatched if click event is cancelled. + function preventDefault(e) { + e.preventDefault(); + } + input.addEventListener("click", preventDefault); + input.click(); + is(nonTextBeforeInput[i], 0, "beforeinput event shouldn't be dispatched if click event is cancelled"); + is(nonTextInput[i], 1, "input event shouldn't be dispatched if click event is cancelled"); + input.removeEventListener("click", preventDefault); + } + } + + // Type changes. + var input = document.createElement('input'); + input.type = 'text'; + input.value = 'foo'; + input.onbeforeinput = function () { + ok(false, "we shouldn't get a beforeinput event when the type changes"); + }; + input.oninput = function() { + ok(false, "we shouldn't get an input event when the type changes"); + }; + input.type = 'range'; + isnot(input.value, 'foo'); + + // Tests for type='range'. + var range = document.getElementById("input_range"); + + range.focus(); + sendString("a"); + range.blur(); + is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on range input " + + "element for key changes that don't change its value"); + is(rangeInput, 0, "input event shouldn't be dispatched on range input " + + "element for key changes that don't change its value"); + + range.focus(); + synthesizeKey("KEY_Home"); + is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched even for key changes"); + is(rangeInput, 1, "input event should be dispatched for key changes"); + range.blur(); + is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on blur"); + is(rangeInput, 1, "input event shouldn't be dispatched on blur"); + + range.focus(); + var bcr = range.getBoundingClientRect(); + var centerOfRangeX = bcr.width / 2; + var centerOfRangeY = bcr.height / 2; + synthesizeMouse(range, centerOfRangeX - 10, centerOfRangeY, { type: "mousedown" }); + is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched on mousedown if the value changes"); + is(rangeInput, 2, "Input event should be dispatched on mousedown if the value changes"); + synthesizeMouse(range, centerOfRangeX - 5, centerOfRangeY, { type: "mousemove" }); + is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched during a drag"); + is(rangeInput, 3, "Input event should be dispatched during a drag"); + synthesizeMouse(range, centerOfRangeX, centerOfRangeY, { type: "mouseup" }); + is(rangeBeforeInput, 0, "beforeinput event shouldn't be dispatched at the end of a drag"); + is(rangeInput, 4, "Input event should be dispatched at the end of a drag"); + + // Tests for type='number'. + // We only test key events here since input events for mouse event changes + // are tested in test_input_number_mouse_events.html + var number = document.getElementById("input_number"); + + if (isDesktop) { // up/down arrow keys not supported on android + number.value = ""; + number.focus(); + // <input type="number">'s inputType value hasn't been decided, see + // https://github.com/w3c/input-events/issues/88 + expectedInputType = "insertReplacementText"; + expectedData = "1"; + expectedBeforeInputCancelable = false; + synthesizeKey("KEY_ArrowUp"); + is(numberBeforeInput, 1, "beforeinput event should be dispatched for up/down arrow key keypress"); + is(numberInput, 1, "input event should be dispatched for up/down arrow key keypress"); + is(number.value, "1", "sanity check value of number control after keypress"); + + // `data` will be the value of the input, but we can't change + // `expectedData` and use {repeat: 3} at the same time. + skipExpectedDataCheck = true; + synthesizeKey("KEY_ArrowDown", {repeat: 3}); + is(numberBeforeInput, 4, "beforeinput event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated"); + is(numberInput, 4, "input event should be dispatched for each up/down arrow key keypress event, even when rapidly repeated"); + is(number.value, "-2", "sanity check value of number control after multiple keydown events"); + skipExpectedDataCheck = false; + + number.blur(); + is(numberBeforeInput, 4, "beforeinput event shouldn't be dispatched on blur"); + is(numberInput, 4, "input event shouldn't be dispatched on blur"); + } + + MockFilePicker.cleanup(); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + document.addEventListener("DOMContentLoaded", () => { + init(); + SimpleTest.waitForFocus(testUserInput); + }, {once: true}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_file_picker.html b/dom/html/test/forms/test_input_file_picker.html new file mode 100644 index 0000000000..296c12bb7e --- /dev/null +++ b/dom/html/test/forms/test_input_file_picker.html @@ -0,0 +1,280 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for <input type='file'> file picker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377624">Mozilla Bug 36619</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377624">Mozilla Bug 377624</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=565274">Mozilla Bug 565274</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=701353">Mozilla Bug 701353</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=826176">Mozilla Bug 826176</a> +<p id="display"></p> +<div id="content"> + <input id='a' type='file' accept="image/*"> + <input id='b' type='file' accept="audio/*"> + <input id='c' type='file' accept="video/*"> + <input id='d' type='file' accept="image/*, audio/* "> + <input id='e' type='file' accept=" image/*,video/*"> + <input id='f' type='file' accept="audio/*,video/*"> + <input id='g' type='file' accept="image/*, audio/* ,video/*"> + <input id='h' type='file' accept="foo/baz,image/*,bogus/duh"> + <input id='i' type='file' accept="mime/type;parameter,video/*"> + <input id='j' type='file' accept="audio/*, audio/*, audio/*"> + <input id='k' type="file" accept="image/gif,image/png"> + <input id='l' type="file" accept="image/*,image/gif,image/png"> + <input id='m' type="file" accept="image/gif,image/gif"> + <input id='n' type="file" accept=""> + <input id='o' type="file" accept=".test"> + <input id='p' type="file" accept="image/gif,.csv"> + <input id='q' type="file" accept="image/gif,.gif"> + <input id='r' type="file" accept=".prefix,.prefixPlusSomething"> + <input id='s' type="file" accept=".xls,.xlsx"> + <input id='t' type="file" accept=".mp3,.wav,.flac"> + <input id='u' type="file" accept=".xls, .xlsx"> + <input id='v' type="file" accept=".xlsx, .xls"> + <input id='w' type="file" accept=".xlsx; .xls"> + <input id='x' type="file" accept=".xls, .xlsx"> + <input id='y' type="file" accept=".xlsx, .xls"> + <input id='z' type='file' accept="i/am,a,pathological,;,,,,test/case"> + <input id='A' type="file" accept=".xlsx, .xls*"> + <input id='mix-ref' type="file" accept="image/jpeg"> + <input id='mix' type="file" accept="image/jpeg,.jpg"> + <input id='hidden' hidden type='file'> + <input id='untrusted-click' type='file'> + <input id='prevent-default' type='file'> + <input id='prevent-default-false' type='file'> + <input id='right-click' type='file'> + <input id='middle-click' type='file'> + <input id='left-click' type='file'> + <label id='label-1'>foo<input type='file'></label> + <label id='label-2' for='labeled-2'>foo</label><input id='labeled-2' type='file'></label> + <label id='label-3'>foo<input type='file'></label> + <label id='label-4' for='labeled-4'>foo</label><input id='labeled-4' type='file'></label> + <input id='by-button' type='file'> + <button id='button-click' onclick="document.getElementById('by-button').click();">foo</button> + <button id='button-down' onclick="document.getElementById('by-button').click();">foo</button> + <button id='button-up' onclick="document.getElementById('by-button').click();">foo</button> + <div id='div-click' onclick="document.getElementById('by-button').click();" tabindex='1'>foo</div> + <div id='div-click-on-demand' onclick="var i=document.createElement('input'); i.type='file'; i.click();" tabindex='1'>foo</div> + <div id='div-keydown' onkeydown="document.getElementById('by-button').click();" tabindex='1'>foo</div> + <a id='link-click' href="javascript:document.getElementById('by-button').click();" tabindex='1'>foo</a> + <input id='show-picker' type='file'> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * This test checks various scenarios and make sure that a file picker is being + * shown in all of them (minus a few exceptions). + * |testData| defines the tests to do and |launchNextTest| can be used to have + * specific behaviour for some tests. Everything else should just work. + */ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +// The following lists are from toolkit/content/filepicker.properties which is used by filePicker +var imageExtensionList = "*.jpe; *.jpg; *.jpeg; *.gif; *.png; *.bmp; *.ico; *.svg; *.svgz; *.tif; *.tiff; *.ai; *.drw; *.pct; *.psp; *.xcf; *.psd; *.raw; *.webp; *.heic" + +var audioExtensionList = "*.aac; *.aif; *.flac; *.iff; *.m4a; *.m4b; *.mid; *.midi; *.mp3; *.mpa; *.mpc; *.oga; *.ogg; *.opus; *.ra; *.ram; *.snd; *.wav; *.wma" + +var videoExtensionList = "*.avi; *.divx; *.flv; *.m4v; *.mkv; *.mov; *.mp4; *.mpeg; *.mpg; *.ogm; *.ogv; *.ogx; *.rm; *.rmvb; *.smil; *.webm; *.wmv; *.xvid" + +// [ element name | number of filters | extension list or filter mask | filter index ] +var testData = [["a", 1, MockFilePicker.filterImages, 1], + ["b", 1, MockFilePicker.filterAudio, 1], + ["c", 1, MockFilePicker.filterVideo, 1], + ["d", 3, imageExtensionList + "; " + audioExtensionList, 1], + ["e", 3, imageExtensionList + "; " + videoExtensionList, 1], + ["f", 3, audioExtensionList + "; " + videoExtensionList, 1], + ["g", 4, imageExtensionList + "; " + audioExtensionList + "; " + videoExtensionList, 1], + ["h", 1, MockFilePicker.filterImages, 1], + ["i", 1, MockFilePicker.filterVideo, 1], + ["j", 1, MockFilePicker.filterAudio, 1], + ["k", 3, "*.gif; *.png", 1], + ["l", 4, imageExtensionList + "; " + "*.gif; *.png", 1], + ["m", 1, "*.gif", 1], + ["n", 0, undefined, 0], + ["o", 1, "*.test", 1], + ["p", 3, "*.gif; *.csv", 1], + ["q", 1, "*.gif", 1], + ["r", 3, "*.prefix; *.prefixPlusSomething", 1], + ["s", 3, "*.xls; *.xlsx", 1], + ["t", 4, "*.mp3; *.wav; *.flac", 1], + ["u", 3, "*.xls; *.xlsx", 1], + ["v", 3, "*.xlsx; *.xls", 1], + ["w", 0, undefined, 0], + ["x", 3, "*.xls; *.xlsx", 1], + ["y", 3, "*.xlsx; *.xls", 1], + ["z", 0, undefined, 0], + ["A", 1, "*.xlsx", 1], + // Note: mix and mix-ref tests extension lists are checked differently: see SimpleTest.executeSoon below + ["mix-ref", undefined, undefined, undefined], + ["mix", 1, undefined, 1], + ["hidden", 0, undefined, 0], + ["untrusted-click", 0, undefined, 0], + ["prevent-default", 0, undefined, 0, true], + ["prevent-default-false", 0, undefined, 0, true], + ["right-click", 0, undefined, 0, true], + ["middle-click", 0, undefined, 0, true], + ["left-click", 0, undefined, 0], + ["label-1", 0, undefined, 0], + ["label-2", 0, undefined, 0], + ["label-3", 0, undefined, 0], + ["label-4", 0, undefined, 0], + ["button-click", 0, undefined, 0], + ["button-down", 0, undefined, 0], + ["button-up", 0, undefined, 0], + ["div-click", 0, undefined, 0], + ["div-click-on-demand", 0, undefined, 0], + ["div-keydown", 0, undefined, 0], + ["link-click", 0, undefined, 0], + ["show-picker", 0, undefined, 0], + ]; + +var currentTest = 0; +var filterAllAdded; +var filters; +var filterIndex; +var mixRefExtensionList; + +// Make sure picker works with popup blocker enabled and no allowed events +SpecialPowers.pushPrefEnv({'set': [["dom.popup_allowed_events", ""]]}, runTests); + +function launchNextTest() { + MockFilePicker.shown = false; + filterAllAdded = false; + filters = []; + filterIndex = 0; + + // Focusing the element will scroll them into view so making sure the clicks + // will work. + document.getElementById(testData[currentTest][0]).focus(); + + if (testData[currentTest][0] == "untrusted-click") { + var e = document.createEvent('MouseEvents'); + e.initEvent('click', true, false); + document.getElementById(testData[currentTest][0]).dispatchEvent(e); + // All tests that should *NOT* show a file picker. + } else if (testData[currentTest][0] == "prevent-default" || + testData[currentTest][0] == "prevent-default-false" || + testData[currentTest][0] == "right-click" || + testData[currentTest][0] == "middle-click") { + if (testData[currentTest][0] == "right-click" || + testData[currentTest][0] == "middle-click") { + var b = testData[currentTest][0] == "middle-click" ? 1 : 2; + synthesizeMouseAtCenter(document.getElementById(testData[currentTest][0]), + { button: b }); + } else { + if (testData[currentTest][0] == "prevent-default-false") { + document.getElementById(testData[currentTest][0]).onclick = function() { + return false; + }; + } else { + document.getElementById(testData[currentTest][0]).onclick = function(event) { + event.preventDefault(); + }; + } + document.getElementById(testData[currentTest][0]).click(); + } + + // Wait a bit and assume we can continue. If the file picker shows later, + // behaviour is uncertain but that would be a random green, no big deal... + setTimeout(function() { + ok(true, "we should be there without a file picker being opened"); + ++currentTest; + launchNextTest(); + }, 500); + } else if (testData[currentTest][0] == 'label-3' || + testData[currentTest][0] == 'label-4') { + synthesizeMouse(document.getElementById(testData[currentTest][0]), 5, 5, {}); + } else if (testData[currentTest][0] == 'button-click' || + testData[currentTest][0] == 'button-down' || + testData[currentTest][0] == 'button-up' || + testData[currentTest][0] == 'div-click' || + testData[currentTest][0] == 'div-click-on-demand' || + testData[currentTest][0] == 'link-click') { + synthesizeMouseAtCenter(document.getElementById(testData[currentTest][0]), {}); + } else if (testData[currentTest][0] == 'div-keydown') { + sendString("a"); + } else if (testData[currentTest][0] == 'show-picker') { + SpecialPowers.wrap(document).notifyUserGestureActivation(); + document.getElementById(testData[currentTest][0]).showPicker(); + } else { + document.getElementById(testData[currentTest][0]).click(); + } +} + +function runTests() { + MockFilePicker.appendFilterCallback = function(filepicker, title, val) { + filters.push(val); + }; + MockFilePicker.appendFiltersCallback = function(filepicker, val) { + if (val === MockFilePicker.filterAll) { + filterAllAdded = true; + } else { + filters.push(val); + } + }; + MockFilePicker.showCallback = function(filepicker) { + if (testData[currentTest][4]) { + ok(false, "we shouldn't have a file picker showing!"); + return; + } + + filterIndex = filepicker.filterIndex; + testName = testData[currentTest][0]; + SimpleTest.executeSoon(function () { + ok(MockFilePicker.shown, + "File picker show method should have been called (" + testName + ")"); + ok(filterAllAdded, + "filterAll is missing (" + testName + ")"); + if (testName == "mix-ref") { + // Used only for reference for next test: nothing to be tested here + mixRefExtensionList = filters[0]; + if (mixRefExtensionList == undefined) { + mixRefExtensionList = ""; + } + } else { + if (testName == "mix") { + // Mixing mime type and file extension filters ("image/jpeg" and + // ".jpg" here) shouldn't restrict the list but only extend it, if file + // extension filter isn't a duplicate + ok(filters[0].includes(mixRefExtensionList), + "Mixing mime types and file extension filters shouldn't restrict extension list: " + + mixRefExtensionList + " | " + filters[0]); + ok(filters[0].includes("*.jpg"), + "Filter should contain '.jpg' extension. Filter was:" + filters[0]); + } else { + is(filters[0], testData[currentTest][2], + "Correct filters should have been added (" + testName + ")"); + is(filters.length, testData[currentTest][1], + "appendFilters not called as often as expected (" + testName + ")"); + } + is(filterIndex, testData[currentTest][3], + "File picker should show the correct filter index (" + testName + ")"); + } + + if (++currentTest == testData.length) { + MockFilePicker.cleanup(); + SimpleTest.finish(); + } else { + launchNextTest(); + } + }); + }; + + launchNextTest(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_hasBeenTypePassword.html b/dom/html/test/forms/test_input_hasBeenTypePassword.html new file mode 100644 index 0000000000..ac577ae3a9 --- /dev/null +++ b/dom/html/test/forms/test_input_hasBeenTypePassword.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1330228 +--> +<head> + <title>Test input.hasBeenTypePassword</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1330228">Mozilla Bug 1330228</a> +<script type="application/javascript"> + +/** Test input.hasBeenTypePassword **/ + +var gInputTestData = [ +/* type result */ + ["tel", false], + ["text", false], + ["button", false], + ["checkbox", false], + ["file", false], + ["hidden", false], + ["reset", false], + ["image", false], + ["radio", false], + ["submit", false], + ["search", false], + ["email", false], + ["url", false], + ["number", false], + ["range", false], + ["date", false], + ["time", false], + ["color", false], + ["month", false], + ["week", false], + ["datetime-local", false], + ["", false], + // "password" must be last since we re-use the same <input>. + ["password", true], +]; + +function checkHasBeenTypePasswordValue(aInput, aResult) { + is(aInput.hasBeenTypePassword, aResult, + "hasBeenTypePassword should return " + aResult + " for " + + aInput.getAttribute("type")); +} + +// Use SpecialPowers since the API is ChromeOnly. +var input = SpecialPowers.wrap(document.createElement("input")); +// Check if the method returns the correct value on the first pass. +for (let [type, expected] of gInputTestData) { + input.type = type; + checkHasBeenTypePasswordValue(input, expected); +} + +// Now do a second pass but expect `hasBeenTypePassword` to always be true now +// that the type was 'password'. +for (let [type] of gInputTestData) { + input.type = type; + checkHasBeenTypePasswordValue(input, true); +} +</script> +</body> +</html> diff --git a/dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html b/dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html new file mode 100644 index 0000000000..70a0f8427e --- /dev/null +++ b/dom/html/test/forms/test_input_hasBeenTypePassword_navigation.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1330228 +--> +<head> + <title>Test hasBeenTypePassword is used with bfcache</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1330228">Mozilla Bug 1330228</a> +<p id="display"> + <iframe id="testframe" src="file_login_fields.html"></iframe> +</p> +<pre id="test"> +<script type="application/javascript"> + +/** Test hasBeenTypePassword is used with bfcache **/ +SimpleTest.waitForExplicitFinish(); + +function afterLoad() { + var iframeDoc = $("testframe").contentDocument; + + /* change all the form controls */ + iframeDoc.getElementById("un").value = "username"; + iframeDoc.getElementById("pw1").value = "password1"; + + // Convert pw2 to a password field temporarily to test hasBeenTypePassword. + // We don't want the initial or final value to be type=password or we may + // not test the right scenario. + iframeDoc.getElementById("pw2").type = "password"; + iframeDoc.getElementById("pw2").value = "password2"; + iframeDoc.getElementById("pw2").type = ""; + + /* navigate the page */ + $("testframe").setAttribute("onload", "afterNavigation()"); + // Use a click on an <a> so that the current page is included in session history. + iframeDoc.getElementById("navigate").click(); +} + +addLoadEvent(afterLoad); + +function afterNavigation() { + info("Navigated to a new document"); + var iframeDoc = $("testframe").contentDocument; + $("testframe").setAttribute("onload", "afterBack()"); + // Calling `history.back()` on the contentWindow from here doesn't use bfcache + // so call it from within the contentDocument. + iframeDoc.getElementById("back").click(); +} + +function afterBack() { + info("Should be back showing the first document from bfcache"); + var iframeDoc = $("testframe").contentDocument; + + is(iframeDoc.getElementById("un").value, "username", + "username field value remembered"); + is(iframeDoc.getElementById("pw1").value, "", + "type=password field value not remembered"); + is(iframeDoc.getElementById("pw2").value, "", + "former type=password field value not remembered"); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_list_attribute.html b/dom/html/test/forms/test_input_list_attribute.html new file mode 100644 index 0000000000..62a07dd91a --- /dev/null +++ b/dom/html/test/forms/test_input_list_attribute.html @@ -0,0 +1,253 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=556007 +--> +<head> + <title>Test for Bug 556007</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=556007">Mozilla Bug 556007</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 556007 **/ + +function test0() { + var input = document.createElement("input"); + ok("list" in input, "list should be an input element IDL attribute"); +} + +// Default .list returns null. +function test1(aContent, aInput) { + return null; +} + +// Regular test case. +function test2(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aContent.appendChild(aInput); + aContent.appendChild(datalist); + aInput.setAttribute('list', 'd'); + + return datalist; +} + +// If none of the element is in doc. +function test3(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aInput.setAttribute('list', 'd'); + + return null; +} + +// If one of the element isn't in doc. +function test4(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aContent.appendChild(aInput); + aInput.setAttribute('list', 'd'); + + return null; +} + +// If one of the element isn't in doc. +function test5(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aContent.appendChild(datalist); + aInput.setAttribute('list', 'd'); + + return null; +} + +// If datalist is added before input. +function test6(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aContent.appendChild(datalist); + aContent.appendChild(aInput); + aInput.setAttribute('list', 'd'); + + return datalist; +} + +// If setAttribute is set before datalist and input are in doc. +function test7(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aInput.setAttribute('list', 'd'); + + aContent.appendChild(datalist); + aContent.appendChild(aInput); + + return datalist; +} + +// If setAttribute is set before datalist is in doc. +function test8(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aContent.appendChild(aInput); + aInput.setAttribute('list', 'd'); + + aContent.appendChild(datalist); + + return datalist; +} + +// If setAttribute is set before datalist is created. +function test9(aContent, aInput) { + aContent.appendChild(aInput); + aInput.setAttribute('list', 'd'); + + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + aContent.appendChild(datalist); + + return datalist; +} + +// If another datalist is added _after_ the first one, with the same id. +function test10(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + var datalist2 = document.createElement("datalist"); + datalist2.id = 'd'; + + aInput.setAttribute('list', 'd'); + aContent.appendChild(aInput); + aContent.appendChild(datalist); + aContent.appendChild(datalist2); + + return datalist; +} + +// If another datalist is added _before_ the first one with the same id. +function test11(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + var datalist2 = document.createElement("datalist"); + datalist2.id = 'd'; + + aInput.setAttribute('list', 'd'); + aContent.appendChild(aInput); + aContent.appendChild(datalist); + aContent.insertBefore(datalist2, datalist); + + return datalist2; +} + +// If datalist changes it's id. +function test12(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aInput.setAttribute('list', 'd'); + aContent.appendChild(aInput); + aContent.appendChild(datalist); + + datalist.id = 'foo'; + + return null; +} + +// If datalist is removed. +function test13(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aInput.setAttribute('list', 'd'); + aContent.appendChild(aInput); + aContent.appendChild(datalist); + aContent.removeChild(datalist); + + return null; +} + +// If id contain spaces. +function test14(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = 'a b c d'; + + aInput.setAttribute('list', 'a b c d'); + aContent.appendChild(aInput); + aContent.appendChild(datalist); + + return datalist; +} + +// If id is the empty string. +function test15(aContent, aInput) { + var datalist = document.createElement("datalist"); + datalist.id = ''; + + aInput.setAttribute('list', ''); + aContent.appendChild(aInput); + aContent.appendChild(datalist); + + return null; +} + +// If the id doesn't point to a datalist. +function test16(aContent, aInput) { + var input = document.createElement("input"); + input.id = 'd'; + + aInput.setAttribute('list', 'd'); + aContent.appendChild(aInput); + aContent.appendChild(input); + + return null; +} + +// If the first element with the id isn't a datalist. +function test17(aContent, aInput) { + var input = document.createElement("input"); + input.id = 'd'; + var datalist = document.createElement("datalist"); + datalist.id = 'd'; + + aInput.setAttribute('list', 'd'); + aContent.appendChild(aInput); + aContent.appendChild(input); + aContent.appendChild(datalist); + + return null; +} + +var tests = [ test1, test2, test3, test4, test5, test6, test7, test8, test9, + test10, test11, test12, test13, test14, test15, test16, test17 ]; + +test0(); + +for (var test of tests) { + var content = document.getElementById('content'); + + // Clean-up. + content.textContent = ''; + + var input = document.createElement("input"); + var res = test(content, input); + + is(input.list, res, "input.list should be " + res); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_number_data.js b/dom/html/test/forms/test_input_number_data.js new file mode 100644 index 0000000000..9ec53f136f --- /dev/null +++ b/dom/html/test/forms/test_input_number_data.js @@ -0,0 +1,54 @@ +var tests = [ + { + desc: "British English", + langTag: "en-GB", + inputWithGrouping: "123,456.78", + inputWithoutGrouping: "123456.78", + value: 123456.78, + }, + { + desc: "Farsi", + langTag: "fa", + inputWithGrouping: "۱۲۳٬۴۵۶٫۷۸", + inputWithoutGrouping: "۱۲۳۴۵۶٫۷۸", + value: 123456.78, + }, + { + desc: "French", + langTag: "fr-FR", + inputWithGrouping: "123 456,78", + inputWithoutGrouping: "123456,78", + value: 123456.78, + }, + { + desc: "German", + langTag: "de", + inputWithGrouping: "123.456,78", + inputWithoutGrouping: "123456,78", + value: 123456.78, + }, + // Bug 1509057 disables grouping separators for now, so this test isn't + // currently relevant. + // Extra german test to check that a locale that uses '.' as its grouping + // separator doesn't result in it being invalid (due to step mismatch) due + // to the de-localization code mishandling numbers that look like other + // numbers formatted for English speakers (i.e. treating this as 123.456 + // instead of 123456): + //{ desc: "German (test 2)", + // langTag: "de", inputWithGrouping: "123.456", + // inputWithoutGrouping: "123456", value: 123456 + //}, + { + desc: "Hebrew", + langTag: "he", + inputWithGrouping: "123,456.78", + inputWithoutGrouping: "123456.78", + value: 123456.78, + }, +]; + +var invalidTests = [ + // Right now this will pass in a 'de' build, but not in the 'en' build that + // are used for testing. See bug 1216831. + // { desc: "Invalid German", langTag: "de", input: "12.34" } +]; diff --git a/dom/html/test/forms/test_input_number_focus.html b/dom/html/test/forms/test_input_number_focus.html new file mode 100644 index 0000000000..4126ecc496 --- /dev/null +++ b/dom/html/test/forms/test_input_number_focus.html @@ -0,0 +1,109 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1268556 +--> +<head> + <title>Test focus behaviour for <input type='number'></title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + #input_test_style_display { + display: none; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1268556">Mozilla Bug 1268556</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1057858">Mozilla Bug 1057858</a> +<p id="display"></p> +<div id="content"> + <input id="input_test_redirect" type="number"> + <input id="input_test_style_display" type="number" > +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 1268556. + * This test checks that when focusing on an input type=number, the focus is + * redirected to the anonymous text control, but the document.activeElement + * still returns the <input type=number>. + * + * Tests for bug 1057858. + * Checks that adding an element and immediately focusing it triggers exactly + * one "focus" event and no "blur" events. The same for switching + * `style.display` from `none` to `block`. + **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test_focus_redirects_to_text_control_but_not_for_activeElement(); + test_add_element_and_focus_check_one_focus_event(); + test_style_display_none_change_to_block_check_one_focus_event(); + SimpleTest.finish(); +}); + +function test_focus_redirects_to_text_control_but_not_for_activeElement() { + document.activeElement.blur(); + var number = document.getElementById("input_test_redirect"); + number.focus(); + + // The active element returns the input type=number. + var activeElement = document.activeElement; + is (activeElement, number, "activeElement should be the number element"); + is (activeElement.localName, "input", "activeElement should be an input element"); + is (activeElement.getAttribute("type"), "number", "activeElement should of type number"); + + // Use FocusManager to check that the actual focus is on the anonymous + // text control. + var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"] + .getService(SpecialPowers.Ci.nsIFocusManager); + var focusedElement = fm.focusedElement; + is (focusedElement.localName, "input", "focusedElement should be an input element"); + is (focusedElement.getAttribute("type"), "number", "focusedElement should of type number"); +} + +var blurEventCounter = 0; +var focusEventCounter = 0; + +function append_input_element_with_event_listeners_to_dom() { + var inputElement = document.createElement("input"); + inputElement.type = "number"; + inputElement.addEventListener("blur", function() { ++blurEventCounter; }); + inputElement.addEventListener("focus", function() { ++focusEventCounter; }); + var content = document.getElementById("content"); + content.appendChild(inputElement); + return inputElement; +} + +function test_add_element_and_focus_check_one_focus_event() { + document.activeElement.blur(); + var inputElement = append_input_element_with_event_listeners_to_dom(); + + blurEventCounter = 0; + focusEventCounter = 0; + inputElement.focus(); + + is(blurEventCounter, 0, "After focus: no blur events observed."); + is(focusEventCounter, 1, "After focus: exactly one focus event observed."); +} + +function test_style_display_none_change_to_block_check_one_focus_event() { + document.activeElement.blur(); + var inputElement = document.getElementById("input_test_style_display"); + inputElement.addEventListener("blur", function() { ++blurEventCounter; }); + inputElement.addEventListener("focus", function() { ++focusEventCounter; }); + + blurEventCounter = 0; + focusEventCounter = 0; + inputElement.style.display = "block"; + inputElement.focus(); + + is(blurEventCounter, 0, "After focus: no blur events observed."); + is(focusEventCounter, 1, "After focus: exactly one focus event observed."); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_number_key_events.html b/dom/html/test/forms/test_input_number_key_events.html new file mode 100644 index 0000000000..eb537f5617 --- /dev/null +++ b/dom/html/test/forms/test_input_number_key_events.html @@ -0,0 +1,238 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=935506 +--> +<head> + <title>Test key events for number control</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935506">Mozilla Bug 935506</a> +<p id="display"></p> +<div id="content"> + <input id="input" type="number"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 935506 + * This test checks how the value of <input type=number> changes in response to + * key events while it is in various states. + **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); +const defaultMinimum = "NaN"; +const defaultMaximum = "NaN"; +const defaultStep = 1; + +// Helpers: +// For the sake of simplicity, we do not currently support fractional value, +// step, etc. + +function getMinimum(element) { + return Number(element.min || defaultMinimum); +} + +function getMaximum(element) { + return Number(element.max || defaultMaximum); +} + +function getDefaultValue(element) { + return 0; +} + +function getValue(element) { + return Number(element.value || getDefaultValue(element)); +} + +function getStep(element) { + if (element.step == "any") { + return "any"; + } + var step = Number(element.step || defaultStep); + return step <= 0 ? defaultStep : step; +} + +function getStepBase(element) { + return Number(element.getAttribute("min") || "NaN") || + Number(element.getAttribute("value") || "NaN") || 0; +} + +function hasStepMismatch(element) { + var value = element.value; + if (value == "") { + value = 0; + } + var step = getStep(element); + if (step == "any") { + return false; + } + return ((value - getStepBase(element)) % step) != 0; +} + +function floorModulo(x, y) { + return (x - y * Math.floor(x / y)); +} + +function expectedValueAfterStepUpOrDown(stepFactor, element) { + var value = getValue(element); + if (isNaN(value)) { + value = 0; + } + var step = getStep(element); + if (step == "any") { + step = 1; + } + + var minimum = getMinimum(element); + var maximum = getMaximum(element); + if (!isNaN(maximum)) { + // "max - (max - stepBase) % step" is the nearest valid value to max. + maximum = maximum - floorModulo(maximum - getStepBase(element), step); + } + + // Cases where we are clearly going in the wrong way. + // We don't use ValidityState because we can be higher than the maximal + // allowed value and still not suffer from range overflow in the case of + // of the value specified in @max isn't in the step. + if ((value <= minimum && stepFactor < 0) || + (value >= maximum && stepFactor > 0)) { + return value; + } + + if (hasStepMismatch(element) && + value != minimum && value != maximum) { + if (stepFactor > 0) { + value -= floorModulo(value - getStepBase(element), step); + } else if (stepFactor < 0) { + value -= floorModulo(value - getStepBase(element), step); + value += step; + } + } + + value += step * stepFactor; + + // When stepUp() is called and the value is below minimum, we should clamp on + // minimum unless stepUp() moves us higher than minimum. + if (element.validity.rangeUnderflow && stepFactor > 0 && + value <= minimum) { + value = minimum; + } else if (element.validity.rangeOverflow && stepFactor < 0 && + value >= maximum) { + value = maximum; + } else if (stepFactor < 0 && !isNaN(minimum)) { + value = Math.max(value, minimum); + } else if (stepFactor > 0 && !isNaN(maximum)) { + value = Math.min(value, maximum); + } + + return value; +} + +function expectedValAfterKeyEvent(key, element) { + return expectedValueAfterStepUpOrDown(key == "KEY_ArrowUp" ? 1 : -1, element); +} + +function test() { + var elem = document.getElementById("input"); + elem.focus(); + + elem.min = -5; + elem.max = 5; + elem.step = 2; + var defaultValue = 0; + var oldVal, expectedVal; + + for (key of ["KEY_ArrowUp", "KEY_ArrowDown"]) { + // Start at middle: + oldVal = elem.value = -1; + expectedVal = expectedValAfterKeyEvent(key, elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for number control with value set between min/max (" + oldVal + ")"); + + // Same again: + expectedVal = expectedValAfterKeyEvent(key, elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control"); + + // Start at maximum: + oldVal = elem.value = elem.max; + expectedVal = expectedValAfterKeyEvent(key, elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the maximum (" + oldVal + ")"); + + // Same again: + expectedVal = expectedValAfterKeyEvent(key, elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control"); + + // Start at minimum: + oldVal = elem.value = elem.min; + expectedVal = expectedValAfterKeyEvent(key, elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the minimum (" + oldVal + ")"); + + // Same again: + expectedVal = expectedValAfterKeyEvent(key, elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control"); + + // Test preventDefault(): + elem.addEventListener("keydown", evt => evt.preventDefault(), {once: true}); + oldVal = elem.value = 0; + expectedVal = 0; + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for number control where scripted preventDefault() should prevent the value changing"); + + // Test step="any" behavior: + var oldStep = elem.step; + elem.step = "any"; + oldVal = elem.value = 0; + expectedVal = expectedValAfterKeyEvent(key, elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the midpoint and step='any' (" + oldVal + ")"); + elem.step = oldStep; // restore + + // Test that invalid input blocks UI initiated stepping: + oldVal = elem.value = ""; + elem.select(); + sendString("abc"); + synthesizeKey(key); + is(elem.value, "", "Test " + key + " does nothing when the input is invalid"); + + // Test that no value does not block UI initiated stepping: + oldVal = elem.value = ""; + elem.setAttribute("required", "required"); + elem.select(); + expectedVal = expectedValAfterKeyEvent(key, elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for number control with value set to the empty string and with the 'required' attribute set"); + + // Same again: + expectedVal = expectedValAfterKeyEvent(key, elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test repeat of " + key + " for number control"); + + // Reset 'required' attribute: + elem.removeAttribute("required"); + } + + // Test that key events are correctly dispatched + elem.max = ""; + elem.value = ""; + sendString("7837281"); + is(elem.value, "7837281", "Test keypress event dispatch for number control"); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_number_l10n.html b/dom/html/test/forms/test_input_number_l10n.html new file mode 100644 index 0000000000..c8202028ed --- /dev/null +++ b/dom/html/test/forms/test_input_number_l10n.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=844744 +--> +<head> + <title>Test localization of number control input</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="test_input_number_data.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=844744">Mozilla Bug 844744</a> +<p id="display"></p> +<div id="content"> + <input id="input" type="number" step="any"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 844744 + * This test checks that localized input that is typed into <input type=number> + * is correctly handled. + **/ +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + startTests(); + SimpleTest.finish(); +}); + +var elem; + +function runTest(test) { + elem.lang = test.langTag; + elem.value = 0; + elem.focus(); + elem.select(); + sendString(test.inputWithGrouping); + is(elem.value, "", "Test " + test.desc + " ('" + test.langTag + + "') localization with grouping separator"); + elem.value = 0; + elem.select(); + sendString(test.inputWithoutGrouping); + is(elem.valueAsNumber, test.value, "Test " + test.desc + " ('" + test.langTag + + "') localization without grouping separator"); + is(elem.value, String(test.value), "Test " + test.desc + " ('" + test.langTag + + "') localization without grouping separator as string"); +} + +function runInvalidInputTest(test) { + elem.lang = test.langTag; + elem.value = 0; + elem.focus(); + elem.select(); + sendString(test.input); + is(elem.value, "", "Test " + test.desc + " ('" + test.langTag + + "') with invalid input: " + test.input); +} + +function startTests() { + elem = document.getElementById("input"); + for (var test of tests) { + runTest(test, elem); + } + for (var test of invalidTests) { + runInvalidInputTest(test, elem); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_number_mouse_events.html b/dom/html/test/forms/test_input_number_mouse_events.html new file mode 100644 index 0000000000..a3e5732beb --- /dev/null +++ b/dom/html/test/forms/test_input_number_mouse_events.html @@ -0,0 +1,272 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=935501 +--> +<head> + <title>Test mouse events for number</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <style> + input { + margin: 0; + border: 0; + padding: 0; + width: 200px; + box-sizing: border-box; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=935501">Mozilla Bug 935501</a> +<p id="display"></p> +<div id="content"> + <input id="input" type="number"> +</div> +<pre id="test"> +<script> + +const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +/** + * Test for Bug 935501 + * This test checks how the value of <input type=number> changes in response to + * various mouse events. + **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +SimpleTest.waitForFocus(function() { + test(); +}); + +const kIsWin = AppConstants.platform == "win"; +const kIsLinux = AppConstants.platform == "linux"; + +var input = document.getElementById("input"); +var inputRect = input.getBoundingClientRect(); + +// Points over the input's spin-up and spin-down buttons (as offsets from the +// top-left of the input's bounding client rect): +const SPIN_UP_X = inputRect.width - 3; +const SPIN_UP_Y = 3; +const SPIN_DOWN_X = inputRect.width - 3; +const SPIN_DOWN_Y = inputRect.height - 3; + +function checkInputEvent(aEvent, aDescription) { + // Probably, key operation should fire "input" event with InputEvent interface. + // See https://github.com/w3c/input-events/issues/88 + ok(aEvent instanceof InputEvent, `"input" event should be dispatched with InputEvent interface on input element whose type is number ${aDescription}`); + is(aEvent.cancelable, false, `"input" event should be never cancelable on input element whose type is number ${aDescription}`); + is(aEvent.bubbles, true, `"input" event should always bubble on input element whose type is number ${aDescription}`); + info(`Data: ${aEvent.data}, value: ${aEvent.target.value}`); +} + +function test() { + input.value = 0; + + // Test click on spin-up button: + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" }); + is(input.value, "1", "Test step-up on mousedown on spin-up button"); + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" }); + is(input.value, "1", "Test mouseup on spin-up button"); + + // Test click on spin-down button: + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" }); + is(input.value, "0", "Test step-down on mousedown on spin-down button"); + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" }); + is(input.value, "0", "Test mouseup on spin-down button"); + + // Test clicks with modifiers that mean we should ignore the click: + var modifiersIgnore = ["altGrKey", "fnKey"]; + if (kIsWin || kIsLinux) { + modifiersIgnore.push("metaKey"); + } + for (var modifier of modifiersIgnore) { + input.value = 0; + var eventParams = { type: "mousedown" }; + eventParams[modifier] = true; + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, eventParams); + is(input.value, "0", "We should ignore mousedown on spin-up button with modifier " + modifier); + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" }); + } + + // Test clicks with modifiers that mean we should allow the click: + var modifiersAllow = ["shiftKey", "ctrlKey", "altKey"]; + if (!modifiersIgnore.includes("metaKey")) { + modifiersAllow.push("metaKey"); + } + for (var modifier of modifiersAllow) { + input.value = 0; + var eventParams = { type: "mousedown" }; + eventParams[modifier] = true; + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, eventParams); + is(input.value, "1", "We should allow mousedown on spin-up button with modifier " + modifier); + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" }); + } + + // Test step="any" behavior: + input.value = 0; + var oldStep = input.step; + input.step = "any"; + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" }); + is(input.value, "1", "Test step-up on mousedown on spin-up button with step='any'"); + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" }); + is(input.value, "1", "Test mouseup on spin-up button with step='any'"); + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" }); + is(input.value, "0", "Test step-down on mousedown on spin-down button with step='any'"); + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" }); + is(input.value, "0", "Test mouseup on spin-down button with step='any'"); + input.step = oldStep; // restore + + // Test that preventDefault() works: + function preventDefault(e) { + e.preventDefault(); + } + input.value = 1; + input.addEventListener("mousedown", preventDefault); + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, {}); + is(input.value, "1", "Test that preventDefault() works for click on spin-up button"); + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, {}); + is(input.value, "1", "Test that preventDefault() works for click on spin-down button"); + input.removeEventListener("mousedown", preventDefault); + + // Test for bug 1707070. + input.style.paddingRight = "30px"; + input.getBoundingClientRect(); // flush layout + + input.value = 0; + synthesizeMouse(input, SPIN_UP_X - 30, SPIN_UP_Y, { type: "mousedown" }); + is(input.value, "1", "Spinner down works on with padding (mousedown)"); + synthesizeMouse(input, SPIN_UP_X - 30, SPIN_UP_Y, { type: "mouseup" }); + is(input.value, "1", "Spinner down works with padding (mouseup)"); + + synthesizeMouse(input, SPIN_DOWN_X - 30, SPIN_DOWN_Y, { type: "mousedown" }); + is(input.value, "0", "Spinner works with padding (mousedown)"); + synthesizeMouse(input, SPIN_DOWN_X - 30, SPIN_DOWN_Y, { type: "mouseup" }); + is(input.value, "0", "Spinner works with padding (mouseup)"); + + input.style.paddingRight = ""; + input.getBoundingClientRect(); // flush layout + + // Run the spin tests: + runNextSpinTest(); +} + +function runNextSpinTest() { + var nextTest = spinTests.shift(); + if (!nextTest) { + SimpleTest.finish(); + return; + } + nextTest(); +} + +function waitForTick() { + return new Promise(SimpleTest.executeSoon); +} + +const SETTIMEOUT_DELAY = 500; + +var spinTests = [ + // Test spining when the mouse button is kept depressed on the spin-up + // button, then moved over the spin-down button: + function() { + var inputEventCount = 0; + input.value = 0; + input.addEventListener("input", async function(evt) { + ++inputEventCount; + checkInputEvent(evt, "#1"); + if (inputEventCount == 3) { + is(input.value, "3", "Testing spin-up button"); + await waitForTick(); + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousemove" }); + } else if (inputEventCount == 6) { + is(input.value, "0", "Testing spin direction is reversed after mouse moves from spin-up button to spin-down button"); + input.removeEventListener("input", arguments.callee); + await waitForTick(); + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" }); + runNextSpinTest(); + } + }); + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" }); + }, + + // Test spining when the mouse button is kept depressed on the spin-down + // button, then moved over the spin-up button: + function() { + var inputEventCount = 0; + input.value = 0; + input.addEventListener("input", async function(evt) { + ++inputEventCount; + checkInputEvent(evt, "#2"); + if (inputEventCount == 3) { + is(input.value, "-3", "Testing spin-down button"); + await waitForTick(); + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousemove" }); + } else if (inputEventCount == 6) { + is(input.value, "0", "Testing spin direction is reversed after mouse moves from spin-down button to spin-up button"); + input.removeEventListener("input", arguments.callee); + await waitForTick(); + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mouseup" }); + runNextSpinTest(); + } + }); + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" }); + }, + + // Test that the spin is stopped when the mouse button is depressod on the + // spin-up button, then moved outside both buttons once the spin starts: + function() { + var inputEventCount = 0; + input.value = 0; + input.addEventListener("input", async function(evt) { + ++inputEventCount; + checkInputEvent(evt, "#3"); + if (inputEventCount == 3) { + await waitForTick(); + synthesizeMouse(input, -1, -1, { type: "mousemove" }); + var eventHandler = arguments.callee; + setTimeout(function() { + is(input.value, "3", "Testing moving the mouse outside the spin buttons stops the spin"); + is(inputEventCount, 3, "Testing moving the mouse outside the spin buttons stops the spin input events"); + input.removeEventListener("input", eventHandler); + synthesizeMouse(input, -1, -1, { type: "mouseup" }); + runNextSpinTest(); + }, SETTIMEOUT_DELAY); + } + }); + synthesizeMouse(input, SPIN_UP_X, SPIN_UP_Y, { type: "mousedown" }); + }, + + // Test that changing the input type in the middle of a spin cancels the spin: + function() { + var inputEventCount = 0; + input.value = 0; + input.addEventListener("input", function(evt) { + ++inputEventCount; + checkInputEvent(evt, "#4"); + if (inputEventCount == 3) { + input.type = "text" + var eventHandler = arguments.callee; + setTimeout(function() { + is(input.value, "-3", "Testing changing input type during a spin stops the spin"); + is(inputEventCount, 3, "Testing changing input type during a spin stops the spin input events"); + input.removeEventListener("input", eventHandler); + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mouseup" }); + input.type = "number"; // restore + runNextSpinTest(); + }, SETTIMEOUT_DELAY); + } + }); + synthesizeMouse(input, SPIN_DOWN_X, SPIN_DOWN_Y, { type: "mousedown" }); + } +]; +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_number_placeholder_shown.html b/dom/html/test/forms/test_input_number_placeholder_shown.html new file mode 100644 index 0000000000..c9c2a7f515 --- /dev/null +++ b/dom/html/test/forms/test_input_number_placeholder_shown.html @@ -0,0 +1,30 @@ +<!doctype html> +<title>Test for :placeholder-shown on input elements and invalid values.</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<style> +input { + border: 1px solid purple; +} +input:placeholder-shown { + border-color: blue; +} +</style> +<input type="number" placeholder="foo"> +<script> + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); + }); + + function test() { + let input = document.querySelector('input'); + input.focus(); + is(getComputedStyle(input).borderLeftColor, "rgb(0, 0, 255)", + ":placeholder-shown should apply") + sendString("x"); + isnot(getComputedStyle(input).borderLeftColor, "rgb(0, 0, 255)", + ":placeholder-shown should not apply, even though the value is invalid") + } +</script> diff --git a/dom/html/test/forms/test_input_number_rounding.html b/dom/html/test/forms/test_input_number_rounding.html new file mode 100644 index 0000000000..d162727557 --- /dev/null +++ b/dom/html/test/forms/test_input_number_rounding.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=783607 +--> +<head> + <title>Test rounding behaviour for <input type='number'></title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=783607">Mozilla Bug 783607</a> +<p id="display"></p> +<div id="content"> + <input id=number type=number value=0 step=0.01 max=1> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 783607. + * This test checks that when <input type=number> has fractional step values, + * the values that a content author will see in their script will not have + * ugly rounding errors. + **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +/** + * We can _NOT_ generate these values by looping and simply incrementing a + * variable by 0.01 and stringifying it, since we'll end up with strings like + * "0.060000000000000005" due to the inability of binary floating point numbers + * to accurately represent decimal values. + */ +var stepVals = [ + "0", "0.01", "0.02", "0.03", "0.04", "0.05", "0.06", "0.07", "0.08", "0.09", + "0.1", "0.11", "0.12", "0.13", "0.14", "0.15", "0.16", "0.17", "0.18", "0.19", + "0.2", "0.21", "0.22", "0.23", "0.24", "0.25", "0.26", "0.27", "0.28", "0.29", + "0.3", "0.31", "0.32", "0.33", "0.34", "0.35", "0.36", "0.37", "0.38", "0.39", + "0.4", "0.41", "0.42", "0.43", "0.44", "0.45", "0.46", "0.47", "0.48", "0.49", + "0.5", "0.51", "0.52", "0.53", "0.54", "0.55", "0.56", "0.57", "0.58", "0.59", + "0.6", "0.61", "0.62", "0.63", "0.64", "0.65", "0.66", "0.67", "0.68", "0.69", + "0.7", "0.71", "0.72", "0.73", "0.74", "0.75", "0.76", "0.77", "0.78", "0.79", + "0.8", "0.81", "0.82", "0.83", "0.84", "0.85", "0.86", "0.87", "0.88", "0.89", + "0.9", "0.91", "0.92", "0.93", "0.94", "0.95", "0.96", "0.97", "0.98", "0.99", + "1" +]; + +var pgUpDnVals = [ + "0", "0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "1" +]; + +function test() { + var elem = document.getElementById("number"); + + elem.focus(); + + /** + * TODO: + * When <input type='number'> widget will have a widge we should test PAGE_UP, + * PAGE_DOWN, UP and DOWN keys. For the moment, there is no widget so those + * keys do not have any effect. + * The tests using those keys as marked as todo_is() hoping that at least part + * of them will fail when the widget will be implemented. + */ + +/* No other implementations implement this, so we don't either, for now. + Seems like it might be nice though. + + for (var i = 1; i < pgUpDnVals.length; ++i) { + synthesizeKey("KEY_PageUp"); + todo_is(elem.value, pgUpDnVals[i], "Test KEY_PageUp"); + is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]); + } + + for (var i = pgUpDnVals.length - 2; i >= 0; --i) { + synthesizeKey("KEY_PageDown"); + // TODO: this condition is there because the todo_is() below would pass otherwise. + if (stepVals[i] == 0) { continue; } + todo_is(elem.value, pgUpDnVals[i], "Test KEY_PageDown"); + is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]); + } +*/ + + for (var i = 1; i < stepVals.length; ++i) { + synthesizeKey("KEY_ArrowUp"); + is(elem.value, stepVals[i], "Test KEY_ArrowUp"); + is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]); + } + + for (var i = stepVals.length - 2; i >= 0; --i) { + synthesizeKey("KEY_ArrowDown"); + // TODO: this condition is there because the todo_is() below would pass otherwise. + if (stepVals[i] == 0) { continue; } + is(elem.value, stepVals[i], "Test KEY_ArrowDown"); + is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]); + } + + for (var i = 1; i < stepVals.length; ++i) { + elem.stepUp(); + is(elem.value, stepVals[i], "Test stepUp()"); + is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]); + } + + for (var i = stepVals.length - 2; i >= 0; --i) { + elem.stepDown(); + is(elem.value, stepVals[i], "Test stepDown()"); + is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_number_validation.html b/dom/html/test/forms/test_input_number_validation.html new file mode 100644 index 0000000000..c19c1fde1c --- /dev/null +++ b/dom/html/test/forms/test_input_number_validation.html @@ -0,0 +1,139 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=827161 +--> +<head> + <title>Test validation of number control input</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script type="text/javascript" src="test_input_number_data.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=827161">Mozilla Bug 827161</a> +<p id="display"></p> +<div id="content"> + <input id="input" type="number" step="0.01" oninvalid="invalidEventHandler(event);"> + <input id="requiredinput" type="number" step="0.01" required + oninvalid="invalidEventHandler(event);"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 827161. + * This test checks that validation works correctly for <input type=number>. + **/ +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + startTests(); + SimpleTest.finish(); +}); + +var elem; + +function runTest(test) { + elem.lang = test.langTag; + + gInvalid = false; // reset + var desc = `${test.desc} (lang='${test.langTag}', id='${elem.id}')`; + elem.value = 0; + elem.focus(); + elem.select(); + sendString(test.inputWithGrouping); + checkIsInvalid(elem, `${desc} with grouping separator`); + sendChar("a"); + checkIsInvalid(elem, `${desc} with grouping separator`); + + gInvalid = false; // reset + elem.value = 0; + elem.select(); + sendString(test.inputWithoutGrouping); + checkIsValid(elem, `${desc} without grouping separator`); + sendChar("a"); + checkIsInvalid(elem, `${desc} without grouping separator`); +} + +function runInvalidInputTest(test) { + elem.lang = test.langTag; + + gInvalid = false; // reset + var desc = `${test.desc} (lang='${test.langTag}', id='${elem.id}')`; + elem.value = 0; + elem.focus(); + elem.select(); + sendString(test.input); + checkIsInvalid(elem, `${desc} with invalid input ${test.input}`); +} + +function startTests() { + elem = document.getElementById("input"); + for (var test of tests) { + runTest(test); + } + for (var test of invalidTests) { + runInvalidInputTest(test); + } + elem = document.getElementById("requiredinput"); + for (var test of tests) { + runTest(test); + } + + gInvalid = false; // reset + elem.value = ""; + checkIsInvalidEmptyValue(elem, "empty value"); +} + +var gInvalid = false; + +function invalidEventHandler(e) +{ + is(e.type, "invalid", "Invalid event type should be 'invalid'"); + gInvalid = true; +} + +function checkIsValid(element, infoStr) +{ + ok(!element.validity.badInput, + "Element should not suffer from bad input for " + infoStr); + ok(element.validity.valid, "Element should be valid for " + infoStr); + ok(element.checkValidity(), "checkValidity() should return true for " + infoStr); + ok(!gInvalid, "The invalid event should not have been thrown for " + infoStr); + is(element.validationMessage, '', + "Validation message should be the empty string for " + infoStr); + ok(element.matches(":valid"), ":valid pseudo-class should apply for " + infoStr); +} + +function checkIsInvalid(element, infoStr) +{ + ok(element.validity.badInput, + "Element should suffer from bad input for " + infoStr); + ok(!element.validity.valid, "Element should not be valid for " + infoStr); + ok(!element.checkValidity(), "checkValidity() should return false for " + infoStr); + ok(gInvalid, "The invalid event should have been thrown for " + infoStr); + is(element.validationMessage, "Please enter a number.", + "Validation message is not the expected message for " + infoStr); + ok(element.matches(":invalid"), ":invalid pseudo-class should apply for " + infoStr); +} + +function checkIsInvalidEmptyValue(element, infoStr) +{ + ok(!element.validity.badInput, + "Element should not suffer from bad input for " + infoStr); + ok(element.validity.valueMissing, + "Element should suffer from value missing for " + infoStr); + ok(!element.validity.valid, "Element should not be valid for " + infoStr); + ok(!element.checkValidity(), "checkValidity() should return false for " + infoStr); + ok(gInvalid, "The invalid event should have been thrown for " + infoStr); + is(element.validationMessage, "Please enter a number.", + "Validation message is not the expected message for " + infoStr); + ok(element.matches(":invalid"), ":invalid pseudo-class should apply for " + infoStr); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_password_click_show_password_button.html b/dom/html/test/forms/test_input_password_click_show_password_button.html new file mode 100644 index 0000000000..76f4e066f5 --- /dev/null +++ b/dom/html/test/forms/test_input_password_click_show_password_button.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=502258 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 502258</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script> + + SimpleTest.waitForExplicitFinish(); + + async function click_show_password(aId) { + var wu = SpecialPowers.getDOMWindowUtils(window); + var element = document.getElementById(aId); + element.focus(); + await new Promise(resolve => setTimeout(resolve, 0)); + var rect = element.getBoundingClientRect(); + var x = rect.right - 8; + var y = rect.top + 8; + wu.sendMouseEvent("mousedown", x, y, 0, 1, 0); + wu.sendMouseEvent("mouseup", x, y, 0, 1, 0); + await new Promise(resolve => requestAnimationFrame(resolve)); + } + + async function test_show_password(aId) { + var wu = SpecialPowers.getDOMWindowUtils(window); + var element = document.getElementById(aId); + + var baseSnapshot = await snapshotWindow(window); + + await new Promise(resolve => setTimeout(resolve, 0)); + element.type = "text"; + await new Promise(resolve => requestAnimationFrame(resolve)); + var typeTextSnapshot = await snapshotWindow(window); + results = compareSnapshots(baseSnapshot, typeTextSnapshot, true); + ok(results[0], aId + ": type=text should render the same as type=password that is showing the password"); + + // Re-setting value shouldn't change anything. + // eslint-disable-next-line no-self-assign + element.value = element.value; + var tmpSnapshot = await snapshotWindow(window); + + results = compareSnapshots(baseSnapshot, tmpSnapshot, true); + ok(results[0], aId + ": re-setting the value should change nothing"); + } + + async function reset_show_password(aId, concealedSnapshot) { + var element = document.getElementById(aId); + element.type = "password"; + await new Promise(resolve => requestAnimationFrame(resolve)); + var typePasswordSnapshot = await snapshotWindow(window); + results = compareSnapshots(concealedSnapshot, typePasswordSnapshot, true); + ok(results[0], aId + ": changing the type attribute should conceal the password again"); + } + + async function runTest() { + await SpecialPowers.pushPrefEnv({set: [["layout.forms.reveal-password-button.enabled", true]]}); + document.getElementById("content").style.display = ""; + document.getElementById("content").getBoundingClientRect(); + var concealedSnapshot = await snapshotWindow(window); + // test1 checks that the Show Password button becomes invisible when the value becomes empty + document.getElementById('test1').value = "123"; + await click_show_password('test1'); + document.getElementById('test1').value = ""; + // test2 checks that clicking the Show Password button unmasks the value + await click_show_password('test2'); + await test_show_password('test1'); + await test_show_password('test2'); + // checks that changing the type attribute resets thhe revealed state + await reset_show_password('test2', concealedSnapshot); + SimpleTest.finish(); + } + + SimpleTest.waitForFocus(runTest); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502258">Mozilla Bug 502258</a> +<p id="display"></p> +<style>input {appearance:none} .ref {display:none}</style> +<div id="content" style="display: none"> + <input id="test1" type=password> + <div style="position:relative; margin: 1em 0;"> + <input id="test2" type=password value="123" style="position:absolute"> + <div style="position:absolute; top:0;left:10ch; width:20ch; height:2em; background:black; pointer-events:none"></div> + </div> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_password_show_password_button.html b/dom/html/test/forms/test_input_password_show_password_button.html new file mode 100644 index 0000000000..09bec8ae82 --- /dev/null +++ b/dom/html/test/forms/test_input_password_show_password_button.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=502258 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 502258</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script> + SimpleTest.waitForExplicitFinish(); + + async function test_append_char(aId) { + let element = document.getElementById(aId); + element.focus(); + + let baseSnapshot = await snapshotWindow(window); + + element.selectionStart = element.selectionEnd = element.value.length; + + await new Promise(resolve => setTimeout(resolve, 0)); + sendString('f'); + + await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve))); + + let selectionAtTheEndSnapshot = await snapshotWindow(window); + assertSnapshots(baseSnapshot, selectionAtTheEndSnapshot, /* equal = */ false, /* fuzz = */ null, "baseSnapshot", "selectionAtTheEndSnapshot"); + + // Re-setting value shouldn't change anything. + // eslint-disable-next-line no-self-assign + element.value = element.value; + let tmpSnapshot = await snapshotWindow(window); + + assertSnapshots(baseSnapshot, tmpSnapshot, /* equal = */ false, /* fuzz = */ null, "baseSnapshot", "tmpSnapshot"); + assertSnapshots(selectionAtTheEndSnapshot, tmpSnapshot, /* equal = */ true, /* fuzz = */ null, "selectionAtTheEndSnapshot", "tmpSnapshot"); + + element.selectionStart = element.selectionEnd = 0; + element.blur(); + } + + async function runTest() { + await SpecialPowers.pushPrefEnv({set: [["layout.forms.reveal-password-button.enabled", true]]}); + document.getElementById("content").style.display = ""; + document.getElementById("content").getBoundingClientRect(); + await test_append_char('test1'); + await test_append_char('test2'); + await test_append_char('test3'); + await test_append_char('test4'); + SimpleTest.finish(); + } + + SimpleTest.waitForFocus(runTest); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=502258">Mozilla Bug 502258</a> +<p id="display"></p> +<style>input {appearance:none}</style> +<div id="content" style="display: none"> + <input id="test1" type=password> + <input id="test2" type=password value="123"> + <!-- text value masked off --> + <div style="position:relative; margin: 1em 0;"> + <input id="test3" type=password style="position:absolute"> + <div style="position:absolute; top:0;left:0; width:10ch; height:2em; background:black"></div> + </div> + <br> + <!-- Show Password button masked off --> + <div style="position:relative; margin: 1em 0;"> + <input id="test4" type=password style="position:absolute"> + <div style="position:absolute; top:0;left:10ch; width:20ch; height:2em; background:black"></div> + </div> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_radio_indeterminate.html b/dom/html/test/forms/test_input_radio_indeterminate.html new file mode 100644 index 0000000000..0fe7028b1e --- /dev/null +++ b/dom/html/test/forms/test_input_radio_indeterminate.html @@ -0,0 +1,109 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=885359 +--> +<head> + <title>Test for Bug 885359</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885359">Mozilla Bug 343444</a> +<p id="display"></p> +<form> + <input type="radio" id='radio1'/><br/> + + <input type="radio" id="g1radio1" name="group1"/> + <input type="radio" id="g1radio2" name="group1"/></br> + <input type="radio" id="g1radio3" name="group1"/></br> + + <input type="radio" id="g2radio1" name="group2"/> + <input type="radio" id="g2radio2" name="group2" checked/></br> +</form> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var radio1 = document.getElementById("radio1"); +var g1radio1 = document.getElementById("g1radio1"); +var g1radio2 = document.getElementById("g1radio2"); +var g1radio3 = document.getElementById("g1radio3"); +var g2radio1 = document.getElementById("g2radio1"); +var g2radio2 = document.getElementById("g2radio2"); + +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function verifyIndeterminateState(aElement, aIsIndeterminate, aMessage) { + is(aElement.mozMatchesSelector(':indeterminate'), aIsIndeterminate, aMessage); +} + +function test() { + // Initial State. + verifyIndeterminateState(radio1, true, + "Unchecked radio in its own group (no name attribute)"); + verifyIndeterminateState(g1radio1, true, "No selected radio in its group"); + verifyIndeterminateState(g1radio2, true, "No selected radio in its group"); + verifyIndeterminateState(g1radio3, true, "No selected radio in its group"); + verifyIndeterminateState(g2radio1, false, "Selected radio in its group"); + verifyIndeterminateState(g2radio2, false, "Selected radio in its group"); + + // Selecting radio buttion. + g1radio1.checked = true; + verifyIndeterminateState(g1radio1, false, + "Selecting a radio should affect all radios in the group"); + verifyIndeterminateState(g1radio2, false, + "Selecting a radio should affect all radios in the group"); + verifyIndeterminateState(g1radio3, false, + "Selecting a radio should affect all radios in the group"); + + // Changing the selected radio button. + g1radio3.checked = true; + verifyIndeterminateState(g1radio1, false, + "Selecting a radio should affect all radios in the group"); + verifyIndeterminateState(g1radio2, false, + "Selecting a radio should affect all radios in the group"); + verifyIndeterminateState(g1radio3, false, + "Selecting a radio should affect all radios in the group"); + + // Deselecting radio button. + g2radio2.checked = false; + verifyIndeterminateState(g2radio1, true, + "Deselecting a radio should affect all radios in the group"); + verifyIndeterminateState(g2radio2, true, + "Deselecting a radio should affect all radios in the group"); + + // Move a selected radio button to another group. + g1radio3.name = "group2"; + + // The radios' state in the original group becomes indeterminated. + verifyIndeterminateState(g1radio1, true, + "Removing a radio from a group should affect all radios in the group"); + verifyIndeterminateState(g1radio2, true, + "Removing a radio from a group should affect all radios in the group"); + + // The radios' state in the new group becomes determinated. + verifyIndeterminateState(g1radio3, false, + "Adding a radio from a group should affect all radios in the group"); + verifyIndeterminateState(g2radio1, false, + "Adding a radio from a group should affect all radios in the group"); + verifyIndeterminateState(g2radio2, false, + "Adding a radio from a group should affect all radios in the group"); + + // Change input type to 'text'. + g1radio3.type = "text"; + verifyIndeterminateState(g1radio3, false, + "Input type text does not have an indeterminate state"); + verifyIndeterminateState(g2radio1, true, + "Changing input type should affect all radios in the group"); + verifyIndeterminateState(g2radio2, true, + "Changing input type should affect all radios in the group"); +} +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/forms/test_input_radio_radiogroup.html b/dom/html/test/forms/test_input_radio_radiogroup.html new file mode 100644 index 0000000000..62767def72 --- /dev/null +++ b/dom/html/test/forms/test_input_radio_radiogroup.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=343444 +--> +<head> + <title>Test for Bug 343444</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=343444">Mozilla Bug 343444</a> +<p id="display"></p> +<form> + <fieldset id="testradio"> + <input type="radio" name="testradio" id="start"></input> + <input type="text" name="testradio"></input> + <input type="text" name="testradio"></input> + <input type="radio" name="testradio"></input> + <input type="text" name="testradio"></input> + <input type="radio" name="testradio"></input> + <input type="text" name="testradio"></input> + <input type="radio" name="testradio"></input> + <input type="radio" name="testradio"></input> + <input type="text" name="testradio"></input> + </fieldset> + + <fieldset> + <input type="radio" name="testtwo" id="start2"></input> + <input type="radio" name="testtwo"></input> + <input type="radio" name="error" id="testtwo"></input> + <input type="radio" name="testtwo" id="end"></input> + </fieldset> + + <fieldset> + <input type="radio" name="testthree" id="start3"></input> + <input type="radio" name="errorthree" id="testthree"></input> + </fieldset> +</form> +<script class="testbody" type="text/javascript"> +/** Test for Bug 343444 **/ +SimpleTest.waitForExplicitFinish(); +startTest(); +function startTest() { + document.getElementById("start").focus(); + var count=0; + while (count < 2) { + sendKey("DOWN"); + is(document.activeElement.type, "radio", "radioGroup should ignore non-radio input fields"); + if (document.activeElement.id == "start") { + count++; + } + } + + document.getElementById("start2").focus(); + count = 0; + while (count < 3) { + is(document.activeElement.name, "testtwo", + "radioGroup should only contain elements with the same @name") + sendKey("DOWN"); + count++; + } + + document.getElementById("start3").focus(); + sendKey("DOWN"); + is(document.activeElement.name, "testthree", "we don't have an infinite-loop"); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/forms/test_input_radio_required.html b/dom/html/test/forms/test_input_radio_required.html new file mode 100644 index 0000000000..ae02aab2ff --- /dev/null +++ b/dom/html/test/forms/test_input_radio_required.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id={BUGNUMBER} +--> +<head> + <title>Test for Bug 1100535</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1100535">Mozilla Bug 1100535</a> +<p id="display"></p> +<div id="content" style="display: none"> +<form> + <input type="radio" name="a"> +</form> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + var input = document.querySelector("input"); + input.setAttribute("required", "x"); + input.setAttribute("required", "y"); + is(document.forms[0].checkValidity(), false); + input.required = false; + is(document.forms[0].checkValidity(), true); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_range_attr_order.html b/dom/html/test/forms/test_input_range_attr_order.html new file mode 100644 index 0000000000..dc3f1ac95c --- /dev/null +++ b/dom/html/test/forms/test_input_range_attr_order.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=841941 +--> +<head> + <title>Test @min/@max/@step order for range</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=841941">Mozilla Bug 841941</a> +<p id="display"></p> +<div id="content"> + <input type=range value=2 max=1.5 step=0.5> + <input type=range value=2 step=0.5 max=1.5> + <input type=range value=2 max=1.5 step=0.5> + <input type=range value=2 step=0.5 max=1.5> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 841941 + * This test checks that the order in which @min/@max/@step are specified in + * markup makes no difference to the value that <input type=range> will be + * given. Basically this checks that sanitization of the value does not occur + * until after the parser has finished with the element. + */ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +function test() { + var ranges = document.querySelectorAll("input[type=range]"); + for (var i = 0; i < ranges.length; i++) { + is(ranges.item(i).value, "1.5", "Check sanitization order for range " + i); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_range_key_events.html b/dom/html/test/forms/test_input_range_key_events.html new file mode 100644 index 0000000000..6daf572916 --- /dev/null +++ b/dom/html/test/forms/test_input_range_key_events.html @@ -0,0 +1,207 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=843725 +--> +<head> + <title>Test key events for range</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=843725">Mozilla Bug 843725</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 843725 + * This test checks how the value of <input type=range> changes in response to + * various key events while it is in various states. + **/ +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +const defaultMinimum = 0; +const defaultMaximum = 100; +const defaultStep = 1; + +// Helpers: +// For the sake of simplicity, we do not currently support fractional value, +// step, etc. + +function minimum(element) { + return Number(element.min || defaultMinimum); +} + +function maximum(element) { + return Number(element.max || defaultMaximum); +} + +function range(element) { + var max = maximum(element); + var min = minimum(element); + if (max < min) { + return 0; + } + return max - min; +} + +function defaultValue(element) { + return minimum(element) + range(element)/2; +} + +function value(element) { + return Number(element.value || defaultValue(element)); +} + +function step(element) { + var stepSize = Number(element.step || defaultStep); + return stepSize <= 0 ? defaultStep : stepSize; +} + +function clampToRange(val, element) { + var min = minimum(element); + var max = maximum(element); + if (max < min) { + return min; + } + if (val < min) { + return min; + } + if (val > max) { + return max; + } + return val; +} + +// Functions used to specify expected test results: + +function valuePlusStep(element) { + return clampToRange(value(element) + step(element), element); +} + +function valueMinusStep(element) { + return clampToRange(value(element) - step(element), element); +} + +/** + * Returns the current value of the range plus whichever is greater of either + * 10% of the range or its current step value, clamped to the range's minimum/ + * maximum. The reason for using the step if it is greater than 10% of the + * range is because otherwise the PgUp/PgDn keys would do nothing in that case. + */ +function valuePlusTenPctOrStep(element) { + var tenPct = range(element)/10; + var stp = step(element); + return clampToRange(value(element) + Math.max(tenPct, stp), element); +} + +function valueMinusTenPctOrStep(element) { + var tenPct = range(element)/10; + var stp = step(element); + return clampToRange(value(element) - Math.max(tenPct, stp), element); +} + +// Test table: + +const LTR = "ltr"; +const RTL = "rtl"; + +var testTable = [ + ["KEY_ArrowLeft", LTR, valueMinusStep], + ["KEY_ArrowLeft", RTL, valuePlusStep], + ["KEY_ArrowRight", LTR, valuePlusStep], + ["KEY_ArrowRight", RTL, valueMinusStep], + ["KEY_ArrowUp", LTR, valuePlusStep], + ["KEY_ArrowUp", RTL, valuePlusStep], + ["KEY_ArrowDown", LTR, valueMinusStep], + ["KEY_ArrowDown", RTL, valueMinusStep], + ["KEY_PageUp", LTR, valuePlusTenPctOrStep], + ["KEY_PageUp", RTL, valuePlusTenPctOrStep], + ["KEY_PageDown", LTR, valueMinusTenPctOrStep], + ["KEY_PageDown", RTL, valueMinusTenPctOrStep], + ["KEY_Home", LTR, minimum], + ["KEY_Home", RTL, minimum], + ["KEY_End", LTR, maximum], + ["KEY_End", RTL, maximum], +] + +function test() { + var elem = document.createElement("input"); + elem.type = "range"; + + var content = document.getElementById("content"); + content.appendChild(elem); + elem.focus(); + + for (test of testTable) { + var [key, dir, expectedFunc] = test; + var oldVal, expectedVal; + + elem.step = "2"; + elem.style.direction = dir; + var flush = document.body.clientWidth; + + // Start at middle: + elem.value = oldVal = defaultValue(elem); + expectedVal = expectedFunc(elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with value set to the midpoint (" + oldVal + ")"); + + // Same again: + expectedVal = expectedFunc(elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range"); + + // Start at maximum: + elem.value = oldVal = maximum(elem); + expectedVal = expectedFunc(elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with value set to the maximum (" + oldVal + ")"); + + // Same again: + expectedVal = expectedFunc(elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range"); + + // Start at minimum: + elem.value = oldVal = minimum(elem); + expectedVal = expectedFunc(elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with value set to the minimum (" + oldVal + ")"); + + // Same again: + expectedVal = expectedFunc(elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range"); + + // Test for a step value that is greater than 10% of the range: + elem.step = 20; + elem.value = 60; + expectedVal = expectedFunc(elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test " + key + " for " + dir + " range with a step that is greater than 10% of the range (step=" + elem.step + ")"); + + // Same again: + expectedVal = expectedFunc(elem); + synthesizeKey(key); + is(elem.value, String(expectedVal), "Test repeat of " + key + " for " + dir + " range"); + + // reset step: + elem.step = 2; + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_range_mouse_and_touch_events.html b/dom/html/test/forms/test_input_range_mouse_and_touch_events.html new file mode 100644 index 0000000000..5957ede81d --- /dev/null +++ b/dom/html/test/forms/test_input_range_mouse_and_touch_events.html @@ -0,0 +1,240 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=846380 +--> +<head> + <title>Test mouse and touch events for range</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <style> + /* synthesizeMouse and synthesizeFunc uses getBoundingClientRect. We set + * the following properties to avoid fractional values in the rect returned + * by getBoundingClientRect in order to avoid rounding that would occur + * when event coordinates are internally converted to be relative to the + * top-left of the element. (Such rounding would make it difficult to + * predict exactly what value the input should take on for events at + * certain coordinates.) + */ + input { margin: 0 ! important; width: 200px ! important; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=846380">Mozilla Bug 846380</a> +<p id="display"></p> +<div id="content"> + <input id="range" type="range"> +</div> +<pre id="test"> +<script type="application/javascript"> + +const { AppConstants } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +/** + * Test for Bug 846380 + * This test checks how the value of <input type=range> changes in response to + * various mouse and touch events. + **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(synthesizeMouse, "click", "mousedown", "mousemove", "mouseup"); + test(synthesizeTouch, "tap", "touchstart", "touchmove", "touchend"); + SimpleTest.finish(); +}); + +const kIsWin = AppConstants.platform == "win"; +const kIsLinux = AppConstants.platform == "linux"; + +const MIDDLE_OF_RANGE = "50"; +const MINIMUM_OF_RANGE = "0"; +const MAXIMUM_OF_RANGE = "100"; +const QUARTER_OF_RANGE = "25"; +const THREE_QUARTERS_OF_RANGE = "75"; + +function flush() { + // Flush style, specifically to flush the 'direction' property so that the + // browser uses the new value for thumb positioning. + document.body.clientWidth; +} + +function test(synthesizeFunc, clickOrTap, startName, moveName, endName) { + var elem = document.getElementById("range"); + elem.focus(); + flush(); + + var width = parseFloat(window.getComputedStyle(elem).width); + var height = parseFloat(window.getComputedStyle(elem).height); + var borderLeft = parseFloat(window.getComputedStyle(elem).borderLeftWidth); + var borderTop = parseFloat(window.getComputedStyle(elem).borderTopWidth); + var paddingLeft = parseFloat(window.getComputedStyle(elem).paddingLeft); + var paddingTop = parseFloat(window.getComputedStyle(elem).paddingTop); + + // Extrema for mouse/touch events: + var midY = height / 2 + borderTop + paddingTop; + var minX = borderLeft + paddingLeft; + var midX = minX + width / 2; + var maxX = minX + width; + + // Test click/tap in the middle of the range: + elem.value = QUARTER_OF_RANGE; + synthesizeFunc(elem, midX, midY, {}); + is(elem.value, MIDDLE_OF_RANGE, "Test " + clickOrTap + " in middle of range"); + + // Test mouse/touch dragging of ltr range: + elem.value = QUARTER_OF_RANGE; + synthesizeFunc(elem, midX, midY, { type: startName }); + is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range"); + synthesizeFunc(elem, minX, midY, { type: moveName }); + is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to left of ltr range"); + + synthesizeFunc(elem, maxX, midY, { type: moveName }); + is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to right of ltr range (" + moveName + ")"); + + synthesizeFunc(elem, maxX, midY, { type: endName }); + is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to right of ltr range (" + endName + ")"); + + // Test mouse/touch dragging of rtl range: + elem.value = QUARTER_OF_RANGE; + elem.style.direction = "rtl"; + flush(); + synthesizeFunc(elem, midX, midY, { type: startName }); + is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of rtl range"); + synthesizeFunc(elem, minX, midY, { type: moveName }); + is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to left of rtl range"); + + synthesizeFunc(elem, maxX, midY, { type: moveName }); + is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to right of rtl range (" + moveName + ")"); + + synthesizeFunc(elem, maxX, midY, { type: endName }); + is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to right of rtl range (" + endName + ")"); + + elem.style.direction = "ltr"; // reset direction + flush(); + + // Test mouse/touch capturing by moving pointer to a position outside the range: + elem.value = QUARTER_OF_RANGE; + synthesizeFunc(elem, midX, midY, { type: startName }); + is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range"); + synthesizeFunc(elem, maxX+100, midY, { type: moveName }); + is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to position outside range (" + moveName + ")"); + + synthesizeFunc(elem, maxX+100, midY, { type: endName }); + is(elem.value, MAXIMUM_OF_RANGE, "Test dragging of range to position outside range (" + endName + ")"); + + // Test mouse/touch capturing by moving pointer to a position outside a rtl range: + elem.value = QUARTER_OF_RANGE; + elem.style.direction = "rtl"; + flush(); + synthesizeFunc(elem, midX, midY, { type: startName }); + is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of rtl range"); + synthesizeFunc(elem, maxX+100, midY, { type: moveName }); + is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to position outside range (" + moveName + ")"); + + synthesizeFunc(elem, maxX+100, midY, { type: endName }); + is(elem.value, MINIMUM_OF_RANGE, "Test dragging of range to position outside range (" + endName + ")"); + + elem.style.direction = "ltr"; // reset direction + flush(); + + // Test mouse/touch events with certain modifiers are ignored: + var modifiersIgnore = ["ctrlKey", "altGrKey", "fnKey"]; + if (kIsWin || kIsLinux) { + modifiersIgnore.push("metaKey"); + } + for (var modifier of modifiersIgnore) { + elem.value = QUARTER_OF_RANGE; + var eventParams = {}; + eventParams[modifier] = true; + synthesizeFunc(elem, midX, midY, eventParams); + is(elem.value, QUARTER_OF_RANGE, "Test " + clickOrTap + " in the middle of range with " + modifier + " modifier key is ignored"); + } + + // Test mouse/touch events with certain modifiers are allowed: + var modifiersAllow = ["shiftKey", "altKey"]; + if (!modifiersIgnore.includes("metaKey")) { + modifiersAllow.push("metaKey"); + } + for (var modifier of modifiersAllow) { + elem.value = QUARTER_OF_RANGE; + var eventParams = {}; + eventParams[modifier] = true; + synthesizeFunc(elem, midX, midY, eventParams); + is(elem.value, MIDDLE_OF_RANGE, "Test " + clickOrTap + " in the middle of range with " + modifier + " modifier key is allowed"); + } + + // Test that preventDefault() works: + function preventDefault(e) { + e.preventDefault(); + } + elem.value = QUARTER_OF_RANGE; + elem.addEventListener(startName, preventDefault); + synthesizeFunc(elem, midX, midY, {}); + is(elem.value, QUARTER_OF_RANGE, "Test that preventDefault() works"); + elem.removeEventListener(startName, preventDefault); + + // Test that changing the input type in the middle of a drag cancels the drag: + elem.value = QUARTER_OF_RANGE; + synthesizeFunc(elem, midX, midY, { type: startName }); + is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range"); + elem.type = "text"; + is(elem.value, QUARTER_OF_RANGE, "Test that changing the input type cancels a drag"); + synthesizeFunc(elem, midX, midY, { type: endName }); + is(elem.value, QUARTER_OF_RANGE, "Test that changing the input type cancels a drag (after " + endName + ")"); + elem.type = "range"; + + // Check that we do not drag when the mousedown/touchstart occurs outside the range: + elem.value = QUARTER_OF_RANGE; + synthesizeFunc(elem, maxX+100, midY, { type: startName }); + is(elem.value, QUARTER_OF_RANGE, "Test " + startName + " outside range doesn't change its value"); + synthesizeFunc(elem, midX, midY, { type: moveName }); + is(elem.value, QUARTER_OF_RANGE, "Test dragging is not occurring when " + startName + " was outside range"); + + synthesizeFunc(elem, midX, midY, { type: endName }); + is(elem.value, QUARTER_OF_RANGE, "Test dragging is not occurring when " + startName + " was outside range"); + + elem.focus(); // RESTORE FOCUS SO WE GET THE FOCUSED STYLE FOR TESTING OR ELSE minX/midX/maxX may be wrong! + + // Check what happens when a value changing key is pressed during a drag: + elem.value = QUARTER_OF_RANGE; + synthesizeFunc(elem, midX, midY, { type: startName }); + is(elem.value, MIDDLE_OF_RANGE, "Test " + startName + " in middle of range"); + synthesizeKey("KEY_Home"); + // The KEY_Home tests are disabled until I can figure out why they fail on Android -jwatt + //is(elem.value, MINIMUM_OF_RANGE, "Test KEY_Home during a drag sets the value to the minimum of the range"); + synthesizeFunc(elem, midX+100, midY, { type: moveName }); + is(elem.value, MAXIMUM_OF_RANGE, "Test " + moveName + " outside range after key press that occurred during a drag changes the value"); + synthesizeFunc(elem, midX, midY, { type: moveName }); + is(elem.value, MIDDLE_OF_RANGE, "Test " + moveName + " in middle of range"); + synthesizeKey("KEY_Home"); + //is(elem.value, MINIMUM_OF_RANGE, "Test KEY_Home during a drag sets the value to the minimum of the range (second time)"); + synthesizeFunc(elem, maxX+100, midY, { type: endName }); + is(elem.value, MAXIMUM_OF_RANGE, "Test " + endName + " outside range after key press that occurred during a drag changes the value"); + + function hideElement() { + elem.parentNode.style.display = 'none'; + elem.parentNode.offsetLeft; + } + + if (clickOrTap == "click") { + elem.addEventListener("mousedown", hideElement); + } else if (clickOrTap == "tap") { + elem.addEventListener("touchstart", hideElement); + } + synthesizeFunc(elem, midX, midY, { type: startName }); + synthesizeFunc(elem, midX, midY, { type: endName }); + elem.removeEventListener("mousedown", hideElement); + elem.removeEventListener("touchstart", hideElement); + ok(true, "Hiding the element during mousedown/touchstart shouldn't crash the process."); + elem.parentNode.style.display = "block"; + elem.parentNode.offsetLeft; +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_range_rounding.html b/dom/html/test/forms/test_input_range_rounding.html new file mode 100644 index 0000000000..9c3c21ce6e --- /dev/null +++ b/dom/html/test/forms/test_input_range_rounding.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=853525 +--> +<head> + <title>Test key events for range</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=853525">Mozilla Bug 853525</a> +<p id="display"></p> +<div id="content"> + <input id=range type=range value=0 step=0.01 max=1> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** + * Test for Bug 853525 + * This test checks that when <input type=range> has fractional step values, + * the values that a content author will see in their script will not have + * ugly rounding errors. + **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +/** + * We can _NOT_ generate these values by looping and simply incrementing a + * variable by 0.01 and stringifying it, since we'll end up with strings like + * "0.060000000000000005" due to the inability of binary floating point numbers + * to accurately represent decimal values. + */ +var stepVals = [ + "0", "0.01", "0.02", "0.03", "0.04", "0.05", "0.06", "0.07", "0.08", "0.09", + "0.1", "0.11", "0.12", "0.13", "0.14", "0.15", "0.16", "0.17", "0.18", "0.19", + "0.2", "0.21", "0.22", "0.23", "0.24", "0.25", "0.26", "0.27", "0.28", "0.29", + "0.3", "0.31", "0.32", "0.33", "0.34", "0.35", "0.36", "0.37", "0.38", "0.39", + "0.4", "0.41", "0.42", "0.43", "0.44", "0.45", "0.46", "0.47", "0.48", "0.49", + "0.5", "0.51", "0.52", "0.53", "0.54", "0.55", "0.56", "0.57", "0.58", "0.59", + "0.6", "0.61", "0.62", "0.63", "0.64", "0.65", "0.66", "0.67", "0.68", "0.69", + "0.7", "0.71", "0.72", "0.73", "0.74", "0.75", "0.76", "0.77", "0.78", "0.79", + "0.8", "0.81", "0.82", "0.83", "0.84", "0.85", "0.86", "0.87", "0.88", "0.89", + "0.9", "0.91", "0.92", "0.93", "0.94", "0.95", "0.96", "0.97", "0.98", "0.99", + "1" +]; + +var pgUpDnVals = [ + "0", "0.1", "0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "1" +]; + +function test() { + var elem = document.getElementById("range"); + + elem.focus(); + + for (var i = 1; i < pgUpDnVals.length; ++i) { + synthesizeKey("KEY_PageUp"); + is(elem.value, pgUpDnVals[i], "Test KEY_PageUp"); + is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]); + } + + for (var i = pgUpDnVals.length - 2; i >= 0; --i) { + synthesizeKey("KEY_PageDown"); + is(elem.value, pgUpDnVals[i], "Test KEY_PageDown"); + is(elem.validity.valid, true, "Check element is valid for value " + pgUpDnVals[i]); + } + + for (var i = 1; i < stepVals.length; ++i) { + synthesizeKey("KEY_ArrowUp"); + is(elem.value, stepVals[i], "Test KEY_ArrowUp"); + is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]); + } + + for (var i = stepVals.length - 2; i >= 0; --i) { + synthesizeKey("KEY_ArrowDown"); + is(elem.value, stepVals[i], "Test KEY_ArrowDown"); + is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]); + } + + for (var i = 1; i < stepVals.length; ++i) { + elem.stepUp(); + is(elem.value, stepVals[i], "Test stepUp()"); + is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]); + } + + for (var i = stepVals.length - 2; i >= 0; --i) { + elem.stepDown(); + is(elem.value, stepVals[i], "Test stepDown()"); + is(elem.validity.valid, true, "Check element is valid for value " + stepVals[i]); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_sanitization.html b/dom/html/test/forms/test_input_sanitization.html new file mode 100644 index 0000000000..474ddd621d --- /dev/null +++ b/dom/html/test/forms/test_input_sanitization.html @@ -0,0 +1,585 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=549475 +--> +<head> + <title>Test for Bug 549475</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=549475">Mozilla Bug 549475</a> +<p id="display"></p> +<pre id="test"> +<div id='content'> + <form> + </form> +</div> +<script type="application/javascript"> + +SimpleTest.requestLongerTimeout(2); + +/** + * This files tests the 'value sanitization algorithm' for the various input + * types. Note that an input's value is affected by more than just its type's + * value sanitization algorithm; e.g. some type=range has actions that the user + * agent must perform to change the element's value to avoid underflow/overflow + * and step mismatch (when possible). We specifically avoid triggering these + * other actions here so that this test only tests the value sanitization + * algorithm for the various input types. + * + * XXXjwatt splitting out testing of the value sanitization algorithm and + * "other things" that affect .value makes it harder to know what we're testing + * and what we've missed, because what's included in the value sanitization + * algorithm and what's not is different from input type to input type. It + * seems to me it would be better to have a test (maybe one per type) focused + * on testing .value for permutations of all other inputs that can affect it. + * The value sanitization algorithm is just an internal spec concept after all. + */ + +// We buffer up the results of sets of sub-tests, and avoid outputting log +// entries for them all if they all pass. Otherwise, we have an enormous amount +// of test output. + +var delayedTests = []; +var anyFailedDelayedTests = false; + +function delayed_is(actual, expected, description) +{ + var result = actual == expected; + delayedTests.push({ actual, expected, description }); + if (!result) { + anyFailedDelayedTests = true; + } +} + +function flushDelayedTests(description) +{ + if (anyFailedDelayedTests) { + info("Outputting individual results for \"" + description + "\" due to failures in subtests"); + for (var test of delayedTests) { + is(test.actual, test.expected, test.description); + } + } else { + ok(true, description + " (" + delayedTests.length + " subtests)"); + } + delayedTests = []; + anyFailedDelayedTests = false; +} + +// We are excluding "file" because it's too different from the other types. +// And it has no sanitizing algorithm. +var inputTypes = +[ + "text", "password", "search", "tel", "hidden", "checkbox", "radio", + "submit", "image", "reset", "button", "email", "url", "number", "date", + "time", "range", "color", "month", "week", "datetime-local" +]; + +var valueModeValue = +[ + "text", "search", "url", "tel", "email", "password", "date", "datetime", + "month", "week", "time", "datetime-local", "number", "range", "color", +]; + +function sanitizeDate(aValue) +{ + // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-date-string + function getNumbersOfDaysInMonth(aMonth, aYear) { + if (aMonth === 2) { + return (aYear % 400 === 0 || (aYear % 100 != 0 && aYear % 4 === 0)) ? 29 : 28; + } + return (aMonth === 1 || aMonth === 3 || aMonth === 5 || aMonth === 7 || + aMonth === 8 || aMonth === 10 || aMonth === 12) ? 31 : 30; + } + + var match = /^([0-9]{4,})-([0-9]{2})-([0-9]{2})$/.exec(aValue); + if (!match) { + return ""; + } + var year = Number(match[1]); + if (year === 0) { + return ""; + } + var month = Number(match[2]); + if (month > 12 || month < 1) { + return ""; + } + var day = Number(match[3]); + return 1 <= day && day <= getNumbersOfDaysInMonth(month, year) ? aValue : ""; +} + +function sanitizeTime(aValue) +{ + // http://www.whatwg.org/specs/web-apps/current-work/multipage/common-microsyntaxes.html#valid-time-string + var match = /^([0-9]{2}):([0-9]{2})(.*)$/.exec(aValue); + if (!match) { + return ""; + } + var hours = match[1]; + if (hours < 0 || hours > 23) { + return ""; + } + var minutes = match[2]; + if (minutes < 0 || minutes > 59) { + return ""; + } + var other = match[3]; + if (other == "") { + return aValue; + } + match = /^:([0-9]{2})(.*)$/.exec(other); + if (!match) { + return ""; + } + var seconds = match[1]; + if (seconds < 0 || seconds > 59) { + return ""; + } + var other = match[2]; + if (other == "") { + return aValue; + } + match = /^.([0-9]{1,3})$/.exec(other); + if (!match) { + return ""; + } + return aValue; +} + +function sanitizeDateTimeLocal(aValue) +{ + // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-local-date-and-time-string + if (aValue.length < 16) { + return ""; + } + + var sepIndex = aValue.indexOf("T"); + if (sepIndex == -1) { + sepIndex = aValue.indexOf(" "); + if (sepIndex == -1) { + return ""; + } + } + + var [date, time] = aValue.split(aValue[sepIndex]); + if (!sanitizeDate(date)) { + return ""; + } + + if (!sanitizeTime(time)) { + return ""; + } + + // Normalize datetime-local string. + // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-normalised-local-date-and-time-string + if (aValue[sepIndex] == " ") { + aValue = date + "T" + time; + } + + if ((aValue.length - sepIndex) == 6) { + return aValue; + } + + if ((aValue.length - sepIndex) > 9) { + var milliseconds = aValue.substring(sepIndex + 10); + if (Number(milliseconds) != 0) { + return aValue; + } + aValue = aValue.slice(0, sepIndex + 9); + } + + var seconds = aValue.substring(sepIndex + 7); + if (Number(seconds) != 0) { + return aValue; + } + aValue = aValue.slice(0, sepIndex + 6); + + return aValue; +} + +function sanitizeValue(aType, aValue) +{ + // http://www.whatwg.org/html/#value-sanitization-algorithm + switch (aType) { + case "text": + case "password": + case "search": + case "tel": + return aValue.replace(/[\n\r]/g, ""); + case "url": + case "email": + return aValue.replace(/[\n\r]/g, "").replace(/^[\u0020\u0009\t\u000a\u000c\u000d]+|[\u0020\u0009\t\u000a\u000c\u000d]+$/g, ""); + case "number": + return isNaN(Number(aValue)) ? "" : aValue; + case "range": + var defaultMinimum = 0; + var defaultMaximum = 100; + var value = Number(aValue); + if (isNaN(value)) { + return ((defaultMaximum - defaultMinimum)/2).toString(); // "50" + } + if (value < defaultMinimum) { + return defaultMinimum.toString(); + } + if (value > defaultMaximum) { + return defaultMaximum.toString(); + } + return aValue; + case "date": + return sanitizeDate(aValue); + case "time": + return sanitizeTime(aValue); + case "month": + // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-month-string + var match = /^([0-9]{4,})-([0-9]{2})$/.exec(aValue); + if (!match) { + return ""; + } + var year = Number(match[1]); + if (year === 0) { + return ""; + } + var month = Number(match[2]); + if (month > 12 || month < 1) { + return ""; + } + return aValue; + case "week": + // https://html.spec.whatwg.org/multipage/infrastructure.html#valid-week-string + function isLeapYear(aYear) { + return ((aYear % 4 == 0) && (aYear % 100 != 0)) || (aYear % 400 == 0); + } + function getDayofWeek(aYear, aMonth, aDay) { /* 0 = Sunday */ + // Tomohiko Sakamoto algorithm. + var monthTable = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4]; + aYear -= Number(aMonth < 3); + + return (aYear + parseInt(aYear / 4) - parseInt(aYear / 100) + + parseInt(aYear / 400) + monthTable[aMonth - 1] + aDay) % 7; + } + function getMaximumWeekInYear(aYear) { + var day = getDayofWeek(aYear, 1, 1); + return day == 4 || (day == 3 && isLeapYear(aYear)) ? 53 : 52; + } + + var match = /^([0-9]{4,})-W([0-9]{2})$/.exec(aValue); + if (!match) { + return ""; + } + var year = Number(match[1]); + if (year === 0) { + return ""; + } + var week = Number(match[2]); + if (week > 53 || month < 1) { + return ""; + } + return 1 <= week && week <= getMaximumWeekInYear(year) ? aValue : ""; + case "datetime-local": + return sanitizeDateTimeLocal(aValue); + case "color": + return /^#[0-9A-Fa-f]{6}$/.exec(aValue) ? aValue.toLowerCase() : "#000000"; + default: + return aValue; + } +} + +function checkSanitizing(element, inputTypeDescription) +{ + var testData = + [ + // For text, password, search, tel, email: + "\n\rfoo\n\r", + "foo\n\rbar", + " foo ", + " foo\n\r bar ", + // For url: + "\r\n foobar \n\r", + "\u000B foo \u000B", + "\u000A foo \u000A", + "\u000C foo \u000C", + "\u000d foo \u000d", + "\u0020 foo \u0020", + " \u0009 foo \u0009 ", + // For number and range: + "42", + "13.37", + "1.234567898765432", + "12foo", + "1e2", + "3E42", + // For date: + "1970-01-01", + "1234-12-12", + "1234567890-01-02", + "2012-12-31", + "2012-02-29", + "2000-02-29", + "1234", + "1234-", + "12345", + "1234-01", + "1234-012", + "1234-01-", + "12-12", + "999-01-01", + "1234-56-78-91", + "1234-567-78", + "1234--7-78", + "abcd-12-12", + "thisinotadate", + "2012-13-01", + "1234-12-42", + " 2012-13-01", + " 123-01-01", + "2012- 3-01", + "12- 10- 01", + " 12-0-1", + "2012-3-001", + "2012-12-00", + "2012-12-1r", + "2012-11-31", + "2011-02-29", + "2100-02-29", + "a2000-01-01", + "2000a-01-0'", + "20aa00-01-01", + "2000a2000-01-01", + "2000-1-1", + "2000-1-01", + "2000-01-1", + "2000-01-01 ", + "2000- 01-01", + "-1970-01-01", + "0000-00-00", + "0001-00-00", + "0000-01-01", + "1234-12 12", + "1234 12-12", + "1234 12 12", + // For time: + "1", + "10", + "10:", + "10:1", + "21:21", + ":21:21", + "-21:21", + " 21:21", + "21-21", + "21:21:", + "21:211", + "121:211", + "21:21 ", + "00:00", + "-1:00", + "24:00", + "00:60", + "01:01", + "23:59", + "99:99", + "8:30", + "19:2", + "19:a2", + "4c:19", + "10:.1", + "1.:10", + "13:37:42", + "13:37.42", + "13:37:42 ", + "13:37:42.", + "13:37:61.", + "13:37:00", + "13:37:99", + "13:37:b5", + "13:37:-1", + "13:37:.1", + "13:37:1.", + "13:37:42.001", + "13:37:42.001", + "13:37:42.abc", + "13:37:42.00c", + "13:37:42.a23", + "13:37:42.12e", + "13:37:42.1e1", + "13:37:42.e11", + "13:37:42.1", + "13:37:42.99", + "13:37:42.0", + "13:37:42.00", + "13:37:42.000", + "13:37:42.-1", + "13:37:42.1.1", + "13:37:42.1,1", + "13:37:42.", + "foo12:12", + "13:37:42.100000000000", + // For color + "#00ff00", + "#000000", + "red", + "#0f0", + "#FFFFAA", + "FFAABB", + "fFAaBb", + "FFAAZZ", + "ABCDEF", + "#7654321", + // For month + "1970-01", + "1234-12", + "123456789-01", + "2013-13", + "0000-00", + "2015-00", + "0001-01", + "1-1", + "888-05", + "2013-3", + "2013-may", + "2000-1a", + "2013-03-13", + "december", + "abcdef", + "12", + " 2013-03", + "2013 - 03", + "2013 03", + "2013/03", + // For week + "1970-W01", + "1970-W53", + "1964-W53", + "1900-W10", + "2004-W53", + "2065-W53", + "2099-W53", + "2010-W53", + "2016-W30", + "1900-W3", + "2016-w30", + "2016-30", + "16-W30", + "2016-Week30", + "2000-100", + "0000-W01", + "00-W01", + "123456-W05", + "1985-W100", + "week", + // For datetime-local + "1970-01-01T00:00", + "1970-01-01Z12:00", + "1970-01-01 00:00:00", + "1970-01-01T00:00:00.0", + "1970-01-01T00:00:00.00", + "1970-01-01T00:00:00.000", + "1970-01-01 00:00:00.20", + "1969-12-31 23:59", + "1969-12-31 23:59:00", + "1969-12-31 23:59:00.000", + "1969-12-31 23:59:00.30", + "123456-01-01T12:00", + "123456-01-01T12:00:00", + "123456-01-01T12:00:00.0", + "123456-01-01T12:00:00.00", + "123456-01-01T12:00:00.000", + "123456-01-01T12:00:30", + "123456-01-01T12:00:00.123", + "10000-12-31 20:00", + "10000-12-31 20:00:00", + "10000-12-31 20:00:00.0", + "10000-12-31 20:00:00.00", + "10000-12-31 20:00:00.000", + "10000-12-31 20:00:30", + "10000-12-31 20:00:00.123", + "2016-13-01T12:00", + "2016-12-32T12:00", + "2016-11-08 15:40:30.0", + "2016-11-08T15:40:30.00", + "2016-11-07T17:30:10", + "2016-12-1T12:45", + "2016-12-01T12:45:30.123456", + "2016-12-01T24:00", + "2016-12-01T12:88:30", + "2016-12-01T12:30:99", + "2016-12-01T12:30:100", + "2016-12-01", + "2016-12-01T", + "2016-Dec-01T00:00", + "12-05-2016T00:00", + "datetime-local" + ]; + + for (value of testData) { + element.setAttribute('value', value); + delayed_is(element.value, sanitizeValue(type, value), + "The value has not been correctly sanitized for type=" + type); + delayed_is(element.getAttribute('value'), value, + "The content value should not have been sanitized"); + + if (type in valueModeValue) { + element.setAttribute('value', 'tulip'); + element.value = value; + delayed_is(element.value, sanitizeValue(type, value), + "The value has not been correctly sanitized for type=" + type); + delayed_is(element.getAttribute('value'), 'tulip', + "The content value should not have been sanitized"); + } + + element.setAttribute('value', ''); + form.reset(); + element.type = 'checkbox'; // We know this type has no sanitizing algorithm. + element.setAttribute('value', value); + delayed_is(element.value, value, "The value should not have been sanitized"); + element.type = type; + delayed_is(element.value, sanitizeValue(type, value), + "The value has not been correctly sanitized for type=" + type); + delayed_is(element.getAttribute('value'), value, + "The content value should not have been sanitized"); + + element.setAttribute('value', ''); + form.reset(); + element.setAttribute('value', value); + form.reset(); + delayed_is(element.value, sanitizeValue(type, value), + "The value has not been correctly sanitized for type=" + type); + delayed_is(element.getAttribute('value'), value, + "The content value should not have been sanitized"); + + // Cleaning-up. + element.setAttribute('value', ''); + form.reset(); + } + + flushDelayedTests(inputTypeDescription); +} + +for (type of inputTypes) { + var form = document.forms[0]; + var element = document.createElement("input"); + element.style.display = "none"; + element.type = type; + form.appendChild(element); + + checkSanitizing(element, "type=" + type + ", no frame, no editor"); + + element.style.display = ""; + checkSanitizing(element, "type=" + type + ", frame, no editor"); + + element.focus(); + element.blur(); + checkSanitizing(element, "type=" + type + ", frame, editor"); + + element.style.display = "none"; + checkSanitizing(element, "type=" + type + ", no frame, editor"); + + form.removeChild(element); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_setting_value.html b/dom/html/test/forms/test_input_setting_value.html new file mode 100644 index 0000000000..b6ddd66d24 --- /dev/null +++ b/dom/html/test/forms/test_input_setting_value.html @@ -0,0 +1,619 @@ +<!DOCTYPE> +<html> +<head> + <title>Test for setting input value</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<div id="display"> +</div> +<div id="content"><input type="text"></div> +<pre id="test"> +</pre> + +<script class="testbody" type="application/javascript"> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(() => { + const kSetUserInputCancelable = SpecialPowers.getBoolPref("dom.input_event.allow_to_cancel_set_user_input"); + + let input = document.querySelector("input[type=text]"); + + // Setting value during composition causes committing composition before setting the value. + input.focus(); + let description = 'Setting input value at first "compositionupdate" event: '; + input.addEventListener("compositionupdate", (aEvent) => { + is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`); + input.value = "def"; + is(input.value, "def", `${description}input value should be the specified value at "compositionupdate" event (after setting the value)`); + }, {once: true}); + input.addEventListener("compositionend", (aEvent) => { + todo_is(input.value, "def", `${description}input value should be the specified value at "compositionend" event`); + }, {once: true}); + input.addEventListener("input", (aEvent) => { + todo_is(input.value, "def", `${description}input value should be the specified value at "input" event`); + }, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + }); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + todo_is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`); + + input.value = ""; + description = 'Setting input value at second "compositionupdate" event: '; + synthesizeCompositionChange( + { "composition": + { "string": "ab", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + }); + input.addEventListener("compositionupdate", (aEvent) => { + is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`); + input.value = "def"; + }, {once: true}); + input.addEventListener("compositionend", (aEvent) => { + is(input.value, "def", `${description}input value should be specified value at "compositionend" event`); + }, {once: true}); + input.addEventListener("input", (aEvent) => { + is(input.value, "def", `${description}input value should be specified value at "input" event`); + }, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + }); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`); + + input.value = ""; + description = 'Setting input value at "input" event for first composition update: '; + input.addEventListener("compositionupdate", (aEvent) => { + is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`); + }, {once: true}); + input.addEventListener("compositionend", (aEvent) => { + todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`); + }, {once: true}); + input.addEventListener("input", (aEvent) => { + is(input.value, "abc", `${description}input value should be the composition string at "input" event`); + input.value = "def"; + is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`); + }, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + }); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`); + + input.value = ""; + description = 'Setting input value at "input" event for second composition update: '; + synthesizeCompositionChange( + { "composition": + { "string": "ab", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + }); + input.addEventListener("compositionupdate", (aEvent) => { + is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`); + }, {once: true}); + input.addEventListener("compositionend", (aEvent) => { + todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`); + }, {once: true}); + input.addEventListener("input", (aEvent) => { + is(input.value, "abc", `${description}input value should be the composition string at "input" event`); + input.value = "def"; + is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`); + }, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + }); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`); + + input.value = ""; + description = 'Setting input value and reframing at "input" event for first composition update: '; + input.addEventListener("compositionupdate", (aEvent) => { + is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`); + }, {once: true}); + input.addEventListener("compositionend", (aEvent) => { + todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`); + }, {once: true}); + input.addEventListener("input", (aEvent) => { + is(input.value, "abc", `${description}input value should be the composition string at "input" event`); + input.value = "def"; + input.style.width = "1000px"; + is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`); + }, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + }); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`); + input.style.width = ""; + + input.value = ""; + description = 'Setting input value and reframing at "input" event for second composition update: '; + synthesizeCompositionChange( + { "composition": + { "string": "ab", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + }); + input.addEventListener("compositionupdate", (aEvent) => { + is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`); + }, {once: true}); + input.addEventListener("compositionend", (aEvent) => { + todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`); + }, {once: true}); + input.addEventListener("input", (aEvent) => { + is(input.value, "abc", `${description}input value should be the composition string at "input" event`); + input.value = "def"; + input.style.width = "1000px"; + is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`); + }, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + }); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`); + input.style.width = ""; + + input.value = ""; + description = 'Setting input value and reframing with flushing layout at "input" event for first composition update: '; + input.addEventListener("compositionupdate", (aEvent) => { + is(input.value, "", `${description}input value should not have been modified at first "compositionupdate" event yet`); + }, {once: true}); + input.addEventListener("compositionend", (aEvent) => { + todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`); + }, {once: true}); + input.addEventListener("input", (aEvent) => { + is(input.value, "abc", `${description}input value should be the composition string at "input" event`); + input.value = "def"; + input.style.width = "1000px"; + document.documentElement.scrollTop; + is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`); + }, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + }); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`); + input.style.width = ""; + + input.value = ""; + description = 'Setting input value and reframing with flushing layout at "input" event for second composition update: '; + synthesizeCompositionChange( + { "composition": + { "string": "ab", + "clauses": + [ + { "length": 2, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 2, "length": 0 }, + }); + input.addEventListener("compositionupdate", (aEvent) => { + is(input.value, "ab", `${description}input value should not have been modified at second "compositionupdate" event yet`); + }, {once: true}); + input.addEventListener("compositionend", (aEvent) => { + todo_is(input.value, "abc", `${description}input value should be the composition string at "compositionend" event`); + }, {once: true}); + input.addEventListener("input", (aEvent) => { + is(input.value, "abc", `${description}input value should be the composition string at "input" event`); + input.value = "def"; + input.style.width = "1000px"; + document.documentElement.scrollTop; + is(input.value, "def", `${description}input value should be the specified value at "input" event (after setting the value)`); + }, {once: true}); + synthesizeCompositionChange( + { "composition": + { "string": "abc", + "clauses": + [ + { "length": 3, "attr": COMPOSITION_ATTR_RAW_CLAUSE } + ] + }, + "caret": { "start": 3, "length": 0 }, + }); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should be set to specified value after the last "input" event`); + input.style.width = ""; + + // autocomplete and correcting misspelled word by spellchecker cause an "input" event with same path as setting input value. + input.value = ""; + description = 'Setting input value at "input" event whose inputType is "insertReplacementText'; + let inputEventFired = false; + input.addEventListener("input", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`); + inputEventFired = true; + is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`); + input.value = "def"; + is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`); + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("abc"); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`); + ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`); + + input.value = ""; + description = 'Setting input value and reframing at "input" event whose inputType is "insertReplacementText'; + inputEventFired = false; + input.addEventListener("input", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`); + inputEventFired = true; + is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`); + input.value = "def"; + input.style.width = "1000px"; + is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`); + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("abc"); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`); + ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`); + input.style.width = ""; + + input.value = ""; + description = 'Setting input value and reframing with flushing layout at "input" event whose inputType is "insertReplacementText'; + inputEventFired = false; + input.addEventListener("input", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`); + inputEventFired = true; + is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`); + input.value = "def"; + input.style.width = "1000px"; + document.documentElement.scrollTop; + is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`); + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("abc"); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`); + ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`); + input.style.width = ""; + + input.value = ""; + description = 'Setting input value and destroying the frame at "input" event whose inputType is "insertReplacementText'; + inputEventFired = false; + input.addEventListener("input", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`); + inputEventFired = true; + is(input.value, "abc", `${description}input value should be inserted value at "input" event (before setting value)`); + input.value = "def"; + input.style.display = "none"; + is(input.value, "def", `${description}input value should be specified value at "input" event (after setting value)`); + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("abc"); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "def", `${description}input value should keep the specified value after the last "input" event`); + ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`); + input.style.display = "inline"; + + input.value = ""; + description = 'Changing input type at "input" event whose inputType is "insertReplacementText'; + inputEventFired = false; + input.addEventListener("input", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`); + inputEventFired = true; + is(input.value, "abc", `${description}input value should be inserted value at "input" event (before changing type)`); + input.type = "button"; + is(input.value, "abc", `${description}input value should keep inserted value at "input" event (after changing type)`); + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("abc"); + is(input.value, "abc", `${description}input value should keep inserted value after the last "input" event`); + is(input.type, "button", `${description}input type should be changed correctly`); + ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`); + input.type = "text"; + is(input.value, "abc", `${description}input value should keep inserted value immediately after restoring the type`); + todo(SpecialPowers.wrap(input).hasEditor, `${description}restoring input type should create editor if it's focused element`); + input.blur(); + input.focus(); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "abc", `${description}input value should keep inserted value after creating editor`); + + input.value = ""; + description = 'Changing input type and flush layout at "input" event whose inputType is "insertReplacementText'; + inputEventFired = false; + input.addEventListener("input", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`); + inputEventFired = true; + is(input.value, "abc", `${description}input value should be inserted value at "input" event (before changing type)`); + input.type = "button"; + input.getBoundingClientRect().height; + is(input.value, "abc", `${description}input value should keep inserted value at "input" event (after changing type)`); + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("abc"); + is(input.value, "abc", `${description}input value should keep inserted value after the last "input" event`); + is(input.type, "button", `${description}input type should be changed correctly`); + ok(inputEventFired, `${description}"input" event should've been fired for setUserInput("abc")`); + input.type = "text"; + is(input.value, "abc", `${description}input value should keep inserted value immediately after restoring the type`); + todo(SpecialPowers.wrap(input).hasEditor, `${description}restoring input type should create editor if it's focused element`); + input.blur(); + input.focus(); + is(SpecialPowers.wrap(input).editor.rootElement.firstChild.wholeText, input.value, + `${description}native anonymous text node should have exactly same value as value of <input> element`); + is(input.value, "abc", `${description}input value should keep inserted value after creating editor`); + + function testSettingValueFromBeforeInput(aWithEditor, aPreventDefaultOfBeforeInput) { + let beforeInputEvents = []; + let inputEvents = []; + function recordEvent(aEvent) { + if (aEvent.type === "beforeinput") { + beforeInputEvents.push(aEvent); + } else { + inputEvents.push(aEvent); + } + } + let condition = `(${aWithEditor ? "with editor" : "without editor"}${aPreventDefaultOfBeforeInput ? ' and canceling "beforeinput" event' : ""}, the pref ${kSetUserInputCancelable ? "allows" : "disallows"} to cancel "beforeinput" event})`; + function Reset() { + beforeInputEvents = []; + inputEvents = []; + if (SpecialPowers.wrap(input).hasEditor != aWithEditor) { + if (aWithEditor) { + input.blur(); + input.focus(); // Associate `TextEditor` with input + if (!SpecialPowers.wrap(input).hasEditor) { + ok(false, `${description}Failed to associate TextEditor with the input ${condition}`); + return false; + } + } else { + input.blur(); + input.type = "button"; + input.type = "text"; + if (SpecialPowers.wrap(input).hasEditor) { + ok(false, `${description}Failed to disassociate TextEditor from the input ${condition}`); + return false; + } + } + } + return true; + } + + description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText" ${condition}: `; + input.value = "abc"; + if (!Reset()) { + return; + } + input.addEventListener("beforeinput", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`); + is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`); + is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`); + input.addEventListener("beforeinput", recordEvent); + input.addEventListener("input", recordEvent); + input.value = "hig"; + if (aPreventDefaultOfBeforeInput) { + aEvent.preventDefault(); + } + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("def"); + is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`); + if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) { + is(input.value, "hig", + `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`); + is(inputEvents.length, 0, + `${description}"input" event shouldn't be fired since "beforeinput" was canceled`); + } else { + // XXX This result is different from Chrome (verified with spellchecker). + // Chrome inserts the new text to current value and selected range. + // It might be reasonable, but we don't touch this for now since it + // requires a lot of changes. + is(input.value, "hig", + `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`); + is(inputEvents.length, 1, `${description}"input" event should be fired`); + if (inputEvents.length) { + is(inputEvents[0].inputType, + "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`); + is(inputEvents[0].data, "def", + `${description}data of "input" event should be the value specified by setUserInput()`); + } + } + input.removeEventListener("beforeinput", recordEvent); + input.removeEventListener("input", recordEvent); + + description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText" and changing the type to "button" ${condition}: `; + input.value = "abc"; + if (!Reset()) { + return; + } + input.addEventListener("beforeinput", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`); + is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`); + is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`); + input.addEventListener("beforeinput", recordEvent); + input.addEventListener("input", recordEvent); + input.value = "hig"; + input.type = "button"; + if (aPreventDefaultOfBeforeInput) { + aEvent.preventDefault(); + } + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("def"); + is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`); + if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) { + is(input.value, "hig", + `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`); + is(inputEvents.length, 0, + `${description}"input" event shouldn't be fired since "beforeinput" was canceled`); + } else { + // XXX This result is same as Chrome (verified with spellchecker). + // But this behavior is not consistent with just setting the value on Chrome. + is(input.value, "hig", + `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`); + // Same as Chrome + is(inputEvents.length, 0, + `${description}"input" event shouldn't be fired since the input element's type is changed`); + } + input.type = "text"; + input.removeEventListener("beforeinput", recordEvent); + input.removeEventListener("input", recordEvent); + + description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText" and destroying the frame ${condition}: `; + input.value = "abc"; + if (!Reset()) { + return; + } + input.addEventListener("beforeinput", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`); + is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`); + is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`); + input.addEventListener("beforeinput", recordEvent); + input.addEventListener("input", recordEvent); + input.value = "hig"; + input.style.display = "none"; + if (aPreventDefaultOfBeforeInput) { + aEvent.preventDefault(); + } + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("def"); + is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`); + if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) { + is(input.value, "hig", + `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`); + is(inputEvents.length, 0, + `${description}"input" event shouldn't be fired since "beforeinput" was canceled`); + } else { + // XXX This result is same as Chrome (verified with spellchecker). + // But this behavior is not consistent with just setting the value on Chrome. + is(input.value, "hig", + `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`); + // Different from Chrome + is(inputEvents.length, 1, + `${description}"input" event should be fired even if the frame of target is destroyed`); + if (inputEvents.length) { + is(inputEvents[0].inputType, + "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`); + is(inputEvents[0].data, "def", + `${description}data of "input" event should be the value specified by setUserInput()`); + } + } + input.style.display = "inline"; + input.removeEventListener("beforeinput", recordEvent); + input.removeEventListener("input", recordEvent); + + if (aWithEditor) { + return; + } + + description = `Setting value from "beforeinput" event listener whose inputType is "insertReplacementText and create editor" ${condition}: `; + input.value = "abc"; + if (!Reset()) { + return; + } + input.addEventListener("beforeinput", (aEvent) => { + is(aEvent.inputType, "insertReplacementText", `${description}inputType of "beforeinput" event should be "insertReplacementText"`); + is(aEvent.cancelable, kSetUserInputCancelable, `${description}"beforeinput" event should be cancelable unless it's suppressed by the pref`); + is(input.value, "abc", `${description}The value shouldn't have been modified yet at "beforeinput" event listener`); + input.addEventListener("beforeinput", recordEvent); + input.addEventListener("input", recordEvent); + input.value = "hig"; + input.focus(); + if (aPreventDefaultOfBeforeInput) { + aEvent.preventDefault(); + } + }, {once: true}); + SpecialPowers.wrap(input).setUserInput("def"); + is(beforeInputEvents.length, 0, `${description}"beforeinput" event shouldn't be fired again`); + if (aPreventDefaultOfBeforeInput && kSetUserInputCancelable) { + is(input.value, "hig", + `${description}The value should be set to the specified value in "beforeinput" event listener since "beforeinput" was canceled`); + is(inputEvents.length, 0, + `${description}"input" event shouldn't be fired since "beforeinput" was canceled`); + } else { + // XXX This result is different from Chrome (verified with spellchecker). + // Chrome inserts the new text to current value and selected range. + // It might be reasonable, but we don't touch this for now since it + // requires a lot of changes. + is(input.value, "hig", + `${description}The value should be set to the specified value in "beforeinput" event listener since the event target was already modified`); + is(inputEvents.length, 1, `${description}"input" event should be fired`); + if (inputEvents.length) { + is(inputEvents[0].inputType, + "insertReplacementText", `${description}inputType of "input" event should be "insertReplacementText"`); + is(inputEvents[0].data, "def", + `${description}data of "input" event should be the value specified by setUserInput()`); + } + } + input.removeEventListener("beforeinput", recordEvent); + input.removeEventListener("input", recordEvent); + } + // testSettingValueFromBeforeInput(true, true); + // testSettingValueFromBeforeInput(true, false); + testSettingValueFromBeforeInput(false, true); + testSettingValueFromBeforeInput(false, false); + + SimpleTest.finish(); +}); +</script> +</body> +</html> diff --git a/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html b/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html new file mode 100644 index 0000000000..79a0f3d15a --- /dev/null +++ b/dom/html/test/forms/test_input_textarea_set_value_no_scroll.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=829606 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 829606</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 829606 **/ + /* + * This test checks that setting .value on an text field (input or textarea) + * doesn't scroll the field to its beginning. + */ + + SimpleTest.waitForExplicitFinish(); + + var gTestRunner = null; + + async function test(aElementName) + { + var element = document.getElementsByTagName(aElementName)[0]; + element.focus(); + + var baseSnapshot = await snapshotWindow(window); + + // This is a sanity check. + var s2 = await snapshotWindow(window); + var results = compareSnapshots(baseSnapshot, await snapshotWindow(window), true); + ok(results[0], "sanity check: screenshots should be the same"); + + element.selectionStart = element.selectionEnd = element.value.length; + + setTimeout(function() { + sendString('f'); + + requestAnimationFrame(async function() { + var selectionAtTheEndSnapshot = await snapshotWindow(window); + results = compareSnapshots(baseSnapshot, selectionAtTheEndSnapshot, false); + ok(results[0], "after appending a character, string should have changed"); + + // Re-setting value shouldn't change anything. + // eslint-disable-next-line no-self-assign + element.value = element.value; + var tmpSnapshot = await snapshotWindow(window); + + results = compareSnapshots(baseSnapshot, tmpSnapshot, false); + ok(results[0], "re-setting the value should change nothing"); + + results = compareSnapshots(selectionAtTheEndSnapshot, tmpSnapshot, true); + ok(results[0], "re-setting the value should change nothing"); + + element.selectionStart = element.selectionEnd = 0; + element.blur(); + + gTestRunner.next(); + }); + }, 0); + } + + // This test checks that when a textarea has a long list of values and the + // textarea's value is then changed, the values are shown correctly. + async function testCorrectUpdateOnScroll() + { + var textarea = document.createElement('textarea'); + textarea.rows = 5; + textarea.cols = 10; + textarea.value = 'a\nb\nc\nd'; + document.getElementById('content').appendChild(textarea); + + var baseSnapshot = await snapshotWindow(window); + + textarea.value = '1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n'; + textarea.selectionStart = textarea.selectionEnd = textarea.value.length; + + var fullSnapshot = await snapshotWindow(window); + var results = compareSnapshots(baseSnapshot, fullSnapshot, false); + ok(results[0], "sanity check: screenshots should not be the same"); + + textarea.value = 'a\nb\nc\nd'; + + var tmpSnapshot = await snapshotWindow(window); + results = compareSnapshots(baseSnapshot, tmpSnapshot, true); + ok(results[0], "textarea view should look like the beginning"); + + setTimeout(function() { + gTestRunner.next(); + }, 0); + } + + function* runTest() + { + test('input'); + yield undefined; + test('textarea'); + yield undefined; + testCorrectUpdateOnScroll(); + yield undefined; + SimpleTest.finish(); + } + + gTestRunner = runTest(); + + SimpleTest.waitForFocus(function() { + gTestRunner.next(); + });; + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=829606">Mozilla Bug 829606</a> +<p id="display"></p> +<div id="content"> + <textarea rows='1' cols='5' style='-moz-appearance:none;'>this is a \n long text</textarea> + <input size='5' value="this is a very long text" style='-moz-appearance:none;'> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_time_key_events.html b/dom/html/test/forms/test_input_time_key_events.html new file mode 100644 index 0000000000..c738816653 --- /dev/null +++ b/dom/html/test/forms/test_input_time_key_events.html @@ -0,0 +1,221 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1288591 +--> +<head> + <title>Test key events for time control</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1288591">Mozilla Bug 1288591</a> +<p id="display"></p> +<div id="content"> + <input id="input" type="time"> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +var testData = [ + /** + * keys: keys to send to the input element. + * initialVal: initial value set to the input element. + * expectedVal: expected value of the input element after sending the keys. + */ + { + // Type 16 in the hour field will automatically change time to 4PM in 12-hour clock + keys: ["16"], + initialVal: "01:00", + expectedVal: "16:00" + }, + { + // Type 00 in hour field will automatically convert to 12AM in 12-hour clock + keys: ["00"], + initialVal: "03:00", + expectedVal: "00:00" + }, + { + // Type hour > 23 such as 24 will automatically convert to 2 + keys: ["24"], + initialVal: "04:00", + expectedVal: "02:00" + }, + { + // Type 1030 and select AM. + keys: ["1030"], + initialVal: "", + expectedVal: "10:30" + }, + { + // Type 3 in the hour field will automatically advance to the minute field. + keys: ["330"], + initialVal: "", + expectedVal: "03:30" + }, + { + // Type 5 in the hour field will automatically advance to the minute field. + // Type 7 in the minute field will automatically advance to the AM/PM field. + keys: ["57"], + initialVal: "", + expectedVal: "05:07" + }, + { + // Advance to AM/PM field and change to PM. + keys: ["KEY_Tab", "KEY_Tab", "KEY_ArrowDown"], + initialVal: "10:30", + expectedVal: "22:30" + }, + { + // Right key should do the same thing as TAB key. + keys: ["KEY_ArrowRight", "KEY_ArrowRight", "KEY_ArrowDown"], + initialVal: "10:30", + expectedVal: "22:30" + }, + { + // Advance to minute field then back to hour field and decrement. + keys: ["KEY_ArrowRight", "KEY_ArrowLeft", "KEY_ArrowDown"], + initialVal: "10:30", + expectedVal: "09:30" + }, + { + // Focus starts on the first field, hour in this case, and increment. + keys: ["KEY_ArrowUp"], + initialVal: "16:00", + expectedVal: "17:00" + }, + { + // Advance to minute field and decrement. + keys: ["KEY_Tab", "KEY_ArrowDown"], + initialVal: "16:00", + expectedVal: "16:59" + }, + { + // Advance to minute field and increment. + keys: ["KEY_Tab", "KEY_ArrowUp"], + initialVal: "16:59", + expectedVal: "16:00" + }, + { + // PageUp on hour field increments hour by 3. + keys: ["KEY_PageUp"], + initialVal: "05:00", + expectedVal: "08:00" + }, + { + // PageDown on hour field decrements hour by 3. + keys: ["KEY_PageDown"], + initialVal: "05:00", + expectedVal: "02:00" + }, + { + // PageUp on minute field increments minute by 10. + keys: ["KEY_Tab", "KEY_PageUp"], + initialVal: "14:00", + expectedVal: "14:10" + }, + { + // PageDown on minute field decrements minute by 10. + keys: ["KEY_Tab", "KEY_PageDown"], + initialVal: "14:00", + expectedVal: "14:50" + }, + { + // Home key on hour field sets it to the minimum hour, which is 1 in 12-hour + // clock. + keys: ["KEY_Home"], + initialVal: "03:10", + expectedVal: "01:10" + }, + { + // End key on hour field sets it to the maximum hour, which is 12PM in 12-hour + // clock. + keys: ["KEY_End"], + initialVal: "03:10", + expectedVal: "12:10" + }, + { + // Home key on minute field sets it to the minimum minute, which is 0. + keys: ["KEY_Tab", "KEY_Home"], + initialVal: "19:30", + expectedVal: "19:00" + }, + { + // End key on minute field sets it to the minimum minute, which is 59. + keys: ["KEY_Tab", "KEY_End"], + initialVal: "19:30", + expectedVal: "19:59" + }, + // Second field will show up when needed. + { + // PageUp on second field increments second by 10. + keys: ["KEY_Tab", "KEY_Tab", "KEY_PageUp"], + initialVal: "08:10:10", + expectedVal: "08:10:20" + }, + { + // PageDown on second field increments second by 10. + keys: ["KEY_Tab", "KEY_Tab", "KEY_PageDown"], + initialVal: "08:10:10", + expectedVal: "08:10:00" + }, + { + // Home key on second field sets it to the minimum second, which is 0. + keys: ["KEY_Tab", "KEY_Tab", "KEY_Home"], + initialVal: "16:00:30", + expectedVal: "16:00:00" + }, + { + // End key on second field sets it to the minimum second, which is 59. + keys: ["KEY_Tab", "KEY_Tab", "KEY_End"], + initialVal: "16:00:30", + expectedVal: "16:00:59" + }, + { + // Incomplete value maps to empty .value. + keys: ["1"], + initialVal: "", + expectedVal: "" + }, +]; + +function sendKeys(aKeys, aElem) { + for (let i = 0; i < aKeys.length; i++) { + // Force layout flush between keys to ensure focus is correct. + // This shouldn't be necessary; bug 1450219 tracks this. + aElem.clientTop; + let key = aKeys[i]; + if (key.startsWith("KEY_")) { + synthesizeKey(key); + } else { + sendString(key); + } + } +} + +function test() { + var elem = document.getElementById("input"); + + for (let { keys, initialVal, expectedVal } of testData) { + elem.focus(); + elem.value = initialVal; + sendKeys(keys, elem); + is(elem.value, expectedVal, + "Test with " + keys + ", result should be " + expectedVal); + elem.value = ""; + elem.blur(); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_time_sec_millisec_field.html b/dom/html/test/forms/test_input_time_sec_millisec_field.html new file mode 100644 index 0000000000..71db4942a9 --- /dev/null +++ b/dom/html/test/forms/test_input_time_sec_millisec_field.html @@ -0,0 +1,134 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1374967 +--> +<head> + <title>Test second and millisecond fields in input type=time</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <meta charset="UTF-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1374967">Mozilla Bug 1374967</a> +<p id="display"></p> +<div id="content"> + <input id="input1" type="time"> + <input id="input2" type="time" value="12:30:40"> + <input id="input3" type="time" value="12:30:40.567"> + <input id="input4" type="time" step="1"> + <input id="input5" type="time" step="61"> + <input id="input6" type="time" step="120"> + <input id="input7" type="time" step="0.01"> + <input id="input8" type="time" step="0.001"> + <input id="input9" type="time" step="1.001"> + <input id="input10" type="time" min="01:30:05"> + <input id="input11" type="time" min="01:30:05.100"> + <input id="dummy"> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + test(); + SimpleTest.finish(); +}); + +const NUM_OF_FIELDS_DEFAULT = 3; +const NUM_OF_FIELDS_WITH_SECOND = NUM_OF_FIELDS_DEFAULT + 1; +const NUM_OF_FIELDS_WITH_MILLISEC = NUM_OF_FIELDS_WITH_SECOND + 1; + +function countNumberOfFields(aElement) { + is(aElement.type, "time", "Input element type should be 'time'"); + + let inputRect = aElement.getBoundingClientRect(); + let firstField_X = 15; + let firstField_Y = inputRect.height / 2; + + // Make sure to start on the first field. + synthesizeMouse(aElement, firstField_X, firstField_Y, {}); + is(document.activeElement, aElement, "Input element should be focused"); + + let n = 0; + while (document.activeElement == aElement) { + n++; + synthesizeKey("KEY_Tab"); + } + + return n; +} + +function test() { + // Normal input time element. + let elem = document.getElementById("input1"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_DEFAULT, "Default input time"); + + // Dynamically changing the value with second part. + elem.value = "10:20:30"; + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND, + "Input time after changing value with second part"); + + // Dynamically changing the step to 1 millisecond. + elem.step = "0.001"; + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC, + "Input time after changing step to 1 millisecond"); + + // Input time with value with second part. + elem = document.getElementById("input2"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND, + "Input time with value with second part"); + + // Input time with value with second and millisecond part. + elem = document.getElementById("input3"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC, + "Input time with value with second and millisecond part"); + + // Input time with step set as 1 second. + elem = document.getElementById("input4"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND, + "Input time with step set as 1 second"); + + // Input time with step set as 61 seconds. + elem = document.getElementById("input5"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND, + "Input time with step set as 61 seconds"); + + // Input time with step set as 2 minutes. + elem = document.getElementById("input6"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_DEFAULT, + "Input time with step set as 2 minutes"); + + // Input time with step set as 10 milliseconds. + elem = document.getElementById("input7"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC, + "Input time with step set as 10 milliseconds"); + + // Input time with step set as 100 milliseconds. + elem = document.getElementById("input8"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC, + "Input time with step set as 100 milliseconds"); + + // Input time with step set as 1001 milliseconds. + elem = document.getElementById("input9"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC, + "Input time with step set as 1001 milliseconds"); + + // Input time with min with second part and default step (60 seconds). Note + // that step base is min, when there is a min. + elem = document.getElementById("input10"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_SECOND, + "Input time with min with second part"); + + // Input time with min with second and millisecond part and default step (60 + // seconds). Note that step base is min, when there is a min. + elem = document.getElementById("input11"); + is(countNumberOfFields(elem), NUM_OF_FIELDS_WITH_MILLISEC, + "Input time with min with second and millisecond part"); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_types_pref.html b/dom/html/test/forms/test_input_types_pref.html new file mode 100644 index 0000000000..1222e88a86 --- /dev/null +++ b/dom/html/test/forms/test_input_types_pref.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=764481 +--> +<head> + <title>Test for Bug 764481</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=764481">Mozilla Bug 764481</a> +<p id="display"></p> +<div id="content" style="display: none" > +</div> +<pre id="test"> +<script type="application/javascript"> + + var input = document.createElement("input"); + + var testData = [ + { + prefs: [["dom.forms.datetime.others", false]], + inputType: "month", + expectedType: "text" + }, { + prefs: [["dom.forms.datetime.others", false]], + inputType: "month", + expectedType: "text" + }, { + prefs: [["dom.forms.datetime.others", true]], + inputType: "month", + expectedType: "month" + }, { + prefs: [["dom.forms.datetime.others", false]], + inputType: "week", + expectedType: "text" + }, { + prefs: [["dom.forms.datetime.others", false]], + inputType: "week", + expectedType: "text" + }, { + prefs: [["dom.forms.datetime.others", true]], + inputType: "week", + expectedType: "week" + } + ]; + + function testInputTypePreference(aData) { + return SpecialPowers.pushPrefEnv({'set': aData.prefs}) + .then(() => { + // Change the type of input to text and then back to the tested input type, + // so that HTMLInputElement::ParseAttribute gets called with the pref enabled. + input.type = "text"; + input.type = aData.inputType; + is(input.type, aData.expectedType, "input type should be '" + + aData.expectedType + "'' when pref " + aData.prefs + " is set"); + is(input.getAttribute('type'), aData.inputType, + "input 'type' attribute should not change"); + }); + } + + SimpleTest.waitForExplicitFinish(); + + let promise = Promise.resolve(); + for (let i = 0; i < testData.length; i++) { + let data = testData[i]; + promise = promise.then(() => testInputTypePreference(data)); + } + + promise.catch(error => ok(false, "Promise reject: " + error)) + .then(() => SimpleTest.finish()); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_typing_sanitization.html b/dom/html/test/forms/test_input_typing_sanitization.html new file mode 100644 index 0000000000..fef0ebed06 --- /dev/null +++ b/dom/html/test/forms/test_input_typing_sanitization.html @@ -0,0 +1,217 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=765772 +--> +<head> + <title>Test for Bug 765772</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug 765772</a> +<p id="display"></p> +<iframe name="submit_frame" style="visibility: hidden;"></iframe> +<div id="content"> + <form id='f' target="submit_frame" action="foo"> + <input name=i id="i" step='any' > + </form> +</div> +<pre id="test"> +<script> + +/* + * This test checks that when a user types in some input types, it will not be + * in a state where the value will be un-sanitized and usable (by a script). + */ + +var input = document.getElementById('i'); +var form = document.getElementById('f'); +var submitFrame = document.getElementsByTagName('iframe')[0]; +var testData = []; +var gCurrentTest = null; +var gValidData = []; +var gInvalidData = []; + +function submitForm() { + form.submit(); +} + +function sendKeyEventToSubmitForm() { + sendKey("return"); +} + +function urlify(aStr) { + return aStr.replace(/:/g, '%3A'); +} + +function runTestsForNextInputType() +{ + let {done} = testRunner.next(); + if (done) { + SimpleTest.finish(); + } +} + +function checkValueSubmittedIsValid() +{ + is(frames.submit_frame.location.href, + `${location.origin}/tests/dom/html/test/forms/foo?i=${urlify(gValidData[valueIndex++])}`, + "The submitted value should not have been sanitized"); + + input.value = ""; + + if (valueIndex >= gValidData.length) { + if (gCurrentTest.canHaveBadInputValidityState) { + // Don't run the submission tests on the invalid input if submission + // will be blocked by invalid input. + runTestsForNextInputType(); + return; + } + valueIndex = 0; + submitFrame.onload = checkValueSubmittedIsInvalid; + testData = gInvalidData; + } + testSubmissions(); +} + +function checkValueSubmittedIsInvalid() +{ + is(frames.submit_frame.location.href, + `${location.origin}/tests/dom/html/test/forms/foo?i=`, + "The submitted value should have been sanitized"); + + valueIndex++; + input.value = ""; + + if (valueIndex >= gInvalidData.length) { + if (submitMethod == sendKeyEventToSubmitForm) { + runTestsForNextInputType(); + return; + } + valueIndex = 0; + submitMethod = sendKeyEventToSubmitForm; + submitFrame.onload = checkValueSubmittedIsValid; + testData = gValidData; + } + testSubmissions(); +} + +function testSubmissions() { + input.focus(); + sendString(testData[valueIndex]); + submitMethod(); +} + +var valueIndex = 0; +var submitMethod = submitForm; + +SimpleTest.waitForExplicitFinish(); + +function* runTest() +{ + SimpleTest.requestLongerTimeout(4); + + var data = [ + { + type: 'number', + canHaveBadInputValidityState: true, + validData: [ + "42", + "-42", // should work for negative values + "42.1234", + "123.123456789123", // double precision + "1e2", // e should be usable + "2e1", + "1e-1", // value after e can be negative + "1E2", // E can be used instead of e + ], + invalidData: [ + "e", + "e2", + "1e0.1", + "foo", + "42,13", // comma can't be used as a decimal separator + ] + }, + { + type: 'month', + validData: [ + '0001-01', + '2012-12', + '100000-01', + ], + invalidData: [ + '1-01', + '-', + 'december', + '2012-dec', + '2012/12', + '2012-99', + '2012-1', + ] + }, + { + type: 'week', + validData: [ + '0001-W01', + '1970-W53', + '100000-W52', + '2016-W30', + ], + invalidData: [ + '1-W01', + 'week', + '2016-30', + '2010-W80', + '2000/W30', + '1985-W00', + '1000-W' + ] + }, + ]; + + for (test of data) { + gCurrentTest = test; + + input.type = test.type; + gValidData = test.validData; + gInvalidData = test.invalidData; + + for (data of gValidData) { + input.value = ""; + input.focus(); + sendString(data); + input.blur(); + is(input.value, data, "valid user input should not be sanitized"); + } + + for (data of gInvalidData) { + input.value = ""; + input.focus(); + sendString(data); + input.blur(); + is(input.value, "", "invalid user input should be sanitized"); + } + + input.value = ''; + + testData = gValidData; + valueIndex = 0; + submitFrame.onload = checkValueSubmittedIsValid; + testSubmissions(); + yield undefined; + } +} + +var testRunner = runTest(); + +addLoadEvent(function () { + testRunner.next(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_untrusted_key_events.html b/dom/html/test/forms/test_input_untrusted_key_events.html new file mode 100644 index 0000000000..78e35f525f --- /dev/null +++ b/dom/html/test/forms/test_input_untrusted_key_events.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for untrusted DOM KeyboardEvent on input element</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content"> + <input id="input"> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runNextTest, window); + +const kTests = [ + { type: "text", value: "foo", key: "b", expectedNewValue: "foo" }, + { type: "number", value: "123", key: "4", expectedNewValue: "123" }, + { type: "number", value: "123", key: KeyEvent.DOM_VK_UP, expectedNewValue: "123" }, + { type: "number", value: "123", key: KeyEvent.DOM_VK_DOWN, expectedNewValue: "123" }, +]; + +function sendUntrustedKeyEvent(eventType, keyCode, target) { + var evt = new KeyboardEvent(eventType, { + bubbles: true, + cancelable: true, + view: document.defaultView, + keyCode, + charCode: 0, + }); + target.dispatchEvent(evt); +} + +var input = document.getElementById("input"); + +var gotEvents = {}; + +function handleEvent(event) { + gotEvents[event.type] = true; +} + +input.addEventListener("keydown", handleEvent); +input.addEventListener("keyup", handleEvent); +input.addEventListener("keypress", handleEvent); + +var previousTest = null; + +function runNextTest() { + if (previousTest) { + var msg = "For <input " + "type=" + previousTest.type + ">, "; + is(gotEvents.keydown, true, msg + "checking got keydown"); + is(gotEvents.keyup, true, msg + "checking got keyup"); + is(gotEvents.keypress, true, msg + "checking got keypress"); + is(input.value, previousTest.expectedNewValue, msg + "checking element " + + " after being sent '" + previousTest.key + "' key events"); + } + + // reset flags + gotEvents.keydown = false; + gotEvents.keyup = false; + gotEvents.keypress = false; + + + var test = kTests.shift(); + if (!test) { + SimpleTest.finish(); + return; // We're all done + } + + input.type = test.type; + input.focus(); // make sure we still have focus after type change + input.value = test.value; + + sendUntrustedKeyEvent("keydown", test.key, input); + sendUntrustedKeyEvent("keyup", test.key, input); + sendUntrustedKeyEvent("keypress", test.key, input); + + previousTest = test; + + SimpleTest.executeSoon(runNextTest); +}; + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_input_url.html b/dom/html/test/forms/test_input_url.html new file mode 100644 index 0000000000..3cdf1070bb --- /dev/null +++ b/dom/html/test/forms/test_input_url.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Tests for <input type='url'> validity</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + <input type='url' id='i' oninvalid='invalidEventHandler(event);'> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Tests for <input type='url'> validity **/ + +// More checks are done in test_bug551670.html. + +var gInvalid = false; + +function invalidEventHandler(e) +{ + is(e.type, "invalid", "Invalid event type should be invalid"); + gInvalid = true; +} + +function checkValidURL(element) +{ + info(`Checking ${element.value}\n`); + gInvalid = false; + ok(!element.validity.typeMismatch, + "Element should not suffer from type mismatch"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "Element should be valid"); + ok(!gInvalid, "The invalid event should not have been thrown"); + is(element.validationMessage, '', + "Validation message should be the empty string"); + ok(element.matches(":valid"), ":valid pseudo-class should apply"); +} + +function checkInvalidURL(element) +{ + gInvalid = false; + ok(element.validity.typeMismatch, + "Element should suffer from type mismatch"); + ok(!element.validity.valid, "Element should not be valid"); + ok(!element.checkValidity(), "Element should not be valid"); + ok(gInvalid, "The invalid event should have been thrown"); + is(element.validationMessage, "Please enter a URL.", + "Validation message should be related to invalid URL"); + ok(element.matches(":invalid"), + ":invalid pseudo-class should apply"); +} + +var url = document.getElementById('i'); + +var values = [ + // [ value, validity ] + // The empty string should be considered as valid. + [ "", true ], + [ "foo", false ], + [ "http://mozilla.com/", true ], + [ "http://mozilla.com", true ], + [ "http://mozil\nla\r.com/", true ], + [ " http://mozilla.com/ ", true ], + [ "\r http://mozilla.com/ \n", true ], + [ "file:///usr/bin/tulip", true ], + [ "../../bar.html", false ], + [ "http://mozillá.org", true ], + [ "https://mózillä.org", true ], + [ "http://mózillä.órg", true ], + [ "ht://mózillä.órg", true ], + [ "httŭ://mózillä.órg", false ], + [ "chrome://bookmarks", true ], +]; + +values.forEach(function([value, valid]) { + url.value = value; + + if (valid) { + checkValidURL(url); + } else { + checkInvalidURL(url); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_interactive_content_in_label.html b/dom/html/test/forms/test_interactive_content_in_label.html new file mode 100644 index 0000000000..b8d9c81d51 --- /dev/null +++ b/dom/html/test/forms/test_interactive_content_in_label.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=229925 +--> +<head> + <title>Test for Bug 229925</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=229925">Mozilla Bug 229925</a> +<p id="display"></p> +<form action="#"> + <label> + <span id="text">label</span> + <input type="button" id="target" value="target"> + + <a class="yes" href="#">a</a> + <audio class="yes" controls></audio> + <button class="yes">button</button> + <details class="yes">details</details> + <embed class="yes">embed</embed> + <iframe class="yes" src="data:text/plain," style="width: 16px; height: 16px;"></iframe> + <img class="yes" src="data:image/png," usemap="#map"> + <input class="yes" type="text" size="4"> + <keygen class="no"> + <label class="yes">label</label> + <object class="yes" usemap="#map">object</object> + <select class="yes"><option>select</option></select> + <textarea class="yes" cols="1" rows="1"></textarea> + <video class="yes" controls></video> + + <!-- Tests related to shadow tree. --> + <div id="root1"> <!-- content will be added by script below. --> </div> + <button><div id="root2"> <!-- content will be added by script below. --> </div></button> + + <a class="no">a</a> + <audio class="no"></audio> + <img class="no" src="data:image/png,"> + <input class="no" type="hidden"> + <object class="no">object</object> + <video class="no"></video> + + <span class="no" tabindex="1">tabindex</span> + <audio class="no" tabindex="1"></audio> + <img class="no" src="data:image/png," tabindex="1"> + <input class="no" type="hidden" tabindex="1"> + <object class="no" tabindex="1">object</object> + <video class="no" tabindex="1"></video> + </label> +</form> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 229925 **/ + +var target = document.getElementById("target"); + +var yes_nodes = Array.from(document.getElementsByClassName("yes")); + +var root1 = document.getElementById("root1"); +root1.attachShadow({ mode: "open" }).innerHTML = "<button class=yes>button in shadow tree</button>"; +var root2 = document.getElementById("root2"); +root2.attachShadow({ mode: "open" }).innerHTML = "<div class=yes>text in shadow tree</div>"; +var yes_nodes_in_shadow_tree = + Array.from(root1.shadowRoot.querySelectorAll(".yes")).concat( + Array.from(root2.shadowRoot.querySelectorAll(".yes"))); + +var no_nodes = Array.from(document.getElementsByClassName("no")); + +var target_clicked = false; +target.addEventListener("click", function() { + target_clicked = true; +}); + +var node; +for (node of yes_nodes) { + target_clicked = false; + node.click(); + is(target_clicked, false, "mouse click on interactive content " + node.nodeName + " shouldn't dispatch event to label target"); +} + +for (node of yes_nodes_in_shadow_tree) { + target_clicked = false; + node.click(); + is(target_clicked, false, "mouse click on content in shadow tree " + node.nodeName + " shouldn't dispatch event to label target"); +} + +for (node of no_nodes) { + target_clicked = false; + node.click(); + is(target_clicked, true, "mouse click on non interactive content " + node.nodeName + " should dispatch event to label target"); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/forms/test_interactive_content_in_summary.html b/dom/html/test/forms/test_interactive_content_in_summary.html new file mode 100644 index 0000000000..f8bac77d89 --- /dev/null +++ b/dom/html/test/forms/test_interactive_content_in_summary.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1524893 +--> +<head> + <title>Test for Bug 1524893</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1524893">Mozilla Bug 1524893</a> + +<details id="details"> + <summary> + <a class="yes" href="#">a</a> + <audio class="yes" controls></audio> + <button class="yes">button</button> + <details class="yes">details</details> + <embed class="yes">embed</embed> + <iframe class="yes" src="data:text/plain," style="width: 16px; height: 16px;"></iframe> + <img class="yes" src="data:image/png," usemap="#map"> + <input class="yes" type="text" size="4"> + <keygen class="no"> + <label class="yes">label</label> + <object class="yes" usemap="#map">object</object> + <select class="yes"><option>select</option></select> + <textarea class="yes" cols="1" rows="1"></textarea> + <video class="yes" controls></video> + + <!-- Tests related to shadow tree. --> + <div id="root1"> <!-- content will be added by script below. --> </div> + <button><div id="root2"> <!-- content will be added by script below. --> </div></button> + + <a class="no">a</a> + <audio class="no"></audio> + <img class="no" src="data:image/png,"> + <input class="no" type="hidden"> + <object class="no">object</object> + <video class="no"></video> + + <span class="no" tabindex="1">tabindex</span> + <audio class="no" tabindex="1"></audio> + <img class="no" src="data:image/png," tabindex="1"> + <input class="no" type="hidden" tabindex="1"> + <object class="no" tabindex="1">object</object> + <video class="no" tabindex="1"></video> + </summary> + <div>This is details</div> +</details> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1524893 **/ + +var details = document.getElementById("details"); + +var yes_nodes = Array.from(document.getElementsByClassName("yes")); + +var root1 = document.getElementById("root1"); +root1.attachShadow({ mode: "open" }).innerHTML = "<button class=yes>button in shadow tree</button>"; +var root2 = document.getElementById("root2"); +root2.attachShadow({ mode: "open" }).innerHTML = "<div class=yes>text in shadow tree</div>"; +var yes_nodes_in_shadow_tree = + Array.from(root1.shadowRoot.querySelectorAll(".yes")).concat( + Array.from(root2.shadowRoot.querySelectorAll(".yes"))); + +var no_nodes = Array.from(document.getElementsByClassName("no")); + +var node; +for (node of yes_nodes) { + details.removeAttribute('open'); + node.click(); + ok(!details.hasAttribute('open'), + "mouse click on interactive content " + node.nodeName + " shouldn't not open details"); +} + +for (node of yes_nodes_in_shadow_tree) { + details.removeAttribute('open'); + node.click(); + ok(!details.hasAttribute('open'), + "mouse click on content in shadow tree " + node.nodeName + " shouldn't open details"); +} + +for (node of no_nodes) { + details.removeAttribute('open'); + node.click(); + ok(details.hasAttribute('open'), + "mouse click on non interactive content " + node.nodeName + " should open details"); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/forms/test_label_control_attribute.html b/dom/html/test/forms/test_label_control_attribute.html new file mode 100644 index 0000000000..efc04cd787 --- /dev/null +++ b/dom/html/test/forms/test_label_control_attribute.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=562932 +--> +<head> + <title>Test for Bug 562932</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=562932">Mozilla Bug 562932</a> +<p id="display"></p> +<div id="content" style="display: none"> + <!-- No @for, we have to check the content --> + <label id='l1'><input id='i1'></label> + <label id='l2'><input id='i2'><input></label> + <label id='l3'></label> + <label id='l4a'><fieldset id='f'>foo</fieldset></label> + <label id='l4b'><label id='l4c'><input id='i3'></label></label> + <label id='l4d'><label id='l4e'><input id='i3b'></label><input></label> + + <!-- With @for, we do no check the content --> + <label id='l5' for='i1'></label> + <label id='l6' for='i4'></label> + <label id='l7' for='i4'><input></label> + <label id='l8' for='i1 i2'></label> + <label id='l9' for='i1 i2'><input></label> + <label id='l10' for='f'></label> + <label id='l11' for='i4'></label> + <label id='l12' for='i5'></label> + <label id='l13' for=''><input></label> + <!-- <label id='l14'> is created in script --> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 562932 **/ + +function checkControl(aLabelId, aElementId, aMsg) +{ + var element = null; + + if (aElementId != null) { + element = document.getElementById(aElementId); + } + + is(document.getElementById(aLabelId).control, element, aMsg); +} + +ok('control' in document.createElement('label'), + "label element should have a control IDL attribute"); + +checkControl('l1', 'i1', "label control should be the first form element"); +checkControl('l2', 'i2', "label control should be the first form element"); +checkControl('l3', null, "label control should be null when there is no child"); +checkControl('l4a', null, "label control should be null when there is no \ + labelable form element child"); +checkControl('l4b', 'i3', "label control should be the first labelable element \ + in tree order"); +checkControl('l4c', 'i3', "label control should be the first labelable element \ + in tree order"); +checkControl('l4d', 'i3b', "label control should be the first labelable element \ + in tree order"); +checkControl('l4e', 'i3b', "label control should be the first labelable element \ + in tree order"); +checkControl('l5', 'i1', "label control should be the id in @for"); +checkControl('l6', null, + "label control should be null if the id in @for is not valid"); +checkControl('l7', null, + "label control should be null if the id in @for is not valid"); +checkControl('l8', null, + "label control should be null if there are more than one id in @for"); +checkControl('l9', null, + "label control should be null if there are more than one id in @for"); +checkControl('l10', null, "label control should be null if the id in @for \ + is not an id from a labelable form element"); + +var inputOutOfDocument = document.createElement('input'); +inputOutOfDocument.id = 'i4'; +checkControl('l11', null, "label control should be null if the id in @for \ + is not an id from an element in the document"); + +var inputInDocument = document.createElement('input'); +inputInDocument.id = 'i5'; +document.getElementById('content').appendChild(inputInDocument); +checkControl('l12', 'i5', "label control should be the id in @for"); + +checkControl('l13', null, "label control should be null if the id in @for \ + is empty"); + +var labelOutOfDocument = document.createElement('label'); +labelOutOfDocument.htmlFor = 'i1'; +is(labelOutOfDocument.control, null, "out of document label shouldn't \ + labelize a form control"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_label_input_controls.html b/dom/html/test/forms/test_label_input_controls.html new file mode 100644 index 0000000000..fe9410b608 --- /dev/null +++ b/dom/html/test/forms/test_label_input_controls.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=597650 +--> +<head> + <title>Test for Bug 597650</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=597650">Mozilla Bug 597650</a> + <p id="display"></p> + <div id="content"> + <label id="l"> + <input id="h"></input> + <input type="text" id="i"></input> + </label> + <label id="lh" for="h"></label> + </div> + <pre id="test"> + <script class="testbody" type="text/javascript"> + /** Test for Bug 597650 **/ + label = document.getElementById("l"); + labelForH = document.getElementById("lh"); + inputI = document.getElementById("i"); + inputH = document.getElementById("h"); + + var labelableTypes = ["text", "search", "tel", "url", "email", "password", + "datetime", "date", "month", "week", "time", + "number", "range", "color", "checkbox", "radio", + "file", "submit", "image", "reset", "button"]; + var nonLabelableTypes = ["hidden"]; + + for (var i in labelableTypes) { + test(labelableTypes[i], true); + } + + for (var i in nonLabelableTypes) { + test(nonLabelableTypes[i], false); + } + + function test(type, isLabelable) { + inputH.type = type; + if (isLabelable) { + testControl(label, inputH, type, true); + testControl(labelForH, inputH, type, true); + } else { + testControl(label, inputI, type, false); + testControl(labelForH, null, type, false); + + inputH.type = "text"; + testControl(label, inputH, "text", true); + testControl(labelForH, inputH, "text", true); + + inputH.type = type; + testControl(label, inputI, type, false); + testControl(labelForH, null, type, false); + + label.removeChild(inputH); + testControl(label, inputI, "text", true); + + var element = document.createElement('input'); + element.type = type; + label.insertBefore(element, inputI); + testControl(label, inputI, "text", true); + } + } + + function testControl(label, control, type, labelable) { + if (labelable) { + is(label.control, control, "Input controls of type " + type + + " should be labeled"); + } else { + is(label.control, control, "Input controls of type " + type + + " should be ignored by <label>"); + } + } + </script> + </pre> + </body> +</html> + diff --git a/dom/html/test/forms/test_max_attribute.html b/dom/html/test/forms/test_max_attribute.html new file mode 100644 index 0000000000..f6e9c9bd8e --- /dev/null +++ b/dom/html/test/forms/test_max_attribute.html @@ -0,0 +1,473 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=635499 +--> +<head> + <title>Test for Bug 635499</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635499">Mozilla Bug 635499</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 635499 **/ + +var data = [ + { type: 'hidden', apply: false }, + { type: 'text', apply: false }, + { type: 'search', apply: false }, + { type: 'tel', apply: false }, + { type: 'url', apply: false }, + { type: 'email', apply: false }, + { type: 'password', apply: false }, + { type: 'date', apply: true }, + { type: 'month', apply: true }, + { type: 'week', apply: true }, + { type: 'time', apply: true }, + { type: 'datetime-local', apply: true }, + { type: 'number', apply: true }, + { type: 'range', apply: true }, + { type: 'color', apply: false }, + { type: 'checkbox', apply: false }, + { type: 'radio', apply: false }, + { type: 'file', apply: false }, + { type: 'submit', apply: false }, + { type: 'image', apply: false }, + { type: 'reset', apply: false }, + { type: 'button', apply: false }, +]; + +var input = document.createElement("input"); +document.getElementById('content').appendChild(input); + +/** + * @aValidity - boolean indicating whether the element is expected to be valid + * (aElement.validity.valid is true) or not. The value passed is ignored and + * overridden with true if aApply is false. + * @aApply - boolean indicating whether the min/max attributes apply to this + * element type. + * @aRangeApply - A boolean that's set to true if the current input type is a + * "[candidate] for constraint validation" and it "[has] range limitations" + * per http://www.whatwg.org/specs/web-apps/current-work/multipage/selectors.html#selector-in-range + * (in other words, one of the pseudo classes :in-range and :out-of-range + * should apply (which, depends on aValidity)). + * Else (neither :in-range or :out-of-range should match) set to false. + */ +function checkValidity(aElement, aValidity, aApply, aRangeApply) +{ + aValidity = aApply ? aValidity : true; + + is(aElement.validity.valid, aValidity, + "element validity should be " + aValidity); + is(aElement.validity.rangeOverflow, !aValidity, + "element overflow status should be " + !aValidity); + var overflowMsg = + (aElement.type == "date" || aElement.type == "time" || + aElement.type == "month" || aElement.type == "week" || + aElement.type == "datetime-local") ? + ("Please select a value that is no later than " + aElement.max + ".") : + ("Please select a value that is no more than " + aElement.max + "."); + is(aElement.validationMessage, + aValidity ? "" : overflowMsg, "Checking range overflow validation message"); + + is(aElement.matches(":valid"), aElement.willValidate && aValidity, + (aElement.willValidate && aValidity) ? ":valid should apply" : "valid shouldn't apply"); + is(aElement.matches(":invalid"), aElement.willValidate && !aValidity, + (aElement.wil && aValidity) ? ":invalid shouldn't apply" : "valid should apply"); + + if (!aRangeApply) { + ok(!aElement.matches(":in-range"), ":in-range should not match"); + ok(!aElement.matches(":out-of-range"), + ":out-of-range should not match"); + } else { + is(aElement.matches(":in-range"), aValidity, + ":in-range matches status should be " + aValidity); + is(aElement.matches(":out-of-range"), !aValidity, + ":out-of-range matches status should be " + !aValidity); + } +} + +for (var test of data) { + input.type = test.type; + var apply = test.apply; + + // The element should be valid. Range should not apply when @min and @max are + // undefined, except if the input type is 'range' (since that type has a + // default minimum and maximum). + if (input.type == 'range') { + checkValidity(input, true, apply, true); + } else { + checkValidity(input, true, apply, false); + } + checkValidity(input, true, apply, test.type == 'range'); + + switch (input.type) { + case 'hidden': + case 'text': + case 'search': + case 'password': + case 'url': + case 'tel': + case 'email': + case 'number': + case 'checkbox': + case 'radio': + case 'file': + case 'submit': + case 'reset': + case 'button': + case 'image': + case 'color': + input.max = '-1'; + break; + case 'date': + input.max = '2012-06-27'; + break; + case 'time': + input.max = '02:20'; + break; + case 'range': + // range is special, since setting max to -1 will make it invalid since + // it's default would then be 0, meaning it suffers from overflow. + input.max = '-1'; + checkValidity(input, false, apply, apply); + // Now make it something that won't cause an error below: + input.max = '10'; + break; + case 'month': + input.max = '2016-12'; + break; + case 'week': + input.max = '2016-W39'; + break; + case 'datetime-local': + input.max = '2016-12-31T23:59:59'; + break; + default: + ok(false, 'please, add a case for this new type (' + input.type + ')'); + } + + checkValidity(input, true, apply, apply); + + switch (input.type) { + case 'text': + case 'hidden': + case 'search': + case 'password': + case 'tel': + case 'radio': + case 'checkbox': + case 'reset': + case 'button': + case 'submit': + case 'image': + input.value = '0'; + checkValidity(input, true, apply, apply); + break; + case 'url': + input.value = 'http://mozilla.org'; + checkValidity(input, true, apply, apply); + break; + case 'email': + input.value = 'foo@bar.com'; + checkValidity(input, true, apply, apply); + break; + case 'file': + var file = new File([''], '635499_file'); + + SpecialPowers.wrap(input).mozSetFileArray([file]); + checkValidity(input, true, apply, apply); + + break; + case 'date': + input.max = '2012-06-27'; + input.value = '2012-06-26'; + checkValidity(input, true, apply, apply); + + input.value = '2012-06-27'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2012-06-28'; + checkValidity(input, false, apply, apply); + + input.max = '2012-06-30'; + checkValidity(input, true, apply, apply); + + input.value = '2012-07-05'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01-01'; + checkValidity(input, true, apply, apply); + + input.value = '20120-01-01'; + checkValidity(input, false, apply, apply); + + input.max = '0050-01-01'; + checkValidity(input, false, apply, apply); + + input.value = '0049-01-01'; + checkValidity(input, true, apply, apply); + + input.max = ''; + checkValidity(input, true, apply, false); + + input.max = 'foo'; + checkValidity(input, true, apply, false); + + break; + case 'number': + input.max = '2'; + input.value = '1'; + checkValidity(input, true, apply, apply); + + input.value = '2'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '3'; + checkValidity(input, false, apply, apply); + + input.max = '5'; + checkValidity(input, true, apply, apply); + + input.value = '42'; + checkValidity(input, false, apply, apply); + + input.max = ''; + checkValidity(input, true, apply, false); + + input.max = 'foo'; + checkValidity(input, true, apply, false); + + // Check that we correctly convert input.max to a double in validationMessage. + if (input.type == 'number') { + input.max = "4.333333333333333333333333333333333331"; + input.value = "5"; + is(input.validationMessage, + "Please select a value that is no more than 4.33333333333333.", + "validation message"); + } + + break; + case 'range': + input.max = '2'; + input.value = '1'; + checkValidity(input, true, apply, apply); + + input.value = '2'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '3'; + checkValidity(input, true, apply, apply); + + is(input.value, input.max, "the value should have been set to max"); + + input.max = '5'; + checkValidity(input, true, apply, apply); + + input.value = '42'; + checkValidity(input, true, apply, apply); + + is(input.value, input.max, "the value should have been set to max"); + + input.max = ''; + checkValidity(input, true, apply, apply); + + input.max = 'foo'; + checkValidity(input, true, apply, apply); + + // Check that we correctly convert input.max to a double in validationMessage. + input.step = 'any'; + input.min = 5; + input.max = 0.6666666666666666; + input.value = 1; + is(input.validationMessage, + "Please select a value that is no more than 0.666666666666667.", + "validation message") + + break; + case 'time': + // Don't worry about that. + input.step = 'any'; + + input.max = '10:10'; + input.value = '10:09'; + checkValidity(input, true, apply, apply); + + input.value = '10:10'; + checkValidity(input, true, apply, apply); + + input.value = '10:10:00'; + checkValidity(input, true, apply, apply); + + input.value = '10:10:00.000'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '10:11'; + checkValidity(input, false, apply, apply); + + input.value = '10:10:00.001'; + checkValidity(input, false, apply, apply); + + input.max = '01:00:00.01'; + input.value = '01:00:00.001'; + checkValidity(input, true, apply, apply); + + input.value = '01:00:00'; + checkValidity(input, true, apply, apply); + + input.value = '01:00:00.1'; + checkValidity(input, false, apply, apply); + + input.max = ''; + checkValidity(input, true, apply, false); + + input.max = 'foo'; + checkValidity(input, true, apply, false); + + break; + case 'month': + input.value = '2016-06'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01'; + checkValidity(input, false, apply, apply); + + input.max = '2017-07'; + checkValidity(input, true, apply, apply); + + input.value = '2017-12'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01'; + checkValidity(input, true, apply, apply); + + input.value = '20160-01'; + checkValidity(input, false, apply, apply); + + input.max = '0050-01'; + checkValidity(input, false, apply, apply); + + input.value = '0049-12'; + checkValidity(input, true, apply, apply); + + input.max = ''; + checkValidity(input, true, apply, false); + + input.max = 'foo'; + checkValidity(input, true, apply, false); + + break; + case 'week': + input.value = '2016-W01'; + checkValidity(input, true, apply, apply); + + input.value = '2016-W39'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2017-W01'; + checkValidity(input, false, apply, apply); + + input.max = '2017-W01'; + checkValidity(input, true, apply, apply); + + input.value = '2017-W52'; + checkValidity(input, false, apply, apply); + + input.value = '1000-W01'; + checkValidity(input, true, apply, apply); + + input.value = '2100-W01'; + checkValidity(input, false, apply, apply); + + input.max = '0050-W01'; + checkValidity(input, false, apply, apply); + + input.value = '0049-W52'; + checkValidity(input, true, apply, apply); + + input.max = ''; + checkValidity(input, true, apply, false); + + input.max = 'foo'; + checkValidity(input, true, apply, false); + + break; + case 'datetime-local': + input.value = '2016-01-01T12:00'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12-31T23:59:59'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12-31T23:59:59.123'; + checkValidity(input, false, apply, apply); + + input.value = '2017-01-01T10:00'; + checkValidity(input, false, apply, apply); + + input.max = '2017-01-01T10:00'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01-01T10:00:30'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01-01T12:00'; + checkValidity(input, true, apply, apply); + + input.value = '2100-01-01T12:00'; + checkValidity(input, false, apply, apply); + + input.max = '0050-12-31T23:59:59.999'; + checkValidity(input, false, apply, apply); + + input.value = '0050-12-31T23:59:59'; + checkValidity(input, true, apply, apply); + + input.max = ''; + checkValidity(input, true, apply, false); + + input.max = 'foo'; + checkValidity(input, true, apply, false); + + break; + } + + // Cleaning up, + input.removeAttribute('max'); + input.value = ''; +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_maxlength_attribute.html b/dom/html/test/forms/test_maxlength_attribute.html new file mode 100644 index 0000000000..bd76e277e5 --- /dev/null +++ b/dom/html/test/forms/test_maxlength_attribute.html @@ -0,0 +1,129 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=345624 +--> +<head> + <title>Test for Bug 345624</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input, textarea { background-color: rgb(0,0,0) !important; } + :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; } + :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345624">Mozilla Bug 345624</a> +<p id="display"></p> +<div id="content"> + <input id='i'> + <textarea id='t'></textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 345624 **/ + +/** + * This test is checking only tooLong related features + * related to constraint validation. + */ + +function checkTooLongValidity(element) +{ + element.value = "foo"; + ok(!element.validity.tooLong, + "Element should not be too long when maxlength is not set"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.maxLength = 1; + ok(!element.validity.tooLong, + "Element should not be too long unless the user edits it"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.focus(); + + synthesizeKey("KEY_Backspace"); + is(element.value, "fo", "value should have changed"); + ok(element.validity.tooLong, + "Element should be too long after a user edit that does not make it short enough"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); + ok(!element.validity.valid, "Element should be invalid"); + ok(!element.checkValidity(), "The element should not be valid"); + is(element.validationMessage, + "Please shorten this text to 1 characters or less (you are currently using 2 characters).", + "The validation message text is not correct"); + + synthesizeKey("KEY_Backspace"); + is(element.value, "f", "value should have changed"); + ok(!element.validity.tooLong, + "Element should not be too long after a user edit makes it short enough"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + + element.maxLength = 2; + ok(!element.validity.tooLong, + "Element should remain valid if maxlength changes but maxlength > length"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + + element.maxLength = 1; + ok(!element.validity.tooLong, + "Element should remain valid if maxlength changes but maxlength = length"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.maxLength = 0; + ok(element.validity.tooLong, + "Element should become invalid if maxlength changes and maxlength < length"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); + ok(!element.validity.valid, "Element should be invalid"); + ok(!element.checkValidity(), "The element should not be valid"); + is(element.validationMessage, + "Please shorten this text to 0 characters or less (you are currently using 1 characters).", + "The validation message text is not correct"); + + element.maxLength = 1; + ok(!element.validity.tooLong, + "Element should become valid if maxlength changes and maxlength = length"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.value = "test"; + ok(!element.validity.tooLong, + "Element should stay valid after programmatic edit (even if value is too long)"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.setCustomValidity("custom message"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); + is(element.validationMessage, "custom message", + "Custom message should be shown instead of too long one"); +} + +checkTooLongValidity(document.getElementById('i')); +checkTooLongValidity(document.getElementById('t')); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_meter_element.html b/dom/html/test/forms/test_meter_element.html new file mode 100644 index 0000000000..5e1073d53d --- /dev/null +++ b/dom/html/test/forms/test_meter_element.html @@ -0,0 +1,376 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=657938 +--> +<head> + <title>Test for <meter></title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=657938">Mozilla Bug 657938</a> +<p id="display"></p> +<iframe name="submit_frame" style="visibility: hidden;"></iframe> +<div id="content" style="visibility: hidden;"> + <form id='f' method='get' target='submit_frame' action='foo'> + <meter id='m' value=0.5></meter> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for <meter> **/ + +function checkFormIDLAttribute(aElement) +{ + is('form' in aElement, false, "<meter> shouldn't have a form attribute"); +} + +function checkAttribute(aElement, aAttribute, aNewValue, aExpectedValueForIDL) +{ + var expectedValueForIDL = aNewValue; + var expectedValueForContent = String(aNewValue); + + if (aExpectedValueForIDL !== undefined) { + expectedValueForIDL = aExpectedValueForIDL; + } + + if (aNewValue != null) { + aElement.setAttribute(aAttribute, aNewValue); + is(aElement.getAttribute(aAttribute), expectedValueForContent, + aAttribute + " content attribute should be " + expectedValueForContent); + is(aElement[aAttribute], expectedValueForIDL, + aAttribute + " IDL attribute should be " + expectedValueForIDL); + + if (parseFloat(aNewValue) == aNewValue) { + aElement[aAttribute] = aNewValue; + is(aElement.getAttribute(aAttribute), expectedValueForContent, + aAttribute + " content attribute should be " + expectedValueForContent); + is(aElement[aAttribute], parseFloat(expectedValueForIDL), + aAttribute + " IDL attribute should be " + parseFloat(expectedValueForIDL)); + } + } else { + aElement.removeAttribute(aAttribute); + is(aElement.getAttribute(aAttribute), null, + aAttribute + " content attribute should be null"); + is(aElement[aAttribute], expectedValueForIDL, + aAttribute + " IDL attribute should be " + expectedValueForIDL); + } +} + +function checkValueAttribute() +{ + var tests = [ + // value has to be a valid float, its default value is 0.0 otherwise. + [ null, 0.0 ], + [ 'foo', 0.0 ], + // If value < 0.0, 0.0 is used instead. + [ -1.0, 0.0 ], + // If value >= max, max is used instead (max default value is 1.0). + [ 2.0, 1.0 ], + [ 1.0, 0.5, 0.5 ], + [ 10.0, 5.0, 5.0 ], + [ 13.37, 13.37, 42.0 ], + // If value <= min, min is used instead (min default value is 0.0). + [ 0.5, 1.0, 10.0 ,1.0 ], + [ 10.0, 13.37, 42.0 , 13.37], + // Regular reflection. + [ 0.0 ], + [ 0.5 ], + [ 1.0 ], + // Check double-precision value. + [ 0.234567898765432 ], + ]; + + var element = document.createElement('meter'); + + for (var test of tests) { + if (test[2]) { + element.setAttribute('max', test[2]); + } + + if (test[3]) { + element.setAttribute('min', test[3]); + } + + checkAttribute(element, 'value', test[0], test[1]); + + element.removeAttribute('max'); + element.removeAttribute('min'); + } +} + +function checkMinAttribute() +{ + var tests = [ + // min default value is 0.0. + [ null, 0.0 ], + [ 'foo', 0.0 ], + // Regular reflection. + [ 0.5 ], + [ 1.0 ], + [ 2.0 ], + // Check double-precision value. + [ 0.234567898765432 ], + ]; + + var element = document.createElement('meter'); + + for (var test of tests) { + checkAttribute(element, 'min', test[0], test[1]); + } +} + +function checkMaxAttribute() +{ + var tests = [ + // max default value is 1.0. + [ null, 1.0 ], + [ 'foo', 1.0 ], + // If value <= min, min is used instead. + [ -1.0, 0.0 ], + [ 0.0, 0.5, 0.5 ], + [ 10.0, 15.0, 15.0 ], + [ 42, 42, 13.37 ], + // Regular reflection. + [ 0.5 ], + [ 1.0 ], + [ 2.0 ], + // Check double-precision value. + [ 0.234567898765432 ], + ]; + + var element = document.createElement('meter'); + + for (var test of tests) { + if (test[2]) { + element.setAttribute('min', test[2]); + } + + checkAttribute(element, 'max', test[0], test[1]); + + element.removeAttribute('min'); + } +} + +function checkLowAttribute() +{ + var tests = [ + // low default value is min (min default value is 0.0). + [ null, 0.0 ], + [ 'foo', 0.0 ], + [ 'foo', 1.0, 1.0], + // If low <= min, min is used instead. + [ -1.0, 0.0 ], + [ 0.0, 0.5, 0.5 ], + [ 10.0, 15.0, 15.0, 42.0 ], + [ 42.0, 42.0, 13.37, 100.0 ], + // If low >= max, max is used instead. + [ 2.0, 1.0 ], + [ 10.0, 5.0 , 0.5, 5.0 ], + [ 13.37, 13.37, 0.0, 42.0 ], + // Regular reflection. + [ 0.0 ], + [ 0.5 ], + [ 1.0 ], + // Check double-precision value. + [ 0.234567898765432 ], + ]; + + var element = document.createElement('meter'); + + for (var test of tests) { + if (test[2]) { + element.setAttribute('min', test[2]); + } + if (test[3]) { + element.setAttribute('max', test[3]); + } + + checkAttribute(element, 'low', test[0], test[1]); + + element.removeAttribute('min'); + element.removeAttribute('max'); + } +} + +function checkHighAttribute() +{ + var tests = [ + // high default value is max (max default value is 1.0). + [ null, 1.0 ], + [ 'foo', 1.0 ], + [ 'foo', 42.0, 0.0, 42.0], + // If high <= min, min is used instead. + [ -1.0, 0.0 ], + [ 0.0, 0.5, 0.5 ], + [ 10.0, 15.0, 15.0, 42.0 ], + [ 42.0, 42.0, 13.37, 100.0 ], + // If high >= max, max is used instead. + [ 2.0, 1.0 ], + [ 10.0, 5.0 , 0.5, 5.0 ], + [ 13.37, 13.37, 0.0, 42.0 ], + // Regular reflection. + [ 0.0 ], + [ 0.5 ], + [ 1.0 ], + // Check double-precision value. + [ 0.234567898765432 ], + ]; + + var element = document.createElement('meter'); + + for (var test of tests) { + if (test[2]) { + element.setAttribute('min', test[2]); + } + if (test[3]) { + element.setAttribute('max', test[3]); + } + + checkAttribute(element, 'high', test[0], test[1]); + + element.removeAttribute('min'); + element.removeAttribute('max'); + } +} + +function checkOptimumAttribute() +{ + var tests = [ + // opt default value is (max-min)/2 (thus default value is 0.5). + [ null, 0.5 ], + [ 'foo', 0.5 ], + [ 'foo', 2.0, 1.0, 3.0], + // If opt <= min, min is used instead. + [ -1.0, 0.0 ], + [ 0.0, 0.5, 0.5 ], + [ 10.0, 15.0, 15.0, 42.0 ], + [ 42.0, 42.0, 13.37, 100.0 ], + // If opt >= max, max is used instead. + [ 2.0, 1.0 ], + [ 10.0, 5.0 , 0.5, 5.0 ], + [ 13.37, 13.37, 0.0, 42.0 ], + // Regular reflection. + [ 0.0 ], + [ 0.5 ], + [ 1.0 ], + // Check double-precision value. + [ 0.234567898765432 ], + ]; + + var element = document.createElement('meter'); + + for (var test of tests) { + if (test[2]) { + element.setAttribute('min', test[2]); + } + if (test[3]) { + element.setAttribute('max', test[3]); + } + + checkAttribute(element, 'optimum', test[0], test[1]); + + element.removeAttribute('min'); + element.removeAttribute('max'); + } +} + +function checkFormListedElement(aElement) +{ + is(document.forms[0].elements.length, 0, "the form should have no element"); +} + +function checkLabelable(aElement) +{ + var content = document.getElementById('content'); + var label = document.createElement('label'); + + content.appendChild(label); + label.appendChild(aElement); + is(label.control, aElement, "meter should be labelable"); + + // Cleaning-up. + content.removeChild(label); + content.appendChild(aElement); +} + +function checkNotResetableAndFormSubmission(aElement) +{ + // Creating an input element to check the submission worked. + var form = document.forms[0]; + var input = document.createElement('input'); + + input.name = 'a'; + input.value = 'tulip'; + form.appendChild(input); + + // Setting values. + aElement.value = 42.0; + aElement.max = 100.0; + + document.getElementsByName('submit_frame')[0].addEventListener("load", function() { + is(frames.submit_frame.location.href, + `${location.origin}/tests/dom/html/test/forms/foo?a=tulip`, + "The meter element value should not be submitted"); + + checkNotResetable(); + }, {once: true}); + + form.submit(); +} + +function checkNotResetable() +{ + // Try to reset the form. + var form = document.forms[0]; + var element = document.getElementById('m'); + + element.value = 3.0; + element.max = 42.0; + + form.reset(); + + SimpleTest.executeSoon(function() { + is(element.value, 3.0, "meter.value should not have changed"); + is(element.max, 42.0, "meter.max should not have changed"); + + SimpleTest.finish(); + }); +} + +SimpleTest.waitForExplicitFinish(); + +var m = document.getElementById('m'); + +ok(m instanceof HTMLMeterElement, + "The meter element should be instance of HTMLMeterElement"); +is(m.constructor, HTMLMeterElement, + "The meter element constructor should be HTMLMeterElement"); + +// There is no such attribute. +checkFormIDLAttribute(m); + +checkValueAttribute(); + +checkMinAttribute(); + +checkMaxAttribute(); + +checkLowAttribute(); + +checkHighAttribute(); + +checkOptimumAttribute(); + +checkFormListedElement(m); + +checkLabelable(m); + +checkNotResetableAndFormSubmission(m); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_meter_pseudo-classes.html b/dom/html/test/forms/test_meter_pseudo-classes.html new file mode 100644 index 0000000000..e317a58405 --- /dev/null +++ b/dom/html/test/forms/test_meter_pseudo-classes.html @@ -0,0 +1,169 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=660238 +--> +<head> + <title>Test for Bug 660238</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=770238">Mozilla Bug 660238</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 660238 **/ + +function checkOptimum(aElement, aValue, aOptimum, expectedResult) +{ + var errorString = expectedResult + ? "value attribute should be in the optimum region" + : "value attribute should not be in the optimum region"; + + aElement.setAttribute('value', aValue); + aElement.setAttribute('optimum', aOptimum); + is(aElement.matches(":-moz-meter-optimum"), + expectedResult, errorString); +} + +function checkSubOptimum(aElement, aValue, aOptimum, expectedResult) +{ + var errorString = "value attribute should be in the suboptimal region"; + if (!expectedResult) { + errorString = "value attribute should not be in the suboptimal region"; + } + aElement.setAttribute('value', aValue); + aElement.setAttribute('optimum', aOptimum); + is(aElement.matches(":-moz-meter-sub-optimum"), + expectedResult, errorString); +} + +function checkSubSubOptimum(aElement, aValue, aOptimum, expectedResult) +{ + var errorString = "value attribute should be in the sub-suboptimal region"; + if (!expectedResult) { + errorString = "value attribute should not be in the sub-suboptimal region"; + } + aElement.setAttribute('value', aValue); + aElement.setAttribute('optimum', aOptimum); + is(aElement.matches(":-moz-meter-sub-sub-optimum"), + expectedResult, errorString); +} + +function checkMozMatchesSelector() +{ + var element = document.createElement('meter'); + // all tests realised with default values for min and max (0 and 1) + // low = 0.3 and high = 0.7 + element.setAttribute('low', 0.3); + element.setAttribute('high', 0.7); + + var tests = [ + /* + * optimum = 0.0 => + * optimum region = [ 0.0, 0.3 [ + * suboptimal region = [ 0.3, 0.7 ] + * sub-suboptimal region = ] 0.7, 1.0 ] + */ + [ 0.0, 0.0, true, false, false ], + [ 0.1, 0.0, true, false, false ], + [ 0.3, 0.0, false, true, false ], + [ 0.5, 0.0, false, true, false ], + [ 0.7, 0.0, false, true, false ], + [ 0.8, 0.0, false, false, true ], + [ 1.0, 0.0, false, false, true ], + /* + * optimum = 0.1 => + * optimum region = [ 0.0, 0.3 [ + * suboptimal region = [ 0.3, 0.7 ] + * sub-suboptimal region = ] 0.7, 1.0 ] + */ + [ 0.0, 0.1, true, false, false ], + [ 0.1, 0.1, true, false, false ], + [ 0.3, 0.1, false, true, false ], + [ 0.5, 0.1, false, true, false ], + [ 0.7, 0.1, false, true, false ], + [ 0.8, 0.1, false, false, true ], + [ 1.0, 0.1, false, false, true ], + /* + * optimum = 0.3 => + * suboptimal region = [ 0.0, 0.3 [ + * optimum region = [ 0.3, 0.7 ] + * suboptimal region = ] 0.7, 1.0 ] + */ + [ 0.0, 0.3, false, true, false ], + [ 0.1, 0.3, false, true, false ], + [ 0.3, 0.3, true, false, false ], + [ 0.5, 0.3, true, false, false ], + [ 0.7, 0.3, true, false, false ], + [ 0.8, 0.3, false, true, false ], + [ 1.0, 0.3, false, true, false ], + /* + * optimum = 0.5 => + * suboptimal region = [ 0.0, 0.3 [ + * optimum region = [ 0.3, 0.7 ] + * suboptimal region = ] 0.7, 1.0 ] + */ + [ 0.0, 0.5, false, true, false ], + [ 0.1, 0.5, false, true, false ], + [ 0.3, 0.5, true, false, false ], + [ 0.5, 0.5, true, false, false ], + [ 0.7, 0.5, true, false, false ], + [ 0.8, 0.5, false, true, false ], + [ 1.0, 0.5, false, true, false ], + /* + * optimum = 0.7 => + * suboptimal region = [ 0.0, 0.3 [ + * optimum region = [ 0.3, 0.7 ] + * suboptimal region = ] 0.7, 1.0 ] + */ + [ 0.0, 0.7, false, true, false ], + [ 0.1, 0.7, false, true, false ], + [ 0.3, 0.7, true, false, false ], + [ 0.5, 0.7, true, false, false ], + [ 0.7, 0.7, true, false, false ], + [ 0.8, 0.7, false, true, false ], + [ 1.0, 0.7, false, true, false ], + /* + * optimum = 0.8 => + * sub-suboptimal region = [ 0.0, 0.3 [ + * suboptimal region = [ 0.3, 0.7 ] + * optimum region = ] 0.7, 1.0 ] + */ + [ 0.0, 0.8, false, false, true ], + [ 0.1, 0.8, false, false, true ], + [ 0.3, 0.8, false, true, false ], + [ 0.5, 0.8, false, true, false ], + [ 0.7, 0.8, false, true, false ], + [ 0.8, 0.8, true, false, false ], + [ 1.0, 0.8, true, false, false ], + /* + * optimum = 1.0 => + * sub-suboptimal region = [ 0.0, 0.3 [ + * suboptimal region = [ 0.3, 0.7 ] + * optimum region = ] 0.7, 1.0 ] + */ + [ 0.0, 1.0, false, false, true ], + [ 0.1, 1.0, false, false, true ], + [ 0.3, 1.0, false, true, false ], + [ 0.5, 1.0, false, true, false ], + [ 0.7, 1.0, false, true, false ], + [ 0.8, 1.0, true, false, false ], + [ 1.0, 1.0, true, false, false ], + ]; + + for (var test of tests) { + checkOptimum(element, test[0], test[1], test[2]); + checkSubOptimum(element, test[0], test[1], test[3]); + checkSubSubOptimum(element, test[0], test[1], test[4]); + } +} + +checkMozMatchesSelector(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_min_attribute.html b/dom/html/test/forms/test_min_attribute.html new file mode 100644 index 0000000000..a603a37d29 --- /dev/null +++ b/dom/html/test/forms/test_min_attribute.html @@ -0,0 +1,473 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=635553 +--> +<head> + <title>Test for Bug 635553</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635499">Mozilla Bug 635499</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 635553 **/ + +var data = [ + { type: 'hidden', apply: false }, + { type: 'text', apply: false }, + { type: 'search', apply: false }, + { type: 'tel', apply: false }, + { type: 'url', apply: false }, + { type: 'email', apply: false }, + { type: 'password', apply: false }, + { type: 'date', apply: true }, + { type: 'month', apply: true }, + { type: 'week', apply: true }, + { type: 'time', apply: true }, + { type: 'datetime-local', apply: true }, + { type: 'number', apply: true }, + { type: 'range', apply: true }, + { type: 'color', apply: false }, + { type: 'checkbox', apply: false }, + { type: 'radio', apply: false }, + { type: 'file', apply: false }, + { type: 'submit', apply: false }, + { type: 'image', apply: false }, + { type: 'reset', apply: false }, + { type: 'button', apply: false }, +]; + +var input = document.createElement("input"); +document.getElementById('content').appendChild(input); + +/** + * @aValidity - boolean indicating whether the element is expected to be valid + * (aElement.validity.valid is true) or not. The value passed is ignored and + * overridden with true if aApply is false. + * @aApply - boolean indicating whether the min/max attributes apply to this + * element type. + * @aRangeApply - A boolean that's set to true if the current input type is a + * "[candidate] for constraint validation" and it "[has] range limitations" + * per http://www.whatwg.org/specs/web-apps/current-work/multipage/selectors.html#selector-in-range + * (in other words, one of the pseudo classes :in-range and :out-of-range + * should apply (which, depends on aValidity)). + * Else (neither :in-range or :out-of-range should match) set to false. + */ +function checkValidity(aElement, aValidity, aApply, aRangeApply) +{ + aValidity = aApply ? aValidity : true; + + is(aElement.validity.valid, aValidity, + "element validity should be " + aValidity); + is(aElement.validity.rangeUnderflow, !aValidity, + "element underflow status should be " + !aValidity); + var underflowMsg = + (aElement.type == "date" || aElement.type == "time" || + aElement.type == "month" || aElement.type == "week" || + aElement.type == "datetime-local") ? + ("Please select a value that is no earlier than " + aElement.min + ".") : + ("Please select a value that is no less than " + aElement.min + "."); + is(aElement.validationMessage, + aValidity ? "" : underflowMsg, "Checking range underflow validation message"); + + is(aElement.matches(":valid"), aElement.willValidate && aValidity, + (aElement.willValidate && aValidity) ? ":valid should apply" : "valid shouldn't apply"); + is(aElement.matches(":invalid"), aElement.willValidate && !aValidity, + (aElement.wil && aValidity) ? ":invalid shouldn't apply" : "valid should apply"); + + if (!aRangeApply) { + ok(!aElement.matches(":in-range"), ":in-range should not match"); + ok(!aElement.matches(":out-of-range"), + ":out-of-range should not match"); + } else { + is(aElement.matches(":in-range"), aValidity, + ":in-range matches status should be " + aValidity); + is(aElement.matches(":out-of-range"), !aValidity, + ":out-of-range matches status should be " + !aValidity); + } +} + +for (var test of data) { + input.type = test.type; + var apply = test.apply; + + if (test.todo) { + todo_is(input.type, test.type, test.type + " isn't implemented yet"); + continue; + } + + // The element should be valid. Range should not apply when @min and @max are + // undefined, except if the input type is 'range' (since that type has a + // default minimum and maximum). + if (input.type == 'range') { + checkValidity(input, true, apply, true); + } else { + checkValidity(input, true, apply, false); + } + + switch (input.type) { + case 'hidden': + case 'text': + case 'search': + case 'password': + case 'url': + case 'tel': + case 'email': + case 'number': + case 'checkbox': + case 'radio': + case 'file': + case 'submit': + case 'reset': + case 'button': + case 'image': + case 'color': + input.min = '999'; + break; + case 'date': + input.min = '2012-06-27'; + break; + case 'time': + input.min = '20:20'; + break; + case 'range': + // range is special, since setting min to 999 will make it invalid since + // it's default maximum is 100, its value would be 999, and it would + // suffer from overflow. + break; + case 'month': + input.min = '2016-06'; + break; + case 'week': + input.min = '2016-W39'; + break; + case 'datetime-local': + input.min = '2017-01-01T00:00'; + break; + default: + ok(false, 'please, add a case for this new type (' + input.type + ')'); + } + + // The element should still be valid and range should apply if it can. + checkValidity(input, true, apply, apply); + + switch (input.type) { + case 'text': + case 'hidden': + case 'search': + case 'password': + case 'tel': + case 'radio': + case 'checkbox': + case 'reset': + case 'button': + case 'submit': + case 'image': + case 'color': + input.value = '0'; + checkValidity(input, true, apply, apply); + break; + case 'url': + input.value = 'http://mozilla.org'; + checkValidity(input, true, apply, apply); + break; + case 'email': + input.value = 'foo@bar.com'; + checkValidity(input, true, apply, apply); + break; + case 'file': + var file = new File([''], '635499_file'); + + SpecialPowers.wrap(input).mozSetFileArray([file]); + checkValidity(input, true, apply, apply); + + break; + case 'date': + input.value = '2012-06-28'; + checkValidity(input, true, apply, apply); + + input.value = '2012-06-27'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2012-06-26'; + checkValidity(input, false, apply, apply); + + input.min = '2012-02-29'; + checkValidity(input, true, apply, apply); + + input.value = '2012-02-28'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01-01'; + checkValidity(input, false, apply, apply); + + input.value = '20120-01-01'; + checkValidity(input, true, apply, apply); + + input.min = '0050-01-01'; + checkValidity(input, true, apply, apply); + + input.value = '0049-01-01'; + checkValidity(input, false, apply, apply); + + input.min = ''; + checkValidity(input, true, apply, false); + + input.min = 'foo'; + checkValidity(input, true, apply, false); + break; + case 'number': + input.min = '0'; + input.value = '1'; + checkValidity(input, true, apply, apply); + + input.value = '0'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '-1'; + checkValidity(input, false, apply, apply); + + input.min = '-1'; + checkValidity(input, true, apply, apply); + + input.value = '-42'; + checkValidity(input, false, apply, apply); + + input.min = ''; + checkValidity(input, true, apply, false); + + input.min = 'foo'; + checkValidity(input, true, apply, false); + + // Check that we correctly convert input.min to a double in + // validationMessage. + input.min = "4.333333333333333333333333333333333331"; + input.value = "2"; + is(input.validationMessage, + "Please select a value that is no less than 4.33333333333333.", + "validation message"); + break; + case 'range': + input.min = '0'; + input.value = '1'; + checkValidity(input, true, apply, apply); + + input.value = '0'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '-1'; + checkValidity(input, true, apply, apply); + + is(input.value, input.min, "the value should have been set to min"); + + input.min = '-1'; + checkValidity(input, true, apply, apply); + + input.value = '-42'; + checkValidity(input, true, apply, apply); + + is(input.value, input.min, "the value should have been set to min"); + + input.min = ''; + checkValidity(input, true, apply, true); + + input.min = 'foo'; + checkValidity(input, true, apply, true); + + // We don't check the conversion of input.min to a double in + // validationMessage for 'range' since range will always clamp the value + // up to at least the minimum (so we will never see the min in a + // validationMessage). + + break; + case 'time': + // Don't worry about that. + input.step = 'any'; + + input.min = '20:20'; + input.value = '20:20:01'; + checkValidity(input, true, apply, apply); + + input.value = '20:20:00'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '10:00'; + checkValidity(input, false, apply, apply); + + input.min = '20:20:00.001'; + input.value = '20:20'; + checkValidity(input, false, apply, apply); + + input.value = '00:00'; + checkValidity(input, false, apply, apply); + + input.value = '23:59'; + checkValidity(input, true, apply, apply); + + input.value = '20:20:01'; + checkValidity(input, true, apply, apply); + + input.value = '20:20:00.01'; + checkValidity(input, true, apply, apply); + + input.value = '20:20:00.1'; + checkValidity(input, true, apply, apply); + + input.min = '00:00:00'; + input.value = '01:00'; + checkValidity(input, true, apply, apply); + + input.value = '00:00:00.000'; + checkValidity(input, true, apply, apply); + + input.min = ''; + checkValidity(input, true, apply, false); + + input.min = 'foo'; + checkValidity(input, true, apply, false); + break; + case 'month': + input.value = '2016-07'; + checkValidity(input, true, apply, apply); + + input.value = '2016-06'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2016-05'; + checkValidity(input, false, apply, apply); + + input.min = '2016-01'; + checkValidity(input, true, apply, apply); + + input.value = '2015-12'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01'; + checkValidity(input, false, apply, apply); + + input.value = '10000-01'; + checkValidity(input, true, apply, apply); + + input.min = '0010-01'; + checkValidity(input, true, apply, apply); + + input.value = '0001-01'; + checkValidity(input, false, apply, apply); + + input.min = ''; + checkValidity(input, true, apply, false); + + input.min = 'foo'; + checkValidity(input, true, apply, false); + break; + case 'week': + input.value = '2016-W40'; + checkValidity(input, true, apply, apply); + + input.value = '2016-W39'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2016-W38'; + checkValidity(input, false, apply, apply); + + input.min = '2016-W01'; + checkValidity(input, true, apply, apply); + + input.value = '2015-W53'; + checkValidity(input, false, apply, apply); + + input.value = '1000-W01'; + checkValidity(input, false, apply, apply); + + input.value = '10000-01'; + checkValidity(input, true, apply, apply); + + input.min = '0010-W01'; + checkValidity(input, true, apply, apply); + + input.value = '0001-W01'; + checkValidity(input, false, apply, apply); + + input.min = ''; + checkValidity(input, true, apply, false); + + input.min = 'foo'; + checkValidity(input, true, apply, false); + break; + case 'datetime-local': + input.value = '2017-12-31T23:59'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01-01T00:00'; + checkValidity(input, true, apply, apply); + + input.value = '2017-01-01T00:00:00.123'; + checkValidity(input, true, apply, apply); + + input.value = 'foo'; + checkValidity(input, true, apply, apply); + + input.value = '2016-12-31T23:59'; + checkValidity(input, false, apply, apply); + + input.min = '2016-01-01T00:00'; + checkValidity(input, true, apply, apply); + + input.value = '2015-12-31T23:59'; + checkValidity(input, false, apply, apply); + + input.value = '1000-01-01T00:00'; + checkValidity(input, false, apply, apply); + + input.value = '10000-01-01T00:00'; + checkValidity(input, true, apply, apply); + + input.min = '0010-01-01T12:00'; + checkValidity(input, true, apply, apply); + + input.value = '0010-01-01T10:00'; + checkValidity(input, false, apply, apply); + + input.min = ''; + checkValidity(input, true, apply, false); + + input.min = 'foo'; + checkValidity(input, true, apply, false); + break; + default: + ok(false, 'write tests for ' + input.type); + } + + // Cleaning up, + input.removeAttribute('min'); + input.value = ''; +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_minlength_attribute.html b/dom/html/test/forms/test_minlength_attribute.html new file mode 100644 index 0000000000..154343a512 --- /dev/null +++ b/dom/html/test/forms/test_minlength_attribute.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=345624 +--> +<head> + <title>Test for Bug 345624</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input, textarea { background-color: rgb(0,0,0) !important; } + :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; } + :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345624">Mozilla Bug 345624</a> +<p id="display"></p> +<div id="content"> + <input id='i'> + <textarea id='t'></textarea> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 345624 **/ + +/** + * This test is checking only tooShort related features + * related to constraint validation. + */ + +function checkTooShortValidity(element) +{ + element.value = "foo"; + ok(!element.validity.tooShort, + "Element should not be too short when minlength is not set"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.minLength = 5; + ok(!element.validity.tooShort, + "Element should not be too short unless the user edits it"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.focus(); + + sendString("o"); + is(element.value, "fooo", "value should have changed"); + ok(element.validity.tooShort, + "Element should be too short after a user edit that does not make it short enough"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); + ok(!element.validity.valid, "Element should be invalid"); + ok(!element.checkValidity(), "The element should not be valid"); + is(element.validationMessage, + "Please use at least 5 characters (you are currently using 4 characters).", + "The validation message text is not correct"); + + sendString("o"); + is(element.value, "foooo", "value should have changed"); + ok(!element.validity.tooShort, + "Element should not be too short after a user edit makes it long enough"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + + element.minLength = 2; + ok(!element.validity.tooShort, + "Element should remain valid if minlength changes but minlength < length"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + + element.minLength = 1; + ok(!element.validity.tooShort, + "Element should remain valid if minlength changes but minlength = length"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.minLength = 6; + ok(element.validity.tooShort, + "Element should become invalid if minlength changes and minlength > length"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); + ok(!element.validity.valid, "Element should be invalid"); + ok(!element.checkValidity(), "The element should not be valid"); + is(element.validationMessage, + "Please use at least 6 characters (you are currently using 5 characters).", + "The validation message text is not correct"); + + element.minLength = 5; + ok(!element.validity.tooShort, + "Element should become valid if minlength changes and minlength = length"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.value = "test"; + ok(!element.validity.tooShort, + "Element should stay valid after programmatic edit (even if value is too short)"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "The element should be valid"); + + element.setCustomValidity("custom message"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); + is(element.validationMessage, "custom message", + "Custom message should be shown instead of too short one"); +} + +checkTooShortValidity(document.getElementById('i')); +checkTooShortValidity(document.getElementById('t')); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/forms/test_mozistextfield.html b/dom/html/test/forms/test_mozistextfield.html new file mode 100644 index 0000000000..3f92a3d05d --- /dev/null +++ b/dom/html/test/forms/test_mozistextfield.html @@ -0,0 +1,111 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=565538 +--> +<head> + <title>Test for Bug 565538</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=565538">Mozilla Bug 565538</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 565538 **/ + +var gElementTestData = [ +/* element result */ + ['input', true], + ['button', false], + ['fieldset', false], + ['label', false], + ['option', false], + ['optgroup', false], + ['output', false], + ['legend', false], + ['select', false], + ['textarea', false], + ['object', false], +]; + +var gInputTestData = [ +/* type result */ + ['password', true], + ['tel', true], + ['text', true], + ['button', false], + ['checkbox', false], + ['file', false], + ['hidden', false], + ['reset', false], + ['image', false], + ['radio', false], + ['submit', false], + ['search', true], + ['email', true], + ['url', true], + ['number', false], + ['range', false], + ['date', false], + ['time', false], + ['color', false], + ['month', false], + ['week', false], + ['datetime-local', false], +]; + +function checkMozIsTextFieldDefined(aElement, aResult) +{ + var element = document.createElement(aElement); + + var msg = "mozIsTextField should be " + if (aResult) { + msg += "defined"; + } else { + msg += "undefined"; + } + + is('mozIsTextField' in element, aResult, msg); +} + +function checkMozIsTextFieldValue(aInput, aResult) +{ + is(aInput.mozIsTextField(false), aResult, + "mozIsTextField(false) should return " + aResult); + + if (aInput.type == 'password') { + ok(!aInput.mozIsTextField(true), + "mozIsTextField(true) should return false for password"); + } else { + is(aInput.mozIsTextField(true), aResult, + "mozIsTextField(true) should return " + aResult); + } +} + +function checkMozIsTextFieldValueTodo(aInput, aResult) +{ + todo_is(aInput.mozIsTextField(false), aResult, + "mozIsTextField(false) should return " + aResult); + todo_is(aInput.mozIsTextField(true), aResult, + "mozIsTextField(true) should return " + aResult); +} + +// Check if the method is defined for the correct elements. +for (data of gElementTestData) { + checkMozIsTextFieldDefined(data[0], data[1]); +} + +// Check if the method returns the correct value. +var input = document.createElement('input'); +for (data of gInputTestData) { + input.type = data[0]; + checkMozIsTextFieldValue(input, data[1]); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_novalidate_attribute.html b/dom/html/test/forms/test_novalidate_attribute.html new file mode 100644 index 0000000000..dcea207838 --- /dev/null +++ b/dom/html/test/forms/test_novalidate_attribute.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=556013 +--> +<head> + <title>Test for Bug 556013</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=556013">Mozilla Bug 556013</a> +<p id="display"></p> +<iframe style='width:50px; height: 50px;' name='t'></iframe> +<div id="content"> + <form target='t' action='data:text/html,' novalidate> + <input id='av' required> + <input id='a' type='submit'> + </form> + <form target='t' action='data:text/html,' novalidate> + <input id='bv' type='checkbox' required> + <button id='b' type='submit'></button> + </form> + <form target='t' action='data:text/html,' novalidate> + <input id='c' required> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 556013 **/ + +/** + * novalidate should prevent form validation, thus not blocking form submission. + * + * NOTE: if the MozInvalidForm event doesn't get prevented default, the form + * submission will never be blocked and this test might be a false-positive but + * that should not be a problem. We will remove the check for MozInvalidForm + * event, see bug 587671. + */ +document.forms[0].addEventListener("submit", function(aEvent) { + ok(true, "novalidate has been correctly used for first form"); + document.getElementById('b').click(); +}, {once: true}); + +document.forms[1].addEventListener("submit", function(aEvent) { + ok(true, "novalidate has been correctly used for second form"); + var c = document.getElementById('c'); + c.focus(); + synthesizeKey("KEY_Enter"); +}, {once: true}); + +document.forms[2].addEventListener("submit", function(aEvent) { + ok(true, "novalidate has been correctly used for third form"); + SimpleTest.executeSoon(SimpleTest.finish); +}, {once: true}); + +/** + * We have to be sure invalid events are not send too. + * They should be sent before the submit event so we can just create a test + * failure if we got one. All of them should be catched if sent. + * At worst, we got random green which isn't harmful. + */ +function invalidHandling(aEvent) +{ + aEvent.target.removeEventListener("invalid", invalidHandling); + ok(false, "invalid event should not be sent"); +} + +document.getElementById('av').addEventListener("invalid", invalidHandling); +document.getElementById('bv').addEventListener("invalid", invalidHandling); +document.getElementById('c').addEventListener("invalid", invalidHandling); + +SimpleTest.waitForExplicitFinish(); + +// This is going to call all the tests (with a chain reaction). +SimpleTest.waitForFocus(function() { + document.getElementById('a').click(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_option_disabled.html b/dom/html/test/forms/test_option_disabled.html new file mode 100644 index 0000000000..421e4546be --- /dev/null +++ b/dom/html/test/forms/test_option_disabled.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=759666 +--> +<head> + <meta charset="utf-8"> + <title>Test for HTMLOptionElement disabled attribute and pseudo-class</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=759666">Mozilla Bug 759666</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLOptionElement disabled attribute and pseudo-class **/ + +var testCases = [ + // Static checks. + { html: "<option></option>", + result: { attr: null, idl: false, pseudo: false } }, + { html: "<option disabled></option>", + result: { attr: "", idl: true, pseudo: true } }, + { html: "<optgroup><option></option></otpgroup>", + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup><option disabled></option></optgroup>", + result: { attr: "", idl: true, pseudo: true } }, + { html: "<optgroup disabled><option disabled></option></optgroup>", + result: { attr: "", idl: true, pseudo: true } }, + { html: "<optgroup disabled><option></option></optgroup>", + result: { attr: null, idl: false, pseudo: true } }, + { html: "<optgroup><optgroup disabled><option></option></optgroup></optgroup>", + result: { attr: null, idl: false, pseudo: true } }, + { html: "<optgroup disabled><optgroup><option></option></optgroup></optgroup>", + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>", + result: { attr: "", idl: true, pseudo: true } }, + + // Dynamic checks: changing disable value. + { html: "<option></option>", + modifier(c) { c.querySelector('option').disabled = true; }, + result: { attr: "", idl: true, pseudo: true } }, + { html: "<option disabled></option>", + modifier(c) { c.querySelector('option').disabled = false; }, + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup><option></option></otpgroup>", + modifier(c) { c.querySelector('optgroup').disabled = true; }, + result: { attr: null, idl: false, pseudo: true } }, + { html: "<optgroup><option disabled></option></optgroup>", + modifier(c) { c.querySelector('option').disabled = false; }, + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup disabled><option disabled></option></optgroup>", + modifier(c) { c.querySelector('optgroup').disabled = false; }, + result: { attr: "", idl: true, pseudo: true } }, + { html: "<optgroup disabled><option disabled></option></optgroup>", + modifier(c) { c.querySelector('option').disabled = false; }, + result: { attr: null, idl: false, pseudo: true } }, + { html: "<optgroup disabled><option disabled></option></optgroup>", + modifier(c) { c.querySelector('optgroup').disabled = c.querySelector('option').disabled = false; }, + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup disabled><option></option></optgroup>", + modifier(c) { c.querySelector('optgroup').disabled = false; }, + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup><optgroup disabled><option></option></optgroup></optgroup>", + modifier(c) { c.querySelector('optgroup[disabled]').disabled = false; }, + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup disabled><optgroup><option></option></optgroup></optgroup>", + modifier(c) { c.querySelector('optgroup[disabled]').disabled = false; }, + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>", + modifier(c) { c.querySelector('optgroup').disabled = false; }, + result: { attr: "", idl: true, pseudo: true } }, + { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>", + modifier(c) { c.querySelector('option').disabled = false; }, + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup disabled><optgroup><option disabled></option></optgroup></optgroup>", + modifier(c) { c.querySelector('option').disabled = c.querySelector('option').disabled = false; }, + result: { attr: null, idl: false, pseudo: false } }, + + // Dynamic checks: moving option element. + { html: "<optgroup id='a'><option></option></optgroup><optgroup id='b'></optgroup>", + modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); }, + result: { attr: null, idl: false, pseudo: false } }, + { html: "<optgroup id='a'><option disabled></option></optgroup><optgroup id='b'></optgroup>", + modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); }, + result: { attr: "", idl: true, pseudo: true } }, + { html: "<optgroup id='a'><option></option></optgroup><optgroup disabled id='b'></optgroup>", + modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); }, + result: { attr: null, idl: false, pseudo: true } }, + { html: "<optgroup disabled id='a'><option></option></optgroup><optgroup id='b'></optgroup>", + modifier(c) { c.querySelector('#b').appendChild(c.querySelector('option')); }, + result: { attr: null, idl: false, pseudo: false } }, +]; + +var content = document.getElementById('content'); + +testCases.forEach(function(testCase) { + var result = testCase.result; + + content.innerHTML = testCase.html; + + if (testCase.modifier !== undefined) { + testCase.modifier(content); + } + + var option = content.querySelector('option'); + is(option.getAttribute('disabled'), result.attr, "disabled content attribute value should be " + result.attr); + is(option.disabled, result.idl, "disabled idl attribute value should be " + result.idl); + is(option.matches(":disabled"), result.pseudo, ":disabled state should be " + result.pseudo); + is(option.matches(":enabled"), !result.pseudo, ":enabled state should be " + !result.pseudo); + + content.innerHTML = ""; +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_option_index_attribute.html b/dom/html/test/forms/test_option_index_attribute.html new file mode 100644 index 0000000000..f15520e5e6 --- /dev/null +++ b/dom/html/test/forms/test_option_index_attribute.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<!-- +See those bugs: +https://bugzilla.mozilla.org/show_bug.cgi?id=720385 +--> +<head> + <meta charset="utf-8"> + <title>Test for option.index</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=720385">Mozilla Bug 720385</a> +<p id="display"></p> +<div id="content" style="display: none"> + <datalist> + <option></option> + <option></option> + </datalist> + <select> + <option></option> + <foo> + <option></option> + <optgroup> + <option></option> + </optgroup> + <option></option> + </foo> + <option></option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 720385 **/ + +var initialIndexes = [ 0, 0, 0, 1, 2, 3, 4 ]; +var options = document.getElementsByTagName('option'); + +is(options.length, initialIndexes.length, + "Must have " + initialIndexes.length +" options"); + +for (var i=0; i<options.length; ++i) { + is(options[i].index, initialIndexes[i], "test"); +} + +var o = document.createElement('option'); +is(o.index, 0, "option outside of a document have index=0"); + +document.body.appendChild(o); +is(o.index, 0, "option outside of a select have index=0"); + +var datalist = document.getElementsByTagName('datalist')[0]; + +datalist.appendChild(o); +is(o.index, 0, "option outside of a select have index=0"); + +datalist.removeChild(o); +is(o.index, 0, "option outside of a select have index=0"); + +var select = document.getElementsByTagName('select')[0]; + +select.appendChild(o); +is(o.index, 5, "option inside a select have an index"); + +select.removeChild(select.options[0]); +is(o.index, 4, "option inside a select have an index"); + +select.insertBefore(o, select.options[0]); +is(o.index, 0, "option inside a select have an index"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_option_text.html b/dom/html/test/forms/test_option_text.html new file mode 100644 index 0000000000..3afe3e786a --- /dev/null +++ b/dom/html/test/forms/test_option_text.html @@ -0,0 +1,57 @@ +<!doctype html> +<meta charset=utf-8> +<title>HTMLOptionElement.text</title> +<link rel=author title=Ms2ger href="mailto:Ms2ger@gmail.com"> +<link rel=help href="http://www.whatwg.org/html/#dom-option-text"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id=log></div> +<script> +test(function() { + var option = document.createElement("option"); + option.appendChild(document.createElement("font")) + .appendChild(document.createTextNode(" font ")); + assert_equals(option.text, "font"); +}, "option.text should recurse"); +test(function() { + var option = document.createElement("option"); + option.appendChild(document.createTextNode(" before ")); + option.appendChild(document.createElement("script")) + .appendChild(document.createTextNode(" script ")); + option.appendChild(document.createTextNode(" after ")); + assert_equals(option.text, "before after"); +}, "option.text should not recurse into HTML script elements"); +test(function() { + var option = document.createElement("option"); + option.appendChild(document.createTextNode(" before ")); + option.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "script")) + .appendChild(document.createTextNode(" script ")); + option.appendChild(document.createTextNode(" after ")); + assert_equals(option.text, "before after"); +}, "option.text should not recurse into SVG script elements"); +test(function() { + var option = document.createElement("option"); + option.appendChild(document.createTextNode(" before ")); + option.appendChild(document.createElementNS("http://www.w3.org/1998/Math/MathML", "script")) + .appendChild(document.createTextNode(" script ")); + option.appendChild(document.createTextNode(" after ")); + assert_equals(option.text, "before script after"); +}, "option.text should recurse into MathML script elements"); +test(function() { + var option = document.createElement("option"); + option.appendChild(document.createTextNode(" before ")); + option.appendChild(document.createElementNS(null, "script")) + .appendChild(document.createTextNode(" script ")); + option.appendChild(document.createTextNode(" after ")); + assert_equals(option.text, "before script after"); +}, "option.text should recurse into null script elements"); +test(function() { + var option = document.createElement("option"); + var span = option.appendChild(document.createElement("span")); + span.appendChild(document.createTextNode(" Some ")); + span.appendChild(document.createElement("script")) + .appendChild(document.createTextNode(" script ")); + option.appendChild(document.createTextNode(" Text ")); + assert_equals(option.text, "Some Text"); +}, "option.text should work if a child of the option ends with a script"); +</script> diff --git a/dom/html/test/forms/test_output_element.html b/dom/html/test/forms/test_output_element.html new file mode 100644 index 0000000000..ab11443d83 --- /dev/null +++ b/dom/html/test/forms/test_output_element.html @@ -0,0 +1,182 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=346485 +--> +<head> + <title>Test for Bug 346485</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="../reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + frameLoaded = function() { + is(frames.submit_frame.location.href, "about:blank", + "Blank frame loaded"); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=346485">Mozilla Bug 346485</a> +<p id="display"></p> +<iframe name="submit_frame" onload="frameLoaded()" style="visibility: hidden;"></iframe> +<div id="content" style="display: none"> + <form id='f' method='get' target='submit_frame' action='foo'> + <input name='a' id='a'> + <input name='b' id='b'> + <output id='o' for='a b' name='output-name'>tulip</output> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 346485 **/ + +function checkNameAttribute(element) +{ + is(element.name, "output-name", "Output name IDL attribute is not correct"); + is(element.getAttribute('name'), "output-name", + "Output name content attribute is not correct"); +} + +function checkValueAndDefaultValueIDLAttribute(element) +{ + is(element.value, element.textContent, + "The value IDL attribute should act like the textContent IDL attribute"); + + element.value = "foo"; + is(element.value, "foo", "Value should be 'foo'"); + + is(element.defaultValue, "tulip", "Default defaultValue is 'tulip'"); + + element.defaultValue = "bar"; + is(element.defaultValue, "bar", "defaultValue should be 'bar'"); + + // More complex situation. + element.textContent = 'foo'; + var b = document.createElement('b'); + b.textContent = 'bar' + element.appendChild(b); + is(element.value, element.textContent, + "The value IDL attribute should act like the textContent IDL attribute"); +} + +function checkValueModeFlag(element) +{ + /** + * The value mode flag is the flag used to know if value should represent the + * textContent or the default value. + */ + // value mode flag should be 'value' + isnot(element.defaultValue, element.value, + "When value is set, defaultValue keeps its value"); + + var f = document.getElementById('f'); + f.reset(); + // value mode flag should be 'default' + is(element.defaultValue, element.value, "When reset, defaultValue=value"); + is(element.textContent, element.defaultValue, + "textContent should contain the defaultValue"); +} + +function checkDescendantChanged(element) +{ + /** + * Whenever a descendant is changed if the value mode flag is value, + * the default value should be the textContent value. + */ + element.defaultValue = 'tulip'; + element.value = 'foo'; + + // set value mode flag to 'default' + var f = document.getElementById('f'); + f.reset(); + + is(element.textContent, element.defaultValue, + "textContent should contain the defaultValue"); + element.textContent = "bar"; + is(element.textContent, element.defaultValue, + "textContent should contain the defaultValue"); +} + +function checkFormIDLAttribute(element) +{ + is(element.form, document.getElementById('f'), + "form IDL attribute is invalid"); +} + +function checkHtmlForIDLAttribute(element) +{ + is(String(element.htmlFor), 'a b', + "htmlFor IDL attribute should reflect the for content attribute"); + + // DOMTokenList is tested in another bug so we just test assignation + element.htmlFor.value = 'a b c'; + is(String(element.htmlFor), 'a b c', "htmlFor should have changed"); +} + +function submitForm() +{ + // Setting the values for the submit. + document.getElementById('o').value = 'foo'; + document.getElementById('a').value = 'afield'; + document.getElementById('b').value = 'bfield'; + + frameLoaded = checkFormSubmission; + + // This will call checkFormSubmission() which is going to call ST.finish(). + document.getElementById('f').submit(); +} + +function checkFormSubmission() +{ + /** + * All elements values have been set just before the submission. + * The input elements values should be in the submit url but the ouput + * element value should not appear. + */ + + is(frames.submit_frame.location.href, + `${location.origin}/tests/dom/html/test/forms/foo?a=afield&b=bfield`, + "The output element value should not be submitted"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + reflectString({ + element: document.createElement("output"), + attribute: "name", + }); + + var o = document.getElementsByTagName('output'); + is(o.length, 1, "There should be one output element"); + + o = o[0]; + ok(o instanceof HTMLOutputElement, + "The output should be instance of HTMLOutputElement"); + + o = document.getElementById('o'); + ok(o instanceof HTMLOutputElement, + "The output should be instance of HTMLOutputElement"); + + is(o.type, "output", "Output type IDL attribute should be 'output'"); + + checkNameAttribute(o); + + checkValueAndDefaultValueIDLAttribute(o); + + checkValueModeFlag(o); + + checkDescendantChanged(o); + + checkFormIDLAttribute(o); + + checkHtmlForIDLAttribute(o); + + submitForm(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_pattern_attribute.html b/dom/html/test/forms/test_pattern_attribute.html new file mode 100644 index 0000000000..71d79c1def --- /dev/null +++ b/dom/html/test/forms/test_pattern_attribute.html @@ -0,0 +1,324 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=345512 +--> +<head> + <title>Test for Bug 345512</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input { background-color: rgb(0,0,0) !important; } + input:valid { background-color: rgb(0,255,0) !important; } + input:invalid { background-color: rgb(255,0,0) !important; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345512">Mozilla Bug 345512</a> +<p id="display"></p> +<div id="content" style="display: none"> + <input id='i' pattern="tulip" oninvalid="invalidEventHandler(event);"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 345512 **/ + +var gInvalid = false; + +function invalidEventHandler(e) +{ + is(e.type, "invalid", "Invalid event type should be invalid"); + gInvalid = true; +} + +function completeValidityCheck(element, alwaysValid, isBarred) +{ + // Check when pattern matches. + if (element.type == 'email') { + element.pattern = ".*@bar.com"; + element.value = "foo@bar.com"; + } else if (element.type == 'url') { + element.pattern = "http://.*\\.com$"; + element.value = "http://mozilla.com"; + } else if (element.type == 'file') { + element.pattern = "foo"; + SpecialPowers.wrap(element).mozSetFileArray([new File(["foo"], "foo")]); + } else { + element.pattern = "foo"; + element.value = "foo"; + } + + checkValidPattern(element, true, isBarred); + + // Check when pattern does not match. + + if (element.type == 'email') { + element.pattern = ".*@bar.com"; + element.value = "foo@foo.com"; + } else if (element.type == 'url') { + element.pattern = "http://.*\\.com$"; + element.value = "http://mozilla.org"; + } else if (element.type == 'file') { + element.pattern = "foo"; + SpecialPowers.wrap(element).mozSetFileArray([new File(["bar"], "bar")]); + } else { + element.pattern = "foo"; + element.value = "bar"; + } + + if (!alwaysValid) { + checkInvalidPattern(element, true); + } else { + checkValidPattern(element, true, isBarred); + } +} + +function checkValidPattern(element, completeCheck, isBarred) +{ + if (completeCheck) { + gInvalid = false; + + ok(!element.validity.patternMismatch, + "Element should not suffer from pattern mismatch"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "Element should be valid"); + ok(!gInvalid, "Invalid event shouldn't have been thrown"); + is(element.validationMessage, '', + "Validation message should be the empty string"); + if (element.type != 'radio' && element.type != 'checkbox') { + is(window.getComputedStyle(element).getPropertyValue('background-color'), + isBarred ? "rgb(0, 0, 0)" : "rgb(0, 255, 0)", + "The pseudo-class is not correctly applied"); + } + } else { + ok(!element.validity.patternMismatch, + "Element should not suffer from pattern mismatch"); + } +} + +function checkInvalidPattern(element, completeCheck) +{ + if (completeCheck) { + gInvalid = false; + + ok(element.validity.patternMismatch, + "Element should suffer from pattern mismatch"); + ok(!element.validity.valid, "Element should not be valid"); + ok(!element.checkValidity(), "Element should not be valid"); + ok(gInvalid, "Invalid event should have been thrown"); + is(element.validationMessage, + "Please match the requested format.", + "Validation message is not valid"); + } else { + ok(element.validity.patternMismatch, + "Element should suffer from pattern mismatch"); + } + + if (element.type != 'radio' && element.type != 'checkbox') { + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); + } +} + +function checkSyntaxError(element) +{ + ok(!element.validity.patternMismatch, + "On SyntaxError, element should not suffer"); +} + +function checkPatternValidity(element) +{ + element.pattern = "foo"; + + element.value = ''; + checkValidPattern(element); + + element.value = "foo"; + checkValidPattern(element); + + element.value = "bar"; + checkInvalidPattern(element); + + element.value = "foobar"; + checkInvalidPattern(element); + + element.value = "foofoo"; + checkInvalidPattern(element); + + element.pattern = "foo\"bar"; + element.value = "foo\"bar"; + checkValidPattern(element); + + element.value = 'foo"bar'; + checkValidPattern(element); + + element.pattern = "foo'bar"; + element.value = "foo\'bar"; + checkValidPattern(element); + + element.pattern = "foo\\(bar"; + element.value = "foo(bar"; + checkValidPattern(element); + + element.value = "foo"; + checkInvalidPattern(element); + + element.pattern = "foo\\)bar"; + element.value = "foo)bar"; + checkValidPattern(element); + + element.value = "foo"; + checkInvalidPattern(element); + + // Check for 'i' flag disabled. Should be case sensitive. + element.value = "Foo"; + checkInvalidPattern(element); + + // We can't check for the 'g' flag because we only test, we don't execute. + // We can't check for the 'm' flag because .value shouldn't contain line breaks. + + // We need '\\\\' because '\\' will produce '\\' and we want to escape the '\' + // for the regexp. + element.pattern = "foo\\\\bar"; + element.value = "foo\\bar"; + checkValidPattern(element); + + // We may want to escape the ' in the pattern, but this is a SyntaxError + // when unicode flag is set. + element.pattern = "foo\\'bar"; + element.value = "foo'bar"; + checkSyntaxError(element); + element.value = "baz"; + checkSyntaxError(element); + + // We should check the pattern attribute do not pollute |RegExp.lastParen|. + is(RegExp.lastParen, "", "RegExp.lastParen should be the empty string"); + + element.pattern = "(foo)"; + element.value = "foo"; + checkValidPattern(element); + is(RegExp.lastParen, "", "RegExp.lastParen should be the empty string"); + + // That may sound weird but the empty string is a valid pattern value. + element.pattern = ""; + element.value = ""; + checkValidPattern(element); + + element.value = "foo"; + checkInvalidPattern(element); + + // Checking some complex patterns. As we are using js regexp mechanism, these + // tests doesn't aim to test the regexp mechanism. + element.pattern = "\\d{2}\\s\\d{2}\\s\\d{4}" + element.value = "01 01 2010" + checkValidPattern(element); + + element.value = "01/01/2010" + checkInvalidPattern(element); + + element.pattern = "[0-9a-zA-Z]([\\-.\\w]*[0-9a-zA-Z_+])*@([0-9a-zA-Z][\\-\\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9}"; + element.value = "foo@bar.com"; + checkValidPattern(element); + + element.value = "...@bar.com"; + checkInvalidPattern(element); + + element.pattern = "^(?:\\w{3,})$"; + element.value = "foo"; + checkValidPattern(element); + + element.value = "f"; + checkInvalidPattern(element); + + // If @title is specified, it should be added in the validation message. + if (element.type == 'email') { + element.pattern = "foo@bar.com" + element.value = "bar@foo.com"; + } else if (element.type == 'url') { + element.pattern = "http://mozilla.com"; + element.value = "http://mozilla.org"; + } else { + element.pattern = "foo"; + element.value = "bar"; + } + element.title = "this is an explanation of the regexp"; + is(element.validationMessage, + "Please match the requested format: " + element.title + ".", + "Validation message is not valid"); + element.title = ""; + is(element.validationMessage, + "Please match the requested format.", + "Validation message is not valid"); + + element.pattern = "foo"; + if (element.type == 'email') { + element.value = "bar@foo.com"; + } else if (element.type == 'url') { + element.value = "http://mozilla.org"; + } else { + element.value = "bar"; + } + checkInvalidPattern(element); + + element.removeAttribute('pattern'); + checkValidPattern(element, true); + + // Unicode pattern + for (var pattern of ["\\u{1F438}{2}", "\u{1F438}{2}", + "\\uD83D\\uDC38{2}", "\uD83D\uDC38{2}", + "\u{D83D}\u{DC38}{2}"]) { + element.pattern = pattern; + + element.value = "\u{1F438}\u{1F438}"; + checkValidPattern(element); + + element.value = "\uD83D\uDC38\uD83D\uDC38"; + checkValidPattern(element); + + element.value = "\uD83D\uDC38\uDC38"; + checkInvalidPattern(element); + } + + element.pattern = "\\u{D83D}\\u{DC38}{2}"; + + element.value = "\u{1F438}\u{1F438}"; + checkInvalidPattern(element); + + element.value = "\uD83D\uDC38\uD83D\uDC38"; + checkInvalidPattern(element); + + element.value = "\uD83D\uDC38\uDC38"; + checkInvalidPattern(element); +} + +var input = document.getElementById('i'); + +// |validTypes| are the types which accept @pattern +// and |invalidTypes| are the ones which do not accept it. +var validTypes = Array('text', 'password', 'search', 'tel', 'email', 'url'); +var barredTypes = Array('hidden', 'reset', 'button'); +var invalidTypes = Array('checkbox', 'radio', 'file', 'number', 'range', 'date', + 'time', 'color', 'submit', 'image', 'month', 'week', + 'datetime-local'); + +for (type of validTypes) { + input.type = type; + completeValidityCheck(input, false); + checkPatternValidity(input); +} + +for (type of barredTypes) { + input.type = type; + completeValidityCheck(input, true, true); +} + +for (type of invalidTypes) { + input.type = type; + completeValidityCheck(input, true); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_preserving_metadata_between_reloads.html b/dom/html/test/forms/test_preserving_metadata_between_reloads.html new file mode 100644 index 0000000000..07ca05f7ce --- /dev/null +++ b/dom/html/test/forms/test_preserving_metadata_between_reloads.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Test preserving metadata between page reloads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css" /> + </head> +<body> +<p id="display"></p> +<div id="content"> + <iframe id="test-frame" width="800px" height="600px" srcdoc=' + <html> + <body> + <h3>Bug 1635224: Preserve mLastValueChangeWasInteractive between reloads</h3> + <div> + <form> + <textarea id="maxlen-textarea" maxlength="2" rows="2" cols="10"></textarea><br/> + <input id="maxlen-inputtext" type="text" maxlength="2"><br/> + <textarea id="minlen-textarea" minlength="8" rows="2" cols="10"></textarea><br/> + <input id="minlen-inputtext" type="text" minlength="8"><br/> + </form> + </div> + </body> + </html> +'></iframe> +</div> + +<pre id="test"> +<script> + SimpleTest.waitForExplicitFinish() + const Ci = SpecialPowers.Ci; + const str = "aaaaa"; + + function afterLoad() { + SimpleTest.waitForFocus(async function () { + await SpecialPowers.pushPrefEnv({"set": [["editor.truncate_user_pastes", false]]}); + var iframeDoc = $("test-frame").contentDocument; + var src = iframeDoc.getElementById("src"); + + function test(fieldId, callback) { + var field = iframeDoc.getElementById(fieldId); + field.focus(); + SimpleTest.waitForClipboard(str, + function () { + SpecialPowers.Cc["@mozilla.org/widget/clipboardhelper;1"] + .getService(Ci.nsIClipboardHelper) + .copyString(str); + }, + function () { + synthesizeKey("v", { accelKey: true }); + is(field.value, "aaaaa", "the value of " + fieldId + " was entered correctly"); + is(field.checkValidity(), false, "the validity of " + fieldId + " should be false"); + $("test-frame").contentWindow.location.reload(); + is(field.value, "aaaaa", "the value of " + fieldId + " persisted correctly"); + is(field.checkValidity(), false, "the validity of " + fieldId + " should be false after reload"); + callback(); + }, + function () { + ok(false, "Failed to copy the string"); + SimpleTest.finish(); + } + ); + } + + function runNextTest() { + if (fieldIds.length) { + var currentFieldId = fieldIds.shift(); + test(currentFieldId, runNextTest); + } else { + SimpleTest.finish(); + } + } + + var fieldIds = ["maxlen-textarea", "maxlen-inputtext", "minlen-textarea", "minlen-inputtext"]; + runNextTest(); + }); + } + addLoadEvent(afterLoad); +</script> +</pre> +</body> +</html>
\ No newline at end of file diff --git a/dom/html/test/forms/test_progress_element.html b/dom/html/test/forms/test_progress_element.html new file mode 100644 index 0000000000..065adf94ea --- /dev/null +++ b/dom/html/test/forms/test_progress_element.html @@ -0,0 +1,307 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=514437 +https://bugzilla.mozilla.org/show_bug.cgi?id=633913 +--> +<head> + <title>Test for progress element content and layout</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=514437">Mozilla Bug 514437</a> +and +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=633913">Mozilla Bug 633913</a> +<p id="display"></p> +<iframe name="submit_frame" style="visibility: hidden;"></iframe> +<div id="content" style="visibility: hidden;"> + <form id='f' method='get' target='submit_frame' action='foo'> + <progress id='p'></progress> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.expectAssertions(0, 1); + +/** Test for progress element content and layout **/ + +function checkFormIDLAttribute(aElement) +{ + is("form" in aElement, false, "<progress> shouldn't have a form attribute"); +} + +function checkAttribute(aElement, aAttribute, aNewValue, aExpectedValueForIDL) +{ + var expectedValueForIDL = aNewValue; + var expectedValueForContent = String(aNewValue); + + if (aExpectedValueForIDL !== undefined) { + expectedValueForIDL = aExpectedValueForIDL; + } + + if (aNewValue != null) { + aElement.setAttribute(aAttribute, aNewValue); + is(aElement.getAttribute(aAttribute), expectedValueForContent, + aAttribute + " content attribute should be " + expectedValueForContent); + is(aElement[aAttribute], expectedValueForIDL, + aAttribute + " IDL attribute should be " + expectedValueForIDL); + + if (parseFloat(aNewValue) == aNewValue) { + aElement[aAttribute] = aNewValue; + is(aElement.getAttribute(aAttribute), expectedValueForContent, + aAttribute + " content attribute should be " + expectedValueForContent); + is(aElement[aAttribute], parseFloat(expectedValueForIDL), + aAttribute + " IDL attribute should be " + parseFloat(expectedValueForIDL)); + } + } else { + aElement.removeAttribute(aAttribute); + is(aElement.getAttribute(aAttribute), null, + aAttribute + " content attribute should be null"); + is(aElement[aAttribute], expectedValueForIDL, + aAttribute + " IDL attribute should be " + expectedValueForIDL); + } +} + +function checkValueAttribute() +{ + var tests = [ + // value has to be a valid float, its default value is 0.0 otherwise. + [ null, 0.0 ], + [ 'fo', 0.0 ], + // If value < 0.0, 0.0 is used instead. + [ -1.0, 0.0 ], + // If value >= max, max is used instead (max default value is 1.0). + [ 2.0, 1.0 ], + [ 1.0, 0.5, 0.5 ], + [ 10.0, 5.0, 5.0 ], + [ 13.37, 13.37, 42.0 ], + // Regular reflection. + [ 0.0 ], + [ 0.5 ], + [ 1.0 ], + // Check double-precision value. + [ 0.234567898765432 ], + ]; + + var element = document.createElement('progress'); + + for (var test of tests) { + if (test[2]) { + element.setAttribute('max', test[2]); + } + + checkAttribute(element, 'value', test[0], test[1]); + + element.removeAttribute('max'); + } +} + +function checkMaxAttribute() +{ + var tests = [ + // max default value is 1.0. + [ null, 1.0 ], + // If value <= 0.0, 1.0 is used instead. + [ 0.0, 1.0 ], + [ -1.0, 1.0 ], + // Regular reflection. + [ 0.5 ], + [ 1.0 ], + [ 2.0 ], + // Check double-precision value. + [ 0.234567898765432 ], + ]; + + var element = document.createElement('progress'); + + for (var test of tests) { + checkAttribute(element, 'max', test[0], test[1]); + } +} + +function checkPositionAttribute() +{ + function checkPositionValue(aElement, aValue, aMax, aExpected) { + if (aValue != null) { + aElement.setAttribute('value', aValue); + } else { + aElement.removeAttribute('value'); + } + + if (aMax != null) { + aElement.setAttribute('max', aMax); + } else { + aElement.removeAttribute('max'); + } + + is(aElement.position, aExpected, "position IDL attribute should be " + aExpected); + } + + var tests = [ + // value has to be defined (indeterminate state). + [ null, null, -1.0 ], + [ null, 1.0, -1.0 ], + // value has to be defined to a valid float (indeterminate state). + [ 'foo', 1.0, -1.0 ], + // If value < 0.0, 0.0 is used instead. + [ -1.0, 1.0, 0.0 ], + // If value >= max, max is used instead. + [ 2.0, 1.0, 1.0 ], + // If max isn't present, max is set to 1.0. + [ 1.0, null, 1.0 ], + // If max isn't a valid float, max is set to 1.0. + [ 1.0, 'foo', 1.0 ], + // If max isn't > 0, max is set to 1.0. + [ 1.0, -1.0, 1.0 ], + // A few simple and valid values. + [ 0.0, 1.0, 0.0 ], + [ 0.1, 1.0, 0.1/1.0 ], + [ 0.1, 2.0, 0.1/2.0 ], + [ 10, 50, 10/50 ], + // Values implying .position is a double. + [ 1.0, 3.0, 1.0/3.0 ], + [ 0.1, 0.7, 0.1/0.7 ], + ]; + + var element = document.createElement('progress'); + + for (var test of tests) { + checkPositionValue(element, test[0], test[1], test[2], test[3]); + } +} + +function checkIndeterminatePseudoClass() +{ + function checkIndeterminate(aElement, aValue, aMax, aIndeterminate) { + if (aValue != null) { + aElement.setAttribute('value', aValue); + } else { + aElement.removeAttribute('value'); + } + + if (aMax != null) { + aElement.setAttribute('max', aMax); + } else { + aElement.removeAttribute('max'); + } + + is(aElement.matches("progress:indeterminate"), aIndeterminate, + "<progress> indeterminate state should be " + aIndeterminate); + } + + var tests = [ + // Indeterminate state: (value is undefined, or not a float) + // value has to be defined (indeterminate state). + [ null, null, true ], + [ null, 1.0, true ], + [ 'foo', 1.0, true ], + // Determined state: + [ -1.0, 1.0, false ], + [ 2.0, 1.0, false ], + [ 1.0, null, false ], + [ 1.0, 'foo', false ], + [ 1.0, -1.0, false ], + [ 0.0, 1.0, false ], + ]; + + var element = document.createElement('progress'); + + for (var test of tests) { + checkIndeterminate(element, test[0], test[1], test[2]); + } +} + +function checkFormListedElement(aElement) +{ + is(document.forms[0].elements.length, 0, "the form should have no element"); +} + +function checkLabelable(aElement) +{ + var content = document.getElementById('content'); + var label = document.createElement('label'); + + content.appendChild(label); + label.appendChild(aElement); + is(label.control, aElement, "progress should be labelable"); + + // Cleaning-up. + content.removeChild(label); + content.appendChild(aElement); +} + +function checkNotResetableAndFormSubmission(aElement) +{ + // Creating an input element to check the submission worked. + var form = document.forms[0]; + var input = document.createElement('input'); + + input.name = 'a'; + input.value = 'tulip'; + form.appendChild(input); + + // Setting values. + aElement.value = 42.0; + aElement.max = 100.0; + + document.getElementsByName('submit_frame')[0].addEventListener("load", function() { + is(frames.submit_frame.location.href, + `${location.origin}/tests/dom/html/test/forms/foo?a=tulip`, + "The progress element value should not be submitted"); + + checkNotResetable(); + }, {once: true}); + + form.submit(); +} + +function checkNotResetable() +{ + // Try to reset the form. + var form = document.forms[0]; + var element = document.getElementById('p'); + + element.value = 3.0; + element.max = 42.0; + + form.reset(); + + SimpleTest.executeSoon(function() { + is(element.value, 3.0, "progress.value should not have changed"); + is(element.max, 42.0, "progress.max should not have changed"); + + SimpleTest.finish(); + }); +} + +SimpleTest.waitForExplicitFinish(); + +var p = document.getElementById('p'); + +ok(p instanceof HTMLProgressElement, + "The progress element should be instance of HTMLProgressElement"); +is(p.constructor, HTMLProgressElement, + "The progress element constructor should be HTMLProgressElement"); + +checkFormIDLAttribute(p); + +checkValueAttribute(); + +checkMaxAttribute(); + +checkPositionAttribute(); + +checkIndeterminatePseudoClass(); + +checkFormListedElement(p); + +checkLabelable(p); + +checkNotResetableAndFormSubmission(p); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_radio_in_label.html b/dom/html/test/forms/test_radio_in_label.html new file mode 100644 index 0000000000..7e8a232cc3 --- /dev/null +++ b/dom/html/test/forms/test_radio_in_label.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=229925 +--> +<head> + <title>Test for Bug 229925</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=229925">Mozilla Bug 229925</a> +<p id="display"></p> +<form> + <label> + <span id="s1">LABEL</span> + <input type="radio" name="rdo" value="1" id="r1" onmousedown="document.body.appendChild(document.createTextNode('down'));"> + <input type="radio" name="rdo" value="2" id="r2" checked="checked"> + </label> +</form> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 229925 **/ +SimpleTest.waitForExplicitFinish(); +var r1 = document.getElementById("r1"); +var r2 = document.getElementById("r2"); +var s1 = document.getElementById("s1"); +startTest(); +function startTest() { + r1.click(); + ok(r1.checked, + "The first radio input element should be checked by clicking the element"); + r2.click(); + ok(r2.checked, + "The second radio input element should be checked by clicking the element"); + s1.click(); + ok(r1.checked, + "The first radio input element should be checked by clicking other element"); + + r1.focus(); + synthesizeKey("KEY_ArrowLeft"); + ok(r2.checked, + "The second radio input element should be checked by key"); + synthesizeKey("KEY_ArrowLeft"); + ok(r1.checked, + "The first radio input element should be checked by key"); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_radio_radionodelist.html b/dom/html/test/forms/test_radio_radionodelist.html new file mode 100644 index 0000000000..8761c22b58 --- /dev/null +++ b/dom/html/test/forms/test_radio_radionodelist.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=779723 +--> +<head> + <title>Test for Bug 779723</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=779723">Mozilla Bug 779723</a> +<p id="display"></p> +<form> + <input type="checkbox" name="rdo" value="0" id="r0" checked="checked"> + <input type="radio" name="rdo" id="r1"> + <input type="radio" name="rdo" id="r2" value="2"> +</form> +<script class="testbody" type="text/javascript"> +/** Test for Bug 779723 **/ + +var rdoList = document.forms[0].elements.namedItem('rdo'); +is(rdoList.value, "", "The value attribute should be empty"); + +document.getElementById('r2').checked = true; +is(rdoList.value, "2", "The value attribute should be 2"); + +document.getElementById('r1').checked = true; +is(rdoList.value, "on", "The value attribute should be on"); + +document.getElementById('r1').value = 1; +is(rdoList.value, "1", "The value attribute should be 1"); + +is(rdoList.value, document.getElementById('r1').value, + "The value attribute should be equal to the first checked radio input element's value"); +ok(!document.getElementById('r2').checked, + "The second radio input element should not be checked"); + +rdoList.value = '2'; +is(rdoList.value, document.getElementById('r2').value, + "The value attribute should be equal to the second radio input element's value"); +ok(document.getElementById('r2').checked, + "The second radio input element should be checked"); + +rdoList.value = '3'; +is(rdoList.value, document.getElementById('r2').value, + "The value attribute should be the second radio input element's value"); +ok(document.getElementById('r2').checked, + "The second radio input element should be checked"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/forms/test_reportValidation_preventDefault.html b/dom/html/test/forms/test_reportValidation_preventDefault.html new file mode 100644 index 0000000000..3f3b99d140 --- /dev/null +++ b/dom/html/test/forms/test_reportValidation_preventDefault.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1088761 +--> +<head> + <title>Test for Bug 1088761</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input, textarea, fieldset, button, select, output, object { background-color: rgb(0,0,0) !important; } + :valid { background-color: rgb(0,255,0) !important; } + :invalid { background-color: rgb(255,0,0) !important; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1088761">Mozilla Bug 1088761</a> +<p id="display"></p> +<div id="content" style="display: none"> + <fieldset id='f' oninvalid="invalidEventHandler(event, true);"></fieldset> + <input id='i' required oninvalid="invalidEventHandler(event, true);"> + <button id='b' oninvalid="invalidEventHandler(event, true);"></button> + <select id='s' required oninvalid="invalidEventHandler(event, true);"></select> + <textarea id='t' required oninvalid="invalidEventHandler(event, true);"></textarea> + <output id='o' oninvalid="invalidEventHandler(event, true);"></output> + <object id='obj' oninvalid="invalidEventHandler(event, true);"></object> +</div> +<div id="content2" style="display: none"> + <fieldset id='f2' oninvalid="invalidEventHandler(event, false);"></fieldset> + <input id='i2' required oninvalid="invalidEventHandler(event, false);"> + <button id='b2' oninvalid="invalidEventHandler(event, false);"></button> + <select id='s2' required oninvalid="invalidEventHandler(event, false);"></select> + <textarea id='t2' required oninvalid="invalidEventHandler(event, false);"></textarea> + <output id='o2' oninvalid="invalidEventHandler(event, false);"></output> + <object id='obj2' oninvalid="invalidEventHandler(event, false);"></object> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1088761 **/ + +var gInvalid = false; + +function invalidEventHandler(aEvent, isPreventDefault) +{ + if (isPreventDefault) { + aEvent.preventDefault(); + } + + is(aEvent.type, "invalid", "Invalid event type should be invalid"); + ok(!aEvent.bubbles, "Invalid event should not bubble"); + ok(aEvent.cancelable, "Invalid event should be cancelable"); + gInvalid = true; +} + +function checkReportValidityForInvalid(element) +{ + gInvalid = false; + ok(!element.reportValidity(), "reportValidity() should return false when the element is not valid"); + ok(gInvalid, "Invalid event should have been handled"); +} + +function checkReportValidityForValid(element) +{ + gInvalid = false; + ok(element.reportValidity(), "reportValidity() should return true when the element is valid"); + ok(!gInvalid, "Invalid event shouldn't have been handled"); +} + +checkReportValidityForInvalid(document.getElementById('i')); +checkReportValidityForInvalid(document.getElementById('s')); +checkReportValidityForInvalid(document.getElementById('t')); + +checkReportValidityForInvalid(document.getElementById('i2')); +checkReportValidityForInvalid(document.getElementById('s2')); +checkReportValidityForInvalid(document.getElementById('t2')); + +checkReportValidityForValid(document.getElementById('o')); +checkReportValidityForValid(document.getElementById('obj')); +checkReportValidityForValid(document.getElementById('f')); + +checkReportValidityForValid(document.getElementById('o2')); +checkReportValidityForValid(document.getElementById('obj2')); +checkReportValidityForValid(document.getElementById('f2')); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_required_attribute.html b/dom/html/test/forms/test_required_attribute.html new file mode 100644 index 0000000000..a95a5cc339 --- /dev/null +++ b/dom/html/test/forms/test_required_attribute.html @@ -0,0 +1,416 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=345822 +--> +<head> + <title>Test for Bug 345822</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345822">Mozilla Bug 345822</a> +<p id="display"></p> +<div id="content"> + <form> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 345822 **/ + +function checkNotSufferingFromBeingMissing(element, doNotApply) +{ + ok(!element.validity.valueMissing, + "Element should not suffer from value missing"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "Element should be valid"); + is(element.validationMessage, "", + "Validation message should be the empty string"); + + if (doNotApply) { + ok(!element.matches(':valid'), ":valid should not apply"); + ok(!element.matches(':invalid'), ":invalid should not apply"); + } else { + ok(element.matches(':valid'), ":valid should apply"); + ok(!element.matches(':invalid'), ":invalid should not apply"); + } +} + +function checkSufferingFromBeingMissing(element) +{ + ok(element.validity.valueMissing, "Element should suffer from value missing"); + ok(!element.validity.valid, "Element should not be valid"); + ok(!element.checkValidity(), "Element should not be valid"); + + if (element.type == 'checkbox') + { + is(element.validationMessage, + "Please check this box if you want to proceed.", + "Validation message is wrong"); + } + else if (element.type == 'radio') + { + is(element.validationMessage, + "Please select one of these options.", + "Validation message is wrong"); + } + else if (element.type == 'file') + { + is(element.validationMessage, + "Please select a file.", + "Validation message is wrong"); + } + else if (element.type == 'number') + { + is(element.validationMessage, + "Please enter a number.", + "Validation message is wrong"); + } + else // text fields + { + is(element.validationMessage, + "Please fill out this field.", + "Validation message is wrong"); + } + + ok(!element.matches(':valid'), ":valid should apply"); + ok(element.matches(':invalid'), ":invalid should not apply"); +} + +function checkTextareaRequiredValidity() +{ + var element = document.createElement('textarea'); + document.forms[0].appendChild(element); + + SpecialPowers.wrap(element).value = ''; + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.required = true; + checkSufferingFromBeingMissing(element); + + element.readOnly = true; + checkNotSufferingFromBeingMissing(element, true); + + element.readOnly = false; + checkSufferingFromBeingMissing(element); + + SpecialPowers.wrap(element).value = 'foo'; + checkNotSufferingFromBeingMissing(element); + + SpecialPowers.wrap(element).value = ''; + checkSufferingFromBeingMissing(element); + + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.focus(); + element.required = true; + SpecialPowers.wrap(element).value = 'foobar'; + element.blur(); + element.form.reset(); + checkSufferingFromBeingMissing(element); + + SpecialPowers.wrap(element).value = ''; + element.form.reportValidity(); + checkSufferingFromBeingMissing(element); + + element.form.reset(); + checkSufferingFromBeingMissing(element); + + // TODO: for the moment, a textarea outside of a document is mutable. + SpecialPowers.wrap(element).value = ''; // To make -moz-ui-valid apply. + element.required = false; + document.forms[0].removeChild(element); + checkNotSufferingFromBeingMissing(element); +} + +function checkInputRequiredNotApply(type, isBarred) +{ + var element = document.createElement('input'); + element.type = type; + document.forms[0].appendChild(element); + + SpecialPowers.wrap(element).value = ''; + element.required = false; + checkNotSufferingFromBeingMissing(element, isBarred); + + element.required = true; + checkNotSufferingFromBeingMissing(element, isBarred); + + element.required = false; + + document.forms[0].removeChild(element); + checkNotSufferingFromBeingMissing(element, isBarred); +} + +function checkInputRequiredValidity(type) +{ + var element = document.createElement('input'); + element.type = type; + document.forms[0].appendChild(element); + + SpecialPowers.wrap(element).value = ''; + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.required = true; + checkSufferingFromBeingMissing(element); + + element.readOnly = true; + checkNotSufferingFromBeingMissing(element, true); + + element.readOnly = false; + checkSufferingFromBeingMissing(element); + + if (element.type == 'email') { + SpecialPowers.wrap(element).value = 'foo@bar.com'; + } else if (element.type == 'url') { + SpecialPowers.wrap(element).value = 'http://mozilla.org/'; + } else if (element.type == 'number') { + SpecialPowers.wrap(element).value = '42'; + } else if (element.type == 'date') { + SpecialPowers.wrap(element).value = '2010-10-10'; + } else if (element.type == 'time') { + SpecialPowers.wrap(element).value = '21:21'; + // TODO: Bug 1864327. This test is wrong, and needs fixing properly. + // eslint-disable-next-line no-cond-assign + } else if (element.type = 'month') { + SpecialPowers.wrap(element).value = '2010-10'; + } else { + SpecialPowers.wrap(element).value = 'foo'; + } + checkNotSufferingFromBeingMissing(element); + + SpecialPowers.wrap(element).value = ''; + checkSufferingFromBeingMissing(element); + + element.focus(); + element.required = true; + SpecialPowers.wrap(element).value = 'foobar'; + element.blur(); + element.form.reset(); + checkSufferingFromBeingMissing(element); + + SpecialPowers.wrap(element).value = ''; + element.form.reportValidity(); + checkSufferingFromBeingMissing(element); + + element.form.reset(); + checkSufferingFromBeingMissing(element); + + element.required = true; + SpecialPowers.wrap(element).value = ''; // To make :-moz-ui-valid apply. + checkSufferingFromBeingMissing(element); + document.forms[0].removeChild(element); + // Removing the child changes nothing about whether it's valid + checkSufferingFromBeingMissing(element); +} + +function checkInputRequiredValidityForCheckbox() +{ + var element = document.createElement('input'); + element.type = 'checkbox'; + document.forms[0].appendChild(element); + + element.checked = false; + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.required = true; + checkSufferingFromBeingMissing(element); + + element.checked = true; + checkNotSufferingFromBeingMissing(element); + + element.checked = false; + checkSufferingFromBeingMissing(element); + + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.focus(); + element.required = true; + element.checked = true; + element.blur(); + element.form.reset(); + checkSufferingFromBeingMissing(element); + + element.required = true; + element.checked = false; + element.form.reportValidity(); + checkSufferingFromBeingMissing(element); + + element.form.reset(); + checkSufferingFromBeingMissing(element); + + element.required = true; + element.checked = false; + document.forms[0].removeChild(element); + checkSufferingFromBeingMissing(element); +} + +function checkInputRequiredValidityForRadio() +{ + var element = document.createElement('input'); + element.type = 'radio'; + element.name = 'test' + document.forms[0].appendChild(element); + + element.checked = false; + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.required = true; + checkSufferingFromBeingMissing(element); + + element.checked = true; + checkNotSufferingFromBeingMissing(element); + + element.checked = false; + checkSufferingFromBeingMissing(element); + + // A required radio button should not suffer from value missing if another + // radio button from the same group is checked. + var element2 = document.createElement('input'); + element2.type = 'radio'; + element2.name = 'test'; + + element2.checked = true; + element2.required = false; + document.forms[0].appendChild(element2); + + // Adding a checked radio should make required radio in the group not + // suffering from being missing. + checkNotSufferingFromBeingMissing(element); + + element.checked = false; + element2.checked = false; + checkSufferingFromBeingMissing(element); + + // The other radio button should not be disabled. + // A disabled checked radio button in the radio group + // is enough to not suffer from value missing. + element2.checked = true; + element2.disabled = true; + checkNotSufferingFromBeingMissing(element); + + // If a radio button is not required but another radio button is required in + // the same group, the not required radio button should suffer from value + // missing. + element2.disabled = false; + element2.checked = false; + element.required = false; + element2.required = true; + checkSufferingFromBeingMissing(element); + checkSufferingFromBeingMissing(element2); + + element.checked = true; + checkNotSufferingFromBeingMissing(element2); + + // The checked radio is not in the group anymore, element2 should be invalid. + element.form.removeChild(element); + checkNotSufferingFromBeingMissing(element); + checkSufferingFromBeingMissing(element2); + + element2.focus(); + element2.required = true; + element2.checked = true; + element2.blur(); + element2.form.reset(); + checkSufferingFromBeingMissing(element2); + + element2.required = true; + element2.checked = false; + element2.form.reportValidity(); + checkSufferingFromBeingMissing(element2); + + element2.form.reset(); + checkSufferingFromBeingMissing(element2); + + element2.required = true; + element2.checked = false; + document.forms[0].removeChild(element2); + checkSufferingFromBeingMissing(element2); +} + +function checkInputRequiredValidityForFile() +{ + var element = document.createElement('input'); + element.type = 'file' + document.forms[0].appendChild(element); + + var file = new File([""], "345822_file"); + + SpecialPowers.wrap(element).value = ""; + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.required = true; + checkSufferingFromBeingMissing(element); + + SpecialPowers.wrap(element).mozSetFileArray([file]); + checkNotSufferingFromBeingMissing(element); + + SpecialPowers.wrap(element).value = ""; + checkSufferingFromBeingMissing(element); + + element.required = false; + checkNotSufferingFromBeingMissing(element); + + element.focus(); + SpecialPowers.wrap(element).mozSetFileArray([file]); + element.required = true; + element.blur(); + element.form.reset(); + checkSufferingFromBeingMissing(element); + + element.required = true; + SpecialPowers.wrap(element).value = ''; + element.form.reportValidity(); + checkSufferingFromBeingMissing(element); + + element.form.reset(); + checkSufferingFromBeingMissing(element); + + element.required = true; + SpecialPowers.wrap(element).value = ''; + document.forms[0].removeChild(element); + checkSufferingFromBeingMissing(element); +} + +checkTextareaRequiredValidity(); + +// The require attribute behavior depend of the input type. +// First of all, checks for types that make the element barred from +// constraint validation. +var typeBarredFromConstraintValidation = ["hidden", "button", "reset"]; +for (type of typeBarredFromConstraintValidation) { + checkInputRequiredNotApply(type, true); +} + +// Then, checks for the types which do not use the required attribute. +var typeRequireNotApply = ['range', 'color', 'submit', 'image']; +for (type of typeRequireNotApply) { + checkInputRequiredNotApply(type, false); +} + +// Now, checking for all types which accept the required attribute. +var typeRequireApply = ["text", "password", "search", "tel", "email", "url", + "number", "date", "time", "month", "week", + "datetime-local"]; + +for (type of typeRequireApply) { + checkInputRequiredValidity(type); +} + +checkInputRequiredValidityForCheckbox(); +checkInputRequiredValidityForRadio(); +checkInputRequiredValidityForFile(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_restore_form_elements.html b/dom/html/test/forms/test_restore_form_elements.html new file mode 100644 index 0000000000..be22a29b7b --- /dev/null +++ b/dom/html/test/forms/test_restore_form_elements.html @@ -0,0 +1,174 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=737851 +--> +<head> + <meta charset="utf-8"> + + <title>Test for Bug 737851</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<body> + +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=737851">Mozilla Bug 737851</a> + +<p id="display"></p> + + +<div id="content"> + + <iframe id="frame" width="800px" height="600px" srcdoc=' + <html> + <body style="display:none;"> + + <h3>Checking persistence of inputs through js inserts and moves</h3> + <div id="test"> + <input id="a"/> + <input id="b"/> + <form id="form1"> + <input id="c"/> + <input id="d"/> + </form> + <form id="form2"> + <input id="radio1" type="radio" name="radio"/> + <input type="radio" name="radio"/> + <input type="radio" name="radio"/> + <input type="radio" name="radio"/> + </form> + <input id="e"/> + </div> + + <h3>Bug 728798: checking persistence of inputs when forward-using @form</h3> + <div> + <input id="728798-a" form="728798-form" name="a"/> + <form id="728798-form"> + <input id="728798-b" form="728798-form" name="b"/> + <input id="728798-c" name="c"/> + </form> + <input id="728798-d" form="728798-form" name="d"/> + </div> + + </body> + </html> + '></iframe> + +</div> + + +<pre id="test"> +<script type="text/javascript"> + +var frameElem = document.getElementById("frame"); +var frame = frameElem.contentWindow; + + +/* -- Main test run -- */ + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + shuffle(); + fill(); + frameElem.addEventListener("load", function() { + shuffle(); + checkAllFields(); + SimpleTest.finish(); + }); + frame.location.reload(); +}) + + +/* -- Input fields js changes and moves -- */ + +function shuffle() { + var framedoc = frame.document; + + // Insert a button (toplevel) + var btn = framedoc.createElement("button"); + var testdiv = framedoc.getElementById("test"); + testdiv.insertBefore(btn, framedoc.getElementById("b")); + + // Insert a dynamically generated input (in a form) + var newInput = framedoc.createElement("input"); + newInput.setAttribute("id","c0"); + var form1 = framedoc.getElementById("form1"); + form1.insertBefore(newInput, form1.firstChild); + + // Move an input around + var inputD = framedoc.getElementById("d"); + var form2 = framedoc.getElementById("form2"); + form2.insertBefore(inputD, form2.firstChild) + + // Clone an existing input + var inputE2 = framedoc.getElementById("e").cloneNode(true); + inputE2.setAttribute("id","e2"); + testdiv.appendChild(inputE2); +} + + +/* -- Input fields fill & check -- */ + +/* Values entered in the input fields (by id) */ + +var fieldValues = { + 'a':'simple input', + 'b':'moved by inserting a button before (no form)', + 'c0':'dynamically generated input', + 'c':'moved by inserting an input before (in a form)', + 'd':'moved from a form to another', + 'e':'the original', + 'e2':'the clone', + '728798-a':'before the form', + '728798-b':'from within the form', + '728798-c':'no form attribute in the form', + '728798-d':'after the form' +} + +/* Fields for which the input is changed, and corresponding value + (clone and creation, same behaviour as webkit) */ + +var changedFields = { + // dynamically generated input field not preserved + 'c0':'', + // cloned input field is restored with the value of the original + 'e2':fieldValues.e +} + +/* Simulate user input by entering the values */ + +function fill() { + for (id in fieldValues) { + frame.document.getElementById(id).value = fieldValues[id]; + } + // an input is inserted before the radios (that may move the selected one by 1) + frame.document.getElementById('radio1').checked = true; +} + +/* Check that all the fields are as they have been entered */ + +function checkAllFields() { + + for (id in fieldValues) { + var fieldValue = frame.document.getElementById(id).value; + if (changedFields[id] === undefined) { + is(fieldValue, fieldValues[id], + "Field "+id+" should be restored after reload"); + } else { + is(fieldValue, changedFields[id], + "Field "+id+" normally gets a different value after reload"); + } + } + + ok(frame.document.getElementById('radio1').checked, + "Radio button radio1 should be restored after reload") + +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_save_restore_custom_elements.html b/dom/html/test/forms/test_save_restore_custom_elements.html new file mode 100644 index 0000000000..489ad0ca2f --- /dev/null +++ b/dom/html/test/forms/test_save_restore_custom_elements.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1556358 +--> + +<head> + <title>Test for Bug 1556358</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1556358">Mozilla Bug 1556358</a> + <p id="display"></p> + <div id="content"> + <iframe src="save_restore_custom_elements_sample.html"></iframe> + </div> + <script type="application/javascript"> + /** Test for Bug 1556358 **/ + + function formDataWith(...entries) { + const formData = new FormData(); + for (let [key, value] of entries) { + formData.append(key, value); + } + return formData; + } + + const states = [ + "test state", + new File(["state"], "state.txt"), + formDataWith(["1", "state"], ["2", new Blob(["state_blob"])]), + null, + undefined, + ]; + const values = [ + "test value", + new File(["value"], "value.txt"), + formDataWith(["1", "value"], ["2", new Blob(["value_blob"])]), + "null state", + "both value and state", + ]; + + add_task(async () => { + const frame = document.querySelector("iframe"); + const elementTags = ["c-e", "upgraded-ce"]; + + // Set the custom element values. + for (const tags of elementTags) { + [...frame.contentDocument.querySelectorAll(tags)] + .forEach((e, i) => { + e.set(states[i], values[i]); + }); + } + + await new Promise(resolve => { + frame.addEventListener("load", resolve); + frame.contentWindow.location.reload(); + }); + + for (const tag of elementTags) { + // Retrieve the restored values. + const ceStates = + [...frame.contentDocument.querySelectorAll(tag)].map((e) => e.state); + is(ceStates.length, 5, "Should have 5 custom element states"); + + const [restored, original] = [ceStates, states]; + is(restored[0], original[0], "Value should be restored"); + + const file = restored[1]; + isnot(file, original[1], "Restored file object differs from original object."); + is(file.name, original[1].name, "File name should be restored"); + is(await file.text(), await original[1].text(), "File text should be restored"); + + const formData = restored[2]; + isnot(formData, original[2], "Restored formdata object differs from original object."); + is(formData.get("1"), original[2].get("1"), "Form data string should be restored"); + is(await formData.get("2").text(), await original[2].get("2").text(), "Form data blob should be restored"); + + isnot(restored[3], original[3], "Null values don't get restored"); + is(restored[3], undefined, "Null values don't get restored"); + + is(restored[4], "both value and state", "Undefined state should be set to value"); + } + }); + </script> +</body> + +</html> diff --git a/dom/html/test/forms/test_save_restore_radio_groups.html b/dom/html/test/forms/test_save_restore_radio_groups.html new file mode 100644 index 0000000000..c5ef924a0e --- /dev/null +++ b/dom/html/test/forms/test_save_restore_radio_groups.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=350022 +--> +<head> + <title>Test for Bug 350022</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=350022">Mozilla Bug 350022</a> +<p id="display"></p> +<div id="content"><!-- style="display: none">--> + <iframe src="save_restore_radio_groups.sjs"></iframe> + <iframe src="save_restore_radio_groups.sjs"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 350022 **/ + +function checkRadioGroup(aFrame, aResults) +{ + var radios = frames[aFrame].document.getElementsByTagName('input'); + + is(radios.length, aResults.length, + "Radio group should have " + aResults.length + "elements"); + + for (var i=0; i<aResults.length; ++i) { + is(radios[i].checked, aResults[i], + "Radio checked state should be " + aResults[i]); + } +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + /** + * We have two iframes each containing one radio button group. + * We are going to change the selected radio button in one group. + * Then, both iframes will be reloaded and the new groups will have another + * radio checked by default. + * For the first group (which had a selection change), nothing should change. + * For the second, the selected radio button should change. + */ + checkRadioGroup(0, [true, false, false]); + checkRadioGroup(1, [true, false, false]); + + frames[0].document.getElementsByTagName('input')[2].checked = true; + checkRadioGroup(0, [false, false, true]); + + framesElts = document.getElementsByTagName('iframe'); + framesElts[0].addEventListener("load", function() { + checkRadioGroup(0, [false, false, true]); + + framesElts[1].addEventListener("load", function() { + checkRadioGroup(1, [false, true, false]); + SimpleTest.finish(); + }, {once: true}); + + frames[1].location.reload(); + }, {once: true}); + + frames[0].location.reload(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_select_change_event.html b/dom/html/test/forms/test_select_change_event.html new file mode 100644 index 0000000000..ec3ed58c5e --- /dev/null +++ b/dom/html/test/forms/test_select_change_event.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1265968 +--> +<head> + <title>Test for Bug 1265968</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265968">Mozilla Bug 1265968</a> +<p id="display"></p> +<div id="content"> + <select id="select" onchange="++selectChange;"> + <option>one</option> + <option>two</option> + <option>three</option> + <option>four</option> + <option>five</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + var select = document.getElementById("select"); + var selectChange = 0; + var expectedChange = 0; + + select.focus(); + for (var i = 1; i < select.length; i++) { + synthesizeKey("KEY_ArrowDown"); + is(select.options[i].selected, true, "Option should be selected"); + is(selectChange, ++expectedChange, "Down key should fire change event."); + } + + // We are at the end of the list, going down should not fire change event. + synthesizeKey("KEY_ArrowDown"); + is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list."); + + for (var i = select.length - 2; i >= 0; i--) { + synthesizeKey("KEY_ArrowUp"); + is(select.options[i].selected, true, "Option should be selected"); + is(selectChange, ++expectedChange, "Up key should fire change event."); + } + + // We are at the top of the list, going up should not fire change event. + synthesizeKey("KEY_ArrowUp"); + is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list."); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_select_input_change_event.html b/dom/html/test/forms/test_select_input_change_event.html new file mode 100644 index 0000000000..fcf384e423 --- /dev/null +++ b/dom/html/test/forms/test_select_input_change_event.html @@ -0,0 +1,122 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1265968 +--> +<head> + <title>Test for Bug 1024350</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1024350">Mozilla Bug 1024350</a> +<p id="display"></p> +<div id="content"> + <select oninput='++selectInput;' onchange="++selectChange;"> + <option>one</option> + </select> + <select oninput='++selectInput;' onchange="++selectChange;"> + <option>one</option> + <option>two</option> + </select> + <select multiple size='1' oninput='++selectInput;' onchange="++selectChange;"> + <option>one</option> + </select> + <select multiple oninput='++selectInput;' onchange="++selectChange;"> + <option>one</option> + <option>two</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + var selectSingleOneItem = document.getElementsByTagName('select')[0]; + var selectSingle = document.getElementsByTagName('select')[1]; + var selectMultipleOneItem = document.getElementsByTagName('select')[2]; + var selectMultiple = document.getElementsByTagName('select')[3]; + + var selectChange = 0; + var selectInput = 0; + var expectedChange = 0; + var expectedInput = 0; + + selectSingleOneItem.focus(); + synthesizeKey("KEY_ArrowDown"); + is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list."); + is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list."); + + synthesizeKey("KEY_ArrowUp"); + is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list."); + is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list."); + + selectSingle.focus(); + for (var i = 1; i < selectSingle.length; i++) { + synthesizeKey("KEY_ArrowDown"); + + is(selectSingle.options[i].selected, true, "Option should be selected"); + is(selectInput, ++expectedInput, "Down key should fire input event."); + is(selectChange, ++expectedChange, "Down key should fire change event."); + } + + // We are at the end of the list, going down should not fire change event. + synthesizeKey("KEY_ArrowDown"); + is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list."); + is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list."); + + for (var i = selectSingle.length - 2; i >= 0; i--) { + synthesizeKey("KEY_ArrowUp"); + + is(selectSingle.options[i].selected, true, "Option should be selected"); + is(selectInput, ++expectedInput, "Up key should fire input event."); + is(selectChange, ++expectedChange, "Up key should fire change event."); + } + + // We are at the top of the list, going up should not fire change event. + synthesizeKey("KEY_ArrowUp"); + is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list."); + is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list."); + + selectMultipleOneItem.focus(); + synthesizeKey("KEY_ArrowDown"); + is(selectInput, ++expectedInput, "Down key should fire input event when reaching end of the list."); + is(selectChange, ++expectedChange, "Down key should fire change event when reaching end of the list."); + + synthesizeKey("KEY_ArrowDown"); + is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list."); + is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list."); + + synthesizeKey("KEY_ArrowUp"); + is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list."); + is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list."); + + selectMultiple.focus(); + for (var i = 0; i < selectMultiple.length; i++) { + synthesizeKey("KEY_ArrowDown"); + + is(selectMultiple.options[i].selected, true, "Option should be selected"); + is(selectInput, ++expectedInput, "Down key should fire input event."); + is(selectChange, ++expectedChange, "Down key should fire change event."); + } + + // We are at the end of the list, going down should not fire change event. + synthesizeKey("KEY_ArrowDown"); + is(selectInput, expectedInput, "Down key should not fire input event when reaching end of the list."); + is(selectChange, expectedChange, "Down key should not fire change event when reaching end of the list."); + + for (var i = selectMultiple.length - 2; i >= 0; i--) { + synthesizeKey("KEY_ArrowUp"); + + is(selectMultiple.options[i].selected, true, "Option should be selected"); + is(selectInput, ++expectedInput, "Up key should fire input event."); + is(selectChange, ++expectedChange, "Up key should fire change event."); + } + + // We are at the top of the list, going up should not fire change event. + synthesizeKey("KEY_ArrowUp"); + is(selectInput, expectedInput, "Up key should not fire input event when reaching top of the list."); + is(selectChange, expectedChange, "Up key should not fire change event when reaching top of the list."); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_select_selectedOptions.html b/dom/html/test/forms/test_select_selectedOptions.html new file mode 100644 index 0000000000..745e0ba4f3 --- /dev/null +++ b/dom/html/test/forms/test_select_selectedOptions.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=596681 +--> +<head> + <title>Test for HTMLSelectElement.selectedOptions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596681">Mozilla Bug 596681</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLSelectElement's selectedOptions attribute. + * + * selectedOptions is a live list of the options that have selectedness of true + * (not the selected content attribute). + * + * See http://www.whatwg.org/html/#dom-select-selectedoptions + **/ + +function checkSelectedOptions(size, elements) +{ + is(selectedOptions.length, size, + "select should have " + size + " selected options"); + for (let i = 0; i < size; ++i) { + ok(selectedOptions[i], "selected option is valid"); + if (selectedOptions[i]) { + is(selectedOptions[i].value, elements[i].value, "selected options are correct"); + } + } +} + +let select = document.createElement("select"); +document.body.appendChild(select); +let selectedOptions = select.selectedOptions; + +ok("selectedOptions" in select, + "select element should have a selectedOptions IDL attribute"); + +ok(select.selectedOptions instanceof HTMLCollection, + "selectedOptions should be an HTMLCollection instance"); + +let option1 = document.createElement("option"); +let option2 = document.createElement("option"); +let option3 = document.createElement("option"); +option1.id = "option1"; +option1.value = "option1"; +option2.value = "option2"; +option3.value = "option3"; + +checkSelectedOptions(0, null); + +select.add(option1, null); +is(selectedOptions.namedItem("option1").value, "option1", "named getter works"); +checkSelectedOptions(1, [option1]); + +select.add(option2, null); +checkSelectedOptions(1, [option1]); + +select.options[1].selected = true; +checkSelectedOptions(1, [option2]); + +select.multiple = true; +checkSelectedOptions(1, [option2]); + +select.options[0].selected = true; +checkSelectedOptions(2, [option1, option2]); + +option1.selected = false; +// Usinig selected directly on the option should work. +checkSelectedOptions(1, [option2]); + +select.remove(1); +select.add(option2, 0); +select.options[0].selected = true; +select.options[1].selected = true; +// Should be in tree order. +checkSelectedOptions(2, [option2, option1]); + +select.add(option3, null); +checkSelectedOptions(2, [option2, option1]); + +select.options[2].selected = true; +checkSelectedOptions(3, [option2, option1, option3]); + +select.length = 0; +option1.selected = false; +option2.selected = false; +option3.selected = false; +var optgroup1 = document.createElement("optgroup"); +optgroup1.appendChild(option1); +optgroup1.appendChild(option2); +select.add(optgroup1) +var optgroup2 = document.createElement("optgroup"); +optgroup2.appendChild(option3); +select.add(optgroup2); + +checkSelectedOptions(0, null); + +option2.selected = true; +checkSelectedOptions(1, [option2]); + +option3.selected = true; +checkSelectedOptions(2, [option2, option3]); + +optgroup1.removeChild(option2); +checkSelectedOptions(1, [option3]); + +document.body.removeChild(select); +option1.selected = true; +checkSelectedOptions(2, [option1, option3]); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_select_validation.html b/dom/html/test/forms/test_select_validation.html new file mode 100644 index 0000000000..6d02aa0746 --- /dev/null +++ b/dom/html/test/forms/test_select_validation.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=942321 +--> +<head> + <title>Test for Bug 942321</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=942321">Mozilla Bug 942321</a> +<p id="display"></p> +<form id="form" href=""> + <select required id="testselect"> + <option id="placeholder" value="" selected>placeholder</option> + <option value="test" id="actualvalue">test</option> + <select> + <input type="submit" /> +</form> +<script class="testbody" type="text/javascript"> +/** Test for Bug 942321 **/ +var option = document.getElementById("actualvalue"); +option.selected = true; +is(form.checkValidity(), true, "Select is required and should be valid"); + +var placeholder = document.getElementById("placeholder"); +placeholder.selected = true; +is(form.checkValidity(), false, "Select is required and should be invalid"); + +placeholder.value = "not-invalid-anymore"; +is(form.checkValidity(), true, "Select is required and should be valid when option's value is changed by javascript"); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/forms/test_set_range_text.html b/dom/html/test/forms/test_set_range_text.html new file mode 100644 index 0000000000..f85014ae77 --- /dev/null +++ b/dom/html/test/forms/test_set_range_text.html @@ -0,0 +1,242 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=850364 +--> +<head> +<title>Tests for Bug 850364 && Bug 918940</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=850364">Mozilla Bug 850364</a> +<p id="display"></p> +<div id="content"> + +<!-- "SetRangeText() supported types"--> +<input type="text" id="input_text"></input> +<input type="search" id="input_search"></input> +<input type="url" id="input_url"></input> +<input type="tel" id="input_tel"></input> +<input type="password" id="input_password"></input> +<textarea id="input_textarea"></textarea> + +<!-- "SetRangeText() non-supported types" --> +<input type="button" id="input_button"></input> +<input type="submit" id="input_submit"></input> +<input type="image" id="input_image"></input> +<input type="reset" id="input_reset"></input> +<input type="radio" id="input_radio"></input> +<input type="checkbox" id="input_checkbox"></input> +<input type="range" id="input_range"></input> +<input type="file" id="input_file"></input> +<input type="email" id="input_email"></input> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + /** Tests for Bug 850364 && Bug 918940**/ + + var SupportedTypes = ["text", "search", "url", "tel", "password", "textarea"]; + var NonSupportedTypes = ["button", "submit", "image", "reset", "radio", + "checkbox", "range", "file", "email"]; + + SimpleTest.waitForExplicitFinish(); + + function TestInputs() { + + var opThrows, elem, i, msg; + + //Non-supported types should throw + for (i = 0; i < NonSupportedTypes.length; ++i) { + opThrows = false; + msg = "input_" + NonSupportedTypes[i]; + elem = document.getElementById(msg); + elem.focus(); + try { + elem.setRangeText("abc"); + } catch (ex) { + opThrows = true; + } + ok(opThrows, msg + " should throw InvalidStateError"); + } + + var numOfSelectCalls = 0, expectedNumOfSelectCalls = 0; + //Supported types should not throw + for (i = 0; i < SupportedTypes.length; ++i) { + opThrows = false; + msg = "input_" + SupportedTypes[i]; + elem = document.getElementById(msg); + elem.focus(); + try { + elem.setRangeText("abc"); + } catch (ex) { + opThrows = true; + } + is(opThrows, false, msg + " should not throw InvalidStateError"); + + elem.addEventListener("select", function (aEvent) { + ok(true, "select event should be fired for " + aEvent.target.id); + if (++numOfSelectCalls == expectedNumOfSelectCalls) { + SimpleTest.finish(); + } else if (numOfSelectCalls > expectedNumOfSelectCalls) { + ok(false, "Too many select events were fired"); + } + }); + + elem.addEventListener("input", function (aEvent) { + ok(false, "input event should NOT be fired for " + + aEvent.target.id); + }); + + var test = " setRange(replacement), shrink"; + elem.value = "0123456789ABCDEF"; + elem.setSelectionRange(1, 6); + elem.setRangeText("xyz"); + is(elem.value, "0xyz6789ABCDEF", msg + test); + is(elem.selectionStart, 1, msg + test); + is(elem.selectionEnd, 4, msg + test); + elem.setRangeText("mnk"); + is(elem.value, "0mnk6789ABCDEF", msg + test); + expectedNumOfSelectCalls += 2; + + test = " setRange(replacement), expand"; + elem.value = "0123456789ABCDEF"; + elem.setSelectionRange(1, 2); + elem.setRangeText("xyz"); + is(elem.value, "0xyz23456789ABCDEF", msg + test); + is(elem.selectionStart, 1, msg + test); + is(elem.selectionEnd, 4, msg + test); + elem.setRangeText("mnk"); + is(elem.value, "0mnk23456789ABCDEF", msg + test); + expectedNumOfSelectCalls += 2; + + test = " setRange(replacement) pure insertion at start"; + elem.value = "0123456789ABCDEF"; + elem.setSelectionRange(0, 0); + elem.setRangeText("xyz"); + is(elem.value, "xyz0123456789ABCDEF", msg + test); + is(elem.selectionStart, 0, msg + test); + is(elem.selectionEnd, 0, msg + test); + elem.setRangeText("mnk"); + is(elem.value, "mnkxyz0123456789ABCDEF", msg + test); + expectedNumOfSelectCalls += 1; + + test = " setRange(replacement) pure insertion in the middle"; + elem.value = "0123456789ABCDEF"; + elem.setSelectionRange(4, 4); + elem.setRangeText("xyz"); + is(elem.value, "0123xyz456789ABCDEF", msg + test); + is(elem.selectionStart, 4, msg + test); + is(elem.selectionEnd, 4, msg + test); + elem.setRangeText("mnk"); + is(elem.value, "0123mnkxyz456789ABCDEF", msg + test); + expectedNumOfSelectCalls += 1; + + test = " setRange(replacement) pure insertion at the end"; + elem.value = "0123456789ABCDEF"; + elem.setSelectionRange(16, 16); + elem.setRangeText("xyz"); + is(elem.value, "0123456789ABCDEFxyz", msg + test); + is(elem.selectionStart, 16, msg + test); + is(elem.selectionEnd, 16, msg + test); + elem.setRangeText("mnk"); + is(elem.value, "0123456789ABCDEFmnkxyz", msg + test); + + //test SetRange(replacement, start, end, mode) with start > end + try { + elem.setRangeText("abc", 20, 4); + } catch (ex) { + opThrows = (ex.name == "IndexSizeError" && ex.code == DOMException.INDEX_SIZE_ERR); + } + is(opThrows, true, msg + " should throw IndexSizeError"); + + //test SelectionMode 'select' + elem.value = "0123456789ABCDEF"; + elem.setRangeText("xyz", 4, 9, "select"); + is(elem.value, "0123xyz9ABCDEF", msg + ".value == \"0123xyz9ABCDEF\""); + is(elem.selectionStart, 4, msg + ".selectionStart == 4, with \"select\""); + is(elem.selectionEnd, 7, msg + ".selectionEnd == 7, with \"select\""); + expectedNumOfSelectCalls += 1; + + elem.setRangeText("pqm", 6, 25, "select"); + is(elem.value, "0123xypqm", msg + ".value == \"0123xypqm\""); + is(elem.selectionStart, 6, msg + ".selectionStart == 6, with \"select\""); + is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"select\""); + expectedNumOfSelectCalls += 1; + + //test SelectionMode 'start' + elem.value = "0123456789ABCDEF"; + elem.setRangeText("xyz", 4, 9, "start"); + is(elem.value, "0123xyz9ABCDEF", msg + ".value == \"0123xyz9ABCDEF\""); + is(elem.selectionStart, 4, msg + ".selectionStart == 4, with \"start\""); + is(elem.selectionEnd, 4, msg + ".selectionEnd == 4, with \"start\""); + expectedNumOfSelectCalls += 1; + + elem.setRangeText("pqm", 6, 25, "start"); + is(elem.value, "0123xypqm", msg + ".value == \"0123xypqm\""); + is(elem.selectionStart, 6, msg + ".selectionStart == 6, with \"start\""); + is(elem.selectionEnd, 6, msg + ".selectionEnd == 6, with \"start\""); + expectedNumOfSelectCalls += 1; + + //test SelectionMode 'end' + elem.value = "0123456789ABCDEF"; + elem.setRangeText("xyz", 4, 9, "end"); + is(elem.value, "0123xyz9ABCDEF", msg + ".value == \"0123xyz9ABCDEF\""); + is(elem.selectionStart, 7, msg + ".selectionStart == 7, with \"end\""); + is(elem.selectionEnd, 7, msg + ".selectionEnd == 7, with \"end\""); + expectedNumOfSelectCalls += 1; + + elem.setRangeText("pqm", 6, 25, "end"); + is(elem.value, "0123xypqm", msg + ".value == \"0123xypqm\""); + is(elem.selectionStart, 9, msg + ".selectionStart == 9, with \"end\""); + is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"end\""); + expectedNumOfSelectCalls += 1; + + //test SelectionMode 'preserve' (default) + + //subcase: selection{Start|End} > end + elem.value = "0123456789"; + elem.setSelectionRange(6, 9); + elem.setRangeText("Z", 1, 2, "preserve"); + is(elem.value, "0Z23456789", msg + ".value == \"0Z23456789\""); + is(elem.selectionStart, 6, msg + ".selectionStart == 6, with \"preserve\""); + is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"preserve\""); + expectedNumOfSelectCalls += 1; + + //subcase: selection{Start|End} < end + elem.value = "0123456789"; + elem.setSelectionRange(4, 5); + elem.setRangeText("QRST", 2, 9, "preserve"); + is(elem.value, "01QRST9", msg + ".value == \"01QRST9\""); + is(elem.selectionStart, 2, msg + ".selectionStart == 2, with \"preserve\""); + is(elem.selectionEnd, 6, msg + ".selectionEnd == 6, with \"preserve\""); + expectedNumOfSelectCalls += 2; + + //subcase: selectionStart > end, selectionEnd < end + elem.value = "0123456789"; + elem.setSelectionRange(8, 4); + elem.setRangeText("QRST", 1, 5); + is(elem.value, "0QRST56789", msg + ".value == \"0QRST56789\""); + is(elem.selectionStart, 1, msg + ".selectionStart == 1, with \"default\""); + is(elem.selectionEnd, 5, msg + ".selectionEnd == 5, with \"default\""); + expectedNumOfSelectCalls += 2; + + //subcase: selectionStart < end, selectionEnd > end + elem.value = "0123456789"; + elem.setSelectionRange(4, 9); + elem.setRangeText("QRST", 2, 6); + is(elem.value, "01QRST6789", msg + ".value == \"01QRST6789\""); + is(elem.selectionStart, 2, msg + ".selectionStart == 2, with \"default\""); + is(elem.selectionEnd, 9, msg + ".selectionEnd == 9, with \"default\""); + expectedNumOfSelectCalls += 2; + } + } + + addLoadEvent(TestInputs); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_step_attribute.html b/dom/html/test/forms/test_step_attribute.html new file mode 100644 index 0000000000..f0af250c06 --- /dev/null +++ b/dom/html/test/forms/test_step_attribute.html @@ -0,0 +1,1060 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=635553 +--> +<head> + <title>Test for Bug 635553</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=635499">Mozilla Bug 635499</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 635553 **/ + +var data = [ + { type: 'hidden', apply: false }, + { type: 'text', apply: false }, + { type: 'search', apply: false }, + { type: 'tel', apply: false }, + { type: 'url', apply: false }, + { type: 'email', apply: false }, + { type: 'password', apply: false }, + { type: 'date', apply: true }, + { type: 'month', apply: true }, + { type: 'week', apply: true }, + { type: 'time', apply: true }, + { type: 'datetime-local', apply: true }, + { type: 'number', apply: true }, + { type: 'range', apply: true }, + { type: 'color', apply: false }, + { type: 'checkbox', apply: false }, + { type: 'radio', apply: false }, + { type: 'file', apply: false }, + { type: 'submit', apply: false }, + { type: 'image', apply: false }, + { type: 'reset', apply: false }, + { type: 'button', apply: false }, +]; + +function getFreshElement(type) { + var elmt = document.createElement('input'); + elmt.type = type; + return elmt; +} + +function checkValidity(aElement, aValidity, aApply, aData) +{ + aValidity = aApply ? aValidity : true; + + is(aElement.validity.valid, aValidity, + "element validity should be " + aValidity); + is(aElement.validity.stepMismatch, !aValidity, + "element step mismatch status should be " + !aValidity); + + if (aValidity) { + is(aElement.validationMessage, "", "There should be no validation message."); + } else { + if (aElement.validity.rangeUnderflow) { + var underflowMsg = + (aElement.type == "date" || aElement.type == "time") ? + ("Please select a value that is no earlier than " + aElement.min + ".") : + ("Please select a value that is no less than " + aElement.min + "."); + is(aElement.validationMessage, underflowMsg, + "Checking range underflow validation message."); + } else if (aData.low == aData.high) { + is(aElement.validationMessage, "Please select a valid value. " + + "The nearest valid value is " + aData.low + ".", + "There should be a validation message."); + } else { + is(aElement.validationMessage, "Please select a valid value. " + + "The two nearest valid values are " + aData.low + " and " + aData.high + ".", + "There should be a validation message."); + } + } + + is(aElement.matches(":valid"), aElement.willValidate && aValidity, + (aElement.willValidate && aValidity) ? ":valid should apply" : "valid shouldn't apply"); + is(aElement.matches(":invalid"), aElement.willValidate && !aValidity, + (aElement.wil && aValidity) ? ":invalid shouldn't apply" : "valid should apply"); +} + +for (var test of data) { + var input = getFreshElement(test.type); + var apply = test.apply; + + if (test.todo) { + todo_is(input.type, test.type, test.type + " isn't implemented yet"); + continue; + } + + // The element should be valid, there should be no step mismatch. + checkValidity(input, true, apply); + + // Checks to do for all types that support step: + // - check for @step=0, + // - check for @step behind removed, + // - check for @step being 'any' with different case variations. + switch (input.type) { + case 'text': + case 'hidden': + case 'search': + case 'password': + case 'tel': + case 'radio': + case 'checkbox': + case 'reset': + case 'button': + case 'submit': + case 'image': + case 'color': + input.value = '0'; + checkValidity(input, true, apply); + break; + case 'url': + input.value = 'http://mozilla.org'; + checkValidity(input, true, apply); + break; + case 'email': + input.value = 'foo@bar.com'; + checkValidity(input, true, apply); + break; + case 'file': + var file = new File([''], '635499_file'); + + SpecialPowers.wrap(input).mozSetFileArray([file]); + checkValidity(input, true, apply); + + break; + case 'date': + // For date, the step is calulated on the timestamp since 1970-01-01 + // which mean that for all dates prior to the epoch, this timestamp is < 0 + // and the behavior might differ, therefore we have to test for these cases. + + // When step is invalid, every date is valid + input.step = 0; + input.value = '2012-07-05'; + checkValidity(input, true, apply); + + input.step = 'foo'; + input.value = '1970-01-01'; + checkValidity(input, true, apply); + + input.step = '-1'; + input.value = '1969-12-12'; + checkValidity(input, true, apply); + + input.removeAttribute('step'); + input.value = '1500-01-01'; + checkValidity(input, true, apply); + + input.step = 'any'; + input.value = '1966-12-12'; + checkValidity(input, true, apply); + + input.step = 'ANY'; + input.value = '2013-02-03'; + checkValidity(input, true, apply); + + // When min is set to a valid date, there is a step base. + input.min = '2008-02-28'; + input.step = '2'; + input.value = '2008-03-01'; + checkValidity(input, true, apply); + + input.value = '2008-02-29'; + checkValidity(input, false, apply, { low: "2008-02-28", high: "2008-03-01" }); + + input.min = '2008-02-27'; + input.value = '2008-02-28'; + checkValidity(input, false, apply, { low: "2008-02-27", high: "2008-02-29" }); + + input.min = '2009-02-27'; + input.value = '2009-02-28'; + checkValidity(input, false, apply, { low: "2009-02-27", high: "2009-03-01" }); + + input.min = '2009-02-01'; + input.step = '1.1'; + input.value = '2009-02-02'; + checkValidity(input, true, apply); + + // Without any step attribute the date is valid + input.removeAttribute('step'); + checkValidity(input, true, apply); + + input.min = '1950-01-01'; + input.step = '366'; + input.value = '1951-01-01'; + checkValidity(input, false, apply, { low: "1950-01-01", high: "1951-01-02" }); + + input.min = '1951-01-01'; + input.step = '365'; + input.value = '1952-01-01'; + checkValidity(input, true, apply); + + input.step = '0.9'; + input.value = '1951-01-02'; + is(input.step, '0.9', "check that step value is unchanged"); + checkValidity(input, true, apply); + + input.step = '0.4'; + input.value = '1951-01-02'; + is(input.step, '0.4', "check that step value is unchanged"); + checkValidity(input, true, apply); + + input.step = '1.5'; + input.value = '1951-01-02'; + is(input.step, '1.5', "check that step value is unchanged"); + checkValidity(input, false, apply, { low: "1951-01-01", high: "1951-01-03" }); + + input.value = '1951-01-08'; + checkValidity(input, false, apply, { low: "1951-01-07", high: "1951-01-09" }); + + input.step = '3000'; + input.min= '1968-01-01'; + input.value = '1968-05-12'; + checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" }); + + input.value = '1971-01-01'; + checkValidity(input, false, apply, { low: "1968-01-01", high: "1976-03-19" }); + + input.value = '1991-01-01'; + checkValidity(input, false, apply, { low: "1984-06-05", high: "1992-08-22" }); + + input.value = '1984-06-05'; + checkValidity(input, true, apply); + + input.value = '1992-08-22'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1991-01-01'; + input.value = '1991-01-01'; + checkValidity(input, true, apply); + + input.value = '1991-01-02'; + checkValidity(input, false, apply, { low: "1991-01-01", high: "1991-01-03" }); + + input.value = '1991-01-03'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1969-12-20'; + input.value = '1969-12-20'; + checkValidity(input, true, apply); + + input.value = '1969-12-21'; + checkValidity(input, false, apply, { low: "1969-12-20", high: "1969-12-22" }); + + input.value = '1969-12-22'; + checkValidity(input, true, apply); + + break; + case 'number': + // When step=0, the allowed step is 1. + input.step = '0'; + input.value = '1.2'; + checkValidity(input, false, apply, { low: 1, high: 2 }); + + input.value = '1'; + checkValidity(input, true, apply); + + input.value = '0'; + checkValidity(input, true, apply); + + // When step is NaN, the allowed step value is 1. + input.step = 'foo'; + input.value = '1'; + checkValidity(input, true, apply); + + input.value = '1.5'; + checkValidity(input, false, apply, { low: 1, high: 2 }); + + // When step is negative, the allowed step value is 1. + input.step = '-0.1'; + checkValidity(input, false, apply, { low: 1, high: 2 }); + + input.value = '1'; + checkValidity(input, true, apply); + + // When step is missing, the allowed step value is 1. + input.removeAttribute('step'); + input.value = '1.5'; + checkValidity(input, false, apply, { low: 1, high: 2 }); + + input.value = '1'; + checkValidity(input, true, apply); + + // When step is 'any', all values are fine wrt to step. + input.step = 'any'; + checkValidity(input, true, apply); + + input.step = 'aNy'; + input.value = '1337'; + checkValidity(input, true, apply); + + input.step = 'AnY'; + input.value = '0.1'; + checkValidity(input, true, apply); + + input.step = 'ANY'; + input.value = '-13.37'; + checkValidity(input, true, apply); + + // When min is set to a valid float, there is a step base. + input.min = '1'; + input.step = '2'; + input.value = '3'; + checkValidity(input, true, apply); + + input.value = '2'; + checkValidity(input, false, apply, { low: 1, high: 3 }); + + input.removeAttribute('step'); // step = 1 + input.min = '0.5'; + input.value = '5.5'; + checkValidity(input, true, apply); + + input.value = '1'; + checkValidity(input, false, apply, { low: 0.5, high: 1.5 }); + + input.min = '-0.1'; + input.step = '1'; + input.value = '0.9'; + checkValidity(input, true, apply); + + input.value = '0.1'; + checkValidity(input, false, apply, { low: -0.1, high: 0.9 }); + + // When min is set to NaN, there is no step base (step base=0 actually). + input.min = 'foo'; + input.step = '1'; + input.value = '1'; + checkValidity(input, true, apply); + + input.value = '0.5'; + checkValidity(input, false, apply, { low: 0, high: 1 }); + + input.min = ''; + input.value = '1'; + checkValidity(input, true, apply); + + input.value = '0.5'; + checkValidity(input, false, apply, { low: 0, high: 1 }); + + input.removeAttribute('min'); + + // If value isn't a number, the element isn't invalid. + input.value = ''; + checkValidity(input, true, apply); + + // Regular situations. + input.step = '2'; + input.value = '1.5'; + checkValidity(input, false, apply, { low: 0, high: 2 }); + + input.value = '42.0'; + checkValidity(input, true, apply); + + input.step = '0.1'; + input.value = '-0.1'; + checkValidity(input, true, apply); + + input.step = '2'; + input.removeAttribute('min'); + input.max = '10'; + input.value = '-9'; + checkValidity(input, false, apply, {low: -10, high: -8}); + + // If there is a value defined but no min, the step base is the value. + input = getFreshElement(test.type); + input.setAttribute('value', '1'); + input.step = 2; + checkValidity(input, true, apply); + + input.value = 3; + checkValidity(input, true, apply); + + input.value = 2; + checkValidity(input, false, apply, {low: 1, high: 3}); + + // Should also work with defaultValue. + input = getFreshElement(test.type); + input.defaultValue = 1; + input.step = 2; + checkValidity(input, true, apply); + + input.value = 3; + checkValidity(input, true, apply); + + input.value = 2; + checkValidity(input, false, apply, {low: 1, high: 3}); + + // Rounding issues. + input = getFreshElement(test.type); + input.min = 0.1; + input.step = 0.2; + input.value = 0.3; + checkValidity(input, true, apply); + + // Check that when the higher value is higher than max, we don't show it. + input = getFreshElement(test.type); + input.step = '2'; + input.min = '1'; + input.max = '10.9'; + input.value = '10'; + + is(input.validationMessage, "Please select a valid value. " + + "The nearest valid value is 9.", + "The validation message should not include the higher value."); + break; + case 'range': + // Range is special in that it clamps to valid values, so it is much + // rarer for it to be invalid. + + // When step=0, the allowed value step is 1. + input.step = '0'; + input.value = '1.2'; + is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.value = '1'; + is(input.value, '1', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.value = '0'; + is(input.value, '0', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + // When step is NaN, the allowed step value is 1. + input.step = 'foo'; + input.value = '1'; + is(input.value, '1', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.value = '1.5'; + is(input.value, '2', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + // When step is negative, the allowed step value is 1. + input.step = '-0.1'; + is(input.value, '2', "check that the value still coincides with a step"); + checkValidity(input, true, apply); + + input.value = '1'; + is(input.value, '1', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + // When step is missing, the allowed step value is 1. + input.removeAttribute('step'); + input.value = '1.5'; + is(input.value, '2', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.value = '1'; + is(input.value, '1', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + // When step is 'any', all values are fine wrt to step. + input.step = 'any'; + checkValidity(input, true, apply); + + input.step = 'aNy'; + input.value = '97'; + is(input.value, '97', "check that the value for step=aNy is unchanged"); + checkValidity(input, true, apply); + + input.step = 'AnY'; + input.value = '0.1'; + is(input.value, '0.1', "check that a positive fractional value with step=AnY is unchanged"); + checkValidity(input, true, apply); + + input.step = 'ANY'; + input.min = -100; + input.value = '-13.37'; + is(input.value, '-13.37', "check that a negative fractional value with step=ANY is unchanged"); + checkValidity(input, true, apply); + + // When min is set to a valid float, there is a step base. + input.min = '1'; // the step base + input.step = '2'; + input.value = '3'; + is(input.value, '3', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.value = '2'; + is(input.value, '3', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.value = '1.99'; + is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.removeAttribute('step'); // step = 1 + input.min = '0.5'; // step base + input.value = '5.5'; + is(input.value, '5.5', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.value = '1'; + is(input.value, '1.5', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.min = '-0.1'; // step base + input.step = '1'; + input.value = '0.9'; + is(input.value, '0.9', "the value should be a valid step"); + checkValidity(input, true, apply); + + input.value = '0.1'; + is(input.value, '-0.1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + // When min is set to NaN, the step base is the value. + input.min = 'foo'; + input.step = '1'; + input.value = '1'; + is(input.value, '1', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.value = '0.5'; + is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.min = ''; + input.value = '1'; + is(input.value, '1', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.value = '0.5'; + is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.removeAttribute('min'); + + // Test when the value isn't a number + input.value = ''; + is(input.value, '50', "value be should default to the value midway between the minimum (0) and the maximum (100)"); + checkValidity(input, true, apply); + + // Regular situations. + input.step = '2'; + input.value = '1.5'; + is(input.value, '2', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.value = '42.0'; + is(input.value, '42.0', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.step = '0.1'; + input.value = '-0.1'; + is(input.value, '0', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.step = '2'; + input.removeAttribute('min'); + input.max = '10'; + input.value = '-9'; + is(input.value, '0', "check the value is clamped to the minimum's default of zero"); + checkValidity(input, true, apply); + + // If @value is defined but not @min, the step base is @value. + input = getFreshElement(test.type); + input.setAttribute('value', '1'); + input.step = 2; + is(input.value, '1', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + input.value = 3; + is(input.value, '3', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.value = 2; + is(input.value, '3', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + // Should also work with defaultValue. + input = getFreshElement(test.type); + input.defaultValue = 1; + input.step = 2; + is(input.value, '1', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.value = 3; + is(input.value, '3', "check that the value coincides with a step"); + checkValidity(input, true, apply); + + input.value = 2; + is(input.value, '3', "check that the value changes to the nearest valid step, choosing the higher step if both are equally close"); + checkValidity(input, true, apply); + + // Check contrived error case where there are no valid steps in range: + // No @min, so the step base is the default minimum, zero, the valid + // range is 0-1, -1 gets clamped to zero. + input = getFreshElement(test.type); + input.step = '3'; + input.max = '1'; + input.defaultValue = '-1'; + is(input.value, '0', "the value should have been clamped to the default minimum, zero"); + checkValidity(input, false, apply, {low: -1, high: -1}); + + // Check that when the closest of the two steps that the value is between + // is greater than the maximum we sanitize to the lower step. + input = getFreshElement(test.type); + input.step = '2'; + input.min = '1'; + input.max = '10.9'; + input.value = '10.8'; // closest step in 11, but 11 > maximum + is(input.value, '9', "check that the value coincides with a step"); + + // The way that step base is defined, the converse (the value not being + // on a step, and the nearest step being a value that would be underflow) + // is not possible, so nothing to test there. + + is(input.validationMessage, "", + "The validation message should be empty."); + break; + case 'time': + // Tests invalid step values. That defaults to step = 1 minute (60). + var values = [ '0', '-1', 'foo', 'any', 'ANY', 'aNy' ]; + for (var value of values) { + input.step = value; + input.value = '19:06:00'; + checkValidity(input, true, apply); + input.value = '19:06:51'; + if (value.toLowerCase() != 'any') { + checkValidity(input, false, apply, {low: '19:06', high: '19:07'}); + } else { + checkValidity(input, true, apply); + } + } + + // No step means that we use the default step value. + input.removeAttribute('step'); + input.value = '19:06:00'; + checkValidity(input, true, apply); + input.value = '19:06:51'; + checkValidity(input, false, apply, {low: '19:06', high: '19:07'}); + + var tests = [ + // With step=1, we allow values by the second. + { step: '1', value: '19:11:01', min: '00:00', result: true }, + { step: '1', value: '19:11:01.001', min: '00:00', result: false, + low: '19:11:01', high: '19:11:02' }, + { step: '1', value: '19:11:01.1', min: '00:00', result: false, + low: '19:11:01', high: '19:11:02' }, + // When step >= 86400000, only the minimum value is valid. + // This is actually @value if there is no @min. + { step: '86400000', value: '00:00', result: true }, + { step: '86400000', value: '00:01', result: true }, + { step: '86400000', value: '00:00', min: '00:01', result: false }, + { step: '86400000', value: '00:01', min: '00:00', result: false, + low: '00:00', high: '00:00' }, + // When step < 1, it should just work. + { step: '0.1', value: '15:05:05.1', min: '00:00', result: true }, + { step: '0.1', value: '15:05:05.101', min: '00:00', result: false, + low: '15:05:05.100', high: '15:05:05.200' }, + { step: '0.2', value: '15:05:05.2', min: '00:00', result: true }, + { step: '0.2', value: '15:05:05.1', min: '00:00', result: false, + low: '15:05:05', high: '15:05:05.200' }, + { step: '0.01', value: '15:05:05.01', min: '00:00', result: true }, + { step: '0.01', value: '15:05:05.011', min: '00:00', result: false, + low: '15:05:05.010', high: '15:05:05.020' }, + { step: '0.02', value: '15:05:05.02', min: '00:00', result: true }, + { step: '0.02', value: '15:05:05.01', min: '00:00', result: false, + low: '15:05:05', high: '15:05:05.020' }, + { step: '0.002', value: '15:05:05.002', min: '00:00', result: true }, + { step: '0.002', value: '15:05:05.001', min: '00:00', result: false, + low: '15:05:05', high: '15:05:05.002' }, + // When step<=0.001, any value is allowed. + { step: '0.001', value: '15:05:05.001', min: '00:00', result: true }, + { step: '0.001', value: '15:05:05', min: '00:00', result: true }, + { step: '0.000001', value: '15:05:05', min: '00:00', result: true }, + // This value has conversion to double issues. + { step: '0.0000001', value: '15:05:05', min: '00:00', result: true }, + // Some random values. + { step: '100', value: '15:06:40', min: '00:00', result: true }, + { step: '100', value: '15:05:05.010', min: '00:00', result: false, + low: '15:05', high: '15:06:40' }, + { step: '3600', value: '15:00', min: '00:00', result: true }, + { step: '3600', value: '15:14', min: '00:00', result: false, + low: '15:00', high: '16:00' }, + { step: '7200', value: '14:00', min: '00:00', result: true }, + { step: '7200', value: '15:14', min: '00:00', result: false, + low: '14:00', high: '16:00' }, + { step: '7260', value: '14:07', min: '00:00', result: true }, + { step: '7260', value: '15:14', min: '00:00', result: false, + low: '14:07', high: '16:08' }, + ]; + + var type = test.type; + for (var test of tests) { + var input = getFreshElement(type); + input.step = test.step; + input.setAttribute('value', test.value); + if (test.min !== undefined) { + input.min = test.min; + } + + if (test.todo) { + todo(input.validity.valid, test.result, + "This test should fail for the moment because of precission issues"); + continue; + } + + if (test.result) { + checkValidity(input, true, apply); + } else { + checkValidity(input, false, apply, + { low: test.low, high: test.high }); + } + } + + break; + case 'month': + // When step is invalid, every date is valid + input.step = 0; + input.value = '2016-07'; + checkValidity(input, true, apply); + + input.step = 'foo'; + input.value = '1970-01'; + checkValidity(input, true, apply); + + input.step = '-1'; + input.value = '1970-01'; + checkValidity(input, true, apply); + + input.removeAttribute('step'); + input.value = '1500-01'; + checkValidity(input, true, apply); + + input.step = 'any'; + input.value = '1966-12'; + checkValidity(input, true, apply); + + input.step = 'ANY'; + input.value = '2013-02'; + checkValidity(input, true, apply); + + // When min is set to a valid month, there is a step base. + input.min = '2000-01'; + input.step = '2'; + input.value = '2000-03'; + checkValidity(input, true, apply); + + input.value = '2000-02'; + checkValidity(input, false, apply, { low: "2000-01", high: "2000-03" }); + + input.min = '2012-12'; + input.value = '2013-01'; + checkValidity(input, false, apply, { low: "2012-12", high: "2013-02" }); + + input.min = '2010-10'; + input.value = '2010-11'; + checkValidity(input, false, apply, { low: "2010-10", high: "2010-12" }); + + input.min = '2010-01'; + input.step = '1.1'; + input.value = '2010-02'; + checkValidity(input, true, apply); + + input.min = '2010-05'; + input.step = '1.9'; + input.value = '2010-06'; + checkValidity(input, false, apply, { low: "2010-05", high: "2010-07" }); + + // Without any step attribute the date is valid + input.removeAttribute('step'); + checkValidity(input, true, apply); + + input.min = '1950-01'; + input.step = '13'; + input.value = '1951-01'; + checkValidity(input, false, apply, { low: "1950-01", high: "1951-02" }); + + input.min = '1951-01'; + input.step = '12'; + input.value = '1952-01'; + checkValidity(input, true, apply); + + input.step = '0.9'; + input.value = '1951-02'; + checkValidity(input, true, apply); + + input.step = '1.5'; + input.value = '1951-04'; + checkValidity(input, false, apply, { low: "1951-03", high: "1951-05" }); + + input.value = '1951-08'; + checkValidity(input, false, apply, { low: "1951-07", high: "1951-09" }); + + input.step = '300'; + input.min= '1968-01'; + input.value = '1968-05'; + checkValidity(input, false, apply, { low: "1968-01", high: "1993-01" }); + + input.value = '1971-01'; + checkValidity(input, false, apply, { low: "1968-01", high: "1993-01" }); + + input.value = '1994-01'; + checkValidity(input, false, apply, { low: "1993-01", high: "2018-01" }); + + input.value = '2018-01'; + checkValidity(input, true, apply); + + input.value = '2043-01'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1991-01'; + input.value = '1991-01'; + checkValidity(input, true, apply); + + input.value = '1991-02'; + checkValidity(input, false, apply, { low: "1991-01", high: "1991-03" }); + + input.value = '1991-03'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1969-12'; + input.value = '1969-12'; + checkValidity(input, true, apply); + + input.value = '1970-01'; + checkValidity(input, false, apply, { low: "1969-12", high: "1970-02" }); + + input.value = '1970-02'; + checkValidity(input, true, apply); + + break; + case 'week': + // When step is invalid, every week is valid + input.step = 0; + input.value = '2016-W30'; + checkValidity(input, true, apply); + + input.step = 'foo'; + input.value = '1970-W01'; + checkValidity(input, true, apply); + + input.step = '-1'; + input.value = '1970-W01'; + checkValidity(input, true, apply); + + input.removeAttribute('step'); + input.value = '1500-W01'; + checkValidity(input, true, apply); + + input.step = 'any'; + input.value = '1966-W52'; + checkValidity(input, true, apply); + + input.step = 'ANY'; + input.value = '2013-W10'; + checkValidity(input, true, apply); + + // When min is set to a valid week, there is a step base. + input.min = '2000-W01'; + input.step = '2'; + input.value = '2000-W03'; + checkValidity(input, true, apply); + + input.value = '2000-W02'; + checkValidity(input, false, apply, { low: "2000-W01", high: "2000-W03" }); + + input.min = '2012-W52'; + input.value = '2013-W01'; + checkValidity(input, false, apply, { low: "2012-W52", high: "2013-W02" }); + + input.min = '2010-W01'; + input.step = '1.1'; + input.value = '2010-W02'; + checkValidity(input, true, apply); + + input.min = '2010-W05'; + input.step = '1.9'; + input.value = '2010-W06'; + checkValidity(input, false, apply, { low: "2010-W05", high: "2010-W07" }); + + // Without any step attribute the week is valid + input.removeAttribute('step'); + checkValidity(input, true, apply); + + input.min = '1950-W01'; + input.step = '53'; + input.value = '1951-W01'; + checkValidity(input, false, apply, { low: "1950-W01", high: "1951-W02" }); + + input.min = '1951-W01'; + input.step = '52'; + input.value = '1952-W01'; + checkValidity(input, true, apply); + + input.step = '0.9'; + input.value = '1951-W02'; + checkValidity(input, true, apply); + + input.step = '1.5'; + input.value = '1951-W04'; + checkValidity(input, false, apply, { low: "1951-W03", high: "1951-W05" }); + + input.value = '1951-W20'; + checkValidity(input, false, apply, { low: "1951-W19", high: "1951-W21" }); + + input.step = '300'; + input.min= '1968-W01'; + input.value = '1968-W05'; + checkValidity(input, false, apply, { low: "1968-W01", high: "1973-W40" }); + + input.value = '1971-W01'; + checkValidity(input, false, apply, { low: "1968-W01", high: "1973-W40" }); + + input.value = '1975-W01'; + checkValidity(input, false, apply, { low: "1973-W40", high: "1979-W27" }); + + input.value = '1985-W14'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1991-W01'; + input.value = '1991-W01'; + checkValidity(input, true, apply); + + input.value = '1991-W02'; + checkValidity(input, false, apply, { low: "1991-W01", high: "1991-W03" }); + + input.value = '1991-W03'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1969-W52'; + input.value = '1969-W52'; + checkValidity(input, true, apply); + + input.value = '1970-W01'; + checkValidity(input, false, apply, { low: "1969-W52", high: "1970-W02" }); + + input.value = '1970-W02'; + checkValidity(input, true, apply); + + break; + case 'datetime-local': + // When step is invalid, every datetime is valid + input.step = 0; + input.value = '2017-02-06T12:00'; + checkValidity(input, true, apply); + + input.step = 'foo'; + input.value = '1970-01-01T00:00'; + checkValidity(input, true, apply); + + input.step = '-1'; + input.value = '1969-12-12 00:10'; + checkValidity(input, true, apply); + + input.removeAttribute('step'); + input.value = '1500-01-01T12:00'; + checkValidity(input, true, apply); + + input.step = 'any'; + input.value = '1966-12-12T12:00'; + checkValidity(input, true, apply); + + input.step = 'ANY'; + input.value = '2017-01-01 12:00'; + checkValidity(input, true, apply); + + // When min is set to a valid datetime, there is a step base. + input.min = '2017-01-01T00:00:00'; + input.step = '2'; + input.value = '2017-01-01T00:00:02'; + checkValidity(input, true, apply); + + input.value = '2017-01-01T00:00:03'; + checkValidity(input, false, apply, + { low: "2017-01-01T00:00:02", high: "2017-01-01T00:00:04" }); + + input.min = '2017-01-01T00:00:05'; + input.value = '2017-01-01T00:00:08'; + checkValidity(input, false, apply, + { low: "2017-01-01T00:00:07", high: "2017-01-01T00:00:09" }); + + input.min = '2000-01-01T00:00'; + input.step = '120'; + input.value = '2000-01-01T00:02'; + checkValidity(input, true, apply); + + // Without any step attribute the datetime is valid + input.removeAttribute('step'); + checkValidity(input, true, apply); + + input.min = '1950-01-01T00:00'; + input.step = '129600'; // 1.5 day + input.value = '1950-01-02T00:00'; + checkValidity(input, false, apply, + { low: "1950-01-01T00:00", high: "1950-01-02T12:00" }); + + input.step = '259200'; // 3 days + input.value = '1950-01-04T12:00'; + checkValidity(input, false, apply, + { low: "1950-01-04T00:00", high: "1950-01-07T00:00" }); + + input.value = '1950-01-10T00:00'; + checkValidity(input, true, apply); + + input.step = '0.5'; // half a second + input.value = '1950-01-01T00:00:00.123'; + checkValidity(input, false, apply, + { low: "1950-01-01T00:00", high: "1950-01-01T00:00:00.500" }); + + input.value = '2000-01-01T12:30:30.600'; + checkValidity(input, false, apply, + { low: "2000-01-01T12:30:30.500", high: "2000-01-01T12:30:31" }); + + input.value = '1950-01-05T00:00:00.500'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1991-01-01T12:00'; + input.value = '1991-01-01T12:00'; + checkValidity(input, true, apply); + + input.value = '1991-01-01T12:00:03'; + checkValidity(input, false, apply, + { low: "1991-01-01T12:00:02.100", high: "1991-01-01T12:00:04.200" }); + + input.value = '1991-01-01T12:00:06.3'; + checkValidity(input, true, apply); + + input.step = '2.1'; + input.min = '1969-12-20T10:00:05'; + input.value = '1969-12-20T10:00:05'; + checkValidity(input, true, apply); + + input.value = '1969-12-20T10:00:08'; + checkValidity(input, false, apply, + { low: "1969-12-20T10:00:07.100", high: "1969-12-20T10:00:09.200" }); + + input.value = '1969-12-20T10:00:09.200'; + checkValidity(input, true, apply); + + break; + default: + ok(false, "Implement the tests for <input type='" + test.type + " >"); + break; + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_stepup_stepdown.html b/dom/html/test/forms/test_stepup_stepdown.html new file mode 100644 index 0000000000..8ad7fbfeee --- /dev/null +++ b/dom/html/test/forms/test_stepup_stepdown.html @@ -0,0 +1,1137 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=636627 +--> +<head> + <title>Test for Bug 636627</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=636627">Mozilla Bug 636627</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 636627 **/ + +/** + * This test is testing stepDown() and stepUp(). + */ + +function checkPresence() +{ + var input = document.createElement('input'); + is('stepDown' in input, true, 'stepDown() should be an input function'); + is('stepUp' in input, true, 'stepUp() should be an input function'); +} + +function checkAvailability() +{ + var testData = + [ + ["text", false], + ["password", false], + ["search", false], + ["telephone", false], + ["email", false], + ["url", false], + ["hidden", false], + ["checkbox", false], + ["radio", false], + ["file", false], + ["submit", false], + ["image", false], + ["reset", false], + ["button", false], + ["number", true], + ["range", true], + ["date", true], + ["time", true], + ["month", true], + ["week", true], + ["datetime-local", true], + ["color", false], + ]; + + var element = document.createElement("input"); + element.setAttribute('value', '0'); + + for (data of testData) { + var exceptionCaught = false; + element.type = data[0]; + try { + element.stepDown(); + } catch (e) { + exceptionCaught = true; + } + is(exceptionCaught, !data[1], "stepDown() availability is not correct"); + + exceptionCaught = false; + try { + element.stepUp(); + } catch (e) { + exceptionCaught = true; + } + is(exceptionCaught, !data[1], "stepUp() availability is not correct"); + } +} + +function checkStepDown() +{ + // This testData is very similar to the one in checkStepUp with some changes + // relative to stepDown. + var testData = [ + /* Initial value | step | min | max | stepDown arg | final value | exception */ + { type: 'number', data: [ + // Regular case. + [ '1', null, null, null, null, '0', false ], + // Argument testing. + [ '1', null, null, null, 1, '0', false ], + [ '9', null, null, null, 9, '0', false ], + [ '1', null, null, null, -1, '2', false ], + [ '1', null, null, null, 0, '1', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '1', null, null, null, 1.1, '0', false ], + // With step values. + [ '1', '0.5', null, null, null, '0.5', false ], + [ '1', '0.25', null, null, 4, '0', false ], + // step = 0 isn't allowed (-> step = 1). + [ '1', '0', null, null, null, '0', false ], + // step < 0 isn't allowed (-> step = 1). + [ '1', '-1', null, null, null, '0', false ], + // step = NaN isn't allowed (-> step = 1). + [ '1', 'foo', null, null, null, '0', false ], + // Min values testing. + [ '1', '1', 'foo', null, null, '0', false ], + [ '1', null, '-10', null, null, '0', false ], + [ '1', null, '0', null, null, '0', false ], + [ '1', null, '10', null, null, '1', false ], + [ '1', null, '2', null, null, '1', false ], + [ '1', null, '1', null, null, '1', false ], + // Max values testing. + [ '1', '1', null, 'foo', null, '0', false ], + [ '1', null, null, '10', null, '0', false ], + [ '1', null, null, '0', null, '0', false ], + [ '1', null, null, '-10', null, '-10', false ], + [ '1', null, null, '1', null, '0', false ], + [ '5', null, null, '3', '3', '2', false ], + [ '5', '2', '-6', '3', '2', '2', false ], + [ '-3', '5', '-10', '-3', null, '-5', false ], + // Step mismatch. + [ '1', '2', '-2', null, null, '0', false ], + [ '3', '2', '-2', null, null, '2', false ], + [ '3', '2', '-2', null, '2', '0', false ], + [ '3', '2', '-2', null, '-2', '6', false ], + [ '1', '2', '-6', null, null, '0', false ], + [ '1', '2', '-2', null, null, '0', false ], + [ '1', '3', '-6', null, null, '0', false ], + [ '2', '3', '-6', null, null, '0', false ], + [ '2', '3', '1', null, null, '1', false ], + [ '5', '3', '1', null, null, '4', false ], + [ '3', '2', '-6', null, null, '2', false ], + [ '5', '2', '-6', null, null, '4', false ], + [ '6', '2', '1', null, null, '5', false ], + [ '8', '3', '1', null, null, '7', false ], + [ '9', '2', '-10', null, null, '8', false ], + [ '7', '3', '-10', null, null, '5', false ], + [ '-2', '3', '-10', null, null, '-4', false ], + // Clamping. + [ '0', '2', '-1', null, null, '-1', false ], + [ '10', '2', '0', '4', '10', '0', false ], + [ '10', '2', '0', '4', '5', '0', false ], + // value = "" (NaN). + [ '', null, null, null, null, '-1', false ], + [ '', '2', null, null, null, '-2', false ], + [ '', '2', '3', null, null, '3', false ], + [ '', null, '3', null, null, '3', false ], + [ '', '2', '3', '8', null, '3', false ], + [ '', null, '-10', '10', null, '-1', false ], + [ '', '3', '-10', '10', null, '-1', false ], + // With step = 'any'. + [ '0', 'any', null, null, 1, null, true ], + [ '0', 'ANY', null, null, 1, null, true ], + [ '0', 'AnY', null, null, 1, null, true ], + [ '0', 'aNy', null, null, 1, null, true ], + // With @value = step base. + [ '1', '2', null, null, null, '-1', false ], + ]}, + { type: 'range', data: [ + // Regular case. + [ '1', null, null, null, null, '0', false ], + // Argument testing. + [ '1', null, null, null, 1, '0', false ], + [ '9', null, null, null, 9, '0', false ], + [ '1', null, null, null, -1, '2', false ], + [ '1', null, null, null, 0, '1', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '1', null, null, null, 1.1, '0', false ], + // With step values. + [ '1', '0.5', null, null, null, '0.5', false ], + [ '1', '0.25', null, null, 4, '0', false ], + // step = 0 isn't allowed (-> step = 1). + [ '1', '0', null, null, null, '0', false ], + // step < 0 isn't allowed (-> step = 1). + [ '1', '-1', null, null, null, '0', false ], + // step = NaN isn't allowed (-> step = 1). + [ '1', 'foo', null, null, null, '0', false ], + // Min values testing. + [ '1', '1', 'foo', null, null, '0', false ], + [ '1', null, '-10', null, null, '0', false ], + [ '1', null, '0', null, null, '0', false ], + [ '1', null, '10', null, null, '10', false ], + [ '1', null, '2', null, null, '2', false ], + [ '1', null, '1', null, null, '1', false ], + // Max values testing. + [ '1', '1', null, 'foo', null, '0', false ], + [ '1', null, null, '10', null, '0', false ], + [ '1', null, null, '0', null, '0', false ], + [ '1', null, null, '-10', null, '0', false ], + [ '1', null, null, '1', null, '0', false ], + [ '5', null, null, '3', '3', '0', false ], + [ '5', '2', '-6', '3', '2', '-2', false ], + [ '-3', '5', '-10', '-3', null, '-10', false ], + // Step mismatch. + [ '1', '2', '-2', null, null, '0', false ], + [ '3', '2', '-2', null, null, '2', false ], + [ '3', '2', '-2', null, '2', '0', false ], + [ '3', '2', '-2', null, '-2', '8', false ], + [ '1', '2', '-6', null, null, '0', false ], + [ '1', '2', '-2', null, null, '0', false ], + [ '1', '3', '-6', null, null, '-3', false ], + [ '2', '3', '-6', null, null, '0', false ], + [ '2', '3', '1', null, null, '1', false ], + [ '5', '3', '1', null, null, '1', false ], + [ '3', '2', '-6', null, null, '2', false ], + [ '5', '2', '-6', null, null, '4', false ], + [ '6', '2', '1', null, null, '5', false ], + [ '8', '3', '1', null, null, '4', false ], + [ '9', '2', '-10', null, null, '8', false ], + [ '7', '3', '-10', null, null, '5', false ], + [ '-2', '3', '-10', null, null, '-4', false ], + // Clamping. + [ '0', '2', '-1', null, null, '-1', false ], + [ '10', '2', '0', '4', '10', '0', false ], + [ '10', '2', '0', '4', '5', '0', false ], + // value = "" (default will be 50). + [ '', null, null, null, null, '49', false ], + // With step = 'any'. + [ '0', 'any', null, null, 1, null, true ], + [ '0', 'ANY', null, null, 1, null, true ], + [ '0', 'AnY', null, null, 1, null, true ], + [ '0', 'aNy', null, null, 1, null, true ], + // With @value = step base. + [ '1', '2', null, null, null, '1', false ], + ]}, + { type: 'date', data: [ + // Regular case. + [ '2012-07-09', null, null, null, null, '2012-07-08', false ], + // Argument testing. + [ '2012-07-09', null, null, null, 1, '2012-07-08', false ], + [ '2012-07-09', null, null, null, 5, '2012-07-04', false ], + [ '2012-07-09', null, null, null, -1, '2012-07-10', false ], + [ '2012-07-09', null, null, null, 0, '2012-07-09', false ], + // Month/Year wrapping. + [ '2012-08-01', null, null, null, 1, '2012-07-31', false ], + [ '1969-01-02', null, null, null, 4, '1968-12-29', false ], + [ '1969-01-01', null, null, null, -365, '1970-01-01', false ], + [ '2012-02-29', null, null, null, -1, '2012-03-01', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '2012-01-02', null, null, null, 1.1, '2012-01-01', false ], + [ '2012-01-02', null, null, null, 1.9, '2012-01-01', false ], + // With step values. + [ '2012-01-03', '0.5', null, null, null, '2012-01-02', false ], + [ '2012-01-02', '0.5', null, null, null, '2012-01-01', false ], + [ '2012-01-01', '2', null, null, null, '2011-12-30', false ], + [ '2012-01-02', '0.25',null, null, 4, '2011-12-29', false ], + [ '2012-01-15', '1.1', '2012-01-01', null, 1, '2012-01-14', false ], + [ '2012-01-12', '1.1', '2012-01-01', null, 2, '2012-01-10', false ], + [ '2012-01-23', '1.1', '2012-01-01', null, 10, '2012-01-13', false ], + [ '2012-01-23', '1.1', '2012-01-01', null, 11, '2012-01-12', false ], + [ '1968-01-12', '1.1', '1968-01-01', null, 8, '1968-01-04', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2012-01-02', '0', null, null, null, '2012-01-01', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2012-01-02', '-1', null, null, null, '2012-01-01', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2012-01-02', 'foo', null, null, null, '2012-01-01', false ], + // Min values testing. + [ '2012-01-03', '1', 'foo', null, 2, '2012-01-01', false ], + [ '2012-01-02', '1', '2012-01-01', null, null, '2012-01-01', false ], + [ '2012-01-01', '1', '2012-01-01', null, null, '2012-01-01', false ], + [ '2012-01-01', '1', '2012-01-10', null, 1, '2012-01-01', false ], + [ '2012-01-05', '3', '2012-01-01', null, null, '2012-01-04', false ], + [ '1969-01-01', '5', '1969-01-01', '1969-01-02', null, '1969-01-01', false ], + // Max values testing. + [ '2012-01-02', '1', null, 'foo', null, '2012-01-01', false ], + [ '2012-01-02', null, null, '2012-01-05', null, '2012-01-01', false ], + [ '2012-01-03', null, null, '2012-01-03', null, '2012-01-02', false ], + [ '2012-01-07', null, null, '2012-01-04', 4, '2012-01-03', false ], + [ '2012-01-07', '2', null, '2012-01-04', 3, '2012-01-01', false ], + // Step mismatch. + [ '2012-01-04', '2', '2012-01-01', null, null, '2012-01-03', false ], + [ '2012-01-06', '2', '2012-01-01', null, 2, '2012-01-03', false ], + [ '2012-01-05', '2', '2012-01-04', '2012-01-08', null, '2012-01-04', false ], + [ '1970-01-04', '2', null, null, null, '1970-01-02', false ], + [ '1970-01-09', '3', null, null, null, '1970-01-06', false ], + // Clamping. + [ '2012-05-01', null, null, '2012-01-05', null, '2012-01-05', false ], + [ '1970-01-05', '2', '1970-01-02', '1970-01-05', null, '1970-01-04', false ], + [ '1970-01-01', '5', '1970-01-02', '1970-01-09', 10, '1970-01-01', false ], + [ '1970-01-07', '5', '1969-12-27', '1970-01-06', 2, '1970-01-01', false ], + [ '1970-03-08', '3', '1970-02-01', '1970-02-07', 15, '1970-02-01', false ], + [ '1970-01-10', '3', '1970-01-01', '1970-01-06', 2, '1970-01-04', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1969-12-31', false ], + // With step = 'any'. + [ '2012-01-01', 'any', null, null, 1, null, true ], + [ '2012-01-01', 'ANY', null, null, 1, null, true ], + [ '2012-01-01', 'AnY', null, null, 1, null, true ], + [ '2012-01-01', 'aNy', null, null, 1, null, true ], + ]}, + { type: 'time', data: [ + // Regular case. + [ '16:39', null, null, null, null, '16:38', false ], + // Argument testing. + [ '16:40', null, null, null, 1, '16:39', false ], + [ '16:40', null, null, null, 5, '16:35', false ], + [ '16:40', null, null, null, -1, '16:41', false ], + [ '16:40', null, null, null, 0, '16:40', false ], + // hour/minutes/seconds wrapping. + [ '05:00', null, null, null, null, '04:59', false ], + [ '05:00:00', 1, null, null, null, '04:59:59', false ], + [ '05:00:00', 0.1, null, null, null, '04:59:59.900', false ], + [ '05:00:00', 0.01, null, null, null, '04:59:59.990', false ], + [ '05:00:00', 0.001, null, null, null, '04:59:59.999', false ], + // stepDown() on '00:00' gives '23:59'. + [ '00:00', null, null, null, 1, '23:59', false ], + [ '00:00', null, null, null, 3, '23:57', false ], + // Some random step values.. + [ '16:56', '0.5', null, null, null, '16:55:59.500', false ], + [ '16:56', '2', null, null, null, '16:55:58', false ], + [ '16:56', '0.25',null, null, 4, '16:55:59', false ], + [ '16:57', '1.1', '16:00', null, 1, '16:56:59.900', false ], + [ '16:57', '1.1', '16:00', null, 2, '16:56:58.800', false ], + [ '16:57', '1.1', '16:00', null, 10, '16:56:50', false ], + [ '16:57', '1.1', '16:00', null, 11, '16:56:48.900', false ], + [ '16:57', '1.1', '16:00', null, 8, '16:56:52.200', false ], + // Invalid @step, means that we use the default value. + [ '17:01', '0', null, null, null, '17:00', false ], + [ '17:01', '-1', null, null, null, '17:00', false ], + [ '17:01', 'foo', null, null, null, '17:00', false ], + // Min values testing. + [ '17:02', '60', 'foo', null, 2, '17:00', false ], + [ '17:10', '60', '17:09', null, null, '17:09', false ], + [ '17:10', '60', '17:10', null, null, '17:10', false ], + [ '17:10', '60', '17:30', null, 1, '17:10', false ], + [ '17:10', '180', '17:05', null, null, '17:08', false ], + [ '17:10', '300', '17:10', '17:11', null, '17:10', false ], + // Max values testing. + [ '17:15', '60', null, 'foo', null, '17:14', false ], + [ '17:15', null, null, '17:20', null, '17:14', false ], + [ '17:15', null, null, '17:15', null, '17:14', false ], + [ '17:15', null, null, '17:13', 4, '17:11', false ], + [ '17:15', '120', null, '17:13', 3, '17:09', false ], + // Step mismatch. + [ '17:19', '120', '17:10', null, null, '17:18', false ], + [ '17:19', '120', '17:10', null, 2, '17:16', false ], + [ '17:19', '120', '17:18', '17:25', null, '17:18', false ], + [ '17:19', '120', null, null, null, '17:17', false ], + [ '17:19', '180', null, null, null, '17:16', false ], + // Clamping. + [ '17:22', null, null, '17:11', null, '17:11', false ], + [ '17:22', '120', '17:20', '17:22', null, '17:20', false ], + [ '17:22', '300', '17:12', '17:20', 10, '17:12', false ], + [ '17:22', '300', '17:18', '17:20', 2, '17:18', false ], + [ '17:22', '180', '17:00', '17:20', 15, '17:00', false ], + [ '17:22', '180', '17:10', '17:20', 2, '17:16', false ], + // value = "" (NaN). + [ '', null, null, null, null, '23:59', false ], + // With step = 'any'. + [ '17:26', 'any', null, null, 1, null, true ], + [ '17:26', 'ANY', null, null, 1, null, true ], + [ '17:26', 'AnY', null, null, 1, null, true ], + [ '17:26', 'aNy', null, null, 1, null, true ], + ]}, + { type: 'month', data: [ + // Regular case. + [ '2016-08', null, null, null, null, '2016-07', false ], + // Argument testing. + [ '2016-08', null, null, null, 1, '2016-07', false ], + [ '2016-08', null, null, null, 5, '2016-03', false ], + [ '2016-08', null, null, null, -1, '2016-09', false ], + [ '2016-08', null, null, null, 0, '2016-08', false ], + // Month/Year wrapping. + [ '2016-01', null, null, null, 1, '2015-12', false ], + [ '1969-02', null, null, null, 4, '1968-10', false ], + [ '1969-01', null, null, null, -12, '1970-01', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '2016-08', null, null, null, 1.1, '2016-07', false ], + [ '2016-01', null, null, null, 1.9, '2015-12', false ], + // With step values. + [ '2016-03', '0.5', null, null, null, '2016-02', false ], + [ '2016-03', '2', null, null, null, '2016-01', false ], + [ '2016-03', '0.25',null, null, 4, '2015-11', false ], + [ '2016-12', '1.1', '2016-01', null, 1, '2016-11', false ], + [ '2016-12', '1.1', '2016-01', null, 2, '2016-10', false ], + [ '2016-12', '1.1', '2016-01', null, 10, '2016-02', false ], + [ '2016-12', '1.1', '2016-01', null, 12, '2016-01', false ], + [ '1968-12', '1.1', '1968-01', null, 8, '1968-04', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2016-02', '0', null, null, null, '2016-01', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2016-02', '-1', null, null, null, '2016-01', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2016-02', 'foo', null, null, null, '2016-01', false ], + // Min values testing. + [ '2016-03', '1', 'foo', null, 2, '2016-01', false ], + [ '2016-02', '1', '2016-01', null, null, '2016-01', false ], + [ '2016-01', '1', '2016-01', null, null, '2016-01', false ], + [ '2016-01', '1', '2016-01', null, 1, '2016-01', false ], + [ '2016-05', '3', '2016-01', null, null, '2016-04', false ], + [ '1969-01', '5', '1969-01', '1969-02', null, '1969-01', false ], + // Max values testing. + [ '2016-02', '1', null, 'foo', null, '2016-01', false ], + [ '2016-02', null, null, '2016-05', null, '2016-01', false ], + [ '2016-03', null, null, '2016-03', null, '2016-02', false ], + [ '2016-07', null, null, '2016-04', 4, '2016-03', false ], + [ '2016-07', '2', null, '2016-04', 3, '2016-01', false ], + // Step mismatch. + [ '2016-04', '2', '2016-01', null, null, '2016-03', false ], + [ '2016-06', '2', '2016-01', null, 2, '2016-03', false ], + [ '2016-05', '2', '2016-04', '2016-08', null, '2016-04', false ], + [ '1970-04', '2', null, null, null, '1970-02', false ], + [ '1970-09', '3', null, null, null, '1970-06', false ], + // Clamping. + [ '2016-05', null, null, '2016-01', null, '2016-01', false ], + [ '1970-05', '2', '1970-02', '1970-05', null, '1970-04', false ], + [ '1970-01', '5', '1970-02', '1970-09', 10, '1970-01', false ], + [ '1970-07', '5', '1969-12', '1970-10', 2, '1969-12', false ], + [ '1970-08', '3', '1970-01', '1970-07', 15, '1970-01', false ], + [ '1970-10', '3', '1970-01', '1970-06', 2, '1970-04', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1969-12', false ], + // With step = 'any'. + [ '2016-01', 'any', null, null, 1, null, true ], + [ '2016-01', 'ANY', null, null, 1, null, true ], + [ '2016-01', 'AnY', null, null, 1, null, true ], + [ '2016-01', 'aNy', null, null, 1, null, true ], + ]}, + { type: 'week', data: [ + // Regular case. + [ '2016-W40', null, null, null, null, '2016-W39', false ], + // Argument testing. + [ '2016-W40', null, null, null, 1, '2016-W39', false ], + [ '2016-W40', null, null, null, 5, '2016-W35', false ], + [ '2016-W40', null, null, null, -1, '2016-W41', false ], + [ '2016-W40', null, null, null, 0, '2016-W40', false ], + // Week/Year wrapping. + [ '2016-W01', null, null, null, 1, '2015-W53', false ], + [ '1969-W02', null, null, null, 4, '1968-W50', false ], + [ '1969-W01', null, null, null, -52, '1970-W01', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '2016-W40', null, null, null, 1.1, '2016-W39', false ], + [ '2016-W01', null, null, null, 1.9, '2015-W53', false ], + // With step values. + [ '2016-W03', '0.5', null, null, null, '2016-W02', false ], + [ '2016-W03', '2', null, null, null, '2016-W01', false ], + [ '2016-W03', '0.25', null, null, 4, '2015-W52', false ], + [ '2016-W52', '1.1', '2016-W01', null, 1, '2016-W51', false ], + [ '2016-W52', '1.1', '2016-W01', null, 2, '2016-W50', false ], + [ '2016-W52', '1.1', '2016-W01', null, 10, '2016-W42', false ], + [ '2016-W52', '1.1', '2016-W01', null, 52, '2016-W01', false ], + [ '1968-W52', '1.1', '1968-W01', null, 8, '1968-W44', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2016-W02', '0', null, null, null, '2016-W01', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2016-W02', '-1', null, null, null, '2016-W01', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2016-W02', 'foo', null, null, null, '2016-W01', false ], + // Min values testing. + [ '2016-W03', '1', 'foo', null, 2, '2016-W01', false ], + [ '2016-W02', '1', '2016-01', null, null, '2016-W01', false ], + [ '2016-W01', '1', '2016-W01', null, null, '2016-W01', false ], + [ '2016-W01', '1', '2016-W01', null, 1, '2016-W01', false ], + [ '2016-W05', '3', '2016-W01', null, null, '2016-W04', false ], + [ '1969-W01', '5', '1969-W01', '1969-W02', null, '1969-W01', false ], + // Max values testing. + [ '2016-W02', '1', null, 'foo', null, '2016-W01', false ], + [ '2016-W02', null, null, '2016-W05', null, '2016-W01', false ], + [ '2016-W03', null, null, '2016-W03', null, '2016-W02', false ], + [ '2016-W07', null, null, '2016-W04', 4, '2016-W03', false ], + [ '2016-W07', '2', null, '2016-W04', 3, '2016-W01', false ], + // Step mismatch. + [ '2016-W04', '2', '2016-W01', null, null, '2016-W03', false ], + [ '2016-W06', '2', '2016-W01', null, 2, '2016-W03', false ], + [ '2016-W05', '2', '2016-W04', '2016-W08', null, '2016-W04', false ], + [ '1970-W04', '2', null, null, null, '1970-W02', false ], + [ '1970-W09', '3', null, null, null, '1970-W06', false ], + // Clamping. + [ '2016-W05', null, null, '2016-W01', null, '2016-W01', false ], + [ '1970-W05', '2', '1970-W02', '1970-W05', null, '1970-W04', false ], + [ '1970-W01', '5', '1970-W02', '1970-W09', 10, '1970-W01', false ], + [ '1970-W07', '5', '1969-W52', '1970-W10', 2, '1969-W52', false ], + [ '1970-W08', '3', '1970-W01', '1970-W07', 15, '1970-W01', false ], + [ '1970-W10', '3', '1970-W01', '1970-W06', 2, '1970-W04', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1970-W01', false ], + // With step = 'any'. + [ '2016-W01', 'any', null, null, 1, null, true ], + [ '2016-W01', 'ANY', null, null, 1, null, true ], + [ '2016-W01', 'AnY', null, null, 1, null, true ], + [ '2016-W01', 'aNy', null, null, 1, null, true ], + ]}, + { type: 'datetime-local', data: [ + // Regular case. + [ '2017-02-07T09:30', null, null, null, null, '2017-02-07T09:29', false ], + // Argument testing. + [ '2017-02-07T09:30', null, null, null, 1, '2017-02-07T09:29', false ], + [ '2017-02-07T09:30', null, null, null, 5, '2017-02-07T09:25', false ], + [ '2017-02-07T09:30', null, null, null, -1, '2017-02-07T09:31', false ], + [ '2017-02-07T09:30', null, null, null, 0, '2017-02-07T09:30', false ], + // hour/minutes/seconds wrapping. + [ '2000-01-01T05:00', null, null, null, null, '2000-01-01T04:59', false ], + [ '2000-01-01T05:00:00', 1, null, null, null, '2000-01-01T04:59:59', false ], + [ '2000-01-01T05:00:00', 0.1, null, null, null, '2000-01-01T04:59:59.900', false ], + [ '2000-01-01T05:00:00', 0.01, null, null, null, '2000-01-01T04:59:59.990', false ], + [ '2000-01-01T05:00:00', 0.001, null, null, null, '2000-01-01T04:59:59.999', false ], + // month/year wrapping. + [ '2012-08-01T12:00', null, null, null, 1440, '2012-07-31T12:00', false ], + [ '1969-01-02T12:00', null, null, null, 5760, '1968-12-29T12:00', false ], + [ '1969-12-31T00:00', null, null, null, -1440, '1970-01-01T00:00', false ], + [ '2012-02-29T00:00', null, null, null, -1440, '2012-03-01T00:00', false ], + // stepDown() on '00:00' gives '23:59'. + [ '2017-02-07T00:00', null, null, null, 1, '2017-02-06T23:59', false ], + [ '2017-02-07T00:00', null, null, null, 3, '2017-02-06T23:57', false ], + // Some random step values.. + [ '2017-02-07T16:07', '0.5', null, null, null, '2017-02-07T16:06:59.500', false ], + [ '2017-02-07T16:07', '2', null, null, null, '2017-02-07T16:06:58', false ], + [ '2017-02-07T16:07', '0.25', null, null, 4, '2017-02-07T16:06:59', false ], + [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 1, '2017-02-07T16:06:59.100', false ], + [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 2, '2017-02-07T16:06:58', false ], + [ '2017-02-07T16:07', '1.1', '2017-02-07T16:00', null, 10, '2017-02-07T16:06:49.200', false ], + [ '2017-02-07T16:07', '129600', '2017-02-01T00:00', null, 2, '2017-02-05T12:00', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2017-02-07T10:15', '0', null, null, null, '2017-02-07T10:14', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2017-02-07T10:15', '-1', null, null, null, '2017-02-07T10:14', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2017-02-07T10:15', 'foo', null, null, null, '2017-02-07T10:14', false ], + // Min values testing. + [ '2012-02-02T17:02', '60', 'foo', null, 2, '2012-02-02T17:00', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:09', null, null, '2012-02-02T17:09', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:10', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:10', false ], + [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:08', false ], + [ '2012-02-03T20:05', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ], + [ '2012-02-03T18:00', '129600', '2012-02-01T00:00', null, null, '2012-02-02T12:00', false ], + // Max values testing. + [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:14', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:14', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:14', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:11', false ], + [ '2012-02-02T17:15', '120', null, '2012-02-02T17:13', 3, '2012-02-02T17:09', false ], + [ '2012-02-03T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-02T20:05', false ], + [ '2012-02-03T18:00', '129600', null, '2012-02-03T20:00', null, '2012-02-02T06:00', false ], + // Step mismatch. + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:18', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:16', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:18', false ], + [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:17', false ], + [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:16', false ], + [ '2017-02-07T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-06T17:19', false ], + // Clamping. + [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:11', false ], + [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:20', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:12', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:18', false ], + [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 15, '2017-02-07T15:00', false ], + [ '2017-02-07T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:00', 2, '2017-02-07T17:00', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1969-12-31T23:59', false ], + // With step = 'any'. + [ '2017-02-07T15:20', 'any', null, null, 1, null, true ], + [ '2017-02-07T15:20', 'ANY', null, null, 1, null, true ], + [ '2017-02-07T15:20', 'AnY', null, null, 1, null, true ], + [ '2017-02-07T15:20', 'aNy', null, null, 1, null, true ], + ]}, + ]; + + for (var test of testData) { + for (var data of test.data) { + var element = document.createElement("input"); + element.type = test.type; + + if (data[1] != null) { + element.step = data[1]; + } + + if (data[2] != null) { + element.min = data[2]; + } + + if (data[3] != null) { + element.max = data[3]; + } + + // Set 'value' last for type=range, because the final sanitized value + // after setting 'step', 'min' and 'max' can be affected by the order in + // which those attributes are set. Setting 'value' last makes it simpler + // to reason about what the final value should be. + if (data[0] != null) { + element.setAttribute('value', data[0]); + } + + var exceptionCaught = false; + try { + if (data[4] != null) { + element.stepDown(data[4]); + } else { + element.stepDown(); + } + + is(element.value, data[5], "The value for type=" + test.type + " should be " + data[5]); + } catch (e) { + exceptionCaught = true; + is(element.value, data[0], e.name + "The value should not have changed"); + is(e.name, 'InvalidStateError', + "It should be a InvalidStateError exception."); + } finally { + is(exceptionCaught, data[6], "exception status should be " + data[6]); + } + } + } +} + +function checkStepUp() +{ + // This testData is very similar to the one in checkStepDown with some changes + // relative to stepUp. + var testData = [ + /* Initial value | step | min | max | stepUp arg | final value | exception */ + { type: 'number', data: [ + // Regular case. + [ '1', null, null, null, null, '2', false ], + // Argument testing. + [ '1', null, null, null, 1, '2', false ], + [ '9', null, null, null, 9, '18', false ], + [ '1', null, null, null, -1, '0', false ], + [ '1', null, null, null, 0, '1', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '1', null, null, null, 1.1, '2', false ], + // With step values. + [ '1', '0.5', null, null, null, '1.5', false ], + [ '1', '0.25', null, null, 4, '2', false ], + // step = 0 isn't allowed (-> step = 1). + [ '1', '0', null, null, null, '2', false ], + // step < 0 isn't allowed (-> step = 1). + [ '1', '-1', null, null, null, '2', false ], + // step = NaN isn't allowed (-> step = 1). + [ '1', 'foo', null, null, null, '2', false ], + // Min values testing. + [ '1', '1', 'foo', null, null, '2', false ], + [ '1', null, '-10', null, null, '2', false ], + [ '1', null, '0', null, null, '2', false ], + [ '1', null, '10', null, null, '10', false ], + [ '1', null, '2', null, null, '2', false ], + [ '1', null, '1', null, null, '2', false ], + [ '0', null, '4', null, '5', '5', false ], + [ '0', '2', '5', null, '3', '5', false ], + // Max values testing. + [ '1', '1', null, 'foo', null, '2', false ], + [ '1', null, null, '10', null, '2', false ], + [ '1', null, null, '0', null, '1', false ], + [ '1', null, null, '-10', null, '1', false ], + [ '1', null, null, '1', null, '1', false ], + [ '-3', '5', '-10', '-3', null, '-3', false ], + // Step mismatch. + [ '1', '2', '0', null, null, '2', false ], + [ '1', '2', '0', null, '2', '4', false ], + [ '8', '2', null, '9', null, '8', false ], + [ '-3', '2', '-6', null, null, '-2', false ], + [ '9', '3', '-10', null, null, '11', false ], + [ '7', '3', '-10', null, null, '8', false ], + [ '7', '3', '5', null, null, '8', false ], + [ '9', '4', '3', null, null, '11', false ], + [ '-2', '3', '-6', null, null, '0', false ], + [ '7', '3', '6', null, null, '9', false ], + // Clamping. + [ '1', '2', '0', '3', null, '2', false ], + [ '0', '5', '1', '8', '10', '6', false ], + [ '-9', '3', '-8', '-1', '5', '-2', false ], + [ '-9', '3', '8', '15', '15', '14', false ], + [ '-1', '3', '-1', '4', '3', '2', false ], + [ '-3', '2', '-6', '-2', null, '-2', false ], + [ '-3', '2', '-6', '-1', null, '-2', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1', false ], + [ '', null, null, null, null, '1', false ], + [ '', '2', null, null, null, '2', false ], + [ '', '2', '3', null, null, '3', false ], + [ '', null, '3', null, null, '3', false ], + [ '', '2', '3', '8', null, '3', false ], + [ '', null, '-10', '10', null, '1', false ], + [ '', '3', '-10', '10', null, '2', false ], + // With step = 'any'. + [ '0', 'any', null, null, 1, null, true ], + [ '0', 'ANY', null, null, 1, null, true ], + [ '0', 'AnY', null, null, 1, null, true ], + [ '0', 'aNy', null, null, 1, null, true ], + // With @value = step base. + [ '1', '2', null, null, null, '3', false ], + ]}, + { type: 'range', data: [ + // Regular case. + [ '1', null, null, null, null, '2', false ], + // Argument testing. + [ '1', null, null, null, 1, '2', false ], + [ '9', null, null, null, 9, '18', false ], + [ '1', null, null, null, -1, '0', false ], + [ '1', null, null, null, 0, '1', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '1', null, null, null, 1.1, '2', false ], + // With step values. + [ '1', '0.5', null, null, null, '1.5', false ], + [ '1', '0.25', null, null, 4, '2', false ], + // step = 0 isn't allowed (-> step = 1). + [ '1', '0', null, null, null, '2', false ], + // step < 0 isn't allowed (-> step = 1). + [ '1', '-1', null, null, null, '2', false ], + // step = NaN isn't allowed (-> step = 1). + [ '1', 'foo', null, null, null, '2', false ], + // Min values testing. + [ '1', '1', 'foo', null, null, '2', false ], + [ '1', null, '-10', null, null, '2', false ], + [ '1', null, '0', null, null, '2', false ], + [ '1', null, '10', null, null, '11', false ], + [ '1', null, '2', null, null, '3', false ], + [ '1', null, '1', null, null, '2', false ], + [ '0', null, '4', null, '5', '9', false ], + [ '0', '2', '5', null, '3', '11', false ], + // Max values testing. + [ '1', '1', null, 'foo', null, '2', false ], + [ '1', null, null, '10', null, '2', false ], + [ '1', null, null, '0', null, '0', false ], + [ '1', null, null, '-10', null, '0', false ], + [ '1', null, null, '1', null, '1', false ], + [ '-3', '5', '-10', '-3', null, '-5', false ], + // Step mismatch. + [ '1', '2', '0', null, null, '4', false ], + [ '1', '2', '0', null, '2', '6', false ], + [ '8', '2', null, '9', null, '8', false ], + [ '-3', '2', '-6', null, null, '0', false ], + [ '9', '3', '-10', null, null, '11', false ], + [ '7', '3', '-10', null, null, '11', false ], + [ '7', '3', '5', null, null, '11', false ], + [ '9', '4', '3', null, null, '15', false ], + [ '-2', '3', '-6', null, null, '0', false ], + [ '7', '3', '6', null, null, '9', false ], + // Clamping. + [ '1', '2', '0', '3', null, '2', false ], + [ '0', '5', '1', '8', '10', '6', false ], + [ '-9', '3', '-8', '-1', '5', '-2', false ], + [ '-9', '3', '8', '15', '15', '14', false ], + [ '-1', '3', '-1', '4', '3', '2', false ], + [ '-3', '2', '-6', '-2', null, '-2', false ], + [ '-3', '2', '-6', '-1', null, '-2', false ], + // value = "" (default will be 50). + [ '', null, null, null, null, '51', false ], + // With step = 'any'. + [ '0', 'any', null, null, 1, null, true ], + [ '0', 'ANY', null, null, 1, null, true ], + [ '0', 'AnY', null, null, 1, null, true ], + [ '0', 'aNy', null, null, 1, null, true ], + // With @value = step base. + [ '1', '2', null, null, null, '3', false ], + ]}, + { type: 'date', data: [ + // Regular case. + [ '2012-07-09', null, null, null, null, '2012-07-10', false ], + // Argument testing. + [ '2012-07-09', null, null, null, 1, '2012-07-10', false ], + [ '2012-07-09', null, null, null, 9, '2012-07-18', false ], + [ '2012-07-09', null, null, null, -1, '2012-07-08', false ], + [ '2012-07-09', null, null, null, 0, '2012-07-09', false ], + // Month/Year wrapping. + [ '2012-07-31', null, null, null, 1, '2012-08-01', false ], + [ '1968-12-29', null, null, null, 4, '1969-01-02', false ], + [ '1970-01-01', null, null, null, -365, '1969-01-01', false ], + [ '2012-03-01', null, null, null, -1, '2012-02-29', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '2012-01-01', null, null, null, 1.1, '2012-01-02', false ], + [ '2012-01-01', null, null, null, 1.9, '2012-01-02', false ], + // With step values. + [ '2012-01-01', '0.5', null, null, null, '2012-01-02', false ], + [ '2012-01-01', '2', null, null, null, '2012-01-03', false ], + [ '2012-01-01', '0.25', null, null, 4, '2012-01-05', false ], + [ '2012-01-01', '1.1', '2012-01-01', null, 1, '2012-01-02', false ], + [ '2012-01-01', '1.1', '2012-01-01', null, 2, '2012-01-03', false ], + [ '2012-01-01', '1.1', '2012-01-01', null, 10, '2012-01-11', false ], + [ '2012-01-01', '1.1', '2012-01-01', null, 11, '2012-01-12', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2012-01-01', '0', null, null, null, '2012-01-02', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2012-01-01', '-1', null, null, null, '2012-01-02', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2012-01-01', 'foo', null, null, null, '2012-01-02', false ], + // Min values testing. + [ '2012-01-01', '1', 'foo', null, null, '2012-01-02', false ], + [ '2012-01-01', null, '2011-12-01', null, null, '2012-01-02', false ], + [ '2012-01-01', null, '2012-01-02', null, null, '2012-01-02', false ], + [ '2012-01-01', null, '2012-01-01', null, null, '2012-01-02', false ], + [ '2012-01-01', null, '2012-01-04', null, 4, '2012-01-05', false ], + [ '2012-01-01', '2', '2012-01-04', null, 3, '2012-01-06', false ], + // Max values testing. + [ '2012-01-01', '1', null, 'foo', 2, '2012-01-03', false ], + [ '2012-01-01', '1', null, '2012-01-10', 1, '2012-01-02', false ], + [ '2012-01-02', null, null, '2012-01-01', null, '2012-01-02', false ], + [ '2012-01-02', null, null, '2012-01-02', null, '2012-01-02', false ], + [ '1969-01-02', '5', '1969-01-01', '1969-01-02', null, '1969-01-02', false ], + // Step mismatch. + [ '2012-01-02', '2', '2012-01-01', null, null, '2012-01-03', false ], + [ '2012-01-02', '2', '2012-01-01', null, 2, '2012-01-05', false ], + [ '2012-01-05', '2', '2012-01-01', '2012-01-06', null, '2012-01-05', false ], + [ '1970-01-02', '2', null, null, null, '1970-01-04', false ], + [ '1970-01-05', '3', null, null, null, '1970-01-08', false ], + [ '1970-01-03', '3', null, null, null, '1970-01-06', false ], + [ '1970-01-03', '3', '1970-01-02', null, null, '1970-01-05', false ], + // Clamping. + [ '2012-01-01', null, '2012-01-31', null, null, '2012-01-31', false ], + [ '1970-01-02', '2', '1970-01-01', '1970-01-04', null, '1970-01-03', false ], + [ '1970-01-01', '5', '1970-01-02', '1970-01-09', 10, '1970-01-07', false ], + [ '1969-12-28', '5', '1969-12-29', '1970-01-06', 3, '1970-01-03', false ], + [ '1970-01-01', '3', '1970-02-01', '1970-02-07', 15, '1970-02-07', false ], + [ '1970-01-01', '3', '1970-01-01', '1970-01-06', 2, '1970-01-04', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1970-01-02', false ], + // With step = 'any'. + [ '2012-01-01', 'any', null, null, 1, null, true ], + [ '2012-01-01', 'ANY', null, null, 1, null, true ], + [ '2012-01-01', 'AnY', null, null, 1, null, true ], + [ '2012-01-01', 'aNy', null, null, 1, null, true ], + ]}, + { type: 'time', data: [ + // Regular case. + [ '16:39', null, null, null, null, '16:40', false ], + // Argument testing. + [ '16:40', null, null, null, 1, '16:41', false ], + [ '16:40', null, null, null, 5, '16:45', false ], + [ '16:40', null, null, null, -1, '16:39', false ], + [ '16:40', null, null, null, 0, '16:40', false ], + // hour/minutes/seconds wrapping. + [ '04:59', null, null, null, null, '05:00', false ], + [ '04:59:59', 1, null, null, null, '05:00', false ], + [ '04:59:59.900', 0.1, null, null, null, '05:00', false ], + [ '04:59:59.990', 0.01, null, null, null, '05:00', false ], + [ '04:59:59.999', 0.001, null, null, null, '05:00', false ], + // stepUp() on '23:59' gives '00:00'. + [ '23:59', null, null, null, 1, '00:00', false ], + [ '23:59', null, null, null, 3, '00:02', false ], + // Some random step values.. + [ '16:56', '0.5', null, null, null, '16:56:00.500', false ], + [ '16:56', '2', null, null, null, '16:56:02', false ], + [ '16:56', '0.25',null, null, 4, '16:56:01', false ], + [ '16:57', '1.1', '16:00', null, 1, '16:57:01', false ], + [ '16:57', '1.1', '16:00', null, 2, '16:57:02.100', false ], + [ '16:57', '1.1', '16:00', null, 10, '16:57:10.900', false ], + [ '16:57', '1.1', '16:00', null, 11, '16:57:12', false ], + [ '16:57', '1.1', '16:00', null, 8, '16:57:08.700', false ], + // Invalid @step, means that we use the default value. + [ '17:01', '0', null, null, null, '17:02', false ], + [ '17:01', '-1', null, null, null, '17:02', false ], + [ '17:01', 'foo', null, null, null, '17:02', false ], + // Min values testing. + [ '17:02', '60', 'foo', null, 2, '17:04', false ], + [ '17:10', '60', '17:09', null, null, '17:11', false ], + [ '17:10', '60', '17:10', null, null, '17:11', false ], + [ '17:10', '60', '17:30', null, 1, '17:30', false ], + [ '17:10', '180', '17:05', null, null, '17:11', false ], + [ '17:10', '300', '17:10', '17:11', null,'17:10', false ], + // Max values testing. + [ '17:15', '60', null, 'foo', null, '17:16', false ], + [ '17:15', null, null, '17:20', null, '17:16', false ], + [ '17:15', null, null, '17:15', null, '17:15', false ], + [ '17:15', null, null, '17:13', 4, '17:15', false ], + [ '17:15', '120', null, '17:13', 3, '17:15', false ], + // Step mismatch. + [ '17:19', '120', '17:10', null, null, '17:20', false ], + [ '17:19', '120', '17:10', null, 2, '17:22', false ], + [ '17:19', '120', '17:18', '17:25', null, '17:20', false ], + [ '17:19', '120', null, null, null, '17:21', false ], + [ '17:19', '180', null, null, null, '17:22', false ], + // Clamping. + [ '17:22', null, null, '17:11', null, '17:22', false ], + [ '17:22', '120', '17:20', '17:22', null, '17:22', false ], + [ '17:22', '300', '17:12', '17:20', 10, '17:22', false ], + [ '17:22', '300', '17:18', '17:20', 2, '17:22', false ], + [ '17:22', '180', '17:00', '17:20', 15, '17:22', false ], + [ '17:22', '180', '17:10', '17:20', 2, '17:22', false ], + // value = "" (NaN). + [ '', null, null, null, null, '00:01', false ], + // With step = 'any'. + [ '17:26', 'any', null, null, 1, null, true ], + [ '17:26', 'ANY', null, null, 1, null, true ], + [ '17:26', 'AnY', null, null, 1, null, true ], + [ '17:26', 'aNy', null, null, 1, null, true ], + ]}, + { type: 'month', data: [ + // Regular case. + [ '2016-08', null, null, null, null, '2016-09', false ], + // Argument testing. + [ '2016-08', null, null, null, 1, '2016-09', false ], + [ '2016-08', null, null, null, 9, '2017-05', false ], + [ '2016-08', null, null, null, -1, '2016-07', false ], + [ '2016-08', null, null, null, 0, '2016-08', false ], + // Month/Year wrapping. + [ '2015-12', null, null, null, 1, '2016-01', false ], + [ '1968-12', null, null, null, 4, '1969-04', false ], + [ '1970-01', null, null, null, -12, '1969-01', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '2016-01', null, null, null, 1.1, '2016-02', false ], + [ '2016-01', null, null, null, 1.9, '2016-02', false ], + // With step values. + [ '2016-01', '0.5', null, null, null, '2016-02', false ], + [ '2016-01', '2', null, null, null, '2016-03', false ], + [ '2016-01', '0.25', null, null, 4, '2016-05', false ], + [ '2016-01', '1.1', '2016-01', null, 1, '2016-02', false ], + [ '2016-01', '1.1', '2016-01', null, 2, '2016-03', false ], + [ '2016-01', '1.1', '2016-01', null, 10, '2016-11', false ], + [ '2016-01', '1.1', '2016-01', null, 11, '2016-12', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2016-01', '0', null, null, null, '2016-02', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2016-01', '-1', null, null, null, '2016-02', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2016-01', 'foo', null, null, null, '2016-02', false ], + // Min values testing. + [ '2016-01', '1', 'foo', null, null, '2016-02', false ], + [ '2016-01', null, '2015-12', null, null, '2016-02', false ], + [ '2016-01', null, '2016-02', null, null, '2016-02', false ], + [ '2016-01', null, '2016-01', null, null, '2016-02', false ], + [ '2016-01', null, '2016-04', null, 4, '2016-05', false ], + [ '2016-01', '2', '2016-04', null, 3, '2016-06', false ], + // Max values testing. + [ '2016-01', '1', null, 'foo', 2, '2016-03', false ], + [ '2016-01', '1', null, '2016-02', 1, '2016-02', false ], + [ '2016-02', null, null, '2016-01', null, '2016-02', false ], + [ '2016-02', null, null, '2016-02', null, '2016-02', false ], + [ '1969-02', '5', '1969-01', '1969-02', null, '1969-02', false ], + // Step mismatch. + [ '2016-02', '2', '2016-01', null, null, '2016-03', false ], + [ '2016-02', '2', '2016-01', null, 2, '2016-05', false ], + [ '2016-05', '2', '2016-01', '2016-06', null, '2016-05', false ], + [ '1970-02', '2', null, null, null, '1970-04', false ], + [ '1970-05', '3', null, null, null, '1970-08', false ], + [ '1970-03', '3', null, null, null, '1970-06', false ], + [ '1970-03', '3', '1970-02', null, null, '1970-05', false ], + // Clamping. + [ '2016-01', null, '2016-12', null, null, '2016-12', false ], + [ '1970-02', '2', '1970-01', '1970-04', null, '1970-03', false ], + [ '1970-01', '5', '1970-02', '1970-09', 10, '1970-07', false ], + [ '1969-11', '5', '1969-12', '1970-06', 3, '1970-05', false ], + [ '1970-01', '3', '1970-02', '1971-07', 15, '1971-05', false ], + [ '1970-01', '3', '1970-01', '1970-06', 2, '1970-04', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1970-02', false ], + // With step = 'any'. + [ '2016-01', 'any', null, null, 1, null, true ], + [ '2016-01', 'ANY', null, null, 1, null, true ], + [ '2016-01', 'AnY', null, null, 1, null, true ], + [ '2016-01', 'aNy', null, null, 1, null, true ], + ]}, + { type: 'week', data: [ + // Regular case. + [ '2016-W40', null, null, null, null, '2016-W41', false ], + // Argument testing. + [ '2016-W40', null, null, null, 1, '2016-W41', false ], + [ '2016-W40', null, null, null, 20, '2017-W08', false ], + [ '2016-W40', null, null, null, -1, '2016-W39', false ], + [ '2016-W40', null, null, null, 0, '2016-W40', false ], + // Week/Year wrapping. + [ '2015-W53', null, null, null, 1, '2016-W01', false ], + [ '1968-W52', null, null, null, 4, '1969-W04', false ], + [ '1970-W01', null, null, null, -52, '1969-W01', false ], + // Float values are rounded to integer (1.1 -> 1). + [ '2016-W01', null, null, null, 1.1, '2016-W02', false ], + [ '2016-W01', null, null, null, 1.9, '2016-W02', false ], + // With step values. + [ '2016-W01', '0.5', null, null, null, '2016-W02', false ], + [ '2016-W01', '2', null, null, null, '2016-W03', false ], + [ '2016-W01', '0.25', null, null, 4, '2016-W05', false ], + [ '2016-W01', '1.1', '2016-01', null, 1, '2016-W02', false ], + [ '2016-W01', '1.1', '2016-01', null, 2, '2016-W03', false ], + [ '2016-W01', '1.1', '2016-01', null, 10, '2016-W11', false ], + [ '2016-W01', '1.1', '2016-01', null, 20, '2016-W21', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2016-W01', '0', null, null, null, '2016-W02', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2016-W01', '-1', null, null, null, '2016-W02', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2016-W01', 'foo', null, null, null, '2016-W02', false ], + // Min values testing. + [ '2016-W01', '1', 'foo', null, null, '2016-W02', false ], + [ '2016-W01', null, '2015-W53', null, null, '2016-W02', false ], + [ '2016-W01', null, '2016-W02', null, null, '2016-W02', false ], + [ '2016-W01', null, '2016-W01', null, null, '2016-W02', false ], + [ '2016-W01', null, '2016-W04', null, 4, '2016-W05', false ], + [ '2016-W01', '2', '2016-W04', null, 3, '2016-W06', false ], + // Max values testing. + [ '2016-W01', '1', null, 'foo', 2, '2016-W03', false ], + [ '2016-W01', '1', null, '2016-W02', 1, '2016-W02', false ], + [ '2016-W02', null, null, '2016-W01', null, '2016-W02', false ], + [ '2016-W02', null, null, '2016-W02', null, '2016-W02', false ], + [ '1969-W02', '5', '1969-W01', '1969-W02', null, '1969-W02', false ], + // Step mismatch. + [ '2016-W02', '2', '2016-W01', null, null, '2016-W03', false ], + [ '2016-W02', '2', '2016-W01', null, 2, '2016-W05', false ], + [ '2016-W05', '2', '2016-W01', '2016-W06', null, '2016-W05', false ], + [ '1970-W02', '2', null, null, null, '1970-W04', false ], + [ '1970-W05', '3', null, null, null, '1970-W08', false ], + [ '1970-W03', '3', null, null, null, '1970-W06', false ], + [ '1970-W03', '3', '1970-W02', null, null, '1970-W05', false ], + // Clamping. + [ '2016-W01', null, '2016-W52', null, null, '2016-W52', false ], + [ '1970-W02', '2', '1970-W01', '1970-W04', null, '1970-W03', false ], + [ '1970-W01', '5', '1970-W02', '1970-W09', 10, '1970-W07', false ], + [ '1969-W50', '5', '1969-W52', '1970-W06', 3, '1970-W05', false ], + [ '1970-W01', '3', '1970-W02', '1971-W07', 15, '1970-W44', false ], + [ '1970-W01', '3', '1970-W01', '1970-W06', 2, '1970-W04', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1970-W02', false ], + // With step = 'any'. + [ '2016-W01', 'any', null, null, 1, null, true ], + [ '2016-W01', 'ANY', null, null, 1, null, true ], + [ '2016-W01', 'AnY', null, null, 1, null, true ], + [ '2016-W01', 'aNy', null, null, 1, null, true ], + ]}, + { type: 'datetime-local', data: [ + // Regular case. + [ '2017-02-07T17:09', null, null, null, null, '2017-02-07T17:10', false ], + // Argument testing. + [ '2017-02-07T17:10', null, null, null, 1, '2017-02-07T17:11', false ], + [ '2017-02-07T17:10', null, null, null, 5, '2017-02-07T17:15', false ], + [ '2017-02-07T17:10', null, null, null, -1, '2017-02-07T17:09', false ], + [ '2017-02-07T17:10', null, null, null, 0, '2017-02-07T17:10', false ], + // hour/minutes/seconds wrapping. + [ '2000-01-01T04:59', null, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59', 1, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59.900', 0.1, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59.990', 0.01, null, null, null, '2000-01-01T05:00', false ], + [ '2000-01-01T04:59:59.999', 0.001, null, null, null, '2000-01-01T05:00', false ], + // month/year wrapping. + [ '2012-07-31T12:00', null, null, null, 1440, '2012-08-01T12:00', false ], + [ '1968-12-29T12:00', null, null, null, 5760, '1969-01-02T12:00', false ], + [ '1970-01-01T00:00', null, null, null, -1440, '1969-12-31T00:00', false ], + [ '2012-03-01T00:00', null, null, null, -1440, '2012-02-29T00:00', false ], + // stepUp() on '23:59' gives '00:00'. + [ '2017-02-07T23:59', null, null, null, 1, '2017-02-08T00:00', false ], + [ '2017-02-07T23:59', null, null, null, 3, '2017-02-08T00:02', false ], + // Some random step values.. + [ '2017-02-07T17:40', '0.5', null, null, null, '2017-02-07T17:40:00.500', false ], + [ '2017-02-07T17:40', '2', null, null, null, '2017-02-07T17:40:02', false ], + [ '2017-02-07T17:40', '0.25', null, null, 4, '2017-02-07T17:40:01', false ], + [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 1, '2017-02-07T17:40:00.200', false ], + [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 2, '2017-02-07T17:40:01.300', false ], + [ '2017-02-07T17:40', '1.1', '2017-02-07T17:00', null, 10, '2017-02-07T17:40:10.100', false ], + [ '2017-02-07T17:40', '129600', '2017-02-01T00:00', null, 2, '2017-02-10T00:00', false ], + // step = 0 isn't allowed (-> step = 1). + [ '2017-02-07T17:39', '0', null, null, null, '2017-02-07T17:40', false ], + // step < 0 isn't allowed (-> step = 1). + [ '2017-02-07T17:39', '-1', null, null, null, '2017-02-07T17:40', false ], + // step = NaN isn't allowed (-> step = 1). + [ '2017-02-07T17:39', 'foo', null, null, null, '2017-02-07T17:40', false ], + // Min values testing. + [ '2012-02-02T17:00', '60', 'foo', null, 2, '2012-02-02T17:02', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:10', null, null, '2012-02-02T17:11', false ], + [ '2012-02-02T17:10', '60', '2012-02-02T17:30', null, 1, '2012-02-02T17:30', false ], + [ '2012-02-02T17:10', '180', '2012-02-02T17:05', null, null, '2012-02-02T17:11', false ], + [ '2012-02-02T17:10', '86400', '2012-02-02T17:05', null, null, '2012-02-03T17:05', false ], + [ '2012-02-02T17:10', '129600', '2012-02-01T00:00', null, null, '2012-02-04T00:00', false ], + // Max values testing. + [ '2012-02-02T17:15', '60', null, 'foo', null, '2012-02-02T17:16', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:20', null, '2012-02-02T17:16', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:15', null, '2012-02-02T17:15', false ], + [ '2012-02-02T17:15', null, null, '2012-02-02T17:13', 4, '2012-02-02T17:15', false ], + [ '2012-02-02T20:05', '86400', null, '2012-02-03T20:05', null, '2012-02-03T20:05', false ], + [ '2012-02-02T18:00', '129600', null, '2012-02-04T20:00', null, '2012-02-04T06:00', false ], + // Step mismatch. + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, null, '2017-02-07T17:20', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:10', null, 2, '2017-02-07T17:22', false ], + [ '2017-02-07T17:19', '120', '2017-02-07T17:18', '2017-02-07T17:25', null, '2017-02-07T17:20', false ], + [ '2017-02-07T17:19', '120', null, null, null, '2017-02-07T17:21', false ], + [ '2017-02-07T17:19', '180', null, null, null, '2017-02-07T17:22', false ], + [ '2017-02-03T17:19', '172800', '2017-02-02T17:19', '2017-02-10T17:19', null, '2017-02-04T17:19', false ], + // Clamping. + [ '2017-02-07T17:22', null, null, '2017-02-07T17:11', null, '2017-02-07T17:22', false ], + [ '2017-02-07T17:22', '120', '2017-02-07T17:20', '2017-02-07T17:22', null, '2017-02-07T17:22', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:12', '2017-02-07T17:20', 10, '2017-02-07T17:22', false ], + [ '2017-02-07T17:22', '300', '2017-02-07T17:18', '2017-02-07T17:20', 2, '2017-02-07T17:22', false ], + [ '2017-02-06T17:22', '600', '2017-02-02T17:00', '2017-02-07T17:20', 15, '2017-02-06T19:50', false ], + [ '2017-02-06T17:22', '600', '2017-02-02T17:10', '2017-02-07T17:20', 2, '2017-02-06T17:40', false ], + // value = "" (NaN). + [ '', null, null, null, null, '1970-01-01T00:01', false ], + // With step = 'any'. + [ '2017-02-07T17:30', 'any', null, null, 1, null, true ], + [ '2017-02-07T17:30', 'ANY', null, null, 1, null, true ], + [ '2017-02-07T17:30', 'AnY', null, null, 1, null, true ], + [ '2017-02-07T17:30', 'aNy', null, null, 1, null, true ], + ]}, + ]; + + for (var test of testData) { + for (var data of test.data) { + var element = document.createElement("input"); + element.type = test.type; + + if (data[1] != null) { + element.step = data[1]; + } + + if (data[2] != null) { + element.min = data[2]; + } + + if (data[3] != null) { + element.max = data[3]; + } + + // Set 'value' last for type=range, because the final sanitized value + // after setting 'step', 'min' and 'max' can be affected by the order in + // which those attributes are set. Setting 'value' last makes it simpler + // to reason about what the final value should be. + if (data[0] != null) { + element.setAttribute('value', data[0]); + } + + var exceptionCaught = false; + try { + if (data[4] != null) { + element.stepUp(data[4]); + } else { + element.stepUp(); + } + + is(element.value, data[5], "The value for type=" + test.type + " should be " + data[5]); + } catch (e) { + exceptionCaught = true; + is(element.value, data[0], e.name + "The value should not have changed"); + is(e.name, 'InvalidStateError', + "It should be a InvalidStateError exception."); + } finally { + is(exceptionCaught, data[6], "exception status should be " + data[6]); + } + } + } +} + +checkPresence(); +checkAvailability(); + +checkStepDown(); +checkStepUp(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_submit_invalid_file.html b/dom/html/test/forms/test_submit_invalid_file.html new file mode 100644 index 0000000000..68b5e44877 --- /dev/null +++ b/dom/html/test/forms/test_submit_invalid_file.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=702949 +--> +<head> + <meta charset="utf-8"> + <title>Test invalid file submission</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=702949">Mozilla Bug 702949</a> +<p id="display"></p> +<div id="content" style="display: none"> + <form action='http://mochi.test:8888/chrome/dom/html/test/forms/submit_invalid_file.sjs' method='post' target='result' + enctype='multipart/form-data'> + <input type='file' name='file'> + </form> + <iframe name='result'></iframe> +</div> +<pre id="test"> +</pre> +<script type="application/javascript"> + /* + * Test invalid file submission by submitting a file that has been deleted + * from the file system before the form has been submitted. + * The form submission triggers a sjs file that shows its output in a frame. + * That means the test might time out if it fails. + */ + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + var { FileUtils } = SpecialPowers.ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" + ); + + var i = document.getElementsByTagName('input')[0]; + + var file = FileUtils.getDir("TmpD", []); + file.append("testfile"); + file.createUnique(SpecialPowers.Ci.nsIFile.NORMAL_FILE_TYPE, 0o644); + + SpecialPowers.wrap(i).value = file.path; + file.remove(/* recursive = */ false); + + document.getElementsByName('result')[0].addEventListener('load', function() { + is(window.frames[0].document.body.textContent, "SUCCESS"); + SimpleTest.finish(); + }); + document.forms[0].submit(); + }); +</script> +</body> +</html> diff --git a/dom/html/test/forms/test_textarea_attributes_reflection.html b/dom/html/test/forms/test_textarea_attributes_reflection.html new file mode 100644 index 0000000000..925f97e751 --- /dev/null +++ b/dom/html/test/forms/test_textarea_attributes_reflection.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLTextAreaElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="../reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLTextAreaElement attributes reflection **/ + +// .autofocus +reflectBoolean({ + element: document.createElement("textarea"), + attribute: "autofocus", +}); + +//.cols +reflectUnsignedInt({ + element: document.createElement("textarea"), + attribute: "cols", + nonZero: true, + defaultValue: 20, + fallback: true, +}); + +//.dirname +reflectString({ + element: document.createElement("textarea"), + attribute: "dirName" +}) + +// .disabled +reflectBoolean({ + element: document.createElement("textarea"), + attribute: "disabled", +}); + +// TODO: form (HTMLFormElement) + +// .maxLength +reflectInt({ + element: document.createElement("textarea"), + attribute: "maxLength", + nonNegative: true, +}); + +// .name +reflectString({ + element: document.createElement("textarea"), + attribute: "name", + otherValues: [ "isindex", "_charset_" ], +}); + +// .placeholder +reflectString({ + element: document.createElement("textarea"), + attribute: "placeholder", + otherValues: [ "foo\nbar", "foo\rbar", "foo\r\nbar" ], +}); + +// .readOnly +reflectBoolean({ + element: document.createElement("textarea"), + attribute: "readOnly", +}); + +// .required +reflectBoolean({ + element: document.createElement("textarea"), + attribute: "required", +}); + +// .rows +reflectUnsignedInt({ + element: document.createElement("textarea"), + attribute: "rows", + nonZero: true, + defaultValue: 2, + fallback: true, +}); + +// .wrap +// TODO: make it an enumerated attributes limited to only known values, bug 670869. +reflectString({ + element: document.createElement("textarea"), + attribute: "wrap", + otherValues: [ "soft", "hard" ], +}); + +// .type doesn't reflect a content attribute. +// .defaultValue doesn't reflect a content attribute. +// .value doesn't reflect a content attribute. +// .textLength doesn't reflect a content attribute. +// .willValidate doesn't reflect a content attribute. +// .validity doesn't reflect a content attribute. +// .validationMessage doesn't reflect a content attribute. +// .labels doesn't reflect a content attribute. + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_validation.html b/dom/html/test/forms/test_validation.html new file mode 100644 index 0000000000..666d4a45c0 --- /dev/null +++ b/dom/html/test/forms/test_validation.html @@ -0,0 +1,343 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=345624 +--> +<head> + <title>Test for Bug 345624</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input, textarea, fieldset, button, select, output, object { background-color: rgb(0,0,0) !important; } + :valid { background-color: rgb(0,255,0) !important; } + :invalid { background-color: rgb(255,0,0) !important; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=345624">Mozilla Bug 345624</a> +<p id="display"></p> +<div id="content" style="display: none"> + <fieldset id='f'></fieldset> + <input id='i' oninvalid="invalidEventHandler(event);"> + <button id='b' oninvalid="invalidEventHandler(event);"></button> + <select id='s' oninvalid="invalidEventHandler(event);"></select> + <textarea id='t' oninvalid="invalidEventHandler(event);"></textarea> + <output id='o' oninvalid="invalidEventHandler(event);"></output> + <object id='obj'></object> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 345624 **/ + +var gInvalid = false; + +function invalidEventHandler(aEvent) +{ + function checkInvalidEvent(event) + { + is(event.type, "invalid", "Invalid event type should be invalid"); + ok(!event.bubbles, "Invalid event should not bubble"); + ok(event.cancelable, "Invalid event should be cancelable"); + } + + checkInvalidEvent(aEvent); + + gInvalid = true; +} + +function checkConstraintValidationAPIExist(element) +{ + ok('willValidate' in element, "willValidate is not available in the DOM"); + ok('validationMessage' in element, "validationMessage is not available in the DOM"); + ok('validity' in element, "validity is not available in the DOM"); + + if ('validity' in element) { + validity = element.validity; + ok('valueMissing' in validity, "validity.valueMissing is not available in the DOM"); + ok('typeMismatch' in validity, "validity.typeMismatch is not available in the DOM"); + ok('badInput' in validity, "validity.badInput is not available in the DOM"); + ok('patternMismatch' in validity, "validity.patternMismatch is not available in the DOM"); + ok('tooLong' in validity, "validity.tooLong is not available in the DOM"); + ok('rangeUnderflow' in validity, "validity.rangeUnderflow is not available in the DOM"); + ok('rangeOverflow' in validity, "validity.rangeOverflow is not available in the DOM"); + ok('stepMismatch' in validity, "validity.stepMismatch is not available in the DOM"); + ok('customError' in validity, "validity.customError is not available in the DOM"); + ok('valid' in validity, "validity.valid is not available in the DOM"); + } +} + +function checkConstraintValidationAPIDefaultValues(element) +{ + // Not checking willValidate because the default value depends of the element + + is(element.validationMessage, "", "validationMessage default value should be empty string"); + + ok(!element.validity.valueMissing, "The element should not suffer from a constraint validation"); + ok(!element.validity.typeMismatch, "The element should not suffer from a constraint validation"); + ok(!element.validity.badInput, "The element should not suffer from a constraint validation"); + ok(!element.validity.patternMismatch, "The element should not suffer from a constraint validation"); + ok(!element.validity.tooLong, "The element should not suffer from a constraint validation"); + ok(!element.validity.rangeUnderflow, "The element should not suffer from a constraint validation"); + ok(!element.validity.rangeOverflow, "The element should not suffer from a constraint validation"); + ok(!element.validity.stepMismatch, "The element should not suffer from a constraint validation"); + ok(!element.validity.customError, "The element should not suffer from a constraint validation"); + ok(element.validity.valid, "The element should be valid by default"); + + ok(element.checkValidity(), "The element should be valid by default"); +} + +function checkDefaultPseudoClass() +{ + is(window.getComputedStyle(document.getElementById('f')) + .getPropertyValue('background-color'), "rgb(0, 255, 0)", + ":valid should apply"); + + is(window.getComputedStyle(document.getElementById('o')) + .getPropertyValue('background-color'), "rgb(0, 0, 0)", + "Nor :valid and :invalid should apply"); + + is(window.getComputedStyle(document.getElementById('obj')) + .getPropertyValue('background-color'), "rgb(0, 0, 0)", + "Nor :valid and :invalid should apply"); + + is(window.getComputedStyle(document.getElementById('s')) + .getPropertyValue('background-color'), "rgb(0, 255, 0)", + ":valid pseudo-class should apply"); + + is(window.getComputedStyle(document.getElementById('i')) + .getPropertyValue('background-color'), "rgb(0, 255, 0)", + ":valid pseudo-class should apply"); + + is(window.getComputedStyle(document.getElementById('t')) + .getPropertyValue('background-color'), "rgb(0, 255, 0)", + ":valid pseudo-class should apply"); + + is(window.getComputedStyle(document.getElementById('b')) + .getPropertyValue('background-color'), "rgb(0, 255, 0)", + ":valid pseudo-class should apply"); +} + +function checkSpecificWillValidate() +{ + // fieldset, output, object (TODO) and select elements + ok(!document.getElementById('f').willValidate, "Fielset element should be barred from constraint validation"); + ok(!document.getElementById('obj').willValidate, "Object element should be barred from constraint validation"); + ok(!document.getElementById('o').willValidate, "Output element should be barred from constraint validation"); + ok(document.getElementById('s').willValidate, "Select element should not be barred from constraint validation"); + + // input element + i = document.getElementById('i'); + i.type = "hidden"; + ok(!i.willValidate, "Hidden state input should be barred from constraint validation"); + is(window.getComputedStyle(i).getPropertyValue('background-color'), + "rgb(0, 0, 0)", "Nor :valid and :invalid should apply"); + i.type = "reset"; + ok(!i.willValidate, "Reset button state input should be barred from constraint validation"); + is(window.getComputedStyle(i).getPropertyValue('background-color'), + "rgb(0, 0, 0)", "Nor :valid and :invalid should apply"); + i.type = "button"; + ok(!i.willValidate, "Button state input should be barred from constraint validation"); + is(window.getComputedStyle(i).getPropertyValue('background-color'), + "rgb(0, 0, 0)", "Nor :valid and :invalid should apply"); + i.type = "image"; + ok(i.willValidate, "Image state input should not be barred from constraint validation"); + is(window.getComputedStyle(i).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid and :invalid should apply"); + i.type = "submit"; + ok(i.willValidate, "Submit state input should not be barred from constraint validation"); + is(window.getComputedStyle(i).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid and :invalid should apply"); + i.type = "number"; + ok(i.willValidate, "Number state input should not be barred from constraint validation"); + is(window.getComputedStyle(i).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + i.type = ""; + i.readOnly = 'true'; + ok(!i.willValidate, "Readonly input should be barred from constraint validation"); + is(window.getComputedStyle(i).getPropertyValue('background-color'), + "rgb(0, 0, 0)", "Nor :valid and :invalid should apply"); + i.removeAttribute('readOnly'); + ok(i.willValidate, "Default input element should not be barred from constraint validation"); + is(window.getComputedStyle(i).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + + // button element + b = document.getElementById('b'); + b.type = "reset"; + ok(!b.willValidate, "Reset state button should be barred from constraint validation"); + is(window.getComputedStyle(b).getPropertyValue('background-color'), + "rgb(0, 0, 0)", "Nor :valid and :invalid should apply"); + b.type = "button"; + ok(!b.willValidate, "Button state button should be barred from constraint validation"); + is(window.getComputedStyle(b).getPropertyValue('background-color'), + "rgb(0, 0, 0)", "Nor :valid and :invalid should apply"); + b.type = "submit"; + ok(b.willValidate, "Submit state button should not be barred from constraint validation"); + is(window.getComputedStyle(b).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid and :invalid should apply"); + b.type = ""; + ok(b.willValidate, "Default button element should not be barred from constraint validation"); + is(window.getComputedStyle(b).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + + // textarea element + t = document.getElementById('t'); + t.readOnly = true; + ok(!t.willValidate, "Readonly textarea should be barred from constraint validation"); + is(window.getComputedStyle(t).getPropertyValue('background-color'), + "rgb(0, 0, 0)", "Nor :valid and :invalid should apply"); + t.removeAttribute('readOnly'); + ok(t.willValidate, "Default textarea element should not be barred from constraint validation"); + is(window.getComputedStyle(t).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); + + // TODO: PROGRESS + // TODO: METER +} + +function checkCommonWillValidate(element) +{ + // Not checking the default value because it has been checked previously. + + element.disabled = true; + ok(!element.willValidate, "Disabled element should be barred from constraint validation"); + + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 0, 0)", "Nor :valid and :invalid should apply"); + + element.removeAttribute('disabled'); + + // TODO: If an element has a datalist element ancestor, it is barred from constraint validation. +} + +function checkCustomError(element, isBarred) +{ + element.setCustomValidity("message"); + if (!isBarred) { + is(element.validationMessage, "message", + "When the element has a custom validity message, validation message should return it"); + } else { + is(element.validationMessage, "", + "An element barred from constraint validation can't have a validation message"); + } + ok(element.validity.customError, "The element should suffer from a custom error"); + ok(!element.validity.valid, "The element should not be valid with a custom error"); + + if (element.tagName == "FIELDSET") { + is(window.getComputedStyle(element).getPropertyValue('background-color'), + isBarred ? "rgb(0, 255, 0)" : "rgb(255, 0, 0)", + ":invalid pseudo-classs should apply to " + element.tagName); + } + else { + is(window.getComputedStyle(element).getPropertyValue('background-color'), + isBarred ? "rgb(0, 0, 0)" : "rgb(255, 0, 0)", + ":invalid pseudo-classs should apply to " + element.tagName); + } + + element.setCustomValidity(""); + is(element.validationMessage, "", "The element should not have a validation message when reseted"); + ok(!element.validity.customError, "The element should not suffer anymore from a custom error"); + ok(element.validity.valid, "The element should now be valid"); + + is(window.getComputedStyle(element).getPropertyValue('background-color'), + isBarred && element.tagName != "FIELDSET" ? "rgb(0, 0, 0)" : "rgb(0, 255, 0)", + ":valid pseudo-classs should apply"); +} + +function checkCheckValidity(element) +{ + element.setCustomValidity("message"); + ok(!element.checkValidity(), "checkValidity() should return false when the element is not valid"); + + ok(gInvalid, "Invalid event should have been handled"); + + gInvalid = false; + element.setCustomValidity(""); + + ok(element.checkValidity(), "Element should be valid"); + ok(!gInvalid, "Invalid event should not have been handled"); +} + +function checkValidityStateObjectAliveWithoutElement(element) +{ + // We are creating a temporary element and getting it's ValidityState object. + // Then, we make sure it is removed by the garbage collector and we check the + // ValidityState default values (it should not crash). + + var v = document.createElement(element).validity; + SpecialPowers.gc(); + + ok(!v.valueMissing, + "When the element is not alive, it shouldn't suffer from constraint validation"); + ok(!v.typeMismatch, + "When the element is not alive, it shouldn't suffer from constraint validation"); + ok(!v.badInput, + "When the element is not alive, it shouldn't suffer from constraint validation"); + ok(!v.patternMismatch, + "When the element is not alive, it shouldn't suffer from constraint validation"); + ok(!v.tooLong, + "When the element is not alive, it shouldn't suffer from constraint validation"); + ok(!v.rangeUnderflow, + "When the element is not alive, it shouldn't suffer from constraint validation"); + ok(!v.rangeOverflow, + "When the element is not alive, it shouldn't suffer from constraint validation"); + ok(!v.stepMismatch, + "When the element is not alive, it shouldn't suffer from constraint validation"); + ok(!v.customError, + "When the element is not alive, it shouldn't suffer from constraint validation"); + ok(v.valid, "When the element is not alive, it should be valid"); +} + +checkConstraintValidationAPIExist(document.getElementById('f')); +checkConstraintValidationAPIExist(document.getElementById('i')); +checkConstraintValidationAPIExist(document.getElementById('b')); +checkConstraintValidationAPIExist(document.getElementById('s')); +checkConstraintValidationAPIExist(document.getElementById('t')); +checkConstraintValidationAPIExist(document.getElementById('o')); +checkConstraintValidationAPIExist(document.getElementById('obj')); + +checkConstraintValidationAPIDefaultValues(document.getElementById('f')); +checkConstraintValidationAPIDefaultValues(document.getElementById('i')); +checkConstraintValidationAPIDefaultValues(document.getElementById('b')); +checkConstraintValidationAPIDefaultValues(document.getElementById('s')); +checkConstraintValidationAPIDefaultValues(document.getElementById('t')); +checkConstraintValidationAPIDefaultValues(document.getElementById('o')); +checkConstraintValidationAPIDefaultValues(document.getElementById('obj')); + +checkDefaultPseudoClass(); + +checkSpecificWillValidate(); + +// Not checking button, fieldset, output and object +// because they are always barred from constraint validation. +checkCommonWillValidate(document.getElementById('i')); +checkCommonWillValidate(document.getElementById('s')); +checkCommonWillValidate(document.getElementById('t')); + +checkCustomError(document.getElementById('i'), false); +checkCustomError(document.getElementById('s'), false); +checkCustomError(document.getElementById('t'), false); +checkCustomError(document.getElementById('o'), true); +checkCustomError(document.getElementById('b'), false); +checkCustomError(document.getElementById('f'), true); +checkCustomError(document.getElementById('obj'), true); + +// Not checking button, fieldset, output and object +// because they are always barred from constraint validation. +checkCheckValidity(document.getElementById('i')); +checkCheckValidity(document.getElementById('s')); +checkCheckValidity(document.getElementById('t')); + +checkValidityStateObjectAliveWithoutElement("fieldset"); +checkValidityStateObjectAliveWithoutElement("input"); +checkValidityStateObjectAliveWithoutElement("button"); +checkValidityStateObjectAliveWithoutElement("select"); +checkValidityStateObjectAliveWithoutElement("textarea"); +checkValidityStateObjectAliveWithoutElement("output"); +checkValidityStateObjectAliveWithoutElement("object"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_validation_not_in_doc.html b/dom/html/test/forms/test_validation_not_in_doc.html new file mode 100644 index 0000000000..1500c60869 --- /dev/null +++ b/dom/html/test/forms/test_validation_not_in_doc.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for constraint validation of form controls not in documents</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +test(function() { + var input = document.createElement('input'); + input.required = true; + assert_false(input.checkValidity()); +}, "Should validate input not in document"); + +test(function() { + var textarea = document.createElement('textarea'); + textarea.required = true; + assert_false(textarea.checkValidity()); +}, "Should validate textarea not in document"); +</script> diff --git a/dom/html/test/forms/test_valueasdate_attribute.html b/dom/html/test/forms/test_valueasdate_attribute.html new file mode 100644 index 0000000000..9055879a85 --- /dev/null +++ b/dom/html/test/forms/test_valueasdate_attribute.html @@ -0,0 +1,751 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=769370 +--> +<head> + <title>Test for input.valueAsDate</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=769370">Mozilla Bug 769370</a> +<iframe name="testFrame" style="display: none"></iframe> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 769370**/ + +/** + * This test is checking .valueAsDate. + */ + +var element = document.createElement("input"); + +var validTypes = +[ + ["text", false], + ["password", false], + ["search", false], + ["tel", false], + ["email", false], + ["url", false], + ["hidden", false], + ["checkbox", false], + ["radio", false], + ["file", false], + ["submit", false], + ["image", false], + ["reset", false], + ["button", false], + ["number", false], + ["range", false], + ["date", true], + ["time", true], + ["color", false], + ["month", true], + ["week", true], + ["datetime-local", true], +]; + +function checkAvailability() +{ + for (let data of validTypes) { + var exceptionCatched = false; + element.type = data[0]; + try { + element.valueAsDate; + } catch (e) { + exceptionCatched = true; + } + is(exceptionCatched, false, + "valueAsDate shouldn't throw exception on getting"); + + exceptionCatched = false; + try { + element.valueAsDate = new Date(); + } catch (e) { + exceptionCatched = true; + } + is(exceptionCatched, !data[1], "valueAsDate for " + data[0] + + " availability is not correct"); + } +} + +function checkGarbageValues() +{ + for (let type of validTypes) { + if (!type[1]) { + continue; + } + type = type[0]; + + var inputElement = document.createElement('input'); + inputElement.type = type; + + inputElement.value = "test"; + inputElement.valueAsDate = null; + is(inputElement.value, "", "valueAsDate should set the value to the empty string"); + + inputElement.value = "test"; + inputElement.valueAsDate = undefined; + is(inputElement.value, "", "valueAsDate should set the value to the empty string"); + + inputElement.value = "test"; + inputElement.valueAsDate = new Date(NaN); + is(inputElement.value, "", "valueAsDate should set the value to the empty string"); + + var illegalValues = [ + "foobar", 42, {}, function() { return 42; }, function() { return Date(); } + ]; + + for (let value of illegalValues) { + try { + var caught = false; + inputElement.valueAsDate = value; + } catch(e) { + is(e.name, "TypeError", "Exception should be 'TypeError'."); + caught = true; + } + ok(caught, "Assigning " + value + " to .valueAsDate should throw"); + } + } +} + +function checkDateGet() +{ + var validData = + [ + [ "2012-07-12", 1342051200000 ], + [ "1970-01-01", 0 ], + [ "1970-01-02", 86400000 ], + [ "1969-12-31", -86400000 ], + [ "0311-01-31", -52350451200000 ], + [ "275760-09-13", 8640000000000000 ], + [ "0001-01-01", -62135596800000 ], + [ "2012-02-29", 1330473600000 ], + [ "2011-02-28", 1298851200000 ], + ]; + + var invalidData = + [ + [ "invaliddate" ], + [ "-001-12-31" ], + [ "901-12-31" ], + [ "1901-13-31" ], + [ "1901-12-32" ], + [ "1901-00-12" ], + [ "1901-01-00" ], + [ "1900-02-29" ], + [ "0000-01-01" ], + [ "" ], + // This date is valid for the input element, but is out of + // the date object range. In this case, on getting valueAsDate, + // a Date object will be created, but it will have a NaN internal value, + // and will return the string "Invalid Date". + [ "275760-09-14", true ], + ]; + + element.type = "date"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsDate.valueOf(), data[1], + "valueAsDate should return the " + + "valid date object representing this date"); + } + + for (let data of invalidData) { + element.value = data[0]; + if (data[1]) { + is(String(element.valueAsDate), "Invalid Date", + "valueAsDate should return an invalid Date object " + + "when the element value is not a valid date"); + } else { + is(element.valueAsDate, null, + "valueAsDate should return null " + + "when the element value is not a valid date"); + } + } +} + +function checkDateSet() +{ + var testData = + [ + [ 1342051200000, "2012-07-12" ], + [ 0, "1970-01-01" ], + // Maximum valid date (limited by the ecma date object range). + [ 8640000000000000, "275760-09-13" ], + // Minimum valid date (limited by the input element minimum valid value). + [ -62135596800000 , "0001-01-01" ], + [ 1330473600000, "2012-02-29" ], + [ 1298851200000, "2011-02-28" ], + // "Values must be truncated to valid dates" + [ 42.1234, "1970-01-01" ], + [ 123.123456789123, "1970-01-01" ], + [ 1e-1, "1970-01-01" ], + [ 1298851200010, "2011-02-28" ], + [ -1, "1969-12-31" ], + [ -86400000, "1969-12-31" ], + [ 86400000, "1970-01-02" ], + // Negative years, this is out of range for the input element, + // the corresponding date string is the empty string + [ -62135596800001, "" ], + // Invalid dates. + ]; + + element.type = "date"; + for (let data of testData) { + element.valueAsDate = new Date(data[0]); + is(element.value, data[1], "valueAsDate should set the value to " + + data[1]); + element.valueAsDate = new testFrame.Date(data[0]); + is(element.value, data[1], "valueAsDate with other-global date should " + + "set the value to " + data[1]); + } +} + +function checkTimeGet() +{ + var tests = [ + // Some invalid values to begin. + { value: "", result: null }, + { value: "foobar", result: null }, + { value: "00:", result: null }, + { value: "24:00", result: null }, + { value: "00:99", result: null }, + { value: "00:00:", result: null }, + { value: "00:00:99", result: null }, + { value: "00:00:00:", result: null }, + { value: "00:00:00.", result: null }, + { value: "00:00:00.0000", result: null }, + // Some simple valid values. + { value: "00:00", result: { time: 0, hours: 0, minutes: 0, seconds: 0, ms: 0 } }, + { value: "00:01", result: { time: 60000, hours: 0, minutes: 1, seconds: 0, ms: 0 } }, + { value: "01:00", result: { time: 3600000, hours: 1, minutes: 0, seconds: 0, ms: 0 } }, + { value: "01:01", result: { time: 3660000, hours: 1, minutes: 1, seconds: 0, ms: 0 } }, + { value: "13:37", result: { time: 49020000, hours: 13, minutes: 37, seconds: 0, ms: 0 } }, + // Valid values including seconds. + { value: "00:00:01", result: { time: 1000, hours: 0, minutes: 0, seconds: 1, ms: 0 } }, + { value: "13:37:42", result: { time: 49062000, hours: 13, minutes: 37, seconds: 42, ms: 0 } }, + // Valid values including seconds fractions. + { value: "00:00:00.001", result: { time: 1, hours: 0, minutes: 0, seconds: 0, ms: 1 } }, + { value: "00:00:00.123", result: { time: 123, hours: 0, minutes: 0, seconds: 0, ms: 123 } }, + { value: "00:00:00.100", result: { time: 100, hours: 0, minutes: 0, seconds: 0, ms: 100 } }, + { value: "00:00:00.000", result: { time: 0, hours: 0, minutes: 0, seconds: 0, ms: 0 } }, + { value: "20:17:31.142", result: { time: 73051142, hours: 20, minutes: 17, seconds: 31, ms: 142 } }, + // Highest possible value. + { value: "23:59:59.999", result: { time: 86399999, hours: 23, minutes: 59, seconds: 59, ms: 999 } }, + // Some values with one or two digits for the fraction of seconds. + { value: "00:00:00.1", result: { time: 100, hours: 0, minutes: 0, seconds: 0, ms: 100 } }, + { value: "00:00:00.14", result: { time: 140, hours: 0, minutes: 0, seconds: 0, ms: 140 } }, + { value: "13:37:42.7", result: { time: 49062700, hours: 13, minutes: 37, seconds: 42, ms: 700 } }, + { value: "23:31:12.23", result: { time: 84672230, hours: 23, minutes: 31, seconds: 12, ms: 230 } }, + ]; + + var inputElement = document.createElement('input'); + inputElement.type = 'time'; + + for (let test of tests) { + inputElement.value = test.value; + if (test.result === null) { + is(inputElement.valueAsDate, null, "element.valueAsDate should return null"); + } else { + var date = inputElement.valueAsDate; + isnot(date, null, "element.valueAsDate should not be null"); + + is(date.getTime(), test.result.time); + is(date.getUTCHours(), test.result.hours); + is(date.getUTCMinutes(), test.result.minutes); + is(date.getUTCSeconds(), test.result.seconds); + is(date.getUTCMilliseconds(), test.result.ms); + } + } +} + +function checkTimeSet() +{ + var tests = [ + // Simple tests. + { value: 0, result: "00:00" }, + { value: 1, result: "00:00:00.001" }, + { value: 100, result: "00:00:00.100" }, + { value: 1000, result: "00:00:01" }, + { value: 60000, result: "00:01" }, + { value: 3600000, result: "01:00" }, + { value: 83622234, result: "23:13:42.234" }, + // Some edge cases. + { value: 86400000, result: "00:00" }, + { value: 86400001, result: "00:00:00.001" }, + { value: 170022234, result: "23:13:42.234" }, + { value: 432000000, result: "00:00" }, + { value: -1, result: "23:59:59.999" }, + { value: -86400000, result: "00:00" }, + { value: -86400001, result: "23:59:59.999" }, + { value: -56789, result: "23:59:03.211" }, + { value: 0.9, result: "00:00" }, + ]; + + var inputElement = document.createElement('input'); + inputElement.type = 'time'; + + for (let test of tests) { + inputElement.valueAsDate = new Date(test.value); + is(inputElement.value, test.result, + "element.value should have been changed by setting valueAsDate"); + } +} + +function checkWithBustedPrototype() +{ + for (let type of validTypes) { + if (!type[1]) { + continue; + } + + type = type[0]; + + var inputElement = document.createElement('input'); + inputElement.type = type; + + var backupPrototype = {}; + backupPrototype.getUTCFullYear = Date.prototype.getUTCFullYear; + backupPrototype.getUTCMonth = Date.prototype.getUTCMonth; + backupPrototype.getUTCDate = Date.prototype.getUTCDate; + backupPrototype.getTime = Date.prototype.getTime; + backupPrototype.setUTCFullYear = Date.prototype.setUTCFullYear; + + Date.prototype.getUTCFullYear = function() { return {}; }; + Date.prototype.getUTCMonth = function() { return {}; }; + Date.prototype.getUTCDate = function() { return {}; }; + Date.prototype.getTime = function() { return {}; }; + Date.prototype.setUTCFullYear = function(y,m,d) { }; + + inputElement.valueAsDate = new Date(); + + isnot(inputElement.valueAsDate, null, ".valueAsDate should not return null"); + // The object returned by element.valueAsDate should return a Date object + // with the same prototype: + is(inputElement.valueAsDate.getUTCFullYear, Date.prototype.getUTCFullYear, + "prototype is the same"); + is(inputElement.valueAsDate.getUTCMonth, Date.prototype.getUTCMonth, + "prototype is the same"); + is(inputElement.valueAsDate.getUTCDate, Date.prototype.getUTCDate, + "prototype is the same"); + is(inputElement.valueAsDate.getTime, Date.prototype.getTime, + "prototype is the same"); + is(inputElement.valueAsDate.setUTCFullYear, Date.prototype.setUTCFullYear, + "prototype is the same"); + + // However the Date should have the correct information. + // Skip type=month for now, since .valueAsNumber returns number of months + // and not milliseconds. + if (type != "month") { + var witnessDate = new Date(inputElement.valueAsNumber); + is(inputElement.valueAsDate.valueOf(), witnessDate.valueOf(), "correct Date"); + } + + // Same test as above but using NaN instead of {}. + + Date.prototype.getUTCFullYear = function() { return NaN; }; + Date.prototype.getUTCMonth = function() { return NaN; }; + Date.prototype.getUTCDate = function() { return NaN; }; + Date.prototype.getTime = function() { return NaN; }; + Date.prototype.setUTCFullYear = function(y,m,d) { }; + + inputElement.valueAsDate = new Date(); + + isnot(inputElement.valueAsDate, null, ".valueAsDate should not return null"); + // The object returned by element.valueAsDate should return a Date object + // with the same prototype: + is(inputElement.valueAsDate.getUTCFullYear, Date.prototype.getUTCFullYear, + "prototype is the same"); + is(inputElement.valueAsDate.getUTCMonth, Date.prototype.getUTCMonth, + "prototype is the same"); + is(inputElement.valueAsDate.getUTCDate, Date.prototype.getUTCDate, + "prototype is the same"); + is(inputElement.valueAsDate.getTime, Date.prototype.getTime, + "prototype is the same"); + is(inputElement.valueAsDate.setUTCFullYear, Date.prototype.setUTCFullYear, + "prototype is the same"); + + // However the Date should have the correct information. + // Skip type=month for now, since .valueAsNumber returns number of months + // and not milliseconds. + if (type != "month") { + var witnessDate = new Date(inputElement.valueAsNumber); + is(inputElement.valueAsDate.valueOf(), witnessDate.valueOf(), "correct Date"); + } + + Date.prototype.getUTCFullYear = backupPrototype.getUTCFullYear; + Date.prototype.getUTCMonth = backupPrototype.getUTCMonth; + Date.prototype.getUTCDate = backupPrototype.getUTCDate; + Date.prototype.getTime = backupPrototype.getTime; + Date.prototype.setUTCFullYear = backupPrototype.setUTCFullYear; + } +} + +function checkMonthGet() +{ + var validData = + [ + [ "2016-07", 1467331200000 ], + [ "1970-01", 0 ], + [ "1970-02", 2678400000 ], + [ "1969-12", -2678400000 ], + [ "0001-01", -62135596800000 ], + [ "275760-09", 8639998963200000 ], + ]; + + var invalidData = + [ + [ "invalidmonth" ], + [ "0000-01" ], + [ "2016-00" ], + [ "123-01" ], + [ "2017-13" ], + [ "" ], + // This month is valid for the input element, but is out of + // the date object range. In this case, on getting valueAsDate, + // a Date object will be created, but it will have a NaN internal value, + // and will return the string "Invalid Date". + [ "275760-10", true ], + ]; + + element.type = "month"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsDate.valueOf(), data[1], + "valueAsDate should return the " + + "valid date object representing this month"); + } + + for (let data of invalidData) { + element.value = data[0]; + if (data[1]) { + is(String(element.valueAsDate), "Invalid Date", + "valueAsDate should return an invalid Date object " + + "when the element value is not a valid month"); + } else { + is(element.valueAsDate, null, + "valueAsDate should return null " + + "when the element value is not a valid month"); + } + } +} + +function checkMonthSet() +{ + var testData = + [ + [ 1342051200000, "2012-07" ], + [ 0, "1970-01" ], + // Maximum valid month (limited by the ecma date object range). + [ 8640000000000000, "275760-09" ], + // Minimum valid month (limited by the input element minimum valid value). + [ -62135596800000 , "0001-01" ], + [ 1330473600000, "2012-02" ], + [ 1298851200000, "2011-02" ], + // "Values must be truncated to valid months" + [ 42.1234, "1970-01" ], + [ 123.123456789123, "1970-01" ], + [ 1e-1, "1970-01" ], + [ 1298851200010, "2011-02" ], + [ -1, "1969-12" ], + [ -86400000, "1969-12" ], + [ 86400000, "1970-01" ], + // Negative years, this is out of range for the input element, + // the corresponding month string is the empty string + [ -62135596800001, "" ], + ]; + + element.type = "month"; + for (let data of testData) { + element.valueAsDate = new Date(data[0]); + is(element.value, data[1], "valueAsDate should set the value to " + + data[1]); + element.valueAsDate = new testFrame.Date(data[0]); + is(element.value, data[1], "valueAsDate with other-global date should " + + "set the value to " + data[1]); + } +} + +function checkWeekGet() +{ + var validData = + [ + // Common years starting on different days of week. + [ "2007-W01", Date.UTC(2007, 0, 1) ], // Mon + [ "2013-W01", Date.UTC(2012, 11, 31) ], // Tue + [ "2014-W01", Date.UTC(2013, 11, 30) ], // Wed + [ "2015-W01", Date.UTC(2014, 11, 29) ], // Thu + [ "2010-W01", Date.UTC(2010, 0, 4) ], // Fri + [ "2011-W01", Date.UTC(2011, 0, 3) ], // Sat + [ "2017-W01", Date.UTC(2017, 0, 2) ], // Sun + // Common years ending on different days of week. + [ "2007-W52", Date.UTC(2007, 11, 24) ], // Mon + [ "2013-W52", Date.UTC(2013, 11, 23) ], // Tue + [ "2014-W52", Date.UTC(2014, 11, 22) ], // Wed + [ "2015-W53", Date.UTC(2015, 11, 28) ], // Thu + [ "2010-W52", Date.UTC(2010, 11, 27) ], // Fri + [ "2011-W52", Date.UTC(2011, 11, 26) ], // Sat + [ "2017-W52", Date.UTC(2017, 11, 25) ], // Sun + // Leap years starting on different days of week. + [ "1996-W01", Date.UTC(1996, 0, 1) ], // Mon + [ "2008-W01", Date.UTC(2007, 11, 31) ], // Tue + [ "2020-W01", Date.UTC(2019, 11, 30) ], // Wed + [ "2004-W01", Date.UTC(2003, 11, 29) ], // Thu + [ "2016-W01", Date.UTC(2016, 0, 4) ], // Fri + [ "2000-W01", Date.UTC(2000, 0, 3) ], // Sat + [ "2012-W01", Date.UTC(2012, 0, 2) ], // Sun + // Leap years ending on different days of week. + [ "2012-W52", Date.UTC(2012, 11, 24) ], // Mon + [ "2024-W52", Date.UTC(2024, 11, 23) ], // Tue + [ "1980-W52", Date.UTC(1980, 11, 22) ], // Wed + [ "1992-W53", Date.UTC(1992, 11, 28) ], // Thu + [ "2004-W53", Date.UTC(2004, 11, 27) ], // Fri + [ "1988-W52", Date.UTC(1988, 11, 26) ], // Sat + [ "2000-W52", Date.UTC(2000, 11, 25) ], // Sun + // Other normal cases. + [ "2016-W36", 1473033600000 ], + [ "1969-W52", -864000000 ], + [ "1970-W01", -259200000 ], + [ "275760-W37", 8639999568000000 ], + ]; + + var invalidData = + [ + [ "invalidweek" ], + [ "0000-W01" ], + [ "2016-W00" ], + [ "123-W01" ], + [ "2016-W53" ], + [ "" ], + // This week is valid for the input element, but is out of + // the date object range. In this case, on getting valueAsDate, + // a Date object will be created, but it will have a NaN internal value, + // and will return the string "Invalid Date". + [ "275760-W38", true ], + ]; + + element.type = "week"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsDate.valueOf(), data[1], + "valueAsDate should return the " + + "valid date object representing this week"); + } + + for (let data of invalidData) { + element.value = data[0]; + if (data[1]) { + is(String(element.valueAsDate), "Invalid Date", + "valueAsDate should return an invalid Date object " + + "when the element value is not a valid week"); + } else { + is(element.valueAsDate, null, + "valueAsDate should return null " + + "when the element value is not a valid week"); + } + } +} + +function checkWeekSet() +{ + var testData = + [ + // Common years starting on different days of week. + [ Date.UTC(2007, 0, 1), "2007-W01" ], // Mon + [ Date.UTC(2013, 0, 1), "2013-W01" ], // Tue + [ Date.UTC(2014, 0, 1), "2014-W01" ], // Wed + [ Date.UTC(2015, 0, 1), "2015-W01" ], // Thu + [ Date.UTC(2010, 0, 1), "2009-W53" ], // Fri + [ Date.UTC(2011, 0, 1), "2010-W52" ], // Sat + [ Date.UTC(2017, 0, 1), "2016-W52" ], // Sun + // Common years ending on different days of week. + [ Date.UTC(2007, 11, 31), "2008-W01" ], // Mon + [ Date.UTC(2013, 11, 31), "2014-W01" ], // Tue + [ Date.UTC(2014, 11, 31), "2015-W01" ], // Wed + [ Date.UTC(2015, 11, 31), "2015-W53" ], // Thu + [ Date.UTC(2010, 11, 31), "2010-W52" ], // Fri + [ Date.UTC(2011, 11, 31), "2011-W52" ], // Sat + [ Date.UTC(2017, 11, 31), "2017-W52" ], // Sun + // Leap years starting on different days of week. + [ Date.UTC(1996, 0, 1), "1996-W01" ], // Mon + [ Date.UTC(2008, 0, 1), "2008-W01" ], // Tue + [ Date.UTC(2020, 0, 1), "2020-W01" ], // Wed + [ Date.UTC(2004, 0, 1), "2004-W01" ], // Thu + [ Date.UTC(2016, 0, 1), "2015-W53" ], // Fri + [ Date.UTC(2000, 0, 1), "1999-W52" ], // Sat + [ Date.UTC(2012, 0, 1), "2011-W52" ], // Sun + // Leap years ending on different days of week. + [ Date.UTC(2012, 11, 31), "2013-W01" ], // Mon + [ Date.UTC(2024, 11, 31), "2025-W01" ], // Tue + [ Date.UTC(1980, 11, 31), "1981-W01" ], // Wed + [ Date.UTC(1992, 11, 31), "1992-W53" ], // Thu + [ Date.UTC(2004, 11, 31), "2004-W53" ], // Fri + [ Date.UTC(1988, 11, 31), "1988-W52" ], // Sat + [ Date.UTC(2000, 11, 31), "2000-W52" ], // Sun + // Other normal cases. + [ Date.UTC(2016, 8, 9), "2016-W36" ], + [ Date.UTC(2010, 0, 3), "2009-W53" ], + [ Date.UTC(2010, 0, 4), "2010-W01" ], + [ Date.UTC(2010, 0, 10), "2010-W01" ], + [ Date.UTC(2010, 0, 11), "2010-W02" ], + [ 0, "1970-W01" ], + // Maximum valid month (limited by the ecma date object range). + [ 8640000000000000, "275760-W37" ], + // Minimum valid month (limited by the input element minimum valid value). + [ -62135596800000 , "0001-W01" ], + // "Values must be truncated to valid week" + [ 42.1234, "1970-W01" ], + [ 123.123456789123, "1970-W01" ], + [ 1e-1, "1970-W01" ], + [ -1.1, "1970-W01" ], + [ -345600000, "1969-W52" ], + // Negative years, this is out of range for the input element, + // the corresponding week string is the empty string + [ -62135596800001, "" ], + ]; + + element.type = "week"; + for (let data of testData) { + element.valueAsDate = new Date(data[0]); + is(element.value, data[1], "valueAsDate should set the value to " + + data[1]); + element.valueAsDate = new testFrame.Date(data[0]); + is(element.value, data[1], "valueAsDate with other-global date should " + + "set the value to " + data[1]); + } +} + +function checkDatetimeLocalGet() +{ + var validData = + [ + // Simple cases. + [ "2016-12-27T10:30", Date.UTC(2016, 11, 27, 10, 30, 0) ], + [ "2016-12-27T10:30:40", Date.UTC(2016, 11, 27, 10, 30, 40) ], + [ "2016-12-27T10:30:40.567", Date.UTC(2016, 11, 27, 10, 30, 40, 567) ], + [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ], + [ "1970-01-01T00:00", 0 ], + // Leap years. + [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ], + [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ], + [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ], + [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ], + [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ], + // Maximum valid datetime-local (limited by the ecma date object range). + [ "275760-09-13T00:00", 8640000000000000 ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ "0001-01-01T00:00", -62135596800000 ], + ]; + + var invalidData = + [ + [ "invaliddateime-local" ], + [ "0000-01-01T00:00" ], + [ "2016-12-25T00:00Z" ], + [ "2015-02-29T12:34" ], + [ "1-1-1T12:00" ], + [ "" ], + // This datetime-local is valid for the input element, but is out of the + // date object range. In this case, on getting valueAsDate, a Date object + // will be created, but it will have a NaN internal value, and will return + // the string "Invalid Date". + [ "275760-09-13T12:00", true ], + ]; + + element.type = "datetime-local"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsDate.valueOf(), data[1], + "valueAsDate should return the " + + "valid date object representing this datetime-local"); + } + + for (let data of invalidData) { + element.value = data[0]; + if (data[1]) { + is(String(element.valueAsDate), "Invalid Date", + "valueAsDate should return an invalid Date object " + + "when the element value is not a valid datetime-local"); + } else { + is(element.valueAsDate, null, + "valueAsDate should return null " + + "when the element value is not a valid datetime-local"); + } + } +} + +function checkDatetimeLocalSet() +{ + var testData = + [ + // Simple cases. + [ Date.UTC(2016, 11, 27, 10, 30, 0), "2016-12-27T10:30" ], + [ Date.UTC(2016, 11, 27, 10, 30, 30), "2016-12-27T10:30:30" ], + [ Date.UTC(1999, 11, 31, 23, 59, 59), "1999-12-31T23:59:59" ], + [ Date.UTC(1999, 11, 31, 23, 59, 59, 999), "1999-12-31T23:59:59.999" ], + [ Date.UTC(123456, 7, 8, 9, 10), "123456-08-08T09:10" ], + [ 0, "1970-01-01T00:00" ], + // Maximum valid datetime-local (limited by the ecma date object range). + [ 8640000000000000, "275760-09-13T00:00" ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ -62135596800000, "0001-01-01T00:00" ], + // Leap years. + [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ], + [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ], + [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ], + [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ], + [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ], + // "Values must be truncated to valid datetime-local" + [ 123.123456789123, "1970-01-01T00:00:00.123" ], + [ 1e-1, "1970-01-01T00:00" ], + [ -1.1, "1969-12-31T23:59:59.999" ], + [ -345600000, "1969-12-28T00:00" ], + // Negative years, this is out of range for the input element, + // the corresponding datetime-local string is the empty string + [ -62135596800001, "" ], + ]; + + element.type = "datetime-local"; + for (let data of testData) { + element.valueAsDate = new Date(data[0]); + is(element.value, data[1], "valueAsDate should set the value to " + + data[1]); + element.valueAsDate = new testFrame.Date(data[0]); + is(element.value, data[1], "valueAsDate with other-global date should " + + "set the value to " + data[1]); + } +} + +checkAvailability(); +checkGarbageValues(); +checkWithBustedPrototype(); + +// Test <input type='date'>. +checkDateGet(); +checkDateSet(); + +// Test <input type='time'>. +checkTimeGet(); +checkTimeSet(); + +// Test <input type='month'>. +checkMonthGet(); +checkMonthSet(); + +// Test <input type='week'>. +checkWeekGet(); +checkWeekSet(); + +// Test <input type='datetime-local'>. +checkDatetimeLocalGet(); +checkDatetimeLocalSet(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/test_valueasnumber_attribute.html b/dom/html/test/forms/test_valueasnumber_attribute.html new file mode 100644 index 0000000000..5f7537f7a8 --- /dev/null +++ b/dom/html/test/forms/test_valueasnumber_attribute.html @@ -0,0 +1,858 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=636737 +--> +<head> + <title>Test for Bug input.valueAsNumber</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=636737">Mozilla Bug 636737</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 636737 **/ + +/** + * This test is checking .valueAsNumber. + */ + +function checkAvailability() +{ + var testData = + [ + ["text", false], + ["password", false], + ["search", false], + ["tel", false], + ["email", false], + ["url", false], + ["hidden", false], + ["checkbox", false], + ["radio", false], + ["file", false], + ["submit", false], + ["image", false], + ["reset", false], + ["button", false], + ["number", true], + ["range", true], + ["date", true], + ["time", true], + ["color", false], + ["month", true], + ["week", true], + ["datetime-local", true], + ]; + + var element = document.createElement('input'); + + for (let data of testData) { + var exceptionCatched = false; + element.type = data[0]; + try { + element.valueAsNumber; + } catch (e) { + exceptionCatched = true; + } + is(exceptionCatched, false, + "valueAsNumber shouldn't throw exception on getting"); + + exceptionCatched = false; + try { + element.valueAsNumber = 42; + } catch (e) { + exceptionCatched = true; + } + is(exceptionCatched, !data[1], "valueAsNumber for " + data[0] + + " availability is not correct"); + } +} + +function checkNumberGet() +{ + var testData = + [ + ["42", 42], + ["-42", -42], // should work for negative values + ["42.1234", 42.1234], + ["123.123456789123", 123.123456789123], // double precision + ["1e2", 100], // e should be usable + ["2e1", 20], + ["1e-1", 0.1], // value after e can be negative + ["1E2", 100], // E can be used instead of e + ["e", null], + ["e2", null], + ["1e0.1", null], + ["", null], // the empty string is not a number + ["foo", null], + ["42,13", null], // comma can't be used as a decimal separator + ]; + + var element = document.createElement('input'); + element.type = "number"; + for (let data of testData) { + element.value = data[0]; + + // Given that NaN != NaN, we have to use null when the expected value is NaN. + if (data[1] != null) { + is(element.valueAsNumber, data[1], "valueAsNumber should return the " + + "floating point representation of the value"); + } else { + ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " + + "when the element value is not a number"); + } + } +} + +function checkNumberSet() +{ + var testData = + [ + [42, "42"], + [-42, "-42"], // should work for negative values + [42.1234, "42.1234"], + [123.123456789123, "123.123456789123"], // double precision + [1e2, "100"], // e should be usable + [2e1, "20"], + [1e-1, "0.1"], // value after e can be negative + [1E2, "100"], // E can be used instead of e + // Setting a string will set NaN. + ["foo", ""], + // "" is converted to 0. + ["", "0"], + [42, "42"], // Keep this here, it is used by the next test. + // Setting Infinity should throw and not change the current value. + [Infinity, "42", true], + [-Infinity, "42", true], + // Setting NaN should change the value to the empty string. + [NaN, ""], + ]; + + var element = document.createElement('input'); + element.type = "number"; + for (let data of testData) { + var caught = false; + try { + element.valueAsNumber = data[0]; + is(element.value, data[1], + "valueAsNumber should be able to set the value"); + } catch (e) { + caught = true; + } + + if (data[2]) { + ok(caught, "valueAsNumber should have thrown"); + is(element.value, data[1], "value should not have changed"); + } else { + ok(!caught, "valueAsNumber should not have thrown"); + } + } +} + +function checkRangeGet() +{ + // For type=range we should never get NaN since the user agent is required + // to fix up the input's value to be something sensible. + + var min = -200; + var max = 200; + var defaultValue = min + (max - min)/2; + + var testData = + [ + ["42", 42], + ["-42", -42], // should work for negative values + ["42.1234", 42.1234], + ["123.123456789123", 123.123456789123], // double precision + ["1e2", 100], // e should be usable + ["2e1", 20], + ["1e-1", 0.1], // value after e can be negative + ["1E2", 100], // E can be used instead of e + ["e", defaultValue], + ["e2", defaultValue], + ["1e0.1", defaultValue], + ["", defaultValue], + ["foo", defaultValue], + ["42,13", defaultValue], + ]; + + var element = document.createElement('input'); + element.type = "range"; + element.setAttribute("min", min); // avoids out of range sanitization + element.setAttribute("max", max); + element.setAttribute("step", "any"); // avoids step mismatch sanitization + for (let data of testData) { + element.value = data[0]; + + // Given that NaN != NaN, we have to use null when the expected value is NaN. + is(element.valueAsNumber, data[1], "valueAsNumber should return the " + + "floating point representation of the value"); + } +} + +function checkRangeSet() +{ + var min = -200; + var max = 200; + var defaultValue = String(min + (max - min)/2); + + var testData = + [ + [42, "42"], + [-42, "-42"], // should work for negative values + [42.1234, "42.1234"], + [123.123456789123, "123.123456789123"], // double precision + [1e2, "100"], // e should be usable + [2e1, "20"], + [1e-1, "0.1"], // value after e can be negative + [1E2, "100"], // E can be used instead of e + ["foo", defaultValue], + ["", defaultValue], + [42, "42"], // Keep this here, it is used by the next test. + // Setting Infinity should throw and not change the current value. + [Infinity, "42", true], + [-Infinity, "42", true], + // Setting NaN should change the value to the empty string. + [NaN, defaultValue], + ]; + + var element = document.createElement('input'); + element.type = "range"; + element.setAttribute("min", min); // avoids out of range sanitization + element.setAttribute("max", max); + element.setAttribute("step", "any"); // avoids step mismatch sanitization + for (let data of testData) { + var caught = false; + try { + element.valueAsNumber = data[0]; + is(element.value, data[1], + "valueAsNumber should be able to set the value"); + } catch (e) { + caught = true; + } + + if (data[2]) { + ok(caught, "valueAsNumber should have thrown"); + is(element.value, data[1], "value should not have changed"); + } else { + ok(!caught, "valueAsNumber should not have thrown"); + } + } +} + +function checkDateGet() +{ + var validData = + [ + [ "2012-07-12", 1342051200000 ], + [ "1970-01-01", 0 ], + // We are supposed to support at least until this date. + // (corresponding to the date object maximal value) + [ "275760-09-13", 8640000000000000 ], + // Minimum valid date (limited by the input element minimum valid value) + [ "0001-01-01", -62135596800000 ], + [ "2012-02-29", 1330473600000 ], + [ "2011-02-28", 1298851200000 ], + ]; + + var invalidData = + [ + "invaliddate", + "", + "275760-09-14", + "999-12-31", + "-001-12-31", + "0000-01-01", + "2011-02-29", + "1901-13-31", + "1901-12-32", + "1901-00-12", + "1901-01-00", + "1900-02-29", + ]; + + var element = document.createElement('input'); + element.type = "date"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsNumber, data[1], "valueAsNumber should return the " + + "timestamp representing this date"); + } + + for (let data of invalidData) { + element.value = data; + ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " + + "when the element value is not a valid date"); + } +} + +function checkDateSet() +{ + var testData = + [ + [ 1342051200000, "2012-07-12" ], + [ 0, "1970-01-01" ], + // Maximum valid date (limited by the ecma date object range). + [ 8640000000000000, "275760-09-13" ], + // Minimum valid date (limited by the input element minimum valid value) + [ -62135596800000, "0001-01-01" ], + [ 1330473600000, "2012-02-29" ], + [ 1298851200000, "2011-02-28" ], + // "Values must be truncated to valid dates" + [ 42.1234, "1970-01-01" ], + [ 123.123456789123, "1970-01-01" ], + [ 1e2, "1970-01-01" ], + [ 1E9, "1970-01-12" ], + [ 1e-1, "1970-01-01" ], + [ 2e10, "1970-08-20" ], + [ 1298851200010, "2011-02-28" ], + [ -1, "1969-12-31" ], + [ -86400000, "1969-12-31" ], + [ 86400000, "1970-01-02" ], + // Invalid numbers. + // Those are implicitly converted to numbers + [ "", "1970-01-01" ], + [ true, "1970-01-01" ], + [ false, "1970-01-01" ], + [ null, "1970-01-01" ], + // Those are converted to NaN, the corresponding date string is the empty string + [ "invaliddatenumber", "" ], + [ NaN, "" ], + [ undefined, "" ], + // Out of range, the corresponding date string is the empty string + [ -62135596800001, "" ], + // Infinity will keep the current value and throw (so we need to set a current value). + [ 1298851200010, "2011-02-28" ], + [ Infinity, "2011-02-28", true ], + [ -Infinity, "2011-02-28", true ], + ]; + + var element = document.createElement('input'); + element.type = "date"; + for (let data of testData) { + var caught = false; + + try { + element.valueAsNumber = data[0]; + is(element.value, data[1], "valueAsNumber should set the value to " + data[1]); + } catch(e) { + caught = true; + } + + if (data[2]) { + ok(caught, "valueAsNumber should have thrown"); + is(element.value, data[1], "the value should not have changed"); + } else { + ok(!caught, "valueAsNumber should not have thrown"); + } + } + +} + +function checkTimeGet() +{ + var tests = [ + // Some invalid values to begin. + { value: "", result: NaN }, + { value: "foobar", result: NaN }, + { value: "00:", result: NaN }, + { value: "24:00", result: NaN }, + { value: "00:99", result: NaN }, + { value: "00:00:", result: NaN }, + { value: "00:00:99", result: NaN }, + { value: "00:00:00:", result: NaN }, + { value: "00:00:00.", result: NaN }, + { value: "00:00:00.0000", result: NaN }, + // Some simple valid values. + { value: "00:00", result: 0 }, + { value: "00:01", result: 60000 }, + { value: "01:00", result: 3600000 }, + { value: "01:01", result: 3660000 }, + { value: "13:37", result: 49020000 }, + // Valid values including seconds. + { value: "00:00:01", result: 1000 }, + { value: "13:37:42", result: 49062000 }, + // Valid values including seconds fractions. + { value: "00:00:00.001", result: 1 }, + { value: "00:00:00.123", result: 123 }, + { value: "00:00:00.100", result: 100 }, + { value: "00:00:00.000", result: 0 }, + { value: "20:17:31.142", result: 73051142 }, + // Highest possible value. + { value: "23:59:59.999", result: 86399999 }, + // Some values with one or two digits for the fraction of seconds. + { value: "00:00:00.1", result: 100 }, + { value: "00:00:00.14", result: 140 }, + { value: "13:37:42.7", result: 49062700 }, + { value: "23:31:12.23", result: 84672230 }, + ]; + + var element = document.createElement('input'); + element.type = 'time'; + + for (let test of tests) { + element.value = test.value; + if (isNaN(test.result)) { + ok(isNaN(element.valueAsNumber), + "invalid value should have .valueAsNumber return NaN"); + } else { + is(element.valueAsNumber, test.result, + ".valueAsNumber should return " + test.result); + } + } +} + +function checkTimeSet() +{ + var tests = [ + // Some NaN values (should set to empty string). + { value: NaN, result: "" }, + { value: "foobar", result: "" }, + { value() {}, result: "" }, + // Inifinity (should throw). + { value: Infinity, throw: true }, + { value: -Infinity, throw: true }, + // "" converts to 0... JS is fun :) + { value: "", result: "00:00" }, + // Simple tests. + { value: 0, result: "00:00" }, + { value: 1, result: "00:00:00.001" }, + { value: 100, result: "00:00:00.100" }, + { value: 1000, result: "00:00:01" }, + { value: 60000, result: "00:01" }, + { value: 3600000, result: "01:00" }, + { value: 83622234, result: "23:13:42.234" }, + // Some edge cases. + { value: 86400000, result: "00:00" }, + { value: 86400001, result: "00:00:00.001" }, + { value: 170022234, result: "23:13:42.234" }, + { value: 432000000, result: "00:00" }, + { value: -1, result: "23:59:59.999" }, + { value: -86400000, result: "00:00" }, + { value: -86400001, result: "23:59:59.999" }, + { value: -56789, result: "23:59:03.211" }, + { value: 0.9, result: "00:00" }, + ]; + + var element = document.createElement('input'); + element.type = 'time'; + + for (let test of tests) { + try { + var caught = false; + element.valueAsNumber = test.value; + is(element.value, test.result, "value should return " + test.result); + } catch(e) { + caught = true; + } + + if (!test.throw) { + test.throw = false; + } + + is(caught, test.throw, "the test throwing status should be " + test.throw); + } +} + +function checkMonthGet() +{ + var validData = + [ + [ "2016-07", 558 ], + [ "1970-01", 0 ], + [ "1969-12", -1 ], + [ "0001-01", -23628 ], + [ "10000-12", 96371 ], + [ "275760-09", 3285488 ], + ]; + + var invalidData = + [ + "invalidmonth", + "0000-01", + "2000-00", + "2012-13", + // Out of range. + "275760-10", + ]; + + var element = document.createElement('input'); + element.type = "month"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsNumber, data[1], "valueAsNumber should return the " + + "integer value representing this month"); + } + + for (let data of invalidData) { + element.value = data; + ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " + + "when the element value is not a valid month"); + } +} + +function checkMonthSet() +{ + var testData = + [ + [ 558, "2016-07" ], + [ 0, "1970-01" ], + [ -1, "1969-12" ], + [ 96371, "10000-12" ], + [ 12, "1971-01" ], + [ -12, "1969-01" ], + // Maximum valid month (limited by the ecma date object range) + [ 3285488, "275760-09" ], + // Minimum valid month (limited by the input element minimum valid value) + [ -23628, "0001-01" ], + // "Values must be truncated to valid months" + [ 0.3, "1970-01" ], + [ -1.1, "1969-11" ], + [ 1e2, "1978-05" ], + [ 1e-1, "1970-01" ], + // Invalid numbers. + // Those are implicitly converted to numbers + [ "", "1970-01" ], + [ true, "1970-02" ], + [ false, "1970-01" ], + [ null, "1970-01" ], + // Those are converted to NaN, the corresponding month string is the empty string + [ "invalidmonth", "" ], + [ NaN, "" ], + [ undefined, "" ], + // Out of range, the corresponding month string is the empty string + [ -23629, "" ], + [ 3285489, "" ], + // Infinity will keep the current value and throw (so we need to set a current value) + [ 558, "2016-07" ], + [ Infinity, "2016-07", true ], + [ -Infinity, "2016-07", true ], + ]; + + var element = document.createElement('input'); + element.type = "month"; + for (let data of testData) { + var caught = false; + + try { + element.valueAsNumber = data[0]; + is(element.value, data[1], "valueAsNumber should set the value to " + data[1]); + } catch(e) { + caught = true; + } + + if (data[2]) { + ok(caught, "valueAsNumber should have thrown"); + is(element.value, data[1], "the value should not have changed"); + } else { + ok(!caught, "valueAsNumber should not have thrown"); + } + } +} + +function checkWeekGet() +{ + var validData = + [ + // Common years starting on different days of week. + [ "2007-W01", Date.UTC(2007, 0, 1) ], // Mon + [ "2013-W01", Date.UTC(2012, 11, 31) ], // Tue + [ "2014-W01", Date.UTC(2013, 11, 30) ], // Wed + [ "2015-W01", Date.UTC(2014, 11, 29) ], // Thu + [ "2010-W01", Date.UTC(2010, 0, 4) ], // Fri + [ "2011-W01", Date.UTC(2011, 0, 3) ], // Sat + [ "2017-W01", Date.UTC(2017, 0, 2) ], // Sun + // Common years ending on different days of week. + [ "2007-W52", Date.UTC(2007, 11, 24) ], // Mon + [ "2013-W52", Date.UTC(2013, 11, 23) ], // Tue + [ "2014-W52", Date.UTC(2014, 11, 22) ], // Wed + [ "2015-W53", Date.UTC(2015, 11, 28) ], // Thu + [ "2010-W52", Date.UTC(2010, 11, 27) ], // Fri + [ "2011-W52", Date.UTC(2011, 11, 26) ], // Sat + [ "2017-W52", Date.UTC(2017, 11, 25) ], // Sun + // Leap years starting on different days of week. + [ "1996-W01", Date.UTC(1996, 0, 1) ], // Mon + [ "2008-W01", Date.UTC(2007, 11, 31) ], // Tue + [ "2020-W01", Date.UTC(2019, 11, 30) ], // Wed + [ "2004-W01", Date.UTC(2003, 11, 29) ], // Thu + [ "2016-W01", Date.UTC(2016, 0, 4) ], // Fri + [ "2000-W01", Date.UTC(2000, 0, 3) ], // Sat + [ "2012-W01", Date.UTC(2012, 0, 2) ], // Sun + // Leap years ending on different days of week. + [ "2012-W52", Date.UTC(2012, 11, 24) ], // Mon + [ "2024-W52", Date.UTC(2024, 11, 23) ], // Tue + [ "1980-W52", Date.UTC(1980, 11, 22) ], // Wed + [ "1992-W53", Date.UTC(1992, 11, 28) ], // Thu + [ "2004-W53", Date.UTC(2004, 11, 27) ], // Fri + [ "1988-W52", Date.UTC(1988, 11, 26) ], // Sat + [ "2000-W52", Date.UTC(2000, 11, 25) ], // Sun + // Other normal cases. + [ "2015-W53", Date.UTC(2015, 11, 28) ], + [ "2016-W36", Date.UTC(2016, 8, 5) ], + [ "1970-W01", Date.UTC(1969, 11, 29) ], + [ "275760-W37", Date.UTC(275760, 8, 8) ], + ]; + + var invalidData = + [ + "invalidweek", + "0000-W01", + "2016-W00", + "2016-W53", + // Out of range. + "275760-W38", + ]; + + var element = document.createElement('input'); + element.type = "week"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsNumber, data[1], "valueAsNumber should return the " + + "integer value representing this week"); + } + + for (let data of invalidData) { + element.value = data; + ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " + + "when the element value is not a valid week"); + } +} + +function checkWeekSet() +{ + var testData = + [ + // Common years starting on different days of week. + [ Date.UTC(2007, 0, 1), "2007-W01" ], // Mon + [ Date.UTC(2013, 0, 1), "2013-W01" ], // Tue + [ Date.UTC(2014, 0, 1), "2014-W01" ], // Wed + [ Date.UTC(2015, 0, 1), "2015-W01" ], // Thu + [ Date.UTC(2010, 0, 1), "2009-W53" ], // Fri + [ Date.UTC(2011, 0, 1), "2010-W52" ], // Sat + [ Date.UTC(2017, 0, 1), "2016-W52" ], // Sun + // Common years ending on different days of week. + [ Date.UTC(2007, 11, 31), "2008-W01" ], // Mon + [ Date.UTC(2013, 11, 31), "2014-W01" ], // Tue + [ Date.UTC(2014, 11, 31), "2015-W01" ], // Wed + [ Date.UTC(2015, 11, 31), "2015-W53" ], // Thu + [ Date.UTC(2010, 11, 31), "2010-W52" ], // Fri + [ Date.UTC(2011, 11, 31), "2011-W52" ], // Sat + [ Date.UTC(2017, 11, 31), "2017-W52" ], // Sun + // Leap years starting on different days of week. + [ Date.UTC(1996, 0, 1), "1996-W01" ], // Mon + [ Date.UTC(2008, 0, 1), "2008-W01" ], // Tue + [ Date.UTC(2020, 0, 1), "2020-W01" ], // Wed + [ Date.UTC(2004, 0, 1), "2004-W01" ], // Thu + [ Date.UTC(2016, 0, 1), "2015-W53" ], // Fri + [ Date.UTC(2000, 0, 1), "1999-W52" ], // Sat + [ Date.UTC(2012, 0, 1), "2011-W52" ], // Sun + // Leap years ending on different days of week. + [ Date.UTC(2012, 11, 31), "2013-W01" ], // Mon + [ Date.UTC(2024, 11, 31), "2025-W01" ], // Tue + [ Date.UTC(1980, 11, 31), "1981-W01" ], // Wed + [ Date.UTC(1992, 11, 31), "1992-W53" ], // Thu + [ Date.UTC(2004, 11, 31), "2004-W53" ], // Fri + [ Date.UTC(1988, 11, 31), "1988-W52" ], // Sat + [ Date.UTC(2000, 11, 31), "2000-W52" ], // Sun + // Other normal cases. + [ Date.UTC(2008, 8, 26), "2008-W39" ], + [ Date.UTC(2016, 0, 4), "2016-W01" ], + [ Date.UTC(2016, 0, 10), "2016-W01" ], + [ Date.UTC(2016, 0, 11), "2016-W02" ], + // Maximum valid week (limited by the ecma date object range). + [ 8640000000000000, "275760-W37" ], + // Minimum valid week (limited by the input element minimum valid value) + [ -62135596800000, "0001-W01" ], + // "Values must be truncated to valid weeks" + [ 0.3, "1970-W01" ], + [ 1e-1, "1970-W01" ], + [ -1.1, "1970-W01" ], + [ -345600000, "1969-W52" ], + // Invalid numbers. + // Those are implicitly converted to numbers + [ "", "1970-W01" ], + [ true, "1970-W01" ], + [ false, "1970-W01" ], + [ null, "1970-W01" ], + // Those are converted to NaN, the corresponding week string is the empty string + [ "invalidweek", "" ], + [ NaN, "" ], + [ undefined, "" ], + // Infinity will keep the current value and throw (so we need to set a current value). + [ Date.UTC(2016, 8, 8), "2016-W36" ], + [ Infinity, "2016-W36", true ], + [ -Infinity, "2016-W36", true ], + ]; + + var element = document.createElement('input'); + element.type = "week"; + for (let data of testData) { + var caught = false; + + try { + element.valueAsNumber = data[0]; + is(element.value, data[1], "valueAsNumber should set the value to " + + data[1]); + } catch(e) { + caught = true; + } + + if (data[2]) { + ok(caught, "valueAsNumber should have thrown"); + is(element.value, data[1], "the value should not have changed"); + } else { + ok(!caught, "valueAsNumber should not have thrown"); + } + } +} + +function checkDatetimeLocalGet() { + var validData = + [ + // Simple cases. + [ "2016-12-20T09:58", Date.UTC(2016, 11, 20, 9, 58) ], + [ "2016-12-20T09:58:30", Date.UTC(2016, 11, 20, 9, 58, 30) ], + [ "2016-12-20T09:58:30.123", Date.UTC(2016, 11, 20, 9, 58, 30, 123) ], + [ "2017-01-01T10:00", Date.UTC(2017, 0, 1, 10, 0, 0) ], + [ "1969-12-31T12:00:00", Date.UTC(1969, 11, 31, 12, 0, 0) ], + [ "1970-01-01T00:00", 0 ], + // Leap years. + [ "1804-02-29 12:34", Date.UTC(1804, 1, 29, 12, 34, 0) ], + [ "2016-02-29T12:34", Date.UTC(2016, 1, 29, 12, 34, 0) ], + [ "2016-12-31T12:34:56", Date.UTC(2016, 11, 31, 12, 34, 56) ], + [ "2016-01-01T12:34:56.789", Date.UTC(2016, 0, 1, 12, 34, 56, 789) ], + [ "2017-01-01 12:34:56.789", Date.UTC(2017, 0, 1, 12, 34, 56, 789) ], + // Maximum valid datetime-local (limited by the ecma date object range). + [ "275760-09-13T00:00", 8640000000000000 ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ "0001-01-01T00:00", -62135596800000 ], + ]; + + var invalidData = + [ + "invaliddatetime-local", + "0000-01-01T00:00", + "2016-12-25T00:00Z", + "2015-02-29T12:34", + "1-1-1T12:00", + // Out of range. + "275760-09-13T12:00", + ]; + + var element = document.createElement('input'); + element.type = "datetime-local"; + for (let data of validData) { + element.value = data[0]; + is(element.valueAsNumber, data[1], "valueAsNumber should return the " + + "integer value representing this datetime-local"); + } + + for (let data of invalidData) { + element.value = data; + ok(isNaN(element.valueAsNumber), "valueAsNumber should return NaN " + + "when the element value is not a valid datetime-local"); + } +} + +function checkDatetimeLocalSet() +{ + var testData = + [ + // Simple cases. + [ Date.UTC(2016, 11, 20, 9, 58, 0), "2016-12-20T09:58", ], + [ Date.UTC(2016, 11, 20, 9, 58, 30), "2016-12-20T09:58:30" ], + [ Date.UTC(2016, 11, 20, 9, 58, 30, 123), "2016-12-20T09:58:30.123" ], + [ Date.UTC(2017, 0, 1, 10, 0, 0), "2017-01-01T10:00" ], + [ Date.UTC(1969, 11, 31, 12, 0, 0), "1969-12-31T12:00" ], + [ 0, "1970-01-01T00:00" ], + // Maximum valid week (limited by the ecma date object range). + [ 8640000000000000, "275760-09-13T00:00" ], + // Minimum valid datetime-local (limited by the input element minimum valid value). + [ -62135596800000, "0001-01-01T00:00" ], + // Leap years. + [ Date.UTC(1804, 1, 29, 12, 34, 0), "1804-02-29T12:34" ], + [ Date.UTC(2016, 1, 29, 12, 34, 0), "2016-02-29T12:34" ], + [ Date.UTC(2016, 11, 31, 12, 34, 56), "2016-12-31T12:34:56" ], + [ Date.UTC(2016, 0, 1, 12, 34, 56, 789), "2016-01-01T12:34:56.789" ], + [ Date.UTC(2017, 0, 1, 12, 34, 56, 789), "2017-01-01T12:34:56.789" ], + // "Values must be truncated to valid datetime-local" + [ 0.3, "1970-01-01T00:00" ], + [ 1e-1, "1970-01-01T00:00" ], + [ -1 , "1969-12-31T23:59:59.999" ], + [ -345600000, "1969-12-28T00:00" ], + // Invalid numbers. + // Those are implicitly converted to numbers + [ "", "1970-01-01T00:00" ], + [ true, "1970-01-01T00:00:00.001" ], + [ false, "1970-01-01T00:00" ], + [ null, "1970-01-01T00:00" ], + // Those are converted to NaN, the corresponding week string is the empty string + [ "invaliddatetime-local", "" ], + [ NaN, "" ], + [ undefined, "" ], + // Infinity will keep the current value and throw (so we need to set a current value). + [ Date.UTC(2016, 11, 27, 15, 10, 0), "2016-12-27T15:10" ], + [ Infinity, "2016-12-27T15:10", true ], + [ -Infinity, "2016-12-27T15:10", true ], + ]; + + var element = document.createElement('input'); + element.type = "datetime-local"; + for (let data of testData) { + var caught = false; + + try { + element.valueAsNumber = data[0]; + is(element.value, data[1], "valueAsNumber should set the value to " + + data[1]); + } catch(e) { + caught = true; + } + + if (data[2]) { + ok(caught, "valueAsNumber should have thrown"); + is(element.value, data[1], "the value should not have changed"); + } else { + ok(!caught, "valueAsNumber should not have thrown"); + } + } +} + +checkAvailability(); + +// <input type='number'> test +checkNumberGet(); +checkNumberSet(); + +// <input type='range'> test +checkRangeGet(); +checkRangeSet(); + +// <input type='date'> test +checkDateGet(); +checkDateSet(); + +// <input type='time'> test +checkTimeGet(); +checkTimeSet(); + +// <input type='month'> test +checkMonthGet(); +checkMonthSet(); + +// <input type='week'> test +checkWeekGet(); +checkWeekSet(); + +// <input type='datetime-local'> test +checkDatetimeLocalGet(); +checkDatetimeLocalSet(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/forms/without_selectionchange/mochitest.toml b/dom/html/test/forms/without_selectionchange/mochitest.toml new file mode 100644 index 0000000000..8f019d8d80 --- /dev/null +++ b/dom/html/test/forms/without_selectionchange/mochitest.toml @@ -0,0 +1,5 @@ +[DEFAULT] +prefs = ["dom.select_events.textcontrols.enabled=false"] + +["test_select.html"] + diff --git a/dom/html/test/forms/without_selectionchange/test_select.html b/dom/html/test/forms/without_selectionchange/test_select.html new file mode 100644 index 0000000000..3d11611b1b --- /dev/null +++ b/dom/html/test/forms/without_selectionchange/test_select.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<meta charset="utf-8"> +<title>Test for Bug 1717435</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +</head> + +<textarea id="textarea">foo</textarea> +<script> + SimpleTest.waitForExplicitFinish(); + + textarea.addEventListener("select", ev => { + ok(true, "A select event must fire regardless of dom.select_events.textcontrols.enabled"); + SimpleTest.finish(); + }); + + textarea.focus(); + textarea.select(); + is(textarea.selectionStart, 0, "selectionStart") + is(textarea.selectionEnd, 3, "selectionEnd") +</script> diff --git a/dom/html/test/head.js b/dom/html/test/head.js new file mode 100644 index 0000000000..1e3b435d0c --- /dev/null +++ b/dom/html/test/head.js @@ -0,0 +1,65 @@ +function pushPrefs(...aPrefs) { + return SpecialPowers.pushPrefEnv({ set: aPrefs }); +} + +function promiseWaitForEvent( + object, + eventName, + capturing = false, + chrome = false +) { + return new Promise(resolve => { + function listener(event) { + info("Saw " + eventName); + object.removeEventListener(eventName, listener, capturing, chrome); + resolve(event); + } + + info("Waiting for " + eventName); + object.addEventListener(eventName, listener, capturing, chrome); + }); +} + +/** + * Waits for the next load to complete in any browser or the given browser. + * If a <tabbrowser> is given it waits for a load in any of its browsers. + * + * @return promise + */ +function waitForDocLoadComplete(aBrowser = gBrowser) { + return new Promise(resolve => { + let listener = { + onStateChange(webProgress, req, flags, status) { + let docStop = + Ci.nsIWebProgressListener.STATE_IS_NETWORK | + Ci.nsIWebProgressListener.STATE_STOP; + info( + "Saw state " + + flags.toString(16) + + " and status " + + status.toString(16) + ); + // When a load needs to be retargetted to a new process it is cancelled + // with NS_BINDING_ABORTED so ignore that case + if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) { + aBrowser.removeProgressListener(this); + waitForDocLoadComplete.listeners.delete(this); + let chan = req.QueryInterface(Ci.nsIChannel); + info("Browser loaded " + chan.originalURI.spec); + resolve(); + } + }, + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + }; + aBrowser.addProgressListener(listener); + waitForDocLoadComplete.listeners.add(listener); + info("Waiting for browser load"); + }); +} +// Keep a set of progress listeners for waitForDocLoadComplete() to make sure +// they're not GC'ed before we saw the page load. +waitForDocLoadComplete.listeners = new Set(); +registerCleanupFunction(() => waitForDocLoadComplete.listeners.clear()); diff --git a/dom/html/test/image-allow-credentials.png b/dom/html/test/image-allow-credentials.png Binary files differnew file mode 100644 index 0000000000..df24ac6d34 --- /dev/null +++ b/dom/html/test/image-allow-credentials.png diff --git a/dom/html/test/image-allow-credentials.png^headers^ b/dom/html/test/image-allow-credentials.png^headers^ new file mode 100644 index 0000000000..a03f99a9c0 --- /dev/null +++ b/dom/html/test/image-allow-credentials.png^headers^ @@ -0,0 +1,2 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 +Access-Control-Allow-Credentials: true diff --git a/dom/html/test/image.png b/dom/html/test/image.png Binary files differnew file mode 100644 index 0000000000..d26878c9f2 --- /dev/null +++ b/dom/html/test/image.png diff --git a/dom/html/test/image_yellow.png b/dom/html/test/image_yellow.png Binary files differnew file mode 100644 index 0000000000..51e8aaf38c --- /dev/null +++ b/dom/html/test/image_yellow.png diff --git a/dom/html/test/mochitest.toml b/dom/html/test/mochitest.toml new file mode 100644 index 0000000000..d1dd78705b --- /dev/null +++ b/dom/html/test/mochitest.toml @@ -0,0 +1,990 @@ +[DEFAULT] +prefs = ["gfx.font_loader.delay=0"] +support-files = [ + "347174transform.xsl", + "347174transformable.xml", + "allowMedia.sjs", + "bug100533_iframe.html", + "bug100533_load.html", + "bug196523-subframe.html", + "bug199692-nested-d2.html", + "bug199692-nested.html", + "bug199692-popup.html", + "bug199692-scrolled.html", + "bug242709_iframe.html", + "bug242709_load.html", + "bug277724_iframe1.html", + "bug277724_iframe2.xhtml", + "bug277890_iframe.html", + "bug277890_load.html", + "bug340800_iframe.txt", + "bug369370-popup.png", + "bug372098-link-target.html", + "bug441930_iframe.html", + "bug445004-inner.html", + "bug445004-inner.js", + "bug445004-outer-abs.html", + "bug445004-outer-rel.html", + "bug445004-outer-write.html", + "bug446483-iframe.html", + "bug448564-echo.sjs", + "bug448564-iframe-1.html", + "bug448564-iframe-2.html", + "bug448564-iframe-3.html", + "bug448564-submit.js", + "bug499092.html", + "bug499092.xml", + "bug514856_iframe.html", + "bug1260704_iframe.html", + "bug1260704_iframe_empty.html", + "bug1292522_iframe.html", + "bug1292522_page.html", + "bug1315146-iframe.html", + "bug1315146-main.html", + "dummy_page.html", + "test_non-ascii-cookie.html^headers^", + "file_bug209275_1.html", + "file_bug209275_2.html", + "file_bug209275_3.html", + "file_bug297761.html", + "file_bug417760.png", + "file_bug893537.html", + "file_bug1260704.png", + "file_formSubmission_img.jpg", + "file_formSubmission_text.txt", + "file_iframe_sandbox_a_if1.html", + "file_iframe_sandbox_a_if10.html", + "file_iframe_sandbox_a_if11.html", + "file_iframe_sandbox_a_if12.html", + "file_iframe_sandbox_a_if13.html", + "file_iframe_sandbox_a_if14.html", + "file_iframe_sandbox_a_if15.html", + "file_iframe_sandbox_a_if16.html", + "file_iframe_sandbox_a_if17.html", + "file_iframe_sandbox_a_if18.html", + "file_iframe_sandbox_a_if19.html", + "file_iframe_sandbox_a_if2.html", + "file_iframe_sandbox_a_if3.html", + "file_iframe_sandbox_a_if4.html", + "file_iframe_sandbox_a_if5.html", + "file_iframe_sandbox_a_if6.html", + "file_iframe_sandbox_a_if7.html", + "file_iframe_sandbox_a_if8.html", + "file_iframe_sandbox_a_if9.html", + "file_iframe_sandbox_b_if1.html", + "file_iframe_sandbox_b_if2.html", + "file_iframe_sandbox_b_if3.html", + "file_iframe_sandbox_c_if1.html", + "file_iframe_sandbox_c_if2.html", + "file_iframe_sandbox_c_if3.html", + "file_iframe_sandbox_c_if4.html", + "file_iframe_sandbox_c_if5.html", + "file_iframe_sandbox_c_if6.html", + "file_iframe_sandbox_c_if7.html", + "file_iframe_sandbox_c_if8.html", + "file_iframe_sandbox_c_if9.html", + "file_iframe_sandbox_close.html", + "file_iframe_sandbox_d_if1.html", + "file_iframe_sandbox_d_if10.html", + "file_iframe_sandbox_d_if11.html", + "file_iframe_sandbox_d_if12.html", + "file_iframe_sandbox_d_if13.html", + "file_iframe_sandbox_d_if14.html", + "file_iframe_sandbox_d_if15.html", + "file_iframe_sandbox_d_if16.html", + "file_iframe_sandbox_d_if17.html", + "file_iframe_sandbox_d_if18.html", + "file_iframe_sandbox_d_if19.html", + "file_iframe_sandbox_d_if2.html", + "file_iframe_sandbox_d_if20.html", + "file_iframe_sandbox_d_if21.html", + "file_iframe_sandbox_d_if22.html", + "file_iframe_sandbox_d_if23.html", + "file_iframe_sandbox_d_if3.html", + "file_iframe_sandbox_d_if4.html", + "file_iframe_sandbox_d_if5.html", + "file_iframe_sandbox_d_if6.html", + "file_iframe_sandbox_d_if7.html", + "file_iframe_sandbox_d_if8.html", + "file_iframe_sandbox_d_if9.html", + "file_iframe_sandbox_e_if1.html", + "file_iframe_sandbox_e_if10.html", + "file_iframe_sandbox_e_if11.html", + "file_iframe_sandbox_e_if12.html", + "file_iframe_sandbox_e_if13.html", + "file_iframe_sandbox_e_if14.html", + "file_iframe_sandbox_e_if15.html", + "file_iframe_sandbox_e_if16.html", + "file_iframe_sandbox_e_if2.html", + "file_iframe_sandbox_e_if3.html", + "file_iframe_sandbox_e_if4.html", + "file_iframe_sandbox_e_if5.html", + "file_iframe_sandbox_e_if6.html", + "file_iframe_sandbox_e_if7.html", + "file_iframe_sandbox_e_if8.html", + "file_iframe_sandbox_e_if9.html", + "file_iframe_sandbox_fail.js", + "file_iframe_sandbox_form_fail.html", + "file_iframe_sandbox_form_pass.html", + "file_iframe_sandbox_g_if1.html", + "file_iframe_sandbox_h_if1.html", + "file_iframe_sandbox_k_if1.html", + "file_iframe_sandbox_k_if2.html", + "file_iframe_sandbox_k_if3.html", + "file_iframe_sandbox_k_if4.html", + "file_iframe_sandbox_k_if5.html", + "file_iframe_sandbox_k_if6.html", + "file_iframe_sandbox_k_if7.html", + "file_iframe_sandbox_k_if8.html", + "file_iframe_sandbox_k_if9.html", + "file_iframe_sandbox_navigation_fail.html", + "file_iframe_sandbox_navigation_pass.html", + "file_iframe_sandbox_navigation_start.html", + "file_iframe_sandbox_open_window_fail.html", + "file_iframe_sandbox_open_window_pass.html", + "file_iframe_sandbox_pass.js", + "file_iframe_sandbox_redirect.html", + "file_iframe_sandbox_redirect.html^headers^", + "file_iframe_sandbox_redirect_target.html", + "file_iframe_sandbox_refresh.html", + "file_iframe_sandbox_refresh.html^headers^", + "file_iframe_sandbox_srcdoc_allow_scripts.html", + "file_iframe_sandbox_srcdoc_no_allow_scripts.html", + "file_iframe_sandbox_top_navigation_fail.html", + "file_iframe_sandbox_top_navigation_pass.html", + "file_iframe_sandbox_window_form_fail.html", + "file_iframe_sandbox_window_form_pass.html", + "file_iframe_sandbox_window_navigation_fail.html", + "file_iframe_sandbox_window_navigation_pass.html", + "file_iframe_sandbox_window_top_navigation_pass.html", + "file_iframe_sandbox_window_top_navigation_fail.html", + "file_iframe_sandbox_worker.js", + "file_srcdoc-2.html", + "file_srcdoc.html", + "file_srcdoc_iframe3.html", + "file_window_open_close_outer.html", + "file_window_open_close_inner.html", + "formSubmission_chrome.js", + "form_submit_server.sjs", + "formData_worker.js", + "formData_test.js", + "image.png", + "image-allow-credentials.png", + "image-allow-credentials.png^headers^", + "nnc_lockup.gif", + "reflect.js", + "simpleFileOpener.js", + "file_bug1166138_1x.png", + "file_bug1166138_2x.png", + "file_bug1166138_def.png", + "script_fakepath.js", + "sw_formSubmission.js", + "object_bug287465_o1.html", + "object_bug287465_o2.html", + "object_bug556645.html", + "file.webm", + "!/gfx/layers/apz/test/mochitest/apz_test_utils.js", +] + +["test_a_text.html"] + +["test_allowMedia.html"] +skip-if = [ + "verify && (os == 'linux' || os == 'win')", + "!debug && os == 'mac' && bits == 64", + "debug && os == 'win'", + "debug && os == 'linux' && os_version == '18.04'", #Bug 1434744 +] + +["test_anchor_href_cache_invalidation.html"] + +["test_base_attributes_reflection.html"] + +["test_bug589.html"] + +["test_bug691.html"] + +["test_bug694.html"] + +["test_bug696.html"] + +["test_bug1297.html"] + +["test_bug1366.html"] + +["test_bug1400.html"] + +["test_bug1682.html"] + +["test_bug1823.html"] + +["test_bug2082.html"] + +["test_bug3348.html"] + +["test_bug6296.html"] + +["test_bug24958.html"] + +["test_bug57600.html"] + +["test_bug95530.html"] + +["test_bug100533.html"] + +["test_bug109445.html"] + +["test_bug109445.xhtml"] + +["test_bug143220.html"] + +["test_bug182279.html"] + +["test_bug196523.html"] +skip-if = [ + "http3", + "http2", +] + +["test_bug199692.html"] + +["test_bug209275.xhtml"] +skip-if = ["os == 'android'"] #TIMED_OUT + +["test_bug237071.html"] + +["test_bug242709.html"] + +["test_bug255820.html"] + +["test_bug259332.html"] + +["test_bug274626.html"] + +["test_bug277724.html"] + +["test_bug277890.html"] + +["test_bug287465.html"] + +["test_bug295561.html"] + +["test_bug297761.html"] + +["test_bug300691-1.html"] + +["test_bug300691-2.html"] + +["test_bug300691-3.xhtml"] + +["test_bug311681.html"] + +["test_bug311681.xhtml"] + +["test_bug324378.html"] + +["test_bug330705-1.html"] + +["test_bug332246.html"] + +["test_bug332848.xhtml"] + +["test_bug332893-1.html"] + +["test_bug332893-2.html"] + +["test_bug332893-3.html"] + +["test_bug332893-4.html"] + +["test_bug332893-5.html"] + +["test_bug332893-6.html"] + +["test_bug332893-7.html"] + +["test_bug340017.xhtml"] + +["test_bug340800.html"] + +["test_bug347174.html"] + +["test_bug347174_write.html"] + +["test_bug347174_xsl.html"] + +["test_bug347174_xslp.html"] + +["test_bug353415-1.html"] + +["test_bug353415-2.html"] + +["test_bug359657.html"] + +["test_bug369370.html"] +skip-if = [ + "os == 'android'", + "os == 'linux'", # disabled on linux bug 1258103 +] + +["test_bug371375.html"] + +["test_bug372098.html"] + +["test_bug373589.html"] + +["test_bug375003-1.html"] + +["test_bug375003-2.html"] + +["test_bug377624.html"] + +["test_bug380383.html"] + +["test_bug383383.html"] + +["test_bug383383_2.xhtml"] + +["test_bug384419.html"] + +["test_bug386496.html"] + +["test_bug386728.html"] + +["test_bug386996.html"] + +["test_bug388558.html"] + +["test_bug388746.html"] + +["test_bug388794.html"] + +["test_bug389797.html"] + +["test_bug390975.html"] + +["test_bug391994.html"] + +["test_bug394700.html"] + +["test_bug395107.html"] + +["test_bug401160.xhtml"] + +["test_bug402680.html"] + +["test_bug403868.html"] + +["test_bug403868.xhtml"] + +["test_bug405242.html"] + +["test_bug406596.html"] + +["test_bug417760.html"] + +["test_bug421640.html"] + +["test_bug424698.html"] + +["test_bug428135.xhtml"] + +["test_bug430351.html"] +skip-if = ["os == 'android'"] # Bug 1525959 + +["test_bug435128.html"] +skip-if = ["true"] # Disabled for timeouts. + +["test_bug441930.html"] + +["test_bug442801.html"] + +["test_bug445004.html"] +skip-if = ["true"] # Disabled permanently (bug 559932). + +["test_bug446483.html"] + +["test_bug448166.html"] + +["test_bug448564.html"] + +["test_bug456229.html"] + +["test_bug458037.xhtml"] +allow_xul_xbl = true +skip-if = [ + "http3", + "http2", +] + +["test_bug460568.html"] + +["test_bug463104.html"] + +["test_bug478251.html"] + +["test_bug481335.xhtml"] +skip-if = ["os == 'android'"] #TIMED_OUT + +["test_bug481440.html"] + +["test_bug481647.html"] + +["test_bug482659.html"] + +["test_bug486741.html"] + +["test_bug489532.html"] + +["test_bug497242.xhtml"] + +["test_bug499092.html"] + +["test_bug500885.html"] + +["test_bug512367.html"] + +["test_bug514856.html"] + +["test_bug518122.html"] + +["test_bug519987.html"] + +["test_bug523771.html"] + +["test_bug529819.html"] + +["test_bug529859.html"] + +["test_bug535043.html"] + +["test_bug536891.html"] + +["test_bug536895.html"] + +["test_bug546995.html"] + +["test_bug547850.html"] + +["test_bug551846.html"] + +["test_bug555567.html"] + +["test_bug556645.html"] + +["test_bug557087-1.html"] + +["test_bug557087-2.html"] + +["test_bug557087-3.html"] + +["test_bug557087-4.html"] + +["test_bug557087-5.html"] + +["test_bug557087-6.html"] + +["test_bug557620.html"] + +["test_bug558788-1.html"] + +["test_bug558788-2.html"] + +["test_bug560112.html"] + +["test_bug561634.html"] + +["test_bug561636.html"] + +["test_bug561640.html"] + +["test_bug564001.html"] + +["test_bug566046.html"] + +["test_bug567938-1.html"] + +["test_bug567938-2.html"] + +["test_bug567938-3.html"] + +["test_bug567938-4.html"] + +["test_bug569955.html"] + +["test_bug573969.html"] + +["test_bug579079.html"] + +["test_bug582412-1.html"] + +["test_bug582412-2.html"] + +["test_bug583514.html"] + +["test_bug583533.html"] + +["test_bug586763.html"] + +["test_bug586786.html"] + +["test_bug587469.html"] + +["test_bug590353-1.html"] + +["test_bug590353-2.html"] + +["test_bug590363.html"] + +["test_bug592802.html"] + +["test_bug593689.html"] + +["test_bug595429.html"] + +["test_bug595447.html"] + +["test_bug595449.html"] + +["test_bug596350.html"] + +["test_bug596511.html"] + +["test_bug598643.html"] + +["test_bug598833-1.html"] + +["test_bug600155.html"] + +["test_bug601030.html"] + +["test_bug605124-1.html"] + +["test_bug605124-2.html"] + +["test_bug605125-1.html"] + +["test_bug605125-2.html"] + +["test_bug606817.html"] + +["test_bug607145.html"] +skip-if = [ + "http3", + "http2", +] + +["test_bug610212.html"] + +["test_bug610687.html"] + +["test_bug611189.html"] + +["test_bug612730.html"] +skip-if = ["os == 'android'"] # form control not selected/checked with synthesizeMouse + +["test_bug613019.html"] + +["test_bug613113.html"] + +["test_bug613722.html"] + +["test_bug613979.html"] + +["test_bug615595.html"] +fail-if = ["xorigin"] + +["test_bug615833.html"] +skip-if = [ + "os == 'android'", + "os == 'mac'", #TIMED_OUT # form control not selected/checked with synthesizeMouse, osx(bug 1275664) +] + +["test_bug618948.html"] + +["test_bug619278.html"] + +["test_bug622597.html"] + +["test_bug623291.html"] + +["test_bug629801.html"] + +["test_bug633058.html"] + +["test_bug636336.html"] + +["test_bug641219.html"] + +["test_bug643051.html"] + +["test_bug646157.html"] + +["test_bug649134.html"] +# This extra subdirectory is needed due to the nature of this test. +# With the bug, the test loads the base URL of the bug649134/file_*.sjs +# files, and the mochitest server responds with the contents of index.html if +# it exists in that case, which we use to detect failure. +# We cannot have index.html in this directory because it would prevent +# running the tests here. +support-files = [ + "bug649134/file_bug649134-1.sjs", + "bug649134/file_bug649134-2.sjs", + "bug649134/index.html", +] +skip-if = [ + "http3", + "http2", +] + +["test_bug651956.html"] + +["test_bug658746.html"] + +["test_bug659596.html"] + +["test_bug659743.xml"] + +["test_bug660663.html"] + +["test_bug660959-1.html"] + +["test_bug660959-2.html"] + +["test_bug660959-3.html"] + +["test_bug666200.html"] + +["test_bug666666.html"] + +["test_bug669012.html"] + +["test_bug674558.html"] + +["test_bug674927.html"] + +["test_bug677495-1.html"] + +["test_bug677495.html"] + +["test_bug677658.html"] + +["test_bug682886.html"] + +["test_bug694503.html"] +skip-if = ["os == 'android'"] # Bug 1525959 + +["test_bug717819.html"] + +["test_bug741266.html"] +skip-if = [ + "os == 'android'", # Android: needs control of popup window size + "display == 'wayland' && os_version == '22.04' && debug", # Bug 1856975 +] + +["test_bug742030.html"] + +["test_bug742549.html"] + +["test_bug745685.html"] + +["test_bug763626.html"] + +["test_bug765780.html"] + +["test_bug780993.html"] + +["test_bug787134.html"] + +["test_bug797113.html"] + +["test_bug803677.html"] + +["test_bug821307.html"] + +["test_bug827126.html"] + +["test_bug838582.html"] + +["test_bug839371.html"] + +["test_bug839913.html"] + +["test_bug841466.html"] + +["test_bug845057.html"] + +["test_bug869040.html"] + +["test_bug870787.html"] + +["test_bug871161.html"] +support-files = [ + "file_bug871161-1.html", + "file_bug871161-2.html", +] +skip-if = [ + "http3", + "http2", +] + +["test_bug874758.html"] + +["test_bug879319.html"] + +["test_bug885024.html"] + +["test_bug893537.html"] + +["test_bug969346.html"] + +["test_bug982039.html"] + +["test_bug1003539.html"] + +["test_bug1013316.html"] + +["test_bug1045270.html"] + +["test_bug1089326.html"] + +["test_bug1146116.html"] + +["test_bug1166138.html"] + +["test_bug1203668.html"] + +["test_bug1230665.html"] + +["test_bug1250401.html"] + +["test_bug1260664.html"] + +["test_bug1260704.html"] +skip-if = [ + "http3", + "http2", +] + +["test_bug1261673.html"] +skip-if = [ + "os == 'android'", + "os == 'mac'", +] + +["test_bug1261674-1.html"] +skip-if = [ + "os == 'android'", + "os == 'mac'", +] + +["test_bug1261674-2.html"] +skip-if = ["os == 'mac'"] + +["test_bug1264157.html"] + +["test_bug1279218.html"] + +["test_bug1287321.html"] + +["test_bug1292522_same_domain_with_different_port_number.html"] +skip-if = [ + "http3", + "http2", +] + +["test_bug1295719_event_sequence_for_arrow_keys.html"] +skip-if = ["os == 'android'"] # up/down arrow keys not supported on android + +["test_bug1295719_event_sequence_for_number_keys.html"] + +["test_bug1310865.html"] + +["test_bug1315146.html"] +skip-if = [ + "http3", + "http2", +] + +["test_bug1322678.html"] +skip-if = ["os == 'android'"] + +["test_bug1323815.html"] + +["test_bug1472426.html"] + +["test_bug1785739.html"] + +["test_change_crossorigin.html"] +skip-if = [ + "http3", + "http2", +] + +["test_checked.html"] + +["test_dir_attributes_reflection.html"] + +["test_dl_attributes_reflection.html"] + +["test_document-element-inserted.html"] + +["test_documentAll.html"] + +["test_element_prototype.html"] + +["test_embed_attributes_reflection.html"] + +["test_fakepath.html"] + +["test_filepicker_default_directory.html"] + +["test_focusshift_button.html"] + +["test_form-parsing.html"] + +["test_formData.html"] + +["test_formSubmission.html"] +skip-if = ["os == 'android'"] #TIMED_OUT + +["test_formSubmission2.html"] +skip-if = ["os == 'android'"] + +["test_formelements.html"] + +["test_fragment_form_pointer.html"] + +["test_frame_count_with_synthetic_doc.html"] + +["test_getElementsByName_after_mutation.html"] + +["test_hidden.html"] + +["test_html_attributes_reflection.html"] + +["test_htmlcollection.html"] + +["test_iframe_sandbox_general.html"] +tags = "openwindow" +skip-if = [ + "http3", + "http2", +] + +["test_iframe_sandbox_inheritance.html"] +tags = "openwindow" + +["test_iframe_sandbox_navigation.html"] +tags = "openwindow" + +["test_iframe_sandbox_navigation2.html"] +tags = "openwindow" + +["test_iframe_sandbox_popups.html"] +tags = "openwindow" + +["test_iframe_sandbox_popups_inheritance.html"] +tags = "openwindow" + +["test_iframe_sandbox_redirect.html"] + +["test_iframe_sandbox_refresh.html"] + +["test_iframe_sandbox_same_origin.html"] + +["test_iframe_sandbox_workers.html"] + +["test_imageSrcSet.html"] + +["test_image_clone_load.html"] +skip-if = [ + "http3", + "http2", +] + +["test_img_attributes_reflection.html"] + +["test_input_file_cancel_event.html"] + +["test_input_files_not_nsIFile.html"] + +["test_input_lastInteractiveValue.html"] + +["test_inputmode.html"] + +["test_li_attributes_reflection.html"] + +["test_link_attributes_reflection.html"] + +["test_link_sizes.html"] + +["test_map_attributes_reflection.html"] + +["test_meta_attributes_reflection.html"] + +["test_mod_attributes_reflection.html"] + +["test_multipleFilePicker.html"] + +["test_named_options.html"] + +["test_nested_invalid_fieldsets.html"] + +["test_nestediframe.html"] + +["test_non-ascii-cookie.html"] +support-files = ["file_cookiemanager.js"] +skip-if = [ + "xorigin", + "http3", + "http2", +] + +["test_object_attributes_reflection.html"] + +["test_ol_attributes_reflection.html"] + +["test_option_defaultSelected.html"] + +["test_option_selected_state.html"] + +["test_param_attributes_reflection.html"] + +["test_q_attributes_reflection.html"] + +["test_restore_from_parser_fragment.html"] + +["test_rowscollection.html"] + +["test_script_module.html"] +support-files = ["file_script_module.html"] + +["test_set_input_files.html"] + +["test_srcdoc-2.html"] + +["test_srcdoc.html"] + +["test_style_attributes_reflection.html"] + +["test_track.html"] + +["test_ul_attributes_reflection.html"] + +["test_viewport_resize.html"] + +["test_window_open_close.html"] +tags = "openwindow" +skip-if = [ + "os == 'android' && debug", + "os == 'linux'", + "os == 'win' && debug && bits == 64", # Bug 1533759 +] + +["test_window_open_from_closing.html"] +skip-if = ["os == 'android'"] # test does not function on android due to aggressive background tab freezing +support-files = [ + "file_window_close_and_open.html", + "file_broadcast_load.html", +] diff --git a/dom/html/test/nnc_lockup.gif b/dom/html/test/nnc_lockup.gif Binary files differnew file mode 100644 index 0000000000..f746bb71d9 --- /dev/null +++ b/dom/html/test/nnc_lockup.gif diff --git a/dom/html/test/object_bug287465_o1.html b/dom/html/test/object_bug287465_o1.html new file mode 100644 index 0000000000..0a65a7f9e1 --- /dev/null +++ b/dom/html/test/object_bug287465_o1.html @@ -0,0 +1 @@ +<svg xmlns='http://www.w3.org/2000/svg'></svg> diff --git a/dom/html/test/object_bug287465_o2.html b/dom/html/test/object_bug287465_o2.html new file mode 100644 index 0000000000..18ecdcb795 --- /dev/null +++ b/dom/html/test/object_bug287465_o2.html @@ -0,0 +1 @@ +<html></html> diff --git a/dom/html/test/object_bug556645.html b/dom/html/test/object_bug556645.html new file mode 100644 index 0000000000..773837502a --- /dev/null +++ b/dom/html/test/object_bug556645.html @@ -0,0 +1 @@ +<body><button>Child</button></body> diff --git a/dom/html/test/post_action_page.html b/dom/html/test/post_action_page.html new file mode 100644 index 0000000000..ba6ae514f2 --- /dev/null +++ b/dom/html/test/post_action_page.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"/> + <title>Submission Flush Test Post Action Page</title> + </head> + <body> + <h1>Post Action Page</h1> + </body> +</html> diff --git a/dom/html/test/reflect.js b/dom/html/test/reflect.js new file mode 100644 index 0000000000..44f73ae4a2 --- /dev/null +++ b/dom/html/test/reflect.js @@ -0,0 +1,1078 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * reflect.js is a collection of methods to test HTML attribute reflection. + * Each of attribute is reflected differently, depending on various parameters, + * see: + * http://www.whatwg.org/html/#reflecting-content-attributes-in-idl-attributes + * + * Do not forget to add these line at the beginning of each new reflect* method: + * ok(attr in element, attr + " should be an IDL attribute of this element"); + * is(typeof element[attr], <type>, attr + " IDL attribute should be a <type>"); + */ + +/** + * Checks that a given attribute is correctly reflected as a string. + * + * @param aParameters Object object containing the parameters, which are: + * - element Element node to test + * - attribute String name of the attribute + * OR + * attribute Object object containing two attributes, 'content' and 'idl' + * - otherValues Array [optional] other values to test in addition of the default ones + * - extendedAttributes Object object which can have 'TreatNullAs': "EmptyString" + */ +function reflectString(aParameters) { + var element = aParameters.element; + var contentAttr = + typeof aParameters.attribute === "string" + ? aParameters.attribute + : aParameters.attribute.content; + var idlAttr = + typeof aParameters.attribute === "string" + ? aParameters.attribute + : aParameters.attribute.idl; + var otherValues = + aParameters.otherValues !== undefined ? aParameters.otherValues : []; + var treatNullAs = aParameters.extendedAttributes + ? aParameters.extendedAttributes.TreatNullAs + : null; + + ok( + idlAttr in element, + idlAttr + " should be an IDL attribute of this element" + ); + is( + typeof element[idlAttr], + "string", + "'" + idlAttr + "' IDL attribute should be a string" + ); + + // Tests when the attribute isn't set. + is( + element.getAttribute(contentAttr), + null, + "When not set, the content attribute should be null." + ); + is( + element[idlAttr], + "", + "When not set, the IDL attribute should return the empty string" + ); + + /** + * TODO: as long as null stringification doesn't follow the WebIDL + * specifications, don't add it to the loop below and keep it here. + */ + element.setAttribute(contentAttr, null); + is( + element.getAttribute(contentAttr), + "null", + "null should have been stringified to 'null' for '" + contentAttr + "'" + ); + is( + element[idlAttr], + "null", + "null should have been stringified to 'null' for '" + idlAttr + "'" + ); + element.removeAttribute(contentAttr); + + element[idlAttr] = null; + if (treatNullAs == "EmptyString") { + is( + element.getAttribute(contentAttr), + "", + "null should have been stringified to '' for '" + contentAttr + "'" + ); + is( + element[idlAttr], + "", + "null should have been stringified to '' for '" + idlAttr + "'" + ); + } else { + is( + element.getAttribute(contentAttr), + "null", + "null should have been stringified to 'null' for '" + contentAttr + "'" + ); + is( + element[idlAttr], + "null", + "null should have been stringified to 'null' for '" + contentAttr + "'" + ); + } + element.removeAttribute(contentAttr); + + // Tests various strings. + var stringsToTest = [ + // [ test value, expected result ] + ["", ""], + ["null", "null"], + ["undefined", "undefined"], + ["foo", "foo"], + [contentAttr, contentAttr], + [idlAttr, idlAttr], + // TODO: uncomment this when null stringification will follow the specs. + // [ null, "null" ], + [undefined, "undefined"], + [true, "true"], + [false, "false"], + [42, "42"], + // ES5, verse 8.12.8. + [ + { + toString() { + return "foo"; + }, + }, + "foo", + ], + [ + { + valueOf() { + return "foo"; + }, + }, + "[object Object]", + ], + [ + { + valueOf() { + return "quux"; + }, + toString: undefined, + }, + "quux", + ], + [ + { + valueOf() { + return "foo"; + }, + toString() { + return "bar"; + }, + }, + "bar", + ], + ]; + + otherValues.forEach(function (v) { + stringsToTest.push([v, v]); + }); + + stringsToTest.forEach(function ([v, r]) { + element.setAttribute(contentAttr, v); + is( + element[idlAttr], + r, + "IDL attribute '" + + idlAttr + + "' should return the value it has been set to." + ); + is( + element.getAttribute(contentAttr), + r, + "Content attribute '" + + contentAttr + + "'should return the value it has been set to." + ); + element.removeAttribute(contentAttr); + + element[idlAttr] = v; + is( + element[idlAttr], + r, + "IDL attribute '" + + idlAttr + + "' should return the value it has been set to." + ); + is( + element.getAttribute(contentAttr), + r, + "Content attribute '" + + contentAttr + + "' should return the value it has been set to." + ); + element.removeAttribute(contentAttr); + }); + + // Tests after removeAttribute() is called. Should be equivalent with not set. + is( + element.getAttribute(contentAttr), + null, + "When not set, the content attribute should be null." + ); + is( + element[idlAttr], + "", + "When not set, the IDL attribute should return the empty string" + ); +} + +/** + * Checks that a given attribute name for a given element is correctly reflected + * as an unsigned int. + * + * @param aParameters Object object containing the parameters, which are: + * - element Element node to test on + * - attribute String name of the attribute + * - nonZero Boolean whether the attribute should be non-null + * - defaultValue Integer [optional] default value, if different from the default one + */ +function reflectUnsignedInt(aParameters) { + var element = aParameters.element; + var attr = aParameters.attribute; + var nonZero = aParameters.nonZero; + var defaultValue = aParameters.defaultValue; + var fallback = aParameters.fallback; + + if (defaultValue === undefined) { + if (nonZero) { + defaultValue = 1; + } else { + defaultValue = 0; + } + } + + if (fallback === undefined) { + fallback = false; + } + + ok(attr in element, attr + " should be an IDL attribute of this element"); + is( + typeof element[attr], + "number", + attr + " IDL attribute should be a number" + ); + + // Check default value. + is(element[attr], defaultValue, "default value should be " + defaultValue); + ok(!element.hasAttribute(attr), attr + " shouldn't be present"); + + var values = [1, 3, 42, 2147483647]; + + for (var value of values) { + element[attr] = value; + is(element[attr], value, "." + attr + " should be equals " + value); + is( + element.getAttribute(attr), + String(value), + "@" + attr + " should be equals " + value + ); + + element.setAttribute(attr, value); + is(element[attr], value, "." + attr + " should be equals " + value); + is( + element.getAttribute(attr), + String(value), + "@" + attr + " should be equals " + value + ); + } + + // -3000000000 is equivalent to 1294967296 when using the IDL attribute. + element[attr] = -3000000000; + is(element[attr], 1294967296, "." + attr + " should be equals to 1294967296"); + is( + element.getAttribute(attr), + "1294967296", + "@" + attr + " should be equals to 1294967296" + ); + + // When setting the content attribute, it's a string so it will be invalid. + element.setAttribute(attr, -3000000000); + is( + element.getAttribute(attr), + "-3000000000", + "@" + attr + " should be equals to " + -3000000000 + ); + is( + element[attr], + defaultValue, + "." + attr + " should be equals to " + defaultValue + ); + + // When interpreted as unsigned 32-bit integers, all of these fall between + // 2^31 and 2^32 - 1, so per spec they return the default value. + var nonValidValues = [-2147483648, -1, 3147483647]; + + for (var value of nonValidValues) { + element[attr] = value; + is( + element.getAttribute(attr), + String(defaultValue), + "@" + attr + " should be equals to " + defaultValue + ); + is( + element[attr], + defaultValue, + "." + attr + " should be equals to " + defaultValue + ); + } + + for (var values of nonValidValues) { + element.setAttribute(attr, values[0]); + is( + element.getAttribute(attr), + String(values[0]), + "@" + attr + " should be equals to " + values[0] + ); + is( + element[attr], + defaultValue, + "." + attr + " should be equals to " + defaultValue + ); + } + + // Setting to 0 should throw an error if nonZero is true. + var caught = false; + try { + element[attr] = 0; + } catch (e) { + caught = true; + is(e.name, "IndexSizeError", "exception should be IndexSizeError"); + is( + e.code, + DOMException.INDEX_SIZE_ERR, + "exception code should be INDEX_SIZE_ERR" + ); + } + + if (nonZero && !fallback) { + ok(caught, "an exception should have been caught"); + } else { + ok(!caught, "no exception should have been caught"); + } + + // If 0 is set in @attr, it will be ignored when calling .attr. + element.setAttribute(attr, "0"); + is(element.getAttribute(attr), "0", "@" + attr + " should be equals to 0"); + if (nonZero) { + is( + element[attr], + defaultValue, + "." + attr + " should be equals to " + defaultValue + ); + } else { + is(element[attr], 0, "." + attr + " should be equals to 0"); + } +} + +/** + * Checks that a given attribute is correctly reflected as limited to known + * values enumerated attribute. + * + * @param aParameters Object object containing the parameters, which are: + * - element Element node to test on + * - attribute String name of the attribute + * OR + * attribute Object object containing two attributes, 'content' and 'idl' + * - validValues Array valid values we support + * - invalidValues Array invalid values + * - defaultValue String [optional] default value when no valid value is set + * OR + * defaultValue Object [optional] object containing two attributes, 'invalid' and 'missing' + * - unsupportedValues Array [optional] valid values we do not support + * - nullable boolean [optional] whether the attribute is nullable + */ +function reflectLimitedEnumerated(aParameters) { + var element = aParameters.element; + var contentAttr = + typeof aParameters.attribute === "string" + ? aParameters.attribute + : aParameters.attribute.content; + var idlAttr = + typeof aParameters.attribute === "string" + ? aParameters.attribute + : aParameters.attribute.idl; + var validValues = aParameters.validValues; + var invalidValues = aParameters.invalidValues; + var defaultValueInvalid = + aParameters.defaultValue === undefined + ? "" + : typeof aParameters.defaultValue === "string" + ? aParameters.defaultValue + : aParameters.defaultValue.invalid; + var defaultValueMissing = + aParameters.defaultValue === undefined + ? "" + : typeof aParameters.defaultValue === "string" + ? aParameters.defaultValue + : aParameters.defaultValue.missing; + var unsupportedValues = + aParameters.unsupportedValues !== undefined + ? aParameters.unsupportedValues + : []; + var nullable = aParameters.nullable; + + ok( + idlAttr in element, + idlAttr + " should be an IDL attribute of this element" + ); + if (nullable) { + // The missing value default is null, which is typeof == "object" + is( + typeof element[idlAttr], + "object", + "'" + + idlAttr + + "' IDL attribute should be null, which has typeof == object" + ); + is( + element[idlAttr], + null, + "'" + idlAttr + "' IDL attribute should be null" + ); + } else { + is( + typeof element[idlAttr], + "string", + "'" + idlAttr + "' IDL attribute should be a string" + ); + } + + if (nullable) { + element.setAttribute(contentAttr, "something"); + // Now it will be a string + is( + typeof element[idlAttr], + "string", + "'" + idlAttr + "' IDL attribute should be a string" + ); + } + + // Explicitly check the default value. + element.removeAttribute(contentAttr); + is( + element[idlAttr], + defaultValueMissing, + "When no attribute is set, the value should be the default value." + ); + + // Check valid values. + validValues.forEach(function (v) { + element.setAttribute(contentAttr, v); + is( + element[idlAttr], + v, + "'" + v + "' should be accepted as a valid value for " + idlAttr + ); + is( + element.getAttribute(contentAttr), + v, + "Content attribute should return the value it has been set to." + ); + element.removeAttribute(contentAttr); + + element.setAttribute(contentAttr, v.toUpperCase()); + is( + element[idlAttr], + v, + "Enumerated attributes should be case-insensitive." + ); + is( + element.getAttribute(contentAttr), + v.toUpperCase(), + "Content attribute should not be lower-cased." + ); + element.removeAttribute(contentAttr); + + element[idlAttr] = v; + is( + element[idlAttr], + v, + "'" + v + "' should be accepted as a valid value for " + idlAttr + ); + is( + element.getAttribute(contentAttr), + v, + "Content attribute should return the value it has been set to." + ); + element.removeAttribute(contentAttr); + + element[idlAttr] = v.toUpperCase(); + is( + element[idlAttr], + v, + "Enumerated attributes should be case-insensitive." + ); + is( + element.getAttribute(contentAttr), + v.toUpperCase(), + "Content attribute should not be lower-cased." + ); + element.removeAttribute(contentAttr); + }); + + // Check invalid values. + invalidValues.forEach(function (v) { + element.setAttribute(contentAttr, v); + is( + element[idlAttr], + defaultValueInvalid, + "When the content attribute is set to an invalid value, the default value should be returned." + ); + is( + element.getAttribute(contentAttr), + v, + "Content attribute should not have been changed." + ); + element.removeAttribute(contentAttr); + + element[idlAttr] = v; + is( + element[idlAttr], + defaultValueInvalid, + "When the value is set to an invalid value, the default value should be returned." + ); + is( + element.getAttribute(contentAttr), + v, + "Content attribute should not have been changed." + ); + element.removeAttribute(contentAttr); + }); + + // Check valid values we currently do not support. + // Basically, it's like the checks for the valid values but with some todo's. + unsupportedValues.forEach(function (v) { + element.setAttribute(contentAttr, v); + todo_is( + element[idlAttr], + v, + "'" + v + "' should be accepted as a valid value for " + idlAttr + ); + is( + element.getAttribute(contentAttr), + v, + "Content attribute should return the value it has been set to." + ); + element.removeAttribute(contentAttr); + + element.setAttribute(contentAttr, v.toUpperCase()); + todo_is( + element[idlAttr], + v, + "Enumerated attributes should be case-insensitive." + ); + is( + element.getAttribute(contentAttr), + v.toUpperCase(), + "Content attribute should not be lower-cased." + ); + element.removeAttribute(contentAttr); + + element[idlAttr] = v; + todo_is( + element[idlAttr], + v, + "'" + v + "' should be accepted as a valid value for " + idlAttr + ); + is( + element.getAttribute(contentAttr), + v, + "Content attribute should return the value it has been set to." + ); + element.removeAttribute(contentAttr); + + element[idlAttr] = v.toUpperCase(); + todo_is( + element[idlAttr], + v, + "Enumerated attributes should be case-insensitive." + ); + is( + element.getAttribute(contentAttr), + v.toUpperCase(), + "Content attribute should not be lower-cased." + ); + element.removeAttribute(contentAttr); + }); + + if (nullable) { + is( + defaultValueMissing, + null, + "Missing default value should be null for nullable attributes" + ); + ok(validValues.length, "We better have at least one valid value"); + element.setAttribute(contentAttr, validValues[0]); + ok( + element.hasAttribute(contentAttr), + "Should have content attribute: we just set it" + ); + element[idlAttr] = null; + ok( + !element.hasAttribute(contentAttr), + "Should have removed content attribute" + ); + } +} + +/** + * Checks that a given attribute is correctly reflected as a boolean. + * + * @param aParameters Object object containing the parameters, which are: + * - element Element node to test on + * - attribute String name of the attribute + * OR + * attribute Object object containing two attributes, 'content' and 'idl' + */ +function reflectBoolean(aParameters) { + var element = aParameters.element; + var contentAttr = + typeof aParameters.attribute === "string" + ? aParameters.attribute + : aParameters.attribute.content; + var idlAttr = + typeof aParameters.attribute === "string" + ? aParameters.attribute + : aParameters.attribute.idl; + + ok( + idlAttr in element, + idlAttr + " should be an IDL attribute of this element" + ); + is( + typeof element[idlAttr], + "boolean", + idlAttr + " IDL attribute should be a boolean" + ); + + // Tests when the attribute isn't set. + is( + element.getAttribute(contentAttr), + null, + "When not set, the content attribute should be null." + ); + is( + element[idlAttr], + false, + "When not set, the IDL attribute should return false" + ); + + /** + * Test various values. + * Each value to test is actually an object containing a 'value' property + * containing the value to actually test, a 'stringified' property containing + * the stringified value and a 'result' property containing the expected + * result when the value is set to the IDL attribute. + */ + var valuesToTest = [ + { value: true, stringified: "true", result: true }, + { value: false, stringified: "false", result: false }, + { value: "true", stringified: "true", result: true }, + { value: "false", stringified: "false", result: true }, + { value: "foo", stringified: "foo", result: true }, + { value: idlAttr, stringified: idlAttr, result: true }, + { value: contentAttr, stringified: contentAttr, result: true }, + { value: "null", stringified: "null", result: true }, + { value: "undefined", stringified: "undefined", result: true }, + { value: "", stringified: "", result: false }, + { value: undefined, stringified: "undefined", result: false }, + { value: null, stringified: "null", result: false }, + { value: +0, stringified: "0", result: false }, + { value: -0, stringified: "0", result: false }, + { value: NaN, stringified: "NaN", result: false }, + { value: 42, stringified: "42", result: true }, + { value: Infinity, stringified: "Infinity", result: true }, + { value: -Infinity, stringified: "-Infinity", result: true }, + // ES5, verse 9.2. + { + value: { + toString() { + return "foo"; + }, + }, + stringified: "foo", + result: true, + }, + { + value: { + valueOf() { + return "foo"; + }, + }, + stringified: "[object Object]", + result: true, + }, + { + value: { + valueOf() { + return "quux"; + }, + toString: undefined, + }, + stringified: "quux", + result: true, + }, + { + value: { + valueOf() { + return "foo"; + }, + toString() { + return "bar"; + }, + }, + stringified: "bar", + result: true, + }, + { + value: { + valueOf() { + return false; + }, + }, + stringified: "[object Object]", + result: true, + }, + { + value: { foo: false, bar: false }, + stringified: "[object Object]", + result: true, + }, + { value: {}, stringified: "[object Object]", result: true }, + ]; + + valuesToTest.forEach(function (v) { + element.setAttribute(contentAttr, v.value); + is( + element[idlAttr], + true, + "IDL attribute should return always return 'true' if the content attribute has been set" + ); + is( + element.getAttribute(contentAttr), + v.stringified, + "Content attribute should return the stringified value it has been set to." + ); + element.removeAttribute(contentAttr); + + element[idlAttr] = v.value; + is(element[idlAttr], v.result, "IDL attribute should return " + v.result); + is( + element.getAttribute(contentAttr), + v.result ? "" : null, + v.result + ? "Content attribute should return the empty string." + : "Content attribute should return null." + ); + is( + element.hasAttribute(contentAttr), + v.result, + v.result + ? contentAttr + " should not be present" + : contentAttr + " should be present" + ); + element.removeAttribute(contentAttr); + }); + + // Tests after removeAttribute() is called. Should be equivalent with not set. + is( + element.getAttribute(contentAttr), + null, + "When not set, the content attribute should be null." + ); + is( + element[contentAttr], + false, + "When not set, the IDL attribute should return false" + ); +} + +/** + * Checks that a given attribute name for a given element is correctly reflected + * as an signed integer. + * + * @param aParameters Object object containing the parameters, which are: + * - element Element node to test on + * - attribute String name of the attribute + * - nonNegative Boolean true if the attribute is limited to 'non-negative numbers', false otherwise + * - defaultValue Integer [optional] default value, if one exists + */ +function reflectInt(aParameters) { + // Expected value returned by .getAttribute() when |value| has been previously passed to .setAttribute(). + function expectedGetAttributeResult(value) { + return String(value); + } + + function stringToInteger(value, nonNegative, defaultValue) { + // Parse: Ignore leading whitespace, find [+/-][numbers] + var result = /^[ \t\n\f\r]*([\+\-]?[0-9]+)/.exec(value); + if (result) { + var resultInt = parseInt(result[1], 10); + if ( + (nonNegative ? 0 : -0x80000000) <= resultInt && + resultInt <= 0x7fffffff + ) { + // If the value is within allowed value range for signed/unsigned + // integer, return it -- but add 0 to it to convert a possible -0 into + // +0, the only zero present in the signed integer range. + return resultInt + 0; + } + } + return defaultValue; + } + + // Expected value returned by .getAttribute(attr) or .attr if |value| has been set via the IDL attribute. + function expectedIdlAttributeResult(value) { + // This returns the result of calling the ES ToInt32 algorithm on value. + return value << 0; + } + + var element = aParameters.element; + var attr = aParameters.attribute; + var nonNegative = aParameters.nonNegative; + + var defaultValue = + aParameters.defaultValue !== undefined + ? aParameters.defaultValue + : nonNegative + ? -1 + : 0; + + ok(attr in element, attr + " should be an IDL attribute of this element"); + is( + typeof element[attr], + "number", + attr + " IDL attribute should be a number" + ); + + // Check default value. + is(element[attr], defaultValue, "default value should be " + defaultValue); + ok(!element.hasAttribute(attr), attr + " shouldn't be present"); + + /** + * Test various values. + * value: The test value that will be set using both setAttribute(value) and + * element[attr] = value + */ + var valuesToTest = [ + // Test numeric inputs up to max signed integer + 0, + 1, + 55555, + 2147483647, + +42, + // Test string inputs up to max signed integer + "0", + "1", + "777777", + "2147483647", + "+42", + // Test negative numeric inputs up to min signed integer + -0, + -1, + -3333, + -2147483648, + // Test negative string inputs up to min signed integer + "-0", + "-1", + "-222", + "-2147483647", + "-2147483648", + // Test numeric inputs that are outside legal 32 bit signed values + -2147483649, + -3000000000, + -4294967296, + 2147483649, + 4000000000, + -4294967297, + // Test string inputs with extra padding + " 1111111", + " 23456 ", + // Test non-numeric string inputs + "", + " ", + "+", + "-", + "foo", + "+foo", + "-foo", + "+ foo", + "- foo", + "+-2", + "-+2", + "++2", + "--2", + "hello1234", + "1234hello", + "444 world 555", + "why 567 what", + "-3 nots", + "2e5", + "300e2", + "42+-$", + "+42foo", + "-514not", + "\vblah", + "0x10FFFF", + "-0xABCDEF", + // Test decimal numbers + 1.2345, + 42.0, + 3456789.1, + -2.3456, + -6789.12345, + -2147483649.1234, + // Test decimal strings + "1.2345", + "42.0", + "3456789.1", + "-2.3456", + "-6789.12345", + "-2147483649.1234", + // Test special values + undefined, + null, + NaN, + Infinity, + -Infinity, + ]; + + valuesToTest.forEach(function (v) { + var intValue = stringToInteger(v, nonNegative, defaultValue); + + element.setAttribute(attr, v); + + is( + element.getAttribute(attr), + expectedGetAttributeResult(v), + element.localName + + ".setAttribute(" + + attr + + ", " + + v + + "), " + + element.localName + + ".getAttribute(" + + attr + + ") " + ); + + is( + element[attr], + intValue, + element.localName + + ".setAttribute(" + + attr + + ", " + + v + + "), " + + element.localName + + "[" + + attr + + "] " + ); + element.removeAttribute(attr); + + if (nonNegative && expectedIdlAttributeResult(v) < 0) { + try { + element[attr] = v; + ok( + false, + element.localName + + "[" + + attr + + "] = " + + v + + " should throw IndexSizeError" + ); + } catch (e) { + is( + e.name, + "IndexSizeError", + element.localName + + "[" + + attr + + "] = " + + v + + " should throw IndexSizeError" + ); + is( + e.code, + DOMException.INDEX_SIZE_ERR, + element.localName + + "[" + + attr + + "] = " + + v + + " should throw INDEX_SIZE_ERR" + ); + } + } else { + element[attr] = v; + is( + element[attr], + expectedIdlAttributeResult(v), + element.localName + + "[" + + attr + + "] = " + + v + + ", " + + element.localName + + "[" + + attr + + "] " + ); + is( + element.getAttribute(attr), + String(expectedIdlAttributeResult(v)), + element.localName + + "[" + + attr + + "] = " + + v + + ", " + + element.localName + + ".getAttribute(" + + attr + + ") " + ); + } + element.removeAttribute(attr); + }); + + // Tests after removeAttribute() is called. Should be equivalent with not set. + is( + element.getAttribute(attr), + null, + "When not set, the content attribute should be null." + ); + is( + element[attr], + defaultValue, + "When not set, the IDL attribute should return default value." + ); +} + +/** + * Checks that a given attribute is correctly reflected as a url. + * + * @param aParameters Object object containing the parameters, which are: + * - element Element node to test + * - attribute String name of the attribute + * OR + * attribute Object object containing two attributes, 'content' and 'idl' + */ +function reflectURL(aParameters) { + var element = aParameters.element; + var contentAttr = + typeof aParameters.attribute === "string" + ? aParameters.attribute + : aParameters.attribute.content; + var idlAttr = + typeof aParameters.attribute === "string" + ? aParameters.attribute + : aParameters.attribute.idl; + + element[idlAttr] = ""; + is( + element[idlAttr], + document.URL, + "Empty string should resolve to document URL" + ); +} diff --git a/dom/html/test/script_fakepath.js b/dom/html/test/script_fakepath.js new file mode 100644 index 0000000000..f95ac493d2 --- /dev/null +++ b/dom/html/test/script_fakepath.js @@ -0,0 +1,16 @@ +/* eslint-env mozilla/chrome-script */ + +// eslint-disable-next-line mozilla/reject-importGlobalProperties +Cu.importGlobalProperties(["File"]); + +addMessageListener("file.open", function (e) { + var tmpFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + tmpFile.append("prefs.js"); + + File.createFromNsIFile(tmpFile).then(file => { + sendAsyncMessage("file.opened", { data: [file] }); + }); +}); diff --git a/dom/html/test/simpleFileOpener.js b/dom/html/test/simpleFileOpener.js new file mode 100644 index 0000000000..cb98f0c64e --- /dev/null +++ b/dom/html/test/simpleFileOpener.js @@ -0,0 +1,38 @@ +/* eslint-env mozilla/chrome-script */ + +// eslint-disable-next-line mozilla/reject-importGlobalProperties +Cu.importGlobalProperties(["File"]); + +var file; + +addMessageListener("file.open", function (stem) { + try { + if (!file) { + file = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + file.append(stem); + file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + } + + File.createFromNsIFile(file).then(function (domFile) { + sendAsyncMessage("file.opened", { + fullPath: file.path, + leafName: file.leafName, + domFile, + }); + }); + } catch (e) { + sendAsyncMessage("fail", e.toString()); + } +}); + +addMessageListener("file.remove", function () { + try { + file.remove(/* recursive: */ false); + file = undefined; + sendAsyncMessage("file.removed", null); + } catch (e) { + sendAsyncMessage("fail", e.toString()); + } +}); diff --git a/dom/html/test/submission_flush.html b/dom/html/test/submission_flush.html new file mode 100644 index 0000000000..f70884c66a --- /dev/null +++ b/dom/html/test/submission_flush.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"/> + <title>Submission Flush Test</title> + </head> + <body> + <form id="test_form" action="post_action_page.html" target="form_target" method="POST" onsubmit="return false;"> + <button type="submit" id="submit_button">Submit</button> + </form> + <iframe name="form_target" id="test_frame"></iframe> + </body> +</html> diff --git a/dom/html/test/sw_formSubmission.js b/dom/html/test/sw_formSubmission.js new file mode 100644 index 0000000000..2e102ac74c --- /dev/null +++ b/dom/html/test/sw_formSubmission.js @@ -0,0 +1,36 @@ +/** + * We are used by test_formSubmission.html to immediately activate and start + * controlling its page. We operate in 3 modes, conveyed via ?MODE appended to + * our URL. + * + * - "no-fetch": Don't register a fetch listener so that the optimized fetch + * event bypass happens. + * - "reset-fetch": Do register a fetch listener, reset every interception. + * - "proxy-fetch": Do register a fetch listener, resolve every interception + * with fetch(event.request). + */ + +const mode = location.search.slice(1); + +// Fetch handling. +if (mode !== "no-fetch") { + addEventListener("fetch", function (event) { + if (mode === "reset-fetch") { + // Don't invoke respondWith, resetting the interception. + return; + } + if (mode === "proxy-fetch") { + // Per the spec, there's an automatic waitUntil() on this too. + event.respondWith(fetch(event.request)); + } + }); +} + +// Go straight to activation, bypassing waiting. +addEventListener("install", function (event) { + event.waitUntil(skipWaiting()); +}); +// Control the test document ASAP. +addEventListener("activate", function (event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/html/test/test_a_text.html b/dom/html/test/test_a_text.html new file mode 100644 index 0000000000..5ffc1995f8 --- /dev/null +++ b/dom/html/test/test_a_text.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for a.text</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <link rel="help" href="http://www.whatwg.org/html/#dom-a-text"/> +</head> +<body> +<div id="content"> +<a href="a">a b c</a> +<a href="b">a <!--b--> c</a> +<a href="c">a <b>b</b> c</a> +</div> +<pre id="test"> +<script> +var d = document.getElementById("content") + .appendChild(document.createElement("a")); +d.href = "d"; +d.appendChild(document.createTextNode("a ")); +d.appendChild(document.createTextNode("b ")); +d.appendChild(document.createTextNode("c ")); +var expected = ["a b c", "a c", "a b c", "a b c "]; +var list = document.getElementById("content").getElementsByTagName("a"); +for (var i = 0, il = list.length; i < il; ++i) { + is(list[i].text, list[i].textContent); + is(list[i].text, expected[i]); + + list[i].text = "x"; + is(list[i].text, "x"); + is(list[i].textContent, "x"); + is(list[i].firstChild.data, "x"); + is(list[i].childNodes.length, 1); + + list[i].textContent = "y"; + is(list[i].text, "y"); + is(list[i].textContent, "y"); + is(list[i].firstChild.data, "y"); + is(list[i].childNodes.length, 1); +} +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_allowMedia.html b/dom/html/test/test_allowMedia.html new file mode 100644 index 0000000000..46a692283a --- /dev/null +++ b/dom/html/test/test_allowMedia.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=759964 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 759964</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 759964 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runNextTest); + +var SJS = `${location.origin}/tests/dom/html/test/allowMedia.sjs`; +var TEST_PAGE = "data:text/html,<audio src='" + SJS + "?audio'></audio>"; + +function runNextTest() { + var test = tests.shift(); + if (!test) { + SimpleTest.finish(); + return; + } + test(); +} + +var tests = [ + + // Set allowMedia = false, load a page with <audio>, verify the <audio> + // doesn't load its source. + function basic() { + var iframe = insertIframe(); + SpecialPowers.allowMedia(iframe.contentWindow, false); + loadIframe(iframe, TEST_PAGE, function () { + verifyPass(); + iframe.remove(); + runNextTest(); + }); + }, + + // Set allowMedia = false on parent docshell, load a page with <audio> in a + // child iframe, verify the <audio> doesn't load its source. + function inherit() { + SpecialPowers.allowMedia(window, false); + + var iframe = insertIframe(); + loadIframe(iframe, TEST_PAGE, function () { + verifyPass(); + iframe.remove(); + SpecialPowers.allowMedia(window, true); + runNextTest(); + }); + }, + + // In a display:none iframe, set allowMedia = false, load a page with <audio>, + // verify the <audio> doesn't load its source. + function displayNone() { + var iframe = insertIframe(); + iframe.style.display = "none"; + SpecialPowers.allowMedia(iframe.contentWindow, false); + loadIframe(iframe, TEST_PAGE, function () { + verifyPass(); + iframe.remove(); + runNextTest(); + }); + }, +]; + +function insertIframe() { + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + return iframe; +} + +function loadIframe(iframe, url, onDone) { + iframe.setAttribute("src", url); + iframe.addEventListener("load", onDone); +} + +function verifyPass() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", SJS, false); + xhr.send(); + is(xhr.responseText, "PASS", "<audio> source should not have been loaded."); +} + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=759964">Mozilla Bug 759964</a> +<p id="display"> +</p> +</body> +</html> diff --git a/dom/html/test/test_anchor_href_cache_invalidation.html b/dom/html/test/test_anchor_href_cache_invalidation.html new file mode 100644 index 0000000000..c1a8327e62 --- /dev/null +++ b/dom/html/test/test_anchor_href_cache_invalidation.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for anchor cache invalidation</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + <a id="x" href="http://example.com"></a> +</div> +<pre id="test"> +<script type="application/javascript"> + +is($("x").href, "http://example.com/"); +is($("x").host, "example.com"); + +$("x").href = "http://www.example.com"; + +is($("x").href, "http://www.example.com/"); +is($("x").host, "www.example.com"); + +$("x").setAttribute("href", "http://www.example.net/"); +is($("x").host, "www.example.net"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_anchor_ping.html b/dom/html/test/test_anchor_ping.html new file mode 100644 index 0000000000..513e5ee05f --- /dev/null +++ b/dom/html/test/test_anchor_ping.html @@ -0,0 +1,304 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=786347 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 786347</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 786347 **/ + +SimpleTest.waitForExplicitFinish(); + +const {NetUtil} = ChromeUtils.importESModule( + "resource://gre/modules/NetUtil.sys.mjs" +); +const {HttpServer} = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +addLoadEvent(function () { + (async function run_tests() { + while (tests.length) { + let test = tests.shift(); + info("-- running " + test.name); + await test(); + } + + SimpleTest.finish(); + })(); +}); + +let tests = [ + + // Ensure that sending pings is enabled. + function setup() { + Services.prefs.setBoolPref("browser.send_pings", true); + Services.prefs.setIntPref("browser.send_pings.max_per_link", -1); + + SimpleTest.registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.send_pings"); + Services.prefs.clearUserPref("browser.send_pings.max_per_link"); + }); + }, + + // If both the address of the document containing the hyperlink being audited + // and ping URL have the same origin then the request must include a Ping-From + // HTTP header with, as its value, the address of the document containing the + // hyperlink, and a Ping-To HTTP header with, as its value, the target URL. + // The request must not include a Referer (sic) HTTP header. + async function same_origin() { + let from = "/ping-from/" + Math.random(); + let to = "/ping-to/" + Math.random(); + let ping = "/ping/" + Math.random(); + + let base; + let server = new HttpServer(); + + // The page that contains the link. + createFromPathHandler(server, from, to, () => ping); + + // The page that the link's href points to. + let promiseHref = createToPathHandler(server, to); + + // The ping we want to receive. + let promisePing = createPingPathHandler(server, ping, () => { + return {from: base + from, to: base + to}; + }); + + // Start the server, get its base URL and run the test. + server.start(-1); + base = "http://localhost:" + server.identity.primaryPort; + navigate(base + from); + + // Wait until the target and ping url have loaded. + await Promise.all([promiseHref, promisePing]); + + // Cleanup. + await stopServer(server); + }, + + // If the origins are different, but the document containing the hyperlink + // being audited was not retrieved over an encrypted connection then the + // request must include a Referer (sic) HTTP header with, as its value, the + // address of the document containing the hyperlink, a Ping-From HTTP header + // with the same value, and a Ping-To HTTP header with, as its value, target + // URL. + async function diff_origin() { + let from = "/ping-from/" + Math.random(); + let to = "/ping-to/" + Math.random(); + let ping = "/ping/" + Math.random(); + + // We will use two servers to simulate two different origins. + let base, base2; + let server = new HttpServer(); + let server2 = new HttpServer(); + + // The page that contains the link. + createFromPathHandler(server, from, to, () => base2 + ping); + + // The page that the link's href points to. + let promiseHref = createToPathHandler(server, to); + + // Start the first server and get its base URL. + server.start(-1); + base = "http://localhost:" + server.identity.primaryPort; + + // The ping we want to receive. + let promisePing = createPingPathHandler(server2, ping, () => { + return {referrer: base + from, from: base + from, to: base + to}; + }); + + // Start the second server, get its base URL and run the test. + server2.start(-1); + base2 = "http://localhost:" + server2.identity.primaryPort; + navigate(base + from); + + // Wait until the target and ping url have loaded. + await Promise.all([promiseHref, promisePing]); + + // Cleanup. + await stopServer(server); + await stopServer(server2); + }, + + // If the origins are different and the document containing the hyperlink + // being audited was retrieved over an encrypted connection then the request + // must include a Ping-To HTTP header with, as its value, target URL. The + // request must neither include a Referer (sic) HTTP header nor include a + // Ping-From HTTP header. + async function diff_origin_secure_referrer() { + let ping = "/ping/" + Math.random(); + let server = new HttpServer(); + + // The ping we want to receive. + let promisePing = createPingPathHandler(server, ping, () => { + return {to: "https://example.com/"}; + }); + + // Start the server and run the test. + server.start(-1); + + // The referrer will be loaded using a secure channel. + navigate("https://example.com/chrome/dom/html/test/" + + "file_anchor_ping.html?" + "http://127.0.0.1:" + + server.identity.primaryPort + ping); + + // Wait until the ping has been sent. + await promisePing; + + // Cleanup. + await stopServer(server); + }, + + // Test that the <a ping> attribute is properly tokenized using ASCII white + // space characters as separators. + async function tokenize_white_space() { + let from = "/ping-from/" + Math.random(); + let to = "/ping-to/" + Math.random(); + + let base; + let server = new HttpServer(); + + let pings = [ + "/ping1/" + Math.random(), + "/ping2/" + Math.random(), + "/ping3/" + Math.random(), + "/ping4/" + Math.random() + ]; + + // The page that contains the link. + createFromPathHandler(server, from, to, () => { + return " " + pings[0] + " \r " + pings[1] + " \t " + + pings[2] + " \n " + pings[3] + " "; + }); + + // The page that the link's href points to. + let promiseHref = createToPathHandler(server, to); + + // The pings we want to receive. + let pingPathHandlers = createPingPathHandlers(server, pings, () => { + return {from: base + from, to: base + to}; + }); + + // Start the server, get its base URL and run the test. + server.start(-1); + base = "http://localhost:" + server.identity.primaryPort; + navigate(base + from); + + // Wait until the target and ping url have loaded. + await Promise.all([promiseHref, ...pingPathHandlers]); + + // Cleanup. + await stopServer(server); + } +]; + +// Navigate the iframe used for testing to a new URL. +function navigate(uri) { + document.getElementById("frame").src = uri; +} + +// Registers a path handler for the given server that will serve a page +// containing an <a ping> element. The page will automatically simulate +// clicking the link after it has loaded. +function createFromPathHandler(server, path, href, lazyPing) { + server.registerPathHandler(path, function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + response.setHeader("Cache-Control", "no-cache", false); + + let body = '<body onload="document.body.firstChild.click()">' + + '<a href="' + href + '" ping="' + lazyPing() + '"></a></body>'; + response.write(body); + }); +} + +// Registers a path handler for the given server that will serve a simple empty +// page we can use as the href attribute for links. It returns a promise that +// will be resolved once the page has been requested. +function createToPathHandler(server, path) { + return new Promise(resolve => { + + server.registerPathHandler(path, function (request, response) { + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + response.setHeader("Cache-Control", "no-cache", false); + response.write("OK"); + + resolve(); + }); + + }); +} + +// Register multiple path handlers for the given server that will receive +// pings as sent when an <a ping> element is clicked. This method uses +// createPingPathHandler() defined below to ensure all headers are sent +// and received as expected. +function createPingPathHandlers(server, paths, lazyHeaders) { + return Array.from(paths, (path) => createPingPathHandler(server, path, lazyHeaders)); +} + +// Registers a path handler for the given server that will receive pings as +// sent when an <a ping> element has been clicked. It will check that the +// correct http method has been used, the post data is correct and all headers +// are given as expected. It returns a promise that will be resolved once the +// ping has been received. +function createPingPathHandler(server, path, lazyHeaders) { + return new Promise(resolve => { + + server.registerPathHandler(path, function (request, response) { + let headers = lazyHeaders(); + + is(request.method, "POST", "correct http method used"); + is(request.getHeader("Ping-To"), headers.to, "valid ping-to header"); + + if ("from" in headers) { + is(request.getHeader("Ping-From"), headers.from, "valid ping-from header"); + } else { + ok(!request.hasHeader("Ping-From"), "no ping-from header"); + } + + if ("referrer" in headers) { + let expectedReferrer = headers.referrer.match(/https?:\/\/[^\/]+\/?/i)[0]; + is(request.getHeader("Referer"), expectedReferrer, "valid referer header"); + } else { + ok(!request.hasHeader("Referer"), "no referer header"); + } + + let bs = request.bodyInputStream; + let body = NetUtil.readInputStreamToString(bs, bs.available()); + is(body, "PING", "correct body sent"); + + response.setStatusLine(request.httpVersion, 200, "OK"); + response.setHeader("Content-Type", "text/html;charset=utf-8", false); + response.setHeader("Cache-Control", "no-cache", false); + response.write("OK"); + + resolve(); + }); + + }); +} + +// Returns a promise that is resolved when the given http server instance has +// been stopped. +function stopServer(server) { + return new Promise(resolve => { + server.stop(resolve); + }); +} + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=786347">Mozilla Bug 786347</a> +<p id="display"></p> +<iframe id="frame" /> +</body> +</html> diff --git a/dom/html/test/test_base_attributes_reflection.html b/dom/html/test/test_base_attributes_reflection.html new file mode 100644 index 0000000000..cbb8955d5c --- /dev/null +++ b/dom/html/test/test_base_attributes_reflection.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLBaseElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLBaseElement attributes reflection **/ + +// .href is sort of like a URL reflection, but with some special rules. Watch +// out for that! +reflectURL({ + element: document.createElement("base"), + attribute: "href" +}); + +// .target +reflectString({ + element: document.createElement("base"), + attribute: "target" +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1003539.html b/dom/html/test/test_bug1003539.html new file mode 100644 index 0000000000..cbdc1e9fbe --- /dev/null +++ b/dom/html/test/test_bug1003539.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1003539 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1003539</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1003539 **/ +// Refering to this specification: http://www.whatwg.org/specs/web-apps/current-work/multipage/tabular-data.html#dom-table-insertrow +var tab; +tab = document.createElement("table"); +tab.createTHead(); +tab.insertRow(); +is(tab.innerHTML, '<thead></thead><tbody><tr></tr></tbody>', "Row should be inserted in the tbody."); + +tab = document.createElement("table"); +tab.createTBody(); +tab.createTBody(); +tab.insertRow(); +is(tab.innerHTML, '<tbody></tbody><tbody><tr></tr></tbody>', "Row should be inserted in the last tbody."); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1003539">Mozilla Bug 1003539</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug100533.html b/dom/html/test/test_bug100533.html new file mode 100644 index 0000000000..29c52f4f0a --- /dev/null +++ b/dom/html/test/test_bug100533.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=100533 +--> +<head> + <title>Test for Bug 100533</title> + <script type="text/javascript" src="/MochiKit/Base.js"></script> + <script type="text/javascript" src="/MochiKit/DOM.js"></script> + <script type="text/javascript" src="/MochiKit/Style.js"></script> + <script type="text/javascript" src="/MochiKit/Signal.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=100533">Mozilla Bug 100533</a> +<p id="display"></p> +<div id="content" > + +<button id="thebutton">Test</button> +<iframe style='display: none;' src='bug100533_iframe.html' id='a'></iframe> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/** Test for Bug 100533 **/ +var submitIframeForm = function() { + $('a').contentDocument.getElementById('b').submit(); +} + +submitted = function() { + ok(true, "Finished. Form submits when located in iframe set to display:none;"); + SimpleTest.finish(); +}; + +addLoadEvent(function() { + connect("thebutton", "click", submitIframeForm); + signal("thebutton", "click"); +}); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug1013316.html b/dom/html/test/test_bug1013316.html new file mode 100644 index 0000000000..fdb9e5363d --- /dev/null +++ b/dom/html/test/test_bug1013316.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1013316 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1013316</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1013316 **/ + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + is(Object.keys(document.all).length, 15, "We have 15 indexed props"); + var props = Object.getOwnPropertyNames(document.all); + is(props.length, 20, "Should have five names"); + is(props[15], "display", "display first"); + is(props[16], "content", "content second"); + is(props[17], "bar", "bar third"); + is(props[18], "foo", "foo fourth"); + is(props[19], "test", "test fifth"); + + is(Object.keys(document.images).length, 2, "We have 2 indexed props"); + props = Object.getOwnPropertyNames(document.images); + is(props.length, 5, "Should have 3 names"); + is(props[2], "display", "display first on document.images"); + is(props[3], "bar", "bar second on document.images"); + is(props[4], "foo", "foo third on document.images"); + SimpleTest.finish(); + }) + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1013316">Mozilla Bug 1013316</a> +<p id="display"></p> +<div id="content" style="display: none"> + <img id="display"> + <img name="foo" id="bar"> + <div name="baz"> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1045270.html b/dom/html/test/test_bug1045270.html new file mode 100644 index 0000000000..b0c81daf61 --- /dev/null +++ b/dom/html/test/test_bug1045270.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> + <!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1045270 +--> + <head> + <title>Test for Bug 583514</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1045270">Mozilla Bug 1045270</a> + <p id="display"></p> + <div id="content"> + <input type=number> + </div> + <pre id="test"> + <script type="application/javascript"> + + /** Test for Bug 1045270 **/ + + var input = document.querySelector("input"); + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(function() { + input.focus(); + input.addEventListener("input", function() { + // reframe + document.body.style.display = "none"; + document.body.style.display = ""; + document.body.offsetLeft; // flush + }); + sendString("1"); + SimpleTest.executeSoon(function() { + sendString("2"); + SimpleTest.executeSoon(function() { + is(input.value, "12", "Reframe should restore focus and selection properly"); + SimpleTest.finish(); + }); + }); + }); + + </script> + </pre> + </body> +</html> diff --git a/dom/html/test/test_bug1089326.html b/dom/html/test/test_bug1089326.html new file mode 100644 index 0000000000..fed0a467cd --- /dev/null +++ b/dom/html/test/test_bug1089326.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1089326 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1089326</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1089326 **/ + function test() { + var b = document.getElementById("button"); + var b_rect = b.getBoundingClientRect(); + var a = document.getElementById("anchor"); + var a_rect = a.getBoundingClientRect(); + + is(document.elementFromPoint(b_rect.x + 1, b_rect.y + 1), b, + "Should find button when doing hit test on top of it."); + is(document.elementFromPoint(a_rect.x + 1, a_rect.y + 1), a, + "Should find anchor when doing hit test on top of it."); + + var expectedTarget; + var clickCount = 0; + var container = document.getElementById("interactiveContentContainer"); + container.addEventListener("click", function(event) { + is(event.target, expectedTarget, "Got expected click event target."); + ++clickCount; + }, true); + var i1 = document.getElementById("interactiveContent1"); + var s11 = document.getElementById("s11"); + var s12 = document.getElementById("s12"); + + var i2 = document.getElementById("interactiveContent2"); + var s21 = document.getElementById("s21"); + + expectedTarget = i1; + synthesizeMouseAtCenter(s11, { type: "mousedown" }); + synthesizeMouseAtCenter(s12, { type: "mouseup" }); + is(clickCount, 1, "Should have got a click event."); + + expectedTarget = container; + synthesizeMouseAtCenter(s11, { type: "mousedown" }); + synthesizeMouseAtCenter(s21, { type: "mouseup" }); + is(clickCount, 2, "Should not have got a click event."); + + expectedTarget = container; + synthesizeMouseAtCenter(s21, { type: "mousedown" }); + synthesizeMouseAtCenter(s11, { type: "mouseup" }); + is(clickCount, 3, "Should not have got a click event."); + + var span1 = document.getElementById("span1"); + var span2 = document.getElementById("span2"); + expectedTarget = container; + synthesizeMouseAtCenter(span1, { type: "mousedown" }); + synthesizeMouseAtCenter(span2, { type: "mouseup" }); + is(clickCount, 4, "Should not have got a click event."); + + button.addEventListener("click", function(event) { + is(event.target, expectedTarget, "Got expected click event target."); + ++clickCount; + }, true); + + expectedTarget = a; + synthesizeMouseAtCenter(a, { type: "mousedown" }); + synthesizeMouseAtCenter(a, { type: "mouseup" }); + is(clickCount, 5, "Should have got a click event."); + + expectedTarget = a; + synthesizeMouseAtCenter(b, { type: "mousedown" }); + synthesizeMouseAtCenter(b, { type: "mouseup" }); + is(clickCount, 6, "Should have got a click event."); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(test); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1089326">Mozilla Bug 1089326</a> +<p id="display"></p> +<button id="button">button <a id="anchor" href="#">anchor</a>button</button> + +<div id="interactiveContentContainer"> + <a id="interactiveContent1" href="#">foo <span id="s11">s11</span><span id="s12">s12</span> bar</a> + <a id="interactiveContent2" href="#">foo <span id="s21">s21</span><span id="s22">s22</span> bar</a> + + <div> + <span> + <span id="span1">span1</span> + </span> + </div> + + <div> + <span> + <span id="span2">span2</span> + </span> + </div> +</div> + +</body> +</html> diff --git a/dom/html/test/test_bug109445.html b/dom/html/test/test_bug109445.html new file mode 100644 index 0000000000..27ffe22948 --- /dev/null +++ b/dom/html/test/test_bug109445.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=109445 +--> +<head> + <title>Test for Bug 109445</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=109445">Mozilla Bug 109445</a> +<p id="display"> +<map name=a> +<area shape=rect coords=25,25,75,75 href=#x> +</map> +<map id=b> +<area shape=rect coords=25,25,75,75 href=#y> +</map> +<map name=a> +<area shape=rect coords=25,25,75,75 href=#FAIL> +</map> +<map id=b> +<area shape=rect coords=25,25,75,75 href=#FAIL> +</map> + +<img usemap=#a src=image.png> +<img usemap=#b src=image.png> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 109445 **/ +SimpleTest.waitForExplicitFinish(); +var images = document.getElementsByTagName("img"); +var second = false; +onhashchange = function() { + if (!second) { + second = true; + is(location.hash, "#x", "First map"); + SimpleTest.waitForFocus(() => synthesizeMouse(images[1], 50, 50, {})); + } else { + is(location.hash, "#y", "Second map"); + SimpleTest.finish(); + } +}; +SimpleTest.waitForFocus(() => synthesizeMouse(images[0], 50, 50, {})); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug109445.xhtml b/dom/html/test/test_bug109445.xhtml new file mode 100644 index 0000000000..b1524c8ead --- /dev/null +++ b/dom/html/test/test_bug109445.xhtml @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=109445 +--> +<head> + <title>Test for Bug 109445</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=109445">Mozilla Bug 109445</a> +<p id="display"> +<map name="a"> +<area shape="rect" coords="25,25,75,75" href="#x"/> +</map> +<map id="b"> +<area shape="rect" coords="25,25,75,75" href="#y"/> +</map> +<map name="a"> +<area shape="rect" coords="25,25,75,75" href="#FAIL"/> +</map> +<map id="b"> +<area shape="rect" coords="25,25,75,75" href="#FAIL"/> +</map> + +<img usemap="#a" src="image.png"/> +<img usemap="#b" src="image.png"/> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 109445 **/ +SimpleTest.waitForExplicitFinish(); +var images = document.getElementsByTagName("img"); +var second = false; +onhashchange = function() { + if (!second) { + second = true; + is(location.hash, "#x", "First map"); + SimpleTest.waitForFocus(() => synthesizeMouse(images[1], 50, 50, {})); + } else { + is(location.hash, "#y", "Second map"); + SimpleTest.finish(); + } +}; +SimpleTest.waitForFocus(() => synthesizeMouse(images[0], 50, 50, {})); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1146116.html b/dom/html/test/test_bug1146116.html new file mode 100644 index 0000000000..95d52af9eb --- /dev/null +++ b/dom/html/test/test_bug1146116.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1146116 +--> +<head> + <title>Test for Bug 1146116</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1146116">Mozilla Bug 1146116</a> +<p id="display"> + <input type="file" id="file"> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/** Test for bug 1146116 **/ + +SimpleTest.waitForExplicitFinish(); + +const helperURL = SimpleTest.getTestFileURL("simpleFileOpener.js"); +const helper = SpecialPowers.loadChromeScript(helperURL); +helper.addMessageListener("fail", function onFail(message) { + is(message, null, "chrome script failed"); + SimpleTest.finish(); +}); +helper.addMessageListener("file.opened", onFileOpened); +helper.sendAsyncMessage("file.open", "test_bug1146116.txt"); + +function getGlobal(thing) { + return SpecialPowers.unwrap(SpecialPowers.Cu.getGlobalForObject(thing)); +} + +function onFileOpened(message) { + const file = message.domFile; + const elem = document.getElementById("file"); + is(getGlobal(elem), window, + "getGlobal() works as expected"); + is(getGlobal(file), window, + "File from MessageManager is not wrapped"); + SpecialPowers.wrap(elem).mozSetFileArray([file]); + is(getGlobal(elem.files[0]), window, + "File read back from input element is not wrapped"); + helper.addMessageListener("file.removed", onFileRemoved); + helper.sendAsyncMessage("file.remove", null); +} + +function onFileRemoved() { + helper.destroy(); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1166138.html b/dom/html/test/test_bug1166138.html new file mode 100644 index 0000000000..5b65db6c04 --- /dev/null +++ b/dom/html/test/test_bug1166138.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1166138 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1166138</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1166138">Mozilla Bug 1166138</a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + + <script type="application/javascript"> + var img1x = `${location.origin}/tests/dom/html/test/file_bug1166138_1x.png`; + var img2x = `${location.origin}/tests/dom/html/test/file_bug1166138_2x.png`; + var imgdef = `${location.origin}/tests/dom/html/test/file_bug1166138_def.png`; + var onLoadCallback = null; + var done = false; + + var startPromise = new Promise((a) => { + onLoadCallback = () => { + var image = document.querySelector('img'); + // If we aren't starting at 2x scale, resize to 2x scale, and wait for a load + if (image.currentSrc != img2x) { + onLoadCallback = a; + SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 2]]}); + } else { + a(); + } + }; + }); + + // if aLoad is true, waits for a load event. Otherwise, spins the event loop twice to + // ensure that no events were queued to be fired. + function spin(aLoad) { + if (aLoad) { + return new Promise((a) => { + ok(!onLoadCallback, "Shouldn't be an existing callback"); + onLoadCallback = a; + }); + } else { + return new Promise((a) => SimpleTest.executeSoon(() => SimpleTest.executeSoon(a))); + } + } + + function onLoad() { + if (done) return; + ok(onLoadCallback, "Expected a load event"); + if (onLoadCallback) { + var cb = onLoadCallback; + onLoadCallback = null; + cb(); + } + } + + add_task(async function() { + await startPromise; + var image = document.querySelector('img'); + is(image.currentSrc, img2x, "initial scale must be 2x"); + + SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 1]]}); + await spin(true); + is(image.currentSrc, img1x, "pre-existing img tag to 1x"); + + SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 2]]}); + await spin(true); + is(image.currentSrc, img2x, "pre-existing img tag to 2x"); + + // Try removing & re-adding the image + document.body.removeChild(image); + + SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 1]]}); + await spin(false); // No load should occur because the element is unbound + + document.body.appendChild(image); + await spin(true); + is(image.currentSrc, img1x, "remove and re-add tag after changing to 1x"); + + document.body.removeChild(image); + SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 2]]}); + await spin(false); // No load should occur because the element is unbound + + document.body.appendChild(image); + await spin(true); + is(image.currentSrc, img2x, "remove and re-add tag after changing to 2x"); + + // get rid of the srcset attribute! It should become the default + image.removeAttribute('srcset'); + await spin(true); + is(image.currentSrc, imgdef, "remove srcset attribute"); + + // Setting srcset again should return it to the correct value + image.setAttribute('srcset', "file_bug1166138_1x.png 1x, file_bug1166138_2x.png 2x"); + await spin(true); + is(image.currentSrc, img2x, "restore srcset attribute"); + + // Create a new image + var newImage = document.createElement('img'); + // Switch load listening over to newImage + newImage.addEventListener('load', onLoad); + image.removeEventListener('load', onLoad); + + document.body.appendChild(newImage); + await spin(false); // no load event should fire - as the image has no attributes + is(newImage.currentSrc, "", "New element with no attributes"); + newImage.setAttribute('srcset', "file_bug1166138_1x.png 1x, file_bug1166138_2x.png 2x"); + await spin(true); + is(newImage.currentSrc, img2x, "Adding srcset attribute"); + + SpecialPowers.pushPrefEnv({'set': [['layout.css.devPixelsPerPx', 1]]}); + await spin(true); + is(newImage.currentSrc, img1x, "new image after switching to 1x"); + is(image.currentSrc, img1x, "old image after switching to 1x"); + + // Clear the listener + done = true; + }); + </script> + + <img srcset="file_bug1166138_1x.png 1x, file_bug1166138_2x.png 2x" + src="file_bug1166138_def.png" + onload="onLoad()"> + +</body> +</html> diff --git a/dom/html/test/test_bug1203668.html b/dom/html/test/test_bug1203668.html new file mode 100644 index 0000000000..41249d90ab --- /dev/null +++ b/dom/html/test/test_bug1203668.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1203668 +--> +<head> + <title>Test for Bug 1203668</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1203668">Mozilla Bug 1203668</a> +<p id="display"></p> +<div id="content"> + <select class="select" multiple> + <option value="foo" selected>foo</option> + <option value="bar" selected>bar</option> + </select> + <select class="select" multiple> + <option value="foo">foo</option> + <option value="bar" selected>bar</option> + </select> + <select class="select" multiple> + <option value="foo">foo</option> + <option value="bar">bar</option> + </select> + <select class="select" size=1> + <option value="foo">foo</option> + <option value="bar" selected>bar</option> + </select> + <select class="select" size=1> + <option value="foo">foo</option> + <option value="bar">bar</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1203668 **/ + +SimpleTest.waitForExplicitFinish(); + +function runTest() +{ + var selects = document.querySelectorAll('.select'); + for (i=0; i < selects.length; i++) { + var select = selects[i]; + select.value = "bogus" + is(select.selectedIndex, -1, "no option is selected"); + is(select.children[0].selected, false, "first option is not selected"); + is(select.children[1].selected, false, "second option is not selected"); + } + + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1230665.html b/dom/html/test/test_bug1230665.html new file mode 100644 index 0000000000..cbe9c91d30 --- /dev/null +++ b/dom/html/test/test_bug1230665.html @@ -0,0 +1,46 @@ +<html> +<head> + <title>Test for Bug 1230665</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + document.getElementById("flexbutton1").focus(); + synthesizeKey("KEY_Tab"); + var e = document.getElementById("flexbutton2"); + is(document.activeElement, e, "focus in flexbutton2 after TAB"); + + document.getElementById("gridbutton1").focus(); + synthesizeKey("KEY_Tab"); + e = document.getElementById("gridbutton2"); + is(document.activeElement, e, "focus in gridbutton2 after TAB"); + + SimpleTest.finish(); +}); + +</script> + +<div tabindex="0" style="display:flex"> + <button id="flexbutton1"></button> + text <!-- this text will force a :-moz-anonymous-flex-item frame --> + <div style=""> + <button id="flexbutton2"></button> + </div> +</div> + + +<div tabindex="0" style="display:grid"> + <button id="gridbutton1"></button> + text <!-- this text will force a :-moz-anonymous-grid-item frame --> + <div style=""> + <button id="gridbutton2"></button> + </div> +</div> + +</body> +</html> diff --git a/dom/html/test/test_bug1250401.html b/dom/html/test/test_bug1250401.html new file mode 100644 index 0000000000..d4a1073856 --- /dev/null +++ b/dom/html/test/test_bug1250401.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1250401 +--> +<head> + <title>Test for Bug 1250401</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1250401">Bug 1250401</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1250401 **/ +function test_add() { + var select = document.createElement("select"); + + var g1 = document.createElement("optgroup"); + var o1 = document.createElement("option"); + g1.appendChild(o1); + select.appendChild(g1); + + var g2 = document.createElement("optgroup"); + var o2 = document.createElement("option"); + g2.appendChild(o2); + select.add(g2, 0); + + is(select.children.length, 1, "Select has 1 item"); + is(select.firstChild, g1, "First item is g1"); + is(select.firstChild.children.length, 2, "g2 has 2 children"); + is(select.firstChild.children[0], g2, "g1 has 2 children: g2"); + is(select.firstChild.children[1], o1, "g1 has 2 children: o1"); + is(o1.index, 0, "o1.index should be 0"); + is(o2.index, 0, "o2.index should be 0"); +} + +function test_append() { + var select = document.createElement("select"); + + var g1 = document.createElement("optgroup"); + var o1 = document.createElement("option"); + g1.appendChild(o1); + select.appendChild(g1); + + var g2 = document.createElement("optgroup"); + var o2 = document.createElement("option"); + g2.appendChild(o2); + g1.appendChild(g2); + + is(select.children.length, 1, "Select has 1 item"); + is(select.firstChild, g1, "First item is g1"); + is(select.firstChild.children.length, 2, "g2 has 2 children"); + is(select.firstChild.children[0], o1, "g1 has 2 children: o1"); + is(select.firstChild.children[1], g2, "g1 has 2 children: g1"); + is(o1.index, 0, "o1.index should be 0"); + is(o2.index, 0, "o2.index should be 0"); +} + +function test_no_select() { + var g1 = document.createElement("optgroup"); + var o1 = document.createElement("option"); + g1.appendChild(o1); + + var g2 = document.createElement("optgroup"); + var o2 = document.createElement("option"); + g2.appendChild(o2); + g1.appendChild(g2); + + is(g1.children.length, 2, "g2 has 2 children"); + is(g1.children[0], o1, "g1 has 2 children: o1"); + is(g1.children[1], g2, "g1 has 2 children: g1"); + is(o1.index, 0, "o1.index should be 0"); + is(o2.index, 0, "o2.index should be 0"); +} + +function test_no_parent() { + var o1 = document.createElement("option"); + var o2 = document.createElement("option"); + + is(o1.index, 0, "o1.index should be 0"); + is(o2.index, 0, "o2.index should be 0"); +} + +test_add(); +test_append(); +test_no_select(); +test_no_parent(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1260664.html b/dom/html/test/test_bug1260664.html new file mode 100644 index 0000000000..99878a46b6 --- /dev/null +++ b/dom/html/test/test_bug1260664.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1260664 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1260664</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1260664">Mozilla Bug 1260664</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1260664 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + var elements = [ "iframe", "img", "a", "area", "link", "script"]; + + for (var i = 0; i < elements.length; ++i) { + reflectLimitedEnumerated({ + element: document.createElement(elements[i]), + attribute: { content: "referrerpolicy", idl: "referrerPolicy" }, + validValues: [ "no-referrer", + "origin", + /** These 2 below values are still invalid, please see + Bug 1178337 - Valid referrer attribute values **/ + /** "no-referrer-when-downgrade", + "origin-when-cross-origin", **/ + "unsafe-url" ], + invalidValues: [ + "", " orIgin ", " unsafe-uRl ", " No-RefeRRer ", " fOoBaR " + ], + defaultValue: "", + }); + } + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1260704.html b/dom/html/test/test_bug1260704.html new file mode 100644 index 0000000000..ca576051b0 --- /dev/null +++ b/dom/html/test/test_bug1260704.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1260704 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1260704</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="text/javascript"> + /** Test for Bug 1260704 **/ + +function runTests() { + let testIdx = -1; + let testUrls = [ + "bug1260704_iframe.html?noDefault=true&isMap=true", + "bug1260704_iframe.html?noDefault=true&isMap=false", + "bug1260704_iframe.html?noDefault=false&isMap=true", + "bug1260704_iframe.html?noDefault=false&isMap=false" + ]; + + let runningTest = false; + let iframe = document.getElementById("testFrame"); + let iframeWin = iframe.contentWindow; + let rect; + let x; + let y; + + window.addEventListener("message", event => { + if (event.data == "started") { + ok(!runningTest, "Start to test " + testIdx); + runningTest = true; + rect = iframeWin.document.getElementById("testImage").getBoundingClientRect(); + x = rect.width / 2; + y = rect.height / 2; + synthesizeMouseAtPoint(rect.left + x, rect.top + y, { type: 'mousedown' }, iframeWin); + synthesizeMouseAtPoint(rect.left + x, rect.top + y, { type: 'mouseup' }, iframeWin); + } + else if (runningTest && event.data == "empty_frame_loaded") { + ok(testUrls[testIdx].includes("noDefault=false"), "Page unload"); + let search = iframeWin.location.search; + if (testUrls[testIdx].includes("isMap=true")) { + // url trigger by image with ismap attribute should contains coordinates + // try to parse coordinates and check them with small tolerance + let coorStr = search.split("?"); + let coordinates = coorStr[1].split(","); + ok(Math.abs(coordinates[0] - x) <= 1, "expect X=" + x + " got " + coordinates[0]); + ok(Math.abs(coordinates[1] - y) <= 1, "expect Y=" + y + " got " + coordinates[1]); + } else { + ok(search == "", "expect empty search string got:" + search); + } + nextTest(); + } + else if (runningTest && event.data == "finished") { + ok(testUrls[testIdx].includes("noDefault=true"), "Page should not leave"); + nextTest(); + } + }); + + function nextTest() { + testIdx++; + runningTest = false; + if (testIdx >= testUrls.length) { + SimpleTest.finish(); + } else { + ok(true, "Test " + testIdx + " - Set url to " + testUrls[testIdx]); + iframeWin.location.href = testUrls[testIdx]; + } + } + nextTest(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + + </script> +</head> +<body> + +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<iframe id="testFrame" src="about:blank" width="400" height="400"> +</iframe> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1261673.html b/dom/html/test/test_bug1261673.html new file mode 100644 index 0000000000..c574967dd7 --- /dev/null +++ b/dom/html/test/test_bug1261673.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1261673 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1261673</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261673">Mozilla Bug 1261673</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input id="test_number" type="number" value=5> +<script type="text/javascript"> + +/** Test for Bug 1261673 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + let input = window.document.getElementById("test_number"); + + // focus: whether the target input element is focused + // deltaY: deltaY of WheelEvent + // deltaMode: deltaMode of WheelEvent + // valueChanged: expected value changes after input element handled the wheel event + let params = [ + {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: -1}, + {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 1}, + {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: -1}, + {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: 1}, + {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0}, + {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0}, + {focus: false, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0}, + {focus: false, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0} + ]; + + let testIdx = 0; + let result = parseInt(input.value); + let numberChange = 0; + let expectChange = 0; + + input.addEventListener("change", () => { + ++numberChange; + }); + + function runNext() { + let p = params[testIdx]; + (p.focus) ? input.focus() : input.blur(); + expectChange = p.valueChanged == 0 ? expectChange : expectChange + 1; + result += parseInt(p.valueChanged); + sendWheelAndPaint(input, 1, 1, { deltaY: p.deltaY, deltaMode: p.deltaMode }, () => { + ok(input.value == result, + "Handle wheel in number input test-" + testIdx + " expect " + result + + " get " + input.value); + ok(numberChange == expectChange, + "UA should fire change event when input's value changed, expect " + expectChange + " get " + numberChange); + (++testIdx >= params.length) ? SimpleTest.finish() : runNext(); + }); + } + runNext(); +} + +</script> +</body> +</html> diff --git a/dom/html/test/test_bug1261674-1.html b/dom/html/test/test_bug1261674-1.html new file mode 100644 index 0000000000..a9042be733 --- /dev/null +++ b/dom/html/test/test_bug1261674-1.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1261674 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1261674</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261674">Mozilla Bug 1261674</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input id="test_input" type="range" value=5 max=10 min=0> +<script type="text/javascript"> + +/** Test for Bug 1261674 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + let input = window.document.getElementById("test_input"); + + // focus: whether the target input element is focused + // deltaY: deltaY of WheelEvent + // deltaMode: deltaMode of WheelEvent + // valueChanged: expected value changes after input element handled the wheel event + let params = [ + {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: -1}, + {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 1}, + {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: -1}, + {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE, valueChanged: 1}, + {focus: true, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0}, + {focus: true, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL, valueChanged: 0}, + {focus: false, deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0}, + {focus: false, deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE, valueChanged: 0} + ]; + + let testIdx = 0; + let result = parseInt(input.value); + let rangeChange = 0; + let expectChange = 0; + + input.addEventListener("change", () => { + ++rangeChange; + }); + + function runNext() { + let p = params[testIdx]; + (p.focus) ? input.focus() : input.blur(); + expectChange = p.valueChanged == 0 ? expectChange : expectChange + 1; + result += parseInt(p.valueChanged); + sendWheelAndPaint(input, 1, 1, { deltaY: p.deltaY, deltaMode: p.deltaMode }, () => { + ok(input.value == result, + "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value); + ok(rangeChange == expectChange, + "UA should fire change event when input's value changed, expect " + expectChange + " get " + rangeChange); + (++testIdx >= params.length) ? SimpleTest.finish() : runNext(); + }); + } + + input.addEventListener("input", () => { + ok(input.value == result, + "Test-" + testIdx + " receive input event, expect " + result + " get " + input.value); + }); + + runNext(); +} + +</script> +</body> +</html> diff --git a/dom/html/test/test_bug1261674-2.html b/dom/html/test/test_bug1261674-2.html new file mode 100644 index 0000000000..cfda243749 --- /dev/null +++ b/dom/html/test/test_bug1261674-2.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1261674 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1261674</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1261674">Mozilla Bug 1261674</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input id="test_input" type="range" max=0 min=10> +<script type="text/javascript"> + +/** Test for Bug 1261674 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + let input = window.document.getElementById("test_input"); + + // deltaY: deltaY of WheelEvent + // deltaMode: deltaMode of WheelEvent + let params = [ + {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}, + {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}, + {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE}, + {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PAGE}, + {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL}, + {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_PIXEL}, + {deltaY: 1.0, deltaMode: WheelEvent.DOM_DELTA_LINE}, + {deltaY: -1.0, deltaMode: WheelEvent.DOM_DELTA_LINE} + ]; + + let testIdx = 0; + let result = parseInt(input.value); + let rangeChange = 0; + + input.addEventListener("change", () => { + ++rangeChange; + }); + + function runNext() { + let p = params[testIdx]; + (p.focus) ? input.focus() : input.blur(); + sendWheelAndPaint(input, 1, 1, { deltaY: p.deltaY, deltaMode: p.deltaMode }, () => { + ok(input.value == result, + "Handle wheel in range input test-" + testIdx + " expect " + result + " get " + input.value); + ok(rangeChange == 0, "Wheel event should not trigger change event when max < min"); + testIdx++; + (testIdx >= params.length) ? SimpleTest.finish() : runNext(); + }); + } + + input.addEventListener("input", () => { + ok(false, "Wheel event should be no effect to range input element with max < min"); + }); + + runNext(); +} +</script> +</body> +</html> diff --git a/dom/html/test/test_bug1264157.html b/dom/html/test/test_bug1264157.html new file mode 100644 index 0000000000..1bede807da --- /dev/null +++ b/dom/html/test/test_bug1264157.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=535043 +--> +<head> + <title>Test for Bug 535043</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input { + outline: 2px solid lime; + } + input:in-range { + outline: 2px solid red; + } + input:out-of-range { + outline: 2px solid orange; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=535043">Mozilla Bug 535043</a> +<p id="display"></p> +<div id="content"> + +</head> +<body> + <input type="number" value=0 min=0 max=10> Active in-range + <br><br> + <input type="number" value=0 min=0 max=10 disabled> Disabled in-range + <br><br> + <input type="number" value=0 min=0 max=10 readonly> Read-only in-range + <br><br> + <input type="number" value=11 min=0 max=10> Active out-of-range + <br><br> + <input type="number" value=11 min=0 max=10 disabled> Disabled out-of-range + <br><br> + <input type="number" value=11 min=0 max=10 readonly> Read-only out-of-range +</div> +<pre id="test"> +<script> + +/** Test for Bug 1264157 **/ +SimpleTest.waitForFocus(function() { + // Check the initial values. + let active = [].slice.call(document.querySelectorAll("input:not(:disabled):not(:read-only)")); + let disabled = [].slice.call(document.querySelectorAll("input:disabled")); + let readonly = [].slice.call(document.querySelectorAll("input:read-only:not(:disabled)")); + is(active.length, 2, "Test is messed up: missing non-disabled/non-readonly inputs"); + is(disabled.length, 2, "Test is messed up: missing disabled inputs"); + is(readonly.length, 2, "Test is messed up: missing readonly inputs"); + + is(document.querySelectorAll("input:in-range").length, 1, + "Wrong number of in-range elements selected."); + is(document.querySelectorAll("input:out-of-range").length, 1, + "Wrong number of out-of-range elements selected."); + + // Dynamically change the values to see if that works too. + active[0].value = -1; + is(document.querySelectorAll("input:in-range").length, 0, + "Wrong number of in-range elements selected after value changed."); + is(document.querySelectorAll("input:out-of-range").length, 2, + "Wrong number of out-of-range elements selected after value changed."); + active[0].value = 0; + is(document.querySelectorAll("input:in-range").length, 1, + "Wrong number of in-range elements selected after value changed back."); + is(document.querySelectorAll("input:out-of-range").length, 1, + "Wrong number of out-of-range elements selected after value changed back."); + + // Dynamically change the attributes to see if that works too. + disabled.forEach(function(e) { e.removeAttribute("disabled"); }); + readonly.forEach(function(e) { e.removeAttribute("readonly"); }); + active.forEach(function(e) { e.setAttribute("readonly", true); }); + + is(document.querySelectorAll("input:in-range").length, 2, + "Wrong number of in-range elements selected after attribute changed."); + is(document.querySelectorAll("input:out-of-range").length, 2, + "Wrong number of out-of-range elements selected after attribute changed."); + + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1279218.html b/dom/html/test/test_bug1279218.html new file mode 100644 index 0000000000..0d8386280d --- /dev/null +++ b/dom/html/test/test_bug1279218.html @@ -0,0 +1,23 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>Test for Bug 1279218</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript"> + function load() { + let applets = document.applets; + is(applets.length, 0, "Applet list length should be 0, even with applet tag in body"); + SimpleTest.finish(); + } + + window.onload=load; + + SimpleTest.waitForExplicitFinish(); + </script> + </head> + <body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1279218">Mozilla Bug 1279218</a> + <applet id="applet-test"></applet> + </body> +</html> diff --git a/dom/html/test/test_bug1287321.html b/dom/html/test/test_bug1287321.html new file mode 100644 index 0000000000..142b06d104 --- /dev/null +++ b/dom/html/test/test_bug1287321.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1287321 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1287321</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1287321 **/ + + function test() { + var r = document.getElementById("range"); + var rect = r.getBoundingClientRect(); + var y = parseInt((rect.height / 2)); + var movement = parseInt(rect.width / 10); + var x = movement; + synthesizeMouse(r, x, y, { type: "mousedown" }); + x += movement; + var eventCount = 0; + r.oninput = function() { + ++eventCount; + } + synthesizeMouse(r, x, y, { type: "mousemove" }); + is(eventCount, 1, "Got the expected input event"); + + x += movement; + synthesizeMouse(r, x, y, { type: "mousemove" }); + is(eventCount, 2, "Got the expected input event"); + + synthesizeMouse(r, x, y, { type: "mousemove" }); + is(eventCount, 2, "Got the expected input event"); + + x += movement; + synthesizeMouse(r, x, y, { type: "mousemove" }); + is(eventCount, 3, "Got the expected input event"); + + synthesizeMouse(r, x, y, { type: "mouseup" }); + is(eventCount, 3, "Got the expected input event"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + SimpleTest.waitForFocus(test); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1287321">Mozilla Bug 1287321</a> +<input type="range" id="range"> +</body> +</html> diff --git a/dom/html/test/test_bug1292522_same_domain_with_different_port_number.html b/dom/html/test/test_bug1292522_same_domain_with_different_port_number.html new file mode 100644 index 0000000000..b7a443f6a7 --- /dev/null +++ b/dom/html/test/test_bug1292522_same_domain_with_different_port_number.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1292522 +If we set domain using document.domain = "...", a page and iframe must be +treated as the same domain if they differ in port number, +e.g. test1.example.org:8000 and test2.example.org:80 are the same domain if +document.domain = "example.org". +--> +<head> + <title>Test for Bug 1292522</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + <body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1292522">Mozilla Bug 1292522</a> + <p id="display"></p> + + <pre id="test"> + <script class="testbody" type="text/javascript"> + + if (navigator.platform.startsWith("Linux")) { + SimpleTest.expectAssertions(0, 1); + } + SimpleTest.waitForExplicitFinish(); + window.addEventListener("message", onMessageReceived); + + var page; + + function onMessageReceived(event) + { + is(event.data, "testiframe", "Must be able to access the variable," + + " because page and iframe are the " + + "same domain."); + page.close(); + SimpleTest.finish(); + } + + page = window.open("http://test1.example.org:8000/tests/dom/html/test/bug1292522_page.html"); + </script> + </pre> + </body> +</html> diff --git a/dom/html/test/test_bug1295719_event_sequence_for_arrow_keys.html b/dom/html/test/test_bug1295719_event_sequence_for_arrow_keys.html new file mode 100644 index 0000000000..4e622391e8 --- /dev/null +++ b/dom/html/test/test_bug1295719_event_sequence_for_arrow_keys.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1295719 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1295719</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1295719">Mozilla Bug 1295719</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input id="test_number" type="number" value=50> +<input id="test_range" type="range" value=50 max=100 min=0> +<script type="text/javascript"> + +/** Test for Bug 1295719 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + let number = window.document.getElementById("test_number"); + let range = window.document.getElementById("test_range"); + let waiting_event_sequence = ["keydown", "input", "change"]; + let waiting_event_idx = 0; + waiting_event_sequence.forEach((eventType) => { + number.addEventListener(eventType, (event) => { + let waiting_event = waiting_event_sequence[waiting_event_idx]; + is(waiting_event, eventType, "Waiting " + waiting_event + " get " + eventType); + // Input element will fire input and change events when handling keypress + // with keycode=arrows. When user press and hold the keyboard, we expect + // that input element repeatedly fires "keydown"(, "keypress"), "input", and + // "change" events until user release the keyboard. Using + // waiting_event_sequence as a circular buffer and reset waiting_event_idx + // when it point to the end of buffer. + waiting_event_idx = waiting_event_idx == waiting_event_sequence.length -1 ? 0 : waiting_event_idx + 1; + }); + range.addEventListener(eventType, (event) => { + let waiting_event = waiting_event_sequence[waiting_event_idx]; + is(waiting_event, eventType, "Waiting " + waiting_event + " get " + eventType); + waiting_event_idx = waiting_event_idx == waiting_event_sequence.length - 1 ? 0 : waiting_event_idx + 1; + }); + }); + + number.focus(); + synthesizeKey("KEY_ArrowDown", {type: "keydown"}); + synthesizeKey("KEY_ArrowDown", {type: "keydown"}); + synthesizeKey("KEY_ArrowDown", {type: "keyup"}); + number.blur(); + range.focus(); + waiting_event_idx = 0; + synthesizeKey("KEY_ArrowDown", {type: "keydown"}); + synthesizeKey("KEY_ArrowDown", {type: "keydown"}); + synthesizeKey("KEY_ArrowDown", {type: "keyup"}); + + SimpleTest.finish(); +} + +</script> +</body> +</html> diff --git a/dom/html/test/test_bug1295719_event_sequence_for_number_keys.html b/dom/html/test/test_bug1295719_event_sequence_for_number_keys.html new file mode 100644 index 0000000000..f8f0537ddb --- /dev/null +++ b/dom/html/test/test_bug1295719_event_sequence_for_number_keys.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1295719 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1295719</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1295719">Mozilla Bug 1295719</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<input id="test_number" type="number" value=50> +<script type="text/javascript"> + +/** Test for Bug 1295719 **/ +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTests); + +function runTests() { + let number = window.document.getElementById("test_number"); + let waiting_event_sequence = ["keydown", "keypress", "input"]; + let change_event_of_number = 0; + let keyup_event_of_number = 0; + let waiting_event_idx = 0; + waiting_event_sequence.forEach((eventType) => { + number.addEventListener(eventType, (event) => { + let waiting_event = waiting_event_sequence[waiting_event_idx]; + is(eventType, waiting_event, "Waiting " + waiting_event + " get " + eventType); + // Input element will fire input event when handling keypress with + // keycode=numbers. When user press and hold the keyboard, we expect that + // input element repeatedly fires "keydown", "keypress", and "input" until + // user release the keyboard. Input element will fire change event when + // it's blurred. Using waiting_event_sequence as a circular buffer and + // reset waiting_event_idx when it point to the end of buffer. + waiting_event_idx = waiting_event_idx == waiting_event_sequence.length - 1 ? 0 : waiting_event_idx + 1; + }); + }); + number.addEventListener("change", (event) => { + is(keyup_event_of_number, 1, "change event should be fired after blurred"); + ++change_event_of_number; + }); + number.addEventListener("keyup", (event) => { + is(keyup_event_of_number, 0, "keyup event should be fired once"); + is(change_event_of_number, 0, "keyup event should be fired before change event"); + ++keyup_event_of_number; + }); + number.focus(); + synthesizeKey("5", {type: "keydown"}); + synthesizeKey("5", {type: "keydown"}); + synthesizeKey("5", {type: "keyup"}); + is(change_event_of_number, 0, "change event shouldn't be fired when input element is focused"); + number.blur(); + is(change_event_of_number, 1, "change event should be fired when input element is blurred"); + SimpleTest.finish(); +} + +</script> +</body> +</html> diff --git a/dom/html/test/test_bug1297.html b/dom/html/test/test_bug1297.html new file mode 100644 index 0000000000..d0c96c87d4 --- /dev/null +++ b/dom/html/test/test_bug1297.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1297 +--> +<head> + <title>Test for Bug 1297</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1297">Mozilla Bug 1297</a> +<p id="display"></p> +<div id="content" style="display: none"> +<table border=1> +<tr> +<td id="td1" onmousedown="alert(this.cellIndex)">cellIndex=0</td> +<td id="td2" onmousedown="alert(this.cellIndex)">cellIndex=1</td> +<td id="td3" onmousedown="alert(this.cellIndex)">cellIndex=2</td> +<tr id="tr1" +onmousedown="alert(this.rowIndex)"><td>rowIndex=1<td>rowIndex=1<td>rowIndex=1</t +r> +<tr id="tr2" +onmousedown="alert(this.rowIndex)"><td>rowIndex=2<td>rowIndex=2<td>rowIndex=2</t +r> +</tr> +</table> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1297 **/ +is($('td1').cellIndex, 0, "cellIndex / rowIndex working td1"); +is($('td2').cellIndex, 1, "cellIndex / rowIndex working td2"); +is($('td3').cellIndex, 2, "cellIndex / rowIndex working td3"); +is($('tr1').rowIndex, 1, "cellIndex / rowIndex working tr1"); +is($('tr2').rowIndex, 2, "cellIndex / rowIndex working tr2"); + + + + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug1310865.html b/dom/html/test/test_bug1310865.html new file mode 100644 index 0000000000..4dcccbfa0d --- /dev/null +++ b/dom/html/test/test_bug1310865.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<title>Test for Bug 1310865</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<input value="a +b" type="hidden"> +<input type="hidden" value="a +b"> +<script> +var input1 = document.querySelector("input"); +var input2 = document.querySelector("input + input"); +var clone1 = input1.cloneNode(false); +var clone2 = input2.cloneNode(false); +// Newlines must not be stripped +is(clone1.value, "a\nb"); +is(clone2.value, "a\nb"); +</script> diff --git a/dom/html/test/test_bug1315146.html b/dom/html/test/test_bug1315146.html new file mode 100644 index 0000000000..0cf25b36bf --- /dev/null +++ b/dom/html/test/test_bug1315146.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1315146 +--> +<head> + <title>Test for Bug 1315146</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315146">Mozilla Bug 1315146</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 1315146 **/ + +SimpleTest.waitForExplicitFinish(); +onmessage = function(e) { + win.close(); + is(e.data.start, 2, "Correct start offset expected"); + is(e.data.end, 2, "Correct end offset expected"); + SimpleTest.finish(); +}; +let win = window.open("http://test1.example.org/tests/dom/html/test/bug1315146-main.html", "_blank"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1322678.html b/dom/html/test/test_bug1322678.html new file mode 100644 index 0000000000..57b43f039c --- /dev/null +++ b/dom/html/test/test_bug1322678.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1322678 +--> +<head> +<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> +<title>Test for Bug 1322678</title> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + +const CUSTOM_TITLE = "Custom Title"; + +async function openNewWindowForTest() { + let win = window.open("bug369370-popup.png", "bug1322678", + "width=400,height=300,scrollbars=no"); + ok(win, "opened child window"); + + await new Promise(resolve => { + win.onload = function() { + ok(true, "window loaded"); + resolve(); + }; + }); + + return win; +} + +async function testCustomTitle(aWin, aTitle) { + let doc = aWin.document; + let elements = doc.getElementsByTagName("img"); + is(elements.length, 1, "looking for img in ImageDocument"); + let img = elements[0]; + + // Click to zoom in + synthesizeMouse(img, 25, 25, { }, aWin); + is(doc.title, aTitle, "Checking title"); + + // Click there again to zoom out + synthesizeMouse(img, 25, 25, { }, aWin); + is(doc.title, aTitle, "Checking title"); + + // Now try resizing the window so the image fits vertically and horizontally. + await new Promise(resolve => { + aWin.addEventListener("resize", function() { + // Give the image document time to respond + SimpleTest.executeSoon(function() { + is(doc.title, aTitle, "Checking title"); + resolve(); + }); + }, {once: true}); + + let decorationSize = aWin.outerHeight - aWin.innerHeight; + aWin.resizeTo(800 + 50 + decorationSize, 600 + 50 + decorationSize); + }); + + // Now try resizing the window so the image no longer fits. + await new Promise(resolve => { + aWin.addEventListener("resize", function() { + // Give the image document time to respond + SimpleTest.executeSoon(function() { + is(doc.title, aTitle, "Checking title"); + resolve(); + }); + }, {once: true}); + + aWin.resizeTo(400, 300); + }); +} + +// eslint-disable-next-line mozilla/no-addtask-setup +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [ + ["browser.enable_automatic_image_resizing", true], + ]}); +}); + +add_task(async function testUpdateDocumentTitle() { + let win = await openNewWindowForTest(); + // Set custom title. + win.document.title = CUSTOM_TITLE; + await testCustomTitle(win, CUSTOM_TITLE); + win.close(); +}); + +add_task(async function testUpdateTitleElement() { + let win = await openNewWindowForTest(); + // Set custom title. + let title = win.document.getElementsByTagName("title")[0]; + title.text = CUSTOM_TITLE; + await testCustomTitle(win, CUSTOM_TITLE); + win.close(); +}); + +add_task(async function testAppendNewTitleElement() { + let win = await openNewWindowForTest(); + // Set custom title. + let doc = win.document; + doc.getElementsByTagName("title")[0].remove(); + let title = doc.createElement("title"); + title.text = CUSTOM_TITLE; + doc.head.appendChild(title); + await testCustomTitle(win, CUSTOM_TITLE); + win.close(); +}); + +</script> +</body> +</html> diff --git a/dom/html/test/test_bug1323815.html b/dom/html/test/test_bug1323815.html new file mode 100644 index 0000000000..47e223aa7b --- /dev/null +++ b/dom/html/test/test_bug1323815.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1323815 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1323815</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1323815 **/ + +SimpleTest.waitForExplicitFinish(); +function test() { + var n = document.getElementById("number"); + var t = document.getElementById("text"); + t.focus(); + var gotBlur = false; + t.onblur = function(e) { + try { + is(e.relatedTarget.localName, "input"); + } catch(ex) { + ok(false, "Accessing properties on the relatedTarget shouldn't throw! " + ex); + } + gotBlur = true; + } + + n.focus(); + ok(gotBlur); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(test); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1323815">Mozilla Bug 1323815</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<input type="number" id="number"><input type="text" id="text"> +</body> +</html> diff --git a/dom/html/test/test_bug1366.html b/dom/html/test/test_bug1366.html new file mode 100644 index 0000000000..f29179509f --- /dev/null +++ b/dom/html/test/test_bug1366.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1366 +--> +<head> + <title>Test for Bug 1366</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1366">Mozilla Bug 1366</a> +<p id="display"></p> +<div id="content" style="display: none"> +<table id="testtable" width=150 border> + <tbody id="testbody"> + <tr> + <td>cell content</td> + </tr> + </tbody> +</table> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1366 **/ +$('testtable').removeChild($('testbody')); +$('display').innerHTML = "SCRIPT: deleted first ROWGROUP\n"; +is($('testbody'), null, "deleting tbody works"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug1400.html b/dom/html/test/test_bug1400.html new file mode 100644 index 0000000000..38e87a56da --- /dev/null +++ b/dom/html/test/test_bug1400.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1400 +--> +<head> + <title>Test for Bug 1400</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1400">Mozilla Bug 1400</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1400 **/ + +table = document.createElement("TABLE"); +thead = table.createTHead(); +thead2 = table.createTHead(); + +table.appendChild(thead); +table.appendChild(thead); +table.appendChild(thead); +table.appendChild(thead2); +table.appendChild(thead2); +table.appendChild(thead2); +table.appendChild(thead); +table.appendChild(thead2); + +is(table.childNodes.length, 1, + "adding multiple theads results in one thead child"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug1414077.html b/dom/html/test/test_bug1414077.html new file mode 100644 index 0000000000..aa430c2737 --- /dev/null +++ b/dom/html/test/test_bug1414077.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1414077 +--> +<head> +<meta charset="utf-8"> +<title>Test for Bug 1414077</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +<script type="application/javascript"> + +/** Test for Bug 1414077 **/ + +add_task(async function() { + await SpecialPowers.pushPrefEnv({"set": [["browser.enable_automatic_image_resizing", true]]}); + + return new Promise(resolve => { + var testWin = document.querySelector("iframe"); + testWin.src = "image.png"; + testWin.onload = function() { + var testDoc = testWin.contentDocument; + + // testDoc should be a image document. + ok(testDoc.imageIsOverflowing, "image is overflowing"); + ok(testDoc.imageIsResized, "image is resized to fit visible area by default"); + + // Restore image to original size. + testDoc.restoreImage(); + ok(testDoc.imageIsOverflowing, "image is overflowing"); + ok(!testDoc.imageIsResized, "image is restored to original size"); + + // Resize the image to fit visible area + testDoc.shrinkToFit(); + ok(testDoc.imageIsOverflowing, "image is overflowing"); + ok(testDoc.imageIsResized, "image is resized to fit visible area"); + + resolve(); + }; + }) +}); + +</script> +</head> + +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1414077">Mozilla Bug 1414077</a> +<iframe width="0" height="0"></iframe> +</body> +</html> diff --git a/dom/html/test/test_bug143220.html b/dom/html/test/test_bug143220.html new file mode 100644 index 0000000000..f94ec5571e --- /dev/null +++ b/dom/html/test/test_bug143220.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=143220 +--> +<head> + <title>Test for Bug 143220</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=143220">Mozilla Bug 143220</a> +<p id="display"> + <input type="file" id="i1"> + <input type="file" id="i2"> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 143220 **/ +SimpleTest.waitForExplicitFinish(); +const helperURL = SimpleTest.getTestFileURL("simpleFileOpener.js"); +const helper = SpecialPowers.loadChromeScript(helperURL); +helper.addMessageListener("fail", function onFail(message) { + is(message, null, "chrome script failed"); + SimpleTest.finish(); +}); +helper.addMessageListener("file.opened", onFileOpened); +helper.sendAsyncMessage("file.open", "test_bug143220.txt"); + +function onFileOpened(message) { + const { leafName, fullPath, domFile } = message; + + function initControl1() { + SpecialPowers.wrap($("i1")).mozSetFileArray([domFile]); + } + + function initControl2() { + SpecialPowers.wrap($("i2")).mozSetFileArray([domFile]); + } + + // Check that we can't just set the value + try { + $("i1").value = fullPath; + is(0, 1, "Should have thrown exception on set!"); + } catch(e) { + is($("i1").value, "", "Shouldn't have value here"); + } + + initControl1(); + initControl2(); + + is($("i1").value, 'C:\\fakepath\\' + leafName, "Leaking full value?"); + is($("i2").value, 'C:\\fakepath\\' + leafName, "Leaking full value?"); + + helper.addMessageListener("file.removed", onFileRemoved); + helper.sendAsyncMessage("file.remove", null); +} + +function onFileRemoved() { + helper.destroy(); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug1472426.html b/dom/html/test/test_bug1472426.html new file mode 100644 index 0000000000..6f891184b8 --- /dev/null +++ b/dom/html/test/test_bug1472426.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1472426 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 1472426</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 1472426 **/ + + var shadowIframe; + var targetIframe; + var form; + var sr; + + function checkMPSubmission(sub, expected, test) { + function getPropCount(o) { + var x, l = 0; + for (x in o) ++l; + return l; + } + function mpquote(s) { + return s.replace(/\r\n/g, " ") + .replace(/\r/g, " ") + .replace(/\n/g, " ") + .replace(/\"/g, "\\\""); + } + + is(sub.length, expected.length, + "Correct number of multipart items in " + test); + + if (sub.length != expected.length) { + alert(JSON.stringify(sub)); + } + + var i; + for (i = 0; i < expected.length; ++i) { + if (!("fileName" in expected[i])) { + is(sub[i].headers["Content-Disposition"], + "form-data; name=\"" + mpquote(expected[i].name) + "\"", + "Correct name in " + test); + is (getPropCount(sub[i].headers), 1, + "Wrong number of headers in " + test); + is(sub[i].body, + expected[i].value.replace(/\r\n|\r|\n/, "\r\n"), + "Correct value in " + test); + } + else { + is(sub[i].headers["Content-Disposition"], + "form-data; name=\"" + mpquote(expected[i].name) + "\"; filename=\"" + + mpquote(expected[i].fileName) + "\"", + "Correct name in " + test); + is(sub[i].headers["Content-Type"], + expected[i].contentType, + "Correct content type in " + test); + is (getPropCount(sub[i].headers), 2, + "Wrong number of headers in " + test); + is(sub[i].body, + expected[i].value, + "Correct value in " + test); + } + } + } + + function testFormSubmissionInShadowDOM() { + targetIframe = document.getElementById("target_iframe"); + shadowIframe = document.createElement("iframe"); + shadowIframe.src = "about:blank"; + shadowIframe.onload = shadowFrameCreated; + document.body.appendChild(shadowIframe); + } + + function shadowFrameCreated() { + var doc = shadowIframe.contentDocument; + var body = doc.body; + var host = doc.createElement("div"); + body.appendChild(host); + sr = host.attachShadow({ mode: "open" }); + sr.appendChild(document.getElementById('template').content.cloneNode(true)); + targetIframe.onload = checkSubmitValues; + sr.getElementById("form").submit(); + } + + function checkSubmitValues() { + submission = JSON.parse(targetIframe.contentDocument.documentElement.textContent); + var expected = [ + { name: "text", value: "textvalue" }, + { name: "hidden", value: "hiddenvalue" }, + { name: "select", value: "selectvalue" }, + { name: "textarea", value: "textareavalue" } + ]; + checkMPSubmission(submission, expected, "form submission inside shadow DOM"); + SimpleTest.finish(); + } + + window.onload = function() { + SimpleTest.waitForExplicitFinish(); + testFormSubmissionInShadowDOM(); + } + + </script> + <template id="template"> + <form action="form_submit_server.sjs" target="target_iframe" id="form" + method="POST" enctype="multipart/form-data"> + <input name="text" value="textvalue"> + <input name="hidden" value="hiddenvalue" type="hidden"> + <select name="select"><option selected>selectvalue</option></select> + <textarea name="textarea">textareavalue</textarea> + </form> + </template> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1472426">Mozilla Bug 1472426</a> +<iframe name="target_iframe" id="target_iframe"></iframe> +</body> +</html> diff --git a/dom/html/test/test_bug1682.html b/dom/html/test/test_bug1682.html new file mode 100644 index 0000000000..8a0b7abf19 --- /dev/null +++ b/dom/html/test/test_bug1682.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1682 +--> +<head> + <title>Test for Bug 1682</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1682">Mozilla Bug 1682</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1682 **/ +var count = 1; + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function () { + is(count, 1, "onload executes once"); + ++count; +}); +addLoadEvent(SimpleTest.finish); + + + + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug1785739.html b/dom/html/test/test_bug1785739.html new file mode 100644 index 0000000000..2c87c57bd0 --- /dev/null +++ b/dom/html/test/test_bug1785739.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>nsFind::Find() should initialize the editor</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"> +<input value="1abc1 + 2abc2 + 3abc3 + 4abc4 + 5abc5 + 6abc6 + 7abc7 + 8abc8 + 9abc9" id="input"> +<script> + SimpleTest.waitForExplicitFinish(); + + // The current window.find() impl does not support text controls, so import the internal component + const finder = + SpecialPowers + .Cc["@mozilla.org/typeaheadfind;1"] + .getService(SpecialPowers.Ci.nsITypeAheadFind); + + finder.init(SpecialPowers.wrap(window).docShell); + + function find() { + return finder.find( + "abc", + false, + SpecialPowers.Ci.nsITypeAheadFind.FIND_NEXT, + true); + } + + async function runTests() { + finder.find("abc", false, SpecialPowers.Ci.nsITypeAheadFind.FIND_FIRST, true); + // Wait until layout flush as the bug repro needs it + await new Promise(requestAnimationFrame); + + for (let i = 0; i < 9; i++) { + find(); + await new Promise(requestAnimationFrame); + is(input.selectionStart, (i * 19) + 1); + } + + SimpleTest.finish(); + } + window.addEventListener("load", runTests); +</script> diff --git a/dom/html/test/test_bug182279.html b/dom/html/test/test_bug182279.html new file mode 100644 index 0000000000..1421c86ee0 --- /dev/null +++ b/dom/html/test/test_bug182279.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=182279 +--> +<head> + <title>Test for Bug 182279</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=182279">Moozilla Bug 182279</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/** Test for Bug 182279 **/ +var sel = document.createElement("select"); +var opt1 = new Option(); +var opt2 = new Option(); +var opt3 = new Option(); +opt1.value = 1; +opt2.value = 2; +opt3.value = 3; +sel.add(opt1, null); +sel.add(opt2, opt1); +sel.add(opt3); +is(sel[0], opt2, "1st item should be 2"); +is(sel[1], opt1, "2nd item should be 1"); +is(sel[2], opt3, "3rd item should be 3"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug1823.html b/dom/html/test/test_bug1823.html new file mode 100644 index 0000000000..0f42b49980 --- /dev/null +++ b/dom/html/test/test_bug1823.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1823 +--> +<head> + <title>Test for Bug 1823</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1823">Mozilla Bug 1823</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 1823 **/ +ok(!(document.location + "").includes("["), "location object has a toString()"); + + + + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug196523.html b/dom/html/test/test_bug196523.html new file mode 100644 index 0000000000..edd71247a7 --- /dev/null +++ b/dom/html/test/test_bug196523.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=196523 +--> +<head> + <title>Test for Bug 196523</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=196523">Mozilla Bug 196523</a> +<script> + var expectedMessages = 2; + SimpleTest.waitForExplicitFinish(); + window.addEventListener("message", function(e) { + --expectedMessages; + var str = e.data; + var idx = str.indexOf(';'); + var val = str.substring(0, idx); + var msg = str.substring(idx+1); + ok(val == "true", msg); + if (!expectedMessages) { SimpleTest.finish(); } + }); +</script> +<p id="display"> + <iframe src="http://test1.example.org/tests/dom/html/test/bug196523-subframe.html"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 196523 **/ + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug199692.html b/dom/html/test/test_bug199692.html new file mode 100644 index 0000000000..0be6d7ed47 --- /dev/null +++ b/dom/html/test/test_bug199692.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=199692 +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <title>Test for Bug 199692</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <script type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + // The popup calls MochiTest methods in this window through window.opener + window.open("bug199692-popup.html", "bug199692", "width=600,height=600"); + </script> +</body> +</html> + diff --git a/dom/html/test/test_bug2082.html b/dom/html/test/test_bug2082.html new file mode 100644 index 0000000000..5c1ec8f8ec --- /dev/null +++ b/dom/html/test/test_bug2082.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=2082 +--> +<head> + <title>Test for Bug 2082</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=2082">Mozilla Bug 2082</a> +<p id="display"></p> +<div id="content" style="display: none"> +<FORM name="gui" id="gui"> +<INPUT TYPE="text" NAME="field" VALUE="some value"> +</FORM> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 2082 **/ +var guiform = document.getElementById("gui"); +ok(document.getElementById("gui").hasChildNodes(), "form elements should be treated as form's children"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug209275.xhtml b/dom/html/test/test_bug209275.xhtml new file mode 100644 index 0000000000..0cdb64ea00 --- /dev/null +++ b/dom/html/test/test_bug209275.xhtml @@ -0,0 +1,258 @@ +<!DOCTYPE html [ +<!ATTLIST foo:base + id ID #IMPLIED +> +]> +<html xmlns:foo="http://foo.com" xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=209275 +--> +<head> + <title>Test for Bug 209275</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <style> + @namespace svg url("http://www.w3.org/2000/svg"); + svg|a { fill:blue; } + svg|a:visited { fill:purple; } + </style> + + <!-- + base0 should be ignored because it's not in the XHTML namespace + --> + <foo:base id="base0" href="http://www.foo.com" /> + + <!-- + baseEmpty should be ignored because it has no href and never gets one. + --> + <base id="baseEmpty" /> + + <!-- + baseWrongAttrNS should be ignored because its href attribute isn't in the empty + namespace. + --> + <base id="baseWrongAttrNS" foo:href="http://foo.com" /> + + <base id="base1" /> + <base id="base2" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=209275">Mozilla Bug 209275</a> +<p id="display"> +</p> +<div id="content"> + <a href="/" id="link1">link1</a> + <div style="display:none"> + <a href="/" id="link2">link2</a> + </div> + <a href="/" id="link3" style="display:none">link3</a> + <a href="#" id="link4">link4</a> + <a href="" id="colorlink">colorlink</a> + <a href="#" id="link5">link5</a> + <iframe id="iframe"></iframe> + + <svg width="5cm" height="3cm" viewBox="0 0 5 3" version="1.1" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <a xlink:href="" id="ellipselink"> + <ellipse cx="2.5" cy="1.5" rx="2" ry="1" id="ellipse" /> + </a> + </svg> + +</div> +<pre id="test"> +<script type="text/javascript"> +<![CDATA[ + +/** Test for Bug 209275 **/ +SimpleTest.waitForExplicitFinish(); + +function link123HrefIs(href, testNum) { + is($('link1').href, href, "link1 test " + testNum); + is($('link2').href, href, "link2 test " + testNum); + is($('link3').href, href, "link3 test " + testNum); +} + +var gGen; + +function visitedDependentComputedStyle(win, elem, property) { + var utils = SpecialPowers.getDOMWindowUtils(window); + return utils.getVisitedDependentComputedStyle(elem, "", property); +} + +function getColor(elem) { + return visitedDependentComputedStyle(document.defaultView, elem, "color"); +} + +function getFill(elem) { + return visitedDependentComputedStyle(document.defaultView, elem, "fill"); +} + +function setXlinkHref(elem, href) { + elem.setAttributeNS("http://www.w3.org/1999/xlink", "href", href); +} + +function continueTest() { + gGen.next(); +} + +function* run() { + var iframe = document.getElementById("iframe"); + var iframeCw = iframe.contentWindow; + + // First, set the visited/unvisited link/ellipse colors. + const unvisitedColor = "rgb(0, 0, 238)"; + const visitedColor = "rgb(85, 26, 139)"; + const unvisitedFill = "rgb(0, 0, 255)"; + const visitedFill = "rgb(128, 0, 128)"; + + const rand = Date.now() + "-" + Math.random(); + + // Now we can start the tests in earnest. + + var loc = location; + // everything from the location up to and including the final forward slash + var path = /(.*\/)[^\/]*/.exec(location)[1]; + + // Set colorlink's href so we can check that it changes colors after we + // change the base href. + $('colorlink').href = "http://example.com/" + rand; + setXlinkHref($("ellipselink"), "http://example.com/" + rand); + + // Load http://example.com/${rand} into a new window so we can test that + // changing the document's base changes the visitedness of our links. + // + // cross-origin window.open'd windows don't fire load / error events, so we + // wait to close it until we observed the visited color. + let win = window.open("http://example.com/" + rand, "_blank"); + + // Make sure things are what as we expect them at the beginning. + link123HrefIs(`${location.origin}/`, 1); + is($('link4').href, loc + "#", "link 4 test 1"); + is($('link5').href, loc + "#", "link 5 test 1"); + + // Remove link5 from the document. We're going to test that its href changes + // properly when we change our base. + var link5 = $('link5'); + link5.remove(); + + $('base1').href = "http://example.com"; + + // Were the links' hrefs updated after the base change? + link123HrefIs("http://example.com/", 2); + is($('link4').href, "http://example.com/#", "link 4 test 2"); + is(link5.href, "http://example.com/#", "link 5 test 2"); + + // Were colorlink's color and ellipse's fill updated appropriately? + // Because link coloring is asynchronous, we wait until it is updated (or we + // timeout and fail anyway). + while (getColor($('colorlink')) != visitedColor) { + requestIdleCallback(continueTest); + yield undefined; + } + is(getColor($('colorlink')), visitedColor, + "Wrong link color after base change."); + while (getFill($('ellipselink')) != visitedFill) { + requestIdleCallback(continueTest); + yield undefined; + } + is(getFill($('ellipselink')), visitedFill, + "Wrong ellipse fill after base change."); + + win.close(); + + $('base1').href = "foo/"; + // Should be interpreted relative to current URI (not the current base), so + // base should now be http://mochi.test:8888/foo/ + + link123HrefIs(`${location.origin}/`, 3); + is($('link4').href, path + "foo/#", "link 4 test 3"); + + // Changing base2 shouldn't affect anything, because it's not the first base + // tag. + $('base2').href = "http://example.org/bar/"; + link123HrefIs(`${location.origin}/`, 4); + is($('link4').href, path + "foo/#", "link 4 test 4"); + + // If we unset base1's href attribute, the document's base should come from + // base2, whose href is http://example.org/bar/. + $('base1').removeAttribute("href"); + link123HrefIs("http://example.org/", 5); + is($('link4').href, "http://example.org/bar/#", "link 4 test 5"); + + // If we remove base1, base2 should become the first base tag, and the hrefs + // of all the links should change accordingly. + $('base1').remove(); + link123HrefIs("http://example.org/", 6); + is($('link4').href, "http://example.org/bar/#", "link 4 test 6"); + + // If we add a new base after base2, nothing should change. + var base3 = document.createElement("base"); + base3.href = "http://base3.example.org/"; + $('base2').parentNode.insertBefore(base3, $('base2').nextSibling); + link123HrefIs("http://example.org/", 7); + is($('link4').href, "http://example.org/bar/#", "link 4 test 7"); + + // But now if we add a new base before base 2, it should become the primary + // base. + var base4 = document.createElement("base"); + base4.href = "http://base4.example.org/"; + $('base2').parentNode.insertBefore(base4, $('base2')); + link123HrefIs("http://base4.example.org/", 8); + is($('link4').href, "http://base4.example.org/#", "link 4 test 8"); + + // Now if we remove all the base tags, the base should become the page's URI + // again. + $('base2').remove(); + base3.remove(); + base4.remove(); + + link123HrefIs(`${location.origin}/`, 9); + is($('link4').href, loc + "#", "link 4 test 9"); + + // Setting the href of base0 shouldn't do anything because it's not in the + // XHTML namespace. + $('base0').href = "http://bar.com"; + link123HrefIs(`${location.origin}/`, 10); + is($('link4').href, loc + "#", "link 4 test 10"); + + // We load into an iframe a document with a <base href="...">, then remove + // the document element. Then we add an <html>, <body>, and <a>, and make + // sure that the <a> is resolved relative to the page's location, not its + // original base. We do this twice, rebuilding the document in a different + // way each time. + + iframeCw.location = "file_bug209275_1.html"; + yield undefined; // wait for our child to call us back. + is(iframeCw.document.getElementById("link").href, + path + "file_bug209275_1.html#", + "Wrong href after nuking document."); + + iframeCw.location = "file_bug209275_2.html"; + yield undefined; // wait for callback from child + is(iframeCw.document.getElementById("link").href, + `${location.origin}/`, + "Wrong href after nuking document second time."); + + // Make sure that document.open() makes the document forget about any <base> + // tags it has. + iframeCw.location = "file_bug209275_3.html"; + yield undefined; // wait for callback from child + is(iframeCw.document.getElementById("link").href, + "http://mochi.test:8888/", + "Wrong href after document.open()."); + + SimpleTest.finish(); +} + +window.addEventListener("load", function() { + gGen = run(); + gGen.next(); +}); + +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug237071.html b/dom/html/test/test_bug237071.html new file mode 100644 index 0000000000..8360c1eb86 --- /dev/null +++ b/dom/html/test/test_bug237071.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=237071 +--> +<head> + <title>Test for Bug 237071</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=237071">Mozilla Bug 237071</a> +<p id="display"></p> +<div id="content" > + <ol id="theOL" start="22"> + <li id="foo" >should be 22</li> + <li id="foo23">should be 23</li> + </ol> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/** Test for Bug 237071 **/ +is($('theOL').start, 22, "OL start attribute mapped to .start, not just text attribute"); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug242709.html b/dom/html/test/test_bug242709.html new file mode 100644 index 0000000000..7dde04713d --- /dev/null +++ b/dom/html/test/test_bug242709.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=242709 +--> +<head> + <title>Test for Bug 242709</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=242709">Mozilla Bug 242709</a> +<p id="display"></p> +<div id="content"> +<iframe src="bug242709_iframe.html" id="a"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 242709 **/ + +SimpleTest.waitForExplicitFinish(); + +var submitted = function() { + ok(true, "Disabling button after form submission doesn't prevent submitting"); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug24958.html b/dom/html/test/test_bug24958.html new file mode 100644 index 0000000000..a6a077aefe --- /dev/null +++ b/dom/html/test/test_bug24958.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=24958 +--> +<head> + <title>Test for Bug 24958</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <SCRIPT id="foo" TYPE="text/javascript">/*This space intentionally left blank*/</SCRIPT> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=24958">Mozilla Bug 24958</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 24958 **/ +is($("foo").text, "\/*This space intentionally left blank*\/", "HTMLScriptElement.text should return text") + + + + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug255820.html b/dom/html/test/test_bug255820.html new file mode 100644 index 0000000000..5de2fca7c0 --- /dev/null +++ b/dom/html/test/test_bug255820.html @@ -0,0 +1,99 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=255820 +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <title>Test for Bug 255820</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=255820">Mozilla Bug 255820</a> +<p id="display"> + <iframe id="f1"></iframe> + <iframe id="f2"></iframe> + <iframe id="f3"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 255820 **/ +SimpleTest.waitForExplicitFinish(); + +is(document.characterSet, "UTF-8", + "Unexpected character set for our document"); + +var testsLeft = 3; + +function testFinished() { + --testsLeft; + if (testsLeft == 0) { + SimpleTest.finish(); + } +} + +function charsetTestFinished(id, doc, charsetTarget) { + is(doc.characterSet, charsetTarget, "Unexpected charset for subframe " + id); + testFinished(); +} + +function f3Continue() { + var doc = $("f3").contentDocument; + is(doc.defaultView.getComputedStyle(doc.body).color, "rgb(0, 180, 0)", + "Wrong color"); + charsetTestFinished('f3', doc, "UTF-8"); +} + +function runTest() { + var doc = $("f1").contentDocument; + is(doc.characterSet, "UTF-8", + "Unexpected initial character set for first frame"); + doc.open(); + doc.write('<html></html>'); + doc.close(); + charsetTestFinished("f1", doc, "UTF-8"); + + doc = $("f2").contentDocument; + is(doc.characterSet, "UTF-8", + "Unexpected initial character set for second frame"); + doc.open(); + var str = '<html><head>'; + str += '<script src="data:application/javascript,"><'+'/script>'; + str += '<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">'; + str += '</head><body>'; + str += '</body></html>'; + doc.write(str); + doc.close(); + is(doc.characterSet, "UTF-8", + "Unexpected character set for second frame after write"); + $("f2"). + setAttribute("onload", + "charsetTestFinished('f2', this.contentDocument, 'UTF-8');"); + + doc = $("f3").contentDocument; + is(doc.characterSet, "UTF-8", + "Unexpected initial character set for third frame"); + doc.open(); + var str = '<html><head>'; + str += '<style>body { color: rgb(255, 0, 0) }</style>'; + str += '<link type="text/css" rel="stylesheet" href="data:text/css, body { color: rgb(0, 180, 0) }">'; + str += '</head><body>'; + str += '</body></html>'; + doc.write(str); + doc.close(); + is(doc.characterSet, "UTF-8", + "Unexpected character set for third frame after write"); + $("f3").setAttribute("onload", "f3Continue()"); +} + +addLoadEvent(runTest); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug259332.html b/dom/html/test/test_bug259332.html new file mode 100644 index 0000000000..f41f88930c --- /dev/null +++ b/dom/html/test/test_bug259332.html @@ -0,0 +1,64 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=259332 +--> +<head> + <title>Test for Bug 259332</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=259332">Mozilla Bug 259332</a> +<p id="display"></p> +<div id="content"> + <div id="a">a + <div id="a">a</div> + <input name="a" value="a"> + <div id="b">b</div> + <input name="b" value="b"> + <div id="c">c</div> + </div> + <input name="write"> + <input name="write"> + <input id="write"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 259332 **/ + +list = document.all.a; +ok(list.length == 3, "initial a length"); + +blist = document.all.b; +ok(document.all.b.length == 2, "initial b length"); +document.getElementById('b').id = 'a'; +ok(document.all.b.nodeName == "INPUT", "just one b"); + +ok(blist.length == 1, "just one b"); +ok(list.length == 4, "one more a"); + +newDiv = document.createElement('div'); +newDiv.id = 'a'; +newDiv.innerHTML = 'a'; +list[0].appendChild(newDiv); +ok(list.length == 5, "two more a"); + +ok(document.all.c.textContent == 'c', "one c"); +document.all.c.id = 'a'; +ok(!document.all.c, "no c"); +ok(list.length == 6, "three more a"); + +ok(document.all.write.length == 3, "name is write"); + +newDiv = document.createElement('div'); +newDiv.id = 'd'; +newDiv.innerHTML = 'd'; +list[0].appendChild(newDiv); +ok(document.all.d.textContent == 'd', "new d"); + + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug274626.html b/dom/html/test/test_bug274626.html new file mode 100644 index 0000000000..6003722bef --- /dev/null +++ b/dom/html/test/test_bug274626.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=274626 +--> +<head> + <title>Test for Bug 274626</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=274626">Mozilla Bug 274626</a> +<br> + +<input id='textbox_enabled' title='hello' value='hello' /> +<input id='textbox_disabled' title='hello' value='hello' disabled/> + +<br> +<input id='input_button_enabled' title='hello' value='hello' type='button' /> +<input id='input_button_disabled' title='hello' value='hello' type='button' disabled /> + +<br> +<input id='checkbox_enabled' title='hello' type='checkbox'>hello</input> +<input id='checkbox_disabled' title='hello' type='checkbox' disabled >hello</input> + +<br> +<button id='button_enabled' title='hello' value='hello' type='button'>test</button> +<button id='button_disabled' title='hello' value='hello' type='button' disabled>test</button> + +<br> +<textarea id='textarea_enabled' title='hello' value='hello' onclick="alert('click event');"> </textarea> +<textarea id='textarea_disabled' title='hello' value='hello' onclick="alert('click event');" disabled></textarea> + + +<br> +<select id='select_enabled' title='hello' onclick="alert('click event');"> + <option value='item1'>item1</option> + <option value='item2'>item2</option> +</select> +<select id='select_disabled' title='hello' onclick="alert('click event');" disabled> + <option value='item1'>item1</option> + <option value='item2'>item2</option> +</select> + +<br> +<form> + <fieldset id='fieldset_enabled' title='hello' onclick="alert('click event');"> + <legend>Enabled fieldset:</legend> + Name: <input type='text' size='30' /><br /> + Email: <input type='text' size='30' /><br /> + Date of birth: <input type='text' size='10' /> + </fieldset> +</form> +<form> + <fieldset id='fieldset_disabled' title='hello' onclick="alert('click event');" disabled> + <legend>Disabled fieldset:</legend> + Name: <input type='text' size='30' /><br /> + Email: <input type='text' size='30' /><br /> + Date of birth: <input type='text' size='10' /> + </fieldset> +</form> + +<script class="testbody" type="application/javascript"> + +/** Test for Bug 274626 **/ + + function HandlesMouseMove(evt) { + evt.target.handlesMouseMove = true; + } + + var controls=["textbox_enabled","textbox_disabled", + "input_button_enabled", "input_button_disabled", "checkbox_enabled", + "checkbox_disabled", "button_enabled", "button_disabled", + "textarea_enabled", "textarea_disabled", "select_enabled", + "select_disabled", "fieldset_enabled", "fieldset_disabled"]; + + for (id of controls) { + var ctrl = document.getElementById(id); + ctrl.addEventListener('mousemove', HandlesMouseMove); + ctrl.handlesMouseMove = false; + var evt = document.createEvent("MouseEvents"); + evt.initMouseEvent("mousemove", true, true, window, + 0, 0, 0, 0, 0, false, false, false, false, 0, null); + ctrl.dispatchEvent(evt); + + // Mouse move events are what causes tooltips to show up. + // Before this fix we would not allow mouse move events to go through + // which in turn did not allow tooltips to be displayed. + // This test will ensure that all HTML elements handle mouse move events + // so that tooltips can be displayed + ok(ctrl.handlesMouseMove, "Disabled element need mouse move for tooltips"); + } + +</script> +</body> +</html> diff --git a/dom/html/test/test_bug277724.html b/dom/html/test/test_bug277724.html new file mode 100644 index 0000000000..0732a4cf9a --- /dev/null +++ b/dom/html/test/test_bug277724.html @@ -0,0 +1,141 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=277724 +--> +<head> + <title>Test for Bug 277724</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=277724">Mozilla Bug 277724</a> +<p id="display"></p> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 277724 **/ + +var childUnloaded = false; + +var nodes = [ + [ "select", HTMLSelectElement ], + [ "textarea", HTMLTextAreaElement ], + [ "text", HTMLInputElement ], + [ "password", HTMLInputElement ], + [ "checkbox", HTMLInputElement ], + [ "radio", HTMLInputElement ], + [ "image", HTMLInputElement ], + [ "submit", HTMLInputElement ], + [ "reset", HTMLInputElement ], + [ "button input", HTMLInputElement ], + [ "hidden", HTMLInputElement ], + [ "file", HTMLInputElement ], + [ "submit button", HTMLButtonElement ], + [ "reset button", HTMLButtonElement ], + [ "button", HTMLButtonElement ] +]; + +function soon(f) { + return function() { setTimeout(f, 0); } +} + +function startTest(frameid) { + is(childUnloaded, false, "Child not unloaded yet"); + + var doc = $(frameid).contentDocument; + var win = $(frameid).contentWindow; + ok(doc instanceof win.Document, "doc should be a document"); + + for (var i = 0; i < nodes.length; ++i) { + var id = nodes[i][0]; + var node = doc.getElementById(id); + ok(node instanceof win[nodes[i][1].name], id + " should be a " + nodes[i][1]); + is(node.disabled, false, "check for " + id + " state"); + node.disabled = true; + is(node.disabled, true, "check for " + id + " state change"); + } + + $(frameid).onload = soon(function() { continueTest(frameid) }); + + // Do this off a timeout so it's not treated like a replace load. + function loadBlank() { + $(frameid).contentWindow.location = "about:blank"; + } + setTimeout(loadBlank, 0); +} + +function continueTest(frameid) { + is(childUnloaded, true, "Unload handler should have fired"); + var doc = $(frameid).contentDocument; + var win = $(frameid).contentWindow; + ok(doc instanceof win.Document, "doc should be a document"); + + for (var i = 0; i < nodes.length; ++i) { + var id = nodes[i][0]; + var node = doc.getElementById(id); + ok(node === null, id + " should be null"); + } + + $(frameid).onload = soon(function() { finishTest(frameid); }); + + // Do this off a timeout too. Why, I'm not sure. Something in session + // history creates another history state if we don't. :( + function goBack() { + $(frameid).contentWindow.history.back(); + } + setTimeout(goBack, 0); +} + +// XXXbz this is a nasty hack to work around the XML content sink not being +// incremental, so that the _first_ control we test is ok but others are not. +var testIs = is; +var once = false; +function flipper(a, b, c) { + if (once) { + todo(a == b, c); + } else { + once = true; + is(a, b, c); + } +} + +function finishTest(frameid) { + var doc = $(frameid).contentDocument; + var win = $(frameid).contentWindow; + ok(doc instanceof win.Document, "doc should be a document"); + + for (var i = 0; i < nodes.length; ++i) { + var id = nodes[i][0]; + var node = doc.getElementById(id); + ok(node instanceof win[nodes[i][1].name], id + " should be a " + nodes[i][1]); + //testIs(node.disabled, true, "check for " + id + " state restore"); + } + + if (frameid == "frame2") { + SimpleTest.finish(); + } else { + childUnloaded = false; + + // XXXbz this is a nasty hack to deal with the content sink. See above. + testIs = flipper; + + $("frame2").onload = soon(function() { startTest("frame2"); }); + $("frame2").src = "bug277724_iframe2.xhtml"; + } +} + +SimpleTest.waitForExplicitFinish(); +</script> +</pre> + +<!-- Don't use display:none, since we don't support framestate restoration + without a frame tree --> +<div id="content" style="visibility: hidden"> + <iframe src="bug277724_iframe1.html" id="frame1" + onload="setTimeout(function() { startTest('frame1') }, 0)"></iframe> + <iframe src="" id="frame2"></iframe> +</div> +</body> +</html> + diff --git a/dom/html/test/test_bug277890.html b/dom/html/test/test_bug277890.html new file mode 100644 index 0000000000..69bc820880 --- /dev/null +++ b/dom/html/test/test_bug277890.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=277890 +--> +<head> + <title>Test for Bug 277890</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=277890">Mozilla Bug 277890</a> +<p id="display"></p> +<div id="content"> +<iframe src="bug277890_iframe.html" id="a"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 277890 **/ + +SimpleTest.waitForExplicitFinish(); + +var submitted = function() { + ok(true, "Disabling button after form submission doesn't prevent submitting"); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug287465.html b/dom/html/test/test_bug287465.html new file mode 100644 index 0000000000..bc6307423e --- /dev/null +++ b/dom/html/test/test_bug287465.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=287465 +--> +<head> + <title>Test for Bug 287465</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=287465">Mozilla Bug 287465</a> +<p id="display"></p> +<div id="content" style="display:none"> + +<iframe id="i1" srcdoc="<svg xmlns='http://www.w3.org/2000/svg'></svg>"></iframe> +<object id="o1" data="object_bug287465_o1.html"></object> +<iframe id="i2" srcdoc="<html></html>"></iframe> +<object id="o2" data="object_bug287465_o2.html"></object> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doTest); + +function doTest() { + function checkSVGDocument(id) { + var e = document.getElementById(id); + ok(e.contentDocument != null, "check nonnull contentDocument '" + id + "'"); + is(e.contentDocument, e.getSVGDocument(), "check documents match '" + id + "'"); + } + + checkSVGDocument("o1"); + checkSVGDocument("i1"); + checkSVGDocument("o2"); + checkSVGDocument("i2"); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug295561.html b/dom/html/test/test_bug295561.html new file mode 100644 index 0000000000..456985f731 --- /dev/null +++ b/dom/html/test/test_bug295561.html @@ -0,0 +1,86 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=295561 +--> +<head> + <title>Test for Bug 295561</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=295561">Mozilla Bug 295561</a> +<p id="display"></p> +<div id="content" style="display: none"> + +<table id="testTable"> +<thead> +<tr id="headRow"><td></td></tr> +</thead> +<tfoot> +<tr id="footRow"><td></td></tr> +</tfoot> +<tbody id="tBody" name="namedTBody"> +<tr id="trow" name="namedTRow"> +<td id="tcell" name="namedTCell"></td> +<th id="tcellh" name="namedTH"></th> +</tr> +<tr><td></td></tr> +</tbody> +<tbody id="tBody2" name="namedTBody2"> +<tr id="trow2" name="namedTRow2"> +<td id="tcell2" name="namedTCell2"></td> +<th id="tcellh2" name="namedTH2"></th> +</tr> +</table> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +function testItById(id, collection, collectionName) { + is(collection[id], $(id), + "Should be able to get by id '" + id + "' from " + collectionName + + " collection using square brackets.") + is(collection.namedItem(id), $(id), + "Should be able to get by id '" + id + "' from " + collectionName + + " collection using namedItem.") +} + +function testItByName(name, id, collection, collectionName) { + is(collection[name], $(id), + "Should be able to get by name '" + name + "' from " + collectionName + + " collection using square brackets.") + is(collection.namedItem(name), $(id), + "Should be able to get by name '" + name + "' from " + collectionName + + " collection using namedItem.") +} + +function testIt(name, id, collection, collectionName) { + testItByName(name, id, collection, collectionName); + testItById(id, collection, collectionName); +} + +var table = $("testTable") +testIt("namedTBody", "tBody", table.tBodies, "tBodies") +testIt("namedTRow", "trow", table.rows, "table rows") +testIt("namedTRow", "trow", $("tBody").rows, "tbody rows") +testIt("namedTCell", "tcell", $("trow").cells, "cells") +testIt("namedTH", "tcellh", $("trow").cells, "cells") +testIt("namedTBody2", "tBody2", table.tBodies, "tBodies") +testIt("namedTRow2", "trow2", table.rows, "table rows") +testIt("namedTRow2", "trow2", $("tBody2").rows, "tbody rows") +testIt("namedTCell2", "tcell2", $("trow2").cells, "cells") +testIt("namedTH2", "tcellh2", $("trow2").cells, "cells") +is(table.tBodies.length, 2, "Incorrect tBodies length"); +is(table.rows.length, 5, "Incorrect rows length"); +is(table.rows[0], $("headRow"), "THead row in wrong spot"); +is(table.rows[1], $("trow"), "First tbody row in wrong spot"); +is(table.rows[3], $("trow2"), "Second tbody row in wrong spot"); +is(table.rows[4], $("footRow"), "TFoot row in wrong spot"); + + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug297761.html b/dom/html/test/test_bug297761.html new file mode 100644 index 0000000000..0d4532827d --- /dev/null +++ b/dom/html/test/test_bug297761.html @@ -0,0 +1,77 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=297761 +--> +<head> + <title>Test for Bug 297761</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=297761">Mozilla Bug 297761</a> +<p id="display"></p> +<div id="content"> + <iframe src="file_bug297761.html"></iframe> + <iframe src="file_bug297761.html"></iframe> + <iframe src="file_bug297761.html"></iframe> + <iframe src="file_bug297761.html"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 297761 **/ + +SimpleTest.waitForExplicitFinish(); + +var nbTests = 4; +var curTest = 0; + +function nextTest() +{ + if (curTest == 3) { + frames[curTest].document.forms[0].submit(); + } else { + var el = null; + if (curTest == 2) { + el = frames[curTest].document.getElementById('i'); + } else { + el = frames[curTest].document.forms[0].elements[curTest]; + } + + el.focus(); + el.click(); + } +} + +function frameLoaded(aFrame) +{ + var documentLocation = location.href.replace(/\.html.*/, "\.html"); + is(aFrame.contentWindow.location.href.replace(/\?x=0&y=0/, "?"), + documentLocation.replace(/test_bug/, "file_bug") + "?", + "form should have been submitted to the document location"); + + if (++curTest == nbTests) { + SimpleTest.finish(); + } else { + nextTest(); + } +} + +function runTest() +{ + // Initialize event handlers. + var frames = document.getElementsByTagName('iframe'); + for (var i=0; i<nbTests; ++i) { + frames[i].setAttribute('onload', "frameLoaded(this);"); + } + + nextTest(); +} + +addLoadEvent(runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug300691-1.html b/dom/html/test/test_bug300691-1.html new file mode 100644 index 0000000000..44418e8f3a --- /dev/null +++ b/dom/html/test/test_bug300691-1.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=300691 +--> +<head> + <title>Test for Bug 300691</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=300691">Mozilla Bug 300691</a> +<p id="display"> + <textarea id="target"></textarea> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var t = $("target"); + +// FIXME(bug 1838346): This shouldn't need to be async probably? +SimpleTest.waitForExplicitFinish(); +onload = function() { + +/** Test for Bug 300691 **/ +function valueIs(arg, reason) { + is(t.value, arg, reason); +} + +function defValueIs(arg, reason) { + is(t.defaultValue, arg, reason); +} + +valueIs("", "Nothing in the textarea"); +defValueIs("", "Nothing in the textarea 2"); + +t.appendChild(document.createTextNode("ab")); +valueIs("ab", "Appended textnode"); +defValueIs("ab", "Appended textnode 2"); + +t.firstChild.data = "abcd"; +valueIs("abcd", "Modified textnode text"); +defValueIs("abcd", "Modified textnode text 2"); + +t.appendChild(document.createTextNode("efgh")); +valueIs("abcdefgh", "Appended another textnode"); +defValueIs("abcdefgh", "Appended another textnode 2"); + +t.removeChild(t.lastChild); +valueIs("abcd", "Removed textnode"); +defValueIs("abcd", "Removed textnode 2"); + +t.appendChild(document.createTextNode("efgh")); +valueIs("abcdefgh", "Appended yet another textnode"); +defValueIs("abcdefgh", "Appended yet another textnode 2"); + +t.normalize(); +valueIs("abcdefgh", "Normalization changes nothing for the value"); +defValueIs("abcdefgh", "Normalization changes nothing for the value 2"); + +t.defaultValue = "abc"; +valueIs("abc", "Just set the default value on non-edited textarea"); +defValueIs("abc", "Just set the default value on non-edited textarea 2"); + +t.appendChild(document.createTextNode("defgh")); +valueIs("abcdefgh", "Appended another textnode again"); +defValueIs("abcdefgh", "Appended another textnode again 2"); + +t.focus(); // This puts the caret at the end of the textarea, and doing + // something like "home" in a cross-platform way is kinda hard. +sendKey("left"); +sendKey("left"); +sendKey("left"); +sendString("Test"); + +valueIs("abcdeTestfgh", "Typed 'Test' after three left-arrows starting from end"); +defValueIs("abcdefgh", "Typing 'Test' shouldn't affect default value"); + +sendKey("right"); +sendKey("right"); +sendKey("back_space"); +sendKey("back_space"); + +valueIs("abcdeTesth", + "Backspaced twice after two right-arrows starting from end of typing"); +defValueIs("abcdefgh", "Deleting shouldn't affect default value"); + +t.appendChild(document.createTextNode("ijk")); +valueIs("abcdeTesth", + "Appending textnode shouldn't affect value in edited textarea"); +defValueIs("abcdefghijk", "Appended textnode 3"); + +t.lastChild.data = "lmno"; +valueIs("abcdeTesth", + "Modifying textnode text shouldn't affect value in edited textarea"); +defValueIs("abcdefghlmno", "Modified textnode text 3"); + +t.firstChild.remove(); +valueIs("abcdeTesth", + "Removing child textnode shouldn't affect value in edited textarea"); +defValueIs("defghlmno", "Removed textnode 3"); + +t.insertBefore(document.createTextNode("abc"), t.firstChild); +valueIs("abcdeTesth", + "Inserting child textnode shouldn't affect value in edited textarea"); +defValueIs("abcdefghlmno", "Inserted a text node"); + +t.normalize(); +valueIs("abcdeTesth", "Normalization changes nothing for the value 3"); +defValueIs("abcdefghlmno", "Normalization changes nothing for the value 4"); + +t.defaultValue = "abc"; +valueIs("abcdeTesth", "Setting default value shouldn't affect edited textarea"); +defValueIs("abc", "Just set the default value textarea"); +SimpleTest.finish(); + +}; +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug300691-2.html b/dom/html/test/test_bug300691-2.html new file mode 100644 index 0000000000..0dbc5be79a --- /dev/null +++ b/dom/html/test/test_bug300691-2.html @@ -0,0 +1,142 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=300691 +--> +<head> + <title>Test for Bug 300691</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=300691">Mozilla Bug 300691</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="text/javascript"> + // First, setup. We'll be toggling these variables as we go. + var test1Ran = false; + var test2Ran = false; + var test3Ran = false; + var test4Ran = false; + var test5Ran = false; + var test6Ran = false; + var test7Ran = false; + var test8Ran = false; + var test9Ran = false; + var test10Ran = false; + var test11Ran = false; + var test12Ran = false; + var test13Ran = false; + var test14aRan = false; + var test14bRan = false; + var test15aRan = false; + var test15bRan = false; +</script> +<script id="test1" type="text/javascript">test1Ran = true;</script> +<script id="test2" type="text/javascript"></script> +<script id="test3" type="text/javascript">;</script> +<script id="test4" type="text/javascript"> </script> +<script id="test5" type="text/javascript"></script> +<script id="test6" type="text/javascript"></script> +<script id="test7" type="text/javascript"></script> +<script id="test8" type="text/javascript"></script> +<script id="test9" type="text/javascript"></script> +<script id="test10" type="text/javascript" src="data:text/javascript,"> + test10Ran = true; +</script> +<script id="test11" type="text/javascript" + src="data:text/javascript,test11Ran = true"> + test11Ran = false; +</script> +<script id="test12" type="text/javascript"></script> +<script id="test13" type="text/javascript"></script> +<script id="test14" type="text/javascript"></script> +<script id="test15" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> + /** Test for Bug 300691 **/ + $("test2").appendChild(document.createTextNode("test2Ran = true")); + is(test2Ran, true, "Should be 2!"); + + $("test3").appendChild(document.createTextNode("test3Ran = true")); + is(test3Ran, false, "Should have run already 3!"); + + $("test4").appendChild(document.createTextNode("test4Ran = true")); + is(test4Ran, false, "Should have run already 4!"); + + $("test5").appendChild(document.createTextNode(" ")); + $("test5").appendChild(document.createTextNode("test5Ran = true")); + is(test5Ran, false, "Should have run already 5!"); + + $("test6").appendChild(document.createTextNode(" ")); + + $("test7").appendChild(document.createTextNode("")); + + $("test8").appendChild(document.createTextNode("")); + + $("test9").appendChild(document.createTextNode("")); + + $("test12").src = "data:text/javascript,test12Ran = true;"; + is(test12Ran, false, "Not yet 12!"); + + $("test13").setAttribute("src", "data:text/javascript,test13Ran = true;"); + is(test13Ran, false, "Not yet 13!"); + + $("test14").src = "data:text/javascript,test14aRan = true;"; + $("test14").appendChild(document.createTextNode("test14bRan = true")); + is(test14aRan, false, "Not yet 14a!"); + is(test14bRan, false, "Not yet 14b!"); + + $("test15").src = "data:text/javascript,test15aRan = true;"; + $("test15").appendChild(document.createTextNode("test15bRan = true")); + $("test15").removeAttribute("src"); + is(test15aRan, false, "Not yet 15a!"); + is(test15bRan, false, "Not yet 15b!"); +</script> +<script type="text/javascript"> + // Follow up on some of those + $("test6").appendChild(document.createTextNode("test6Ran = true")); + is(test6Ran, false, "Should have run already 6!"); + + $("test7").appendChild(document.createTextNode("test7Ran = true")); + is(test7Ran, true, "Should be 7!"); + + $("test8").insertBefore(document.createTextNode("test8Ran = true"), + $("test8").firstChild); + is(test8Ran, true, "Should be 8!"); + + $("test9").firstChild.data = "test9Ran = true"; + is(test9Ran, true, "Should be 9!"); +</script> +<script type="text/javascript"> +function done() { + is(test1Ran, true, "Should have run!"); + is(test3Ran, false, "Already executed test3 script once"); + is(test4Ran, false, + "Should have executed whitespace-only script already"); + is(test5Ran, false, + "Should have executed once the whitespace node was added"); + is(test6Ran, false, + "Should have executed once the whitespace node was added 2"); + is(test10Ran, false, "Has an src; inline part shouldn't run"); + is(test11Ran, true, "Script with src should have run"); + is(test12Ran, true, "Setting src should execute script"); + is(test13Ran, true, "Setting src attribute should execute script"); + is(test14aRan, true, "src attribute takes precedence over inline content"); + is(test14bRan, false, "src attribute takes precedence over inline content 2"); + is(test15aRan, true, + "src attribute load should have started before the attribute got removed"); + is(test15bRan, false, + "src attribute still got executed, so this shouldn't have been"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(done); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug300691-3.xhtml b/dom/html/test/test_bug300691-3.xhtml new file mode 100644 index 0000000000..788ca2160d --- /dev/null +++ b/dom/html/test/test_bug300691-3.xhtml @@ -0,0 +1,48 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=300691 +--> +<head> + <title>Test for Bug 300691</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=300691">Mozilla Bug 300691</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="text/javascript"> + // First, setup. We'll be toggling these variables as we go. + // Note that scripts don't execute immediately when you put text in them -- + // they wait for the currently-running script to finish. + var test1Ran = false; + var test2Ran = false; + var test3Ran = false; +</script> +<script id="test1" type="text/javascript">test1Ran = true;</script> +<script id="test2" type="text/javascript"></script> +<script id="test3" type="text/javascript"></script> +<script class="testbody" type="text/javascript"> +<![CDATA[ + /** Test for Bug 300691 **/ + $("test2").appendChild(document.createCDATASection("test2Ran = true")); + is(test2Ran, true, "Should be 2!"); + + $("test3").appendChild(document.createCDATASection("")); +]]> +</script> +<script type="text/javascript"> + // Follow up on some of those + $("test3").firstChild.data = "test3Ran = true"; + is(test3Ran, true, "Should be 3!"); +</script> +<script type="text/javascript"> + is(test1Ran, true, "Should have run!"); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug311681.html b/dom/html/test/test_bug311681.html new file mode 100644 index 0000000000..7c74f7664b --- /dev/null +++ b/dom/html/test/test_bug311681.html @@ -0,0 +1,99 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=311681 +--> +<head> + <title>Test for Bug 311681</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=311681">Mozilla Bug 311681</a> +<script class="testbody" type="text/javascript"> + // Setup script + SimpleTest.waitForExplicitFinish(); + + // Make sure to trigger the hashtable case by asking for enough elements + // by ID. + for (var i = 0; i < 256; ++i) { + var x = document.getElementById(i); + } + + // save off the document.getElementById function, since getting it as a + // property off the document it causes a content flush. + var fun = document.getElementById; + + // Slot for our initial element with id "content" + var testNode; + + function getCont() { + return fun.call(document, "content"); + } + + function testClone() { + // Test to make sure that if we have multiple nodes with the same ID in + // a document we don't forget about one of them when the other is + // removed. + var newParent = $("display"); + var node = testNode.cloneNode(true); + isnot(node, testNode, "Clone should be a different node"); + + newParent.appendChild(node); + + // Check what getElementById returns, no flushing + is(getCont(), node, "Should be getting new node pre-flush 1"); + + // Trigger a layout flush, just in case. + var itemHeight = newParent.offsetHeight/10; + + // Check what getElementById returns now. + is(getCont(), node, "Should be getting new node post-flush 1"); + + clear(newParent); + + // Check what getElementById returns, no flushing + is(getCont(), testNode, "Should be getting orig node pre-flush 2"); + + // Trigger a layout flush, just in case. + var itemHeight = newParent.offsetHeight/10; + + // Check what getElementById returns now. + is(getCont(), testNode, "Should be getting orig node post-flush 2"); + + node = testNode.cloneNode(true); + newParent.appendChild(node); + testNode.remove(); + + // Check what getElementById returns, no flushing + is(getCont(), node, "Should be getting clone pre-flush"); + + // Trigger a layout flush, just in case. + var itemHeight = newParent.offsetHeight/10; + + // Check what getElementById returns now. + is(getCont(), node, "Should be getting clone post-flush"); + + } + + function clear(node) { + while (node.hasChildNodes()) { + node.firstChild.remove(); + } + } + + addLoadEvent(testClone); + addLoadEvent(SimpleTest.finish); +</script> +<p id="display"></p> +<div id="content" style="display: none"> + <script class="testbody" type="text/javascript"> + testNode = fun.call(document, "content"); + isnot(testNode, null, "Should have node here"); + </script> +</div> +<pre id="test"> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug311681.xhtml b/dom/html/test/test_bug311681.xhtml new file mode 100644 index 0000000000..15019fa644 --- /dev/null +++ b/dom/html/test/test_bug311681.xhtml @@ -0,0 +1,102 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=311681 +--> +<head> + <title>Test for Bug 311681</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=311681">Mozilla Bug 311681</a> +<script class="testbody" type="text/javascript"> +<![CDATA[ + // Setup script + SimpleTest.waitForExplicitFinish(); + + // Make sure to trigger the hashtable case by asking for enough elements + // by ID. + for (var i = 0; i < 256; ++i) { + var x = document.getElementById(i); + } + + // save off the document.getElementById function, since getting it as a + // property off the document it causes a content flush. + var fun = document.getElementById; + + // Slot for our initial element with id "content" + var testNode; + + function getCont() { + return fun.call(document, "content"); + } + + function testClone() { + // Test to make sure that if we have multiple nodes with the same ID in + // a document we don't forget about one of them when the other is + // removed. + var newParent = $("display"); + var node = testNode.cloneNode(true); + isnot(node, testNode, "Clone should be a different node"); + + newParent.appendChild(node); + + // Check what getElementById returns, no flushing + is(getCont(), node, "Should be getting new node pre-flush 1"); + + // Trigger a layout flush, just in case. + var itemHeight = newParent.offsetHeight/10; + + // Check what getElementById returns now. + is(getCont(), node, "Should be getting new node post-flush 1"); + + clear(newParent); + + // Check what getElementById returns, no flushing + is(getCont(), testNode, "Should be getting orig node pre-flush 2"); + + // Trigger a layout flush, just in case. + var itemHeight = newParent.offsetHeight/10; + + // Check what getElementById returns now. + is(getCont(), testNode, "Should be getting orig node post-flush 2"); + + node = testNode.cloneNode(true); + newParent.appendChild(node); + testNode.remove(); + + // Check what getElementById returns, no flushing + is(getCont(), node, "Should be getting clone pre-flush"); + + // Trigger a layout flush, just in case. + var itemHeight = newParent.offsetHeight/10; + + // Check what getElementById returns now. + is(getCont(), node, "Should be getting clone post-flush"); + + } + + function clear(node) { + while (node.hasChildNodes()) { + node.firstChild.remove(); + } + } + + addLoadEvent(testClone); + addLoadEvent(SimpleTest.finish); +]]> +</script> +<p id="display"></p> +<div id="content" style="display: none"> + <script class="testbody" type="text/javascript"> + <![CDATA[ + testNode = fun.call(document, "content"); + ok(testNode != null, "Should have node here"); + ]]> + </script> +</div> +<pre id="test"> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug324378.html b/dom/html/test/test_bug324378.html new file mode 100644 index 0000000000..8bab3feaf4 --- /dev/null +++ b/dom/html/test/test_bug324378.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html id="a" id="b"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=324378 +--> +<head id="c" id="d"> + <head id="j" foo="k" foo="l"> + <title>Test for Bug 324378</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body id="e" id="f"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=324378">Mozilla Bug 324378</a> +<script> + var html = document.documentElement; + is(document.getElementsByTagName("html").length, 1, + "Unexpected number of htmls"); + is(document.getElementsByTagName("html")[0], html, + "Unexpected <html> element"); + is(document.getElementsByTagName("head").length, 1, + "Unexpected number of heads"); + is(html.getElementsByTagName("head").length, 1, + "Unexpected number of heads in <html>"); + is(document.getElementsByTagName("body").length, 1, + "Unexpected number of bodies"); + is(html.getElementsByTagName("body").length, 1, + "Unexpected number of bodies in <html>"); + var head = document.getElementsByTagName("head")[0]; + var body = document.getElementsByTagName("body")[0]; +</script> +<p id="display"></p> +<div id="content" style="display: none"> + <html id="g" foo="h" foo="i"> + <body id="m" foo="n" foo="o"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 324378 **/ + is(document.getElementsByTagName("html").length, 1, + "Unexpected number of htmls after additions"); + is(document.getElementsByTagName("html")[0], html, + "Unexpected <html> element"); + is(document.documentElement, html, + "Unexpected root node"); + is(document.getElementsByTagName("head").length, 1, + "Unexpected number of heads after additions"); + is(document.getElementsByTagName("head")[0], head, + "Unexpected <head> element"); + is(document.getElementsByTagName("body").length, 1, + "Unexpected number of bodies after additions"); + is(document.getElementsByTagName("body")[0], body, + "Unexpected <body> element"); + + is(html.id, "a", "Unexpected <html> id"); + is(head.id, "c", "Unexpected <head> id"); + is(body.id, "e", "Unexpected <body> id"); + is($("a"), html, "Unexpected node with id=a"); + is($("b"), null, "Unexpected node with id=b"); + is($("c"), head, "Unexpected node with id=c"); + is($("d"), null, "Unexpected node with id=d"); + is($("e"), body, "Unexpected node with id=e"); + is($("f"), null, "Unexpected node with id=f"); + is($("g"), null, "Unexpected node with id=g"); + is($("j"), null, "Unexpected node with id=j"); + is($("m"), null, "Unexpected node with id=m"); + + is(html.getAttribute("foo"), "h", "Unexpected 'foo' value on <html>"); + is(head.getAttribute("foo"), null, "Unexpected 'foo' value on <head>"); + is(body.getAttribute("foo"), "n", "Unexpected 'foo' value on <body>"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug330705-1.html b/dom/html/test/test_bug330705-1.html new file mode 100644 index 0000000000..64b6e89b29 --- /dev/null +++ b/dom/html/test/test_bug330705-1.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=330705 +--> +<head> + <title>Test for Bug 330705</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script> + /* variable is true if the element is focused, false otherwise */ + var inputFocused = false; + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=330705">Mozilla Bug 330705</a> +<p id="display"> + <input onfocus="inputFocused = true" onblur="inputFocused = false" type="text"> + <button></button> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/** Test for Bug 330705 **/ + SimpleTest.waitForExplicitFinish(); + var isFocused = false; + + function onLoad() { + document.getElementsByTagName('input')[0].focus(); + document.getElementsByTagName('button')[0].blur(); + ok(inputFocused == true, "the input element is still focused after blur() has been called on the unfocused element"); + SimpleTest.finish(); + } + + addLoadEvent(onLoad); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug332246.html b/dom/html/test/test_bug332246.html new file mode 100644 index 0000000000..e4fbd20bec --- /dev/null +++ b/dom/html/test/test_bug332246.html @@ -0,0 +1,75 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=332246 +--> +<head> + <title>Test for Bug 332246 - scrollIntoView(false) doesn't work correctly for inline elements that wrap at multiple lines</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=332246">Mozilla Bug 332246</a> +<p id="display"></p> +<div id="content"> + +<div id="a1" style="height: 100px; width: 100px; overflow: hidden; outline:1px dotted black;"> +<div style="height: 100px"></div> +<a id="a2" href="#" style="display:block; background:yellow; height:200px;">Top</a> +<div style="height: 100px"></div> +</div> + +<div id="b1" style="height: 100px; width: 100px; overflow: hidden; outline:1px dotted black;"> +<div style="height: 100px"></div> +<div id="b2" href="#" style="border:10px solid black; background:yellow; height:200px;"></div> +<div style="height: 100px"></div> +</div> + +<br> + +<div id="c1" style="height: 100px; width: 100px; overflow: hidden; position: relative; outline:1px dotted black;"> +<div id="c2" style="border: 10px solid black; height: 200px; width: 50px; position: absolute; top: 100px;"></div> +<div style="height: 100px"></div> +</div> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 332246 **/ + +function isWithFuzz(itIs, itShouldBe, fuzz, description) { + ok(Math.abs(itIs - itShouldBe) <= fuzz, `${description} - expected a value between ${itShouldBe - fuzz} and ${itShouldBe + fuzz}, got ${itIs}`); +} + +var a1 = document.getElementById('a1'); +var a2 = document.getElementById('a2'); +isWithFuzz(a1.scrollHeight, 400, 1, "Wrong a1.scrollHeight"); +is(a1.offsetHeight, 100, "Wrong a1.offsetHeight"); +a2.scrollIntoView(true); +is(a1.scrollTop, 100, "Wrong scrollTop value after a2.scrollIntoView(true)"); +a2.scrollIntoView(false); +is(a1.scrollTop, 200, "Wrong scrollTop value after a2.scrollIntoView(false)"); + +var b1 = document.getElementById('b1'); +var b2 = document.getElementById('b2'); +isWithFuzz(b1.scrollHeight, 420, 1, "Wrong b1.scrollHeight"); +is(b1.offsetHeight, 100, "Wrong b1.offsetHeight"); +b2.scrollIntoView(true); +is(b1.scrollTop, 100, "Wrong scrollTop value after b2.scrollIntoView(true)"); +b2.scrollIntoView(false); +is(b1.scrollTop, 220, "Wrong scrollTop value after b2.scrollIntoView(false)"); + +var c1 = document.getElementById('c1'); +var c2 = document.getElementById('c2'); +isWithFuzz(c1.scrollHeight, 320, 1, "Wrong c1.scrollHeight"); +is(c1.offsetHeight, 100, "Wrong c1.offsetHeight"); +c2.scrollIntoView(true); +is(c1.scrollTop, 100, "Wrong scrollTop value after c2.scrollIntoView(true)"); +c2.scrollIntoView(false); +isWithFuzz(c1.scrollTop, 220, 1, "Wrong scrollTop value after c2.scrollIntoView(false)"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug332848.xhtml b/dom/html/test/test_bug332848.xhtml new file mode 100644 index 0000000000..a7a1950125 --- /dev/null +++ b/dom/html/test/test_bug332848.xhtml @@ -0,0 +1,86 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=332848 +--> +<head> + <title>Test for Bug 332848</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=332848">Mozilla Bug 332848</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +/** Test for Bug 332848 **/ + +// parseChecker will become true if we keep parsing after calling close(). +var parseChecker = false; + +function test() { + try { + document.open(); + is(0, 1, "document.open succeeded"); + } catch (e) { + is (e.name, "InvalidStateError", + "Wrong exception from document.open"); + is (e.code, DOMException.INVALID_STATE_ERR, + "Wrong exception from document.open"); + } + + try { + document.write("aaa"); + is(0, 1, "document.write succeeded"); + } catch (e) { + is (e.name, "InvalidStateError", + "Wrong exception from document.write"); + is (e.code, DOMException.INVALID_STATE_ERR, + "Wrong exception from document.write"); + } + + try { + document.writeln("aaa"); + is(0, 1, "document.write succeeded"); + } catch (e) { + is (e.name, "InvalidStateError", + "Wrong exception from document.write"); + is (e.code, DOMException.INVALID_STATE_ERR, + "Wrong exception from document.write"); + } + + try { + document.close(); + is(0, 1, "document.close succeeded"); + } catch (e) { + is (e.name, "InvalidStateError", + "Wrong exception from document.close"); + is (e.code, DOMException.INVALID_STATE_ERR, + "Wrong exception from document.close"); + } +} + +function loadTest() { + is(parseChecker, true, "Parsing stopped"); + test(); + SimpleTest.finish(); +} + +window.onload = loadTest; + +SimpleTest.waitForExplicitFinish(); + +test(); +]]> +</script> +<script> + parseChecker = true; +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug332893-1.html b/dom/html/test/test_bug332893-1.html new file mode 100644 index 0000000000..552da95c28 --- /dev/null +++ b/dom/html/test/test_bug332893-1.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> + +<form id="form1"> + <input id="F1I1" type="input" value="11"/> + <input id="F1I2" type="input" value="12"/> +</form> +<form id="form2"> + <input id="F2I1" type="input" value="21"/> + <input id="F2I2" type="input" value="22"/> +</form> +<script> +<!-- Create a new input, add it to the first form, move it to the 2nd form, then move it back to the first --> + var form1 = document.getElementById("form1"); + var form2 = document.getElementById("form2"); + var newInput = document.createElement("input"); + newInput.value = "13"; + form1.insertBefore(newInput, form1.firstChild); + var F2I2 = document.getElementById("F2I2"); + form2.insertBefore(newInput, F2I2); + form1.insertBefore(newInput, form1.firstChild); + + is(form1.elements.length, 3, "Form 1 has the correct length"); + is(form1.elements[0].value, "13", "Form 1 element 1 is correct"); + is(form1.elements[1].value, "11", "Form 1 element 2 is correct"); + is(form1.elements[2].value, "12", "Form 1 element 3 is correct"); + + is(form2.elements.length, 2, "Form 2 has the correct length"); + is(form2.elements[0].value, "21", "Form 2 element 1 is correct"); + is(form2.elements[1].value, "22", "Form 2 element 2 is correct"); +</script> +</body> +</html> diff --git a/dom/html/test/test_bug332893-2.html b/dom/html/test/test_bug332893-2.html new file mode 100644 index 0000000000..d24b746566 --- /dev/null +++ b/dom/html/test/test_bug332893-2.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> + + +<form id="form1"> + <table> + <tbody id="table1"> + <tr id="F1I0"><td><input form='form1' type="input" value="10"/></td></tr> + <tr id="F1I1"><td><input type="input" value="11"/></td></tr> + <tr id="F1I2"><td><input type="input" value="12"/></td></tr> + </tbody> + </table> +</form> +<form id="form2"> + <table> + <tbody id="table2"> + <tr id="F2I1"><td><input type="input" value="21"/></td></tr> + <tr id="F2I2"><td><input type="input" value="22"/></td></tr> + </tbody> + </table> +</form> + +<script> + var table1 = document.getElementById("table1"); + var F1I0 = table1.getElementsByTagName("tr")[0]; + var F1I1 = table1.getElementsByTagName("tr")[1]; + table1.removeChild(F1I0); + table1.removeChild(F1I1); + + var table2 = document.getElementById("table2"); + table2.insertBefore(F1I0, table2.firstChild); + table2.insertBefore(F1I1, table2.firstChild); + + var form1 = document.getElementById("form1"); + var form2 = document.getElementById("form2"); + + is(form1.elements.length, 2, "Form 1 length is correct"); + is(form1.elements[0].value, "12", "Form 1 first element is correct"); + is(form1.elements[1].value, "10", "Form 2 second element is correct"); + is(form2.elements.length, 3, "Form 2 length is correct"); + is(form2.elements[0].value, "11", "Form 2 element 1 is correct"); + is(form2.elements[1].value, "21", "Form 2 element 2 is correct"); + is(form2.elements[2].value, "22", "Form 2 element 3 is correct"); + +</script> + +</body> +</html> diff --git a/dom/html/test/test_bug332893-3.html b/dom/html/test/test_bug332893-3.html new file mode 100644 index 0000000000..247607dcb2 --- /dev/null +++ b/dom/html/test/test_bug332893-3.html @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<form id="form1"> + <table> + <tbody> + <tr> + <td> + <table> + <tbody id="table1"> + <tr id="F1I0"><td><input form='form1' type="input" value="10"/></td></tr> + <tr id="F1I1"><td><input type="input" value="11"/></td></tr> + <tr id="F1I2"><td><input type="input" value="12"/></td></tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> +</form> +<form id="form2"> + <table> + <tbody id="table2"> + <tr id="F2I1"><td><input type="input" value="21"/></td></tr> + <tr id="F2I2"><td><input type="input" value="22"/></td></tr> + </tbody> + </table> +</form> + +<script> + var table1 = document.getElementById("table1"); + var F1I0 = table1.getElementsByTagName("tr")[0]; + var F1I1 = table1.getElementsByTagName("tr")[1]; + table1.removeChild(F1I0); + table1.removeChild(F1I1); + + var table2 = document.getElementById("table2"); + table2.insertBefore(F1I0, table2.firstChild); + table2.insertBefore(F1I1, table2.firstChild); + + var form1 = document.getElementById("form1"); + var form2 = document.getElementById("form2"); + + is(form1.elements.length, 2, "Form 1 has the correct length"); + is(form1.elements[0].value, "12", "Form 1 element 1 is correct"); + is(form1.elements[1].value, "10", "Form 1 element 2 is correct"); + + is(form2.elements.length, 3, "Form 2 has the correct length"); + is(form2.elements[0].value, "11", "Form 2 element 1 is correct"); + is(form2.elements[1].value, "21", "Form 2 element 2 is correct"); + is(form2.elements[2].value, "22", "Form 2 element 2 is correct"); +</script> +</body> +</html> diff --git a/dom/html/test/test_bug332893-4.html b/dom/html/test/test_bug332893-4.html new file mode 100644 index 0000000000..72a0239a5d --- /dev/null +++ b/dom/html/test/test_bug332893-4.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<form id="form1"> + <input id="input1" type="input" name="input" value="1"/> + <input id="input2" type="input" name="input" value="2"/> + <input id="input3" type="input" name="input" value="3"/> +</form> +<script> + var input1 = document.getElementById("input1"); + var input2 = document.getElementById("input2"); + var form1 = document.getElementById("form1"); + form1.insertBefore(input2, input1); + + is(form1.elements.input.length, 3, "Form 1 'input' has the correct length"); + is(form1.elements.input[0].value, "2", "Form 1 element 1 is correct"); + is(form1.elements.input[1].value, "1", "Form 1 element 2 is correct"); + is(form1.elements.input[2].value, "3", "Form 1 element 3 is correct"); + + is(form1.elements.input[0].id, "input2", "Form 1 element 1 id is correct"); + is(form1.elements.input[1].id, "input1", "Form 1 element 2 id is correct"); + is(form1.elements.input[2].id, "input3", "Form 1 element 3 id is correct"); +</script> +</body> +</html> diff --git a/dom/html/test/test_bug332893-5.html b/dom/html/test/test_bug332893-5.html new file mode 100644 index 0000000000..e5fb9b94d6 --- /dev/null +++ b/dom/html/test/test_bug332893-5.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<form id="form1"> + <input id="input1" type="input" name="input" value="1"/> + <input id="input" type="input" name="input_other" value="2"/> + <input id="input3" type="input" name="input" value="3"/> +</form> +<script> + var input1 = document.getElementById("input1"); + var input2 = document.getElementById("input"); + var form1 = document.getElementById("form1"); + form1.insertBefore(input2, input1); + + is(form1.elements.input.length, 3, "Form 1 'input' has the correct length"); + is(form1.elements.input[0].value, "2", "Form 1 element 1 is correct"); + is(form1.elements.input[1].value, "1", "Form 1 element 2 is correct"); + is(form1.elements.input[2].value, "3", "Form 1 element 3 is correct"); + + is(form1.elements.input[0].id, "input", "Form 1 element 1 id is correct"); + is(form1.elements.input[1].id, "input1", "Form 1 element 2 id is correct"); + is(form1.elements.input[2].id, "input3", "Form 1 element 3 id is correct"); +</script> +</body> +</html> diff --git a/dom/html/test/test_bug332893-6.html b/dom/html/test/test_bug332893-6.html new file mode 100644 index 0000000000..b12e7a0c3a --- /dev/null +++ b/dom/html/test/test_bug332893-6.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<form id="form1"> + <input id="input1" type="input" name="input" value="1"/> + <input id="input" type="input" name="input_other" value="2"/> + <input id="input3" type="input" name="input" value="3"/> +</form> +<script> + var input1 = document.getElementById("input1"); + var input2 = document.getElementById("input"); + var form1 = document.getElementById("form1"); + form1.insertBefore(input2, input1); + + is(form1.elements.input.length, 3, "Form 1 'input' has the correct length"); + is(form1.elements.input[0].value, "2", "Form 1 element 1 is correct"); + is(form1.elements.input[1].value, "1", "Form 1 element 2 is correct"); + + is(form1.elements.input[0].id, "input", "Form 1 element 1 id is correct"); + is(form1.elements.input[1].id, "input1", "Form 1 element 2 id is correct"); +</script> +</body> +</html> diff --git a/dom/html/test/test_bug332893-7.html b/dom/html/test/test_bug332893-7.html new file mode 100644 index 0000000000..15672d2d20 --- /dev/null +++ b/dom/html/test/test_bug332893-7.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<form id="form1"> + <input id="input1" type="input" name="input" value="1"/> + <input id="input2" type="input" name="input" value="2"/> + <input id="input3" type="input" name="input" value="3"/> + <input id="input4" type="input" name="input" value="4"/> + <input id="input5" type="input" name="input" value="5"/> + <input id="input6" type="input" name="input" value="6"/> + <input id="input7" type="input" name="input" value="7"/> + <input id="input8" type="input" name="input" value="8"/> + <input id="input9" type="input" name="input" value="9"/> + <input id="input10" type="input" name="input" value="10"/> + + + + +</form> +<script> + var input1 = document.getElementById("input1"); + var input2 = document.getElementById("input2"); + var input3 = document.getElementById("input3"); + var input4 = document.getElementById("input4"); + var input5 = document.getElementById("input5"); + var input6 = document.getElementById("input6"); + var input7 = document.getElementById("input7"); + var input8 = document.getElementById("input8"); + var input9 = document.getElementById("input9"); + var input10 = document.getElementById("input10"); + + + var form1 = document.getElementById("form1"); + + form1.insertBefore(input2, input1); + form1.insertBefore(input10, input6); + form1.insertBefore(input8, input4); + form1.insertBefore(input9, input2); + + is(form1.elements.input.length, 10, "Form 1 'input' has the correct length"); + is(form1.elements.input[0].value, "9", "Form 1 element 1 is correct"); + is(form1.elements.input[1].value, "2", "Form 1 element 2 is correct"); + is(form1.elements.input[2].value, "1", "Form 1 element 3 is correct"); + is(form1.elements.input[3].value, "3", "Form 1 element 4 is correct"); + is(form1.elements.input[4].value, "8", "Form 1 element 5 is correct"); + is(form1.elements.input[5].value, "4", "Form 1 element 6 is correct"); + is(form1.elements.input[6].value, "5", "Form 1 element 7 is correct"); + is(form1.elements.input[7].value, "10", "Form 1 element 8 is correct"); + is(form1.elements.input[8].value, "6", "Form 1 element 9 is correct"); + is(form1.elements.input[9].value, "7", "Form 1 element 10 is correct"); + + is(form1.elements.input[0].id, "input9", "Form 1 element 1 id is correct"); + is(form1.elements.input[1].id, "input2", "Form 1 element 2 id is correct"); + is(form1.elements.input[2].id, "input1", "Form 1 element 3 id is correct"); + is(form1.elements.input[3].id, "input3", "Form 1 element 4 id is correct"); + is(form1.elements.input[4].id, "input8", "Form 1 element 5 id is correct"); + is(form1.elements.input[5].id, "input4", "Form 1 element 6 id is correct"); + is(form1.elements.input[6].id, "input5", "Form 1 element 7 id is correct"); + is(form1.elements.input[7].id, "input10", "Form 1 element 8 id is correct"); + is(form1.elements.input[8].id, "input6", "Form 1 element 9 id is correct"); + is(form1.elements.input[9].id, "input7", "Form 1 element 10 id is correct"); + +</script> +</body> +</html> diff --git a/dom/html/test/test_bug3348.html b/dom/html/test/test_bug3348.html new file mode 100644 index 0000000000..a0f99d9c43 --- /dev/null +++ b/dom/html/test/test_bug3348.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=3348 +--> +<head> + <title>Test for Bug 3348</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=3348">Mozilla Bug 3348</a> +<p id="display"></p> +<div id="content" style="display: none"> + +<form id="form1"> +<input type="button" value="click here" onclick="buttonClick();"> +</form> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 3348 **/ + +var oForm = document.getElementById("form1"); +is(oForm.tagName, "FORM", "tagName of HTML element gives tag in upper case"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug340017.xhtml b/dom/html/test/test_bug340017.xhtml new file mode 100644 index 0000000000..69de021e41 --- /dev/null +++ b/dom/html/test/test_bug340017.xhtml @@ -0,0 +1,27 @@ +<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=340017
+-->
+<head>
+ <title>Test for Bug 340017</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=340017">Mozilla Bug 340017</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <form id="frmfoo" name="foo" action="" />
+</div>
+<pre id="test">
+<script type="application/javascript">
+<![CDATA[
+
+/** Test for Bug 340017 **/
+is(document.foo, document.getElementById("frmfoo"),
+ "The form with name 'foo' should be a document accessible property");
+]]>
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug340800.html b/dom/html/test/test_bug340800.html new file mode 100644 index 0000000000..bcb5b2de08 --- /dev/null +++ b/dom/html/test/test_bug340800.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=340800 +--> +<head> + <title>Test for Bug 340800</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=340800">Mozilla Bug 340800</a> +<p id="display"></p> +<div id="content" style="display: none"> + <h1>iframe text/plain as DOM test</h1> + + <div> + + <iframe name="iframe1" width="100%" height="200" + src="bug340800_iframe.txt"></iframe> + </div> + + <div> + <h2>textarea with iframe content</h2> + <textarea rows="10" cols="80" id="textarea1"></textarea> + </div> + + <div> + <h2>div with white-space: pre and iframe content</h2> + <div id="div1"></div> + </div> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 340800 **/ +function populateIframes () { + var iframe, iframeBody; + if ((iframe = window.frames.iframe1) && (iframeBody = iframe.document.body)) { + $('div1').innerHTML = iframeBody.innerHTML; + $('textarea1').value = iframeBody.innerHTML; + } + is($('div1').firstChild.tagName, "PRE", "innerHTML from txt iframe works with div"); + ok($('textarea1').value.indexOf("<pre>") > -1, "innerHTML from txt iframe works with textarea.value"); + SimpleTest.finish(); +} + +addLoadEvent(populateIframes); +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug347174.html b/dom/html/test/test_bug347174.html new file mode 100644 index 0000000000..a453627f06 --- /dev/null +++ b/dom/html/test/test_bug347174.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=347174 +--> +<head> + <title>Test for Bug 347174</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=347174">Mozilla Bug 347174</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 347174 **/ +// simple test of readyState during loading, DOMContentLoaded, and complete +// this test passes in IE7 +window.readyStateText = []; +window.readyStateText.push("script tag: " + document.readyState); +is(document.readyState, "loading", "document.readyState should be 'loading' when scripts runs initially"); + +function attachCustomEventListener(element, eventName, command) { + if (window.addEventListener && !window.opera) + element.addEventListener(eventName, command, true); + else if (window.attachEvent) + element.attachEvent("on" + eventName, command); +} + +function showMessage(msg) { + window.readyStateText.push(msg); + document.getElementById("display").innerHTML = readyStateText.join("<br>"); +} + +function load() { + is(document.readyState, "complete", "document.readyState should be 'complete' on load"); + showMessage("load: " + document.readyState); + SimpleTest.finish(); +} + +function readyStateChange() { + showMessage("readyStateChange: " + document.readyState); +} + +function DOMContentLoaded() { + is(document.readyState, "interactive", "document.readyState should be 'interactive' on DOMContentLoaded"); + showMessage("DOMContentLoaded: " + document.readyState); +} + +window.onload=load; + +attachCustomEventListener(document, "readystatechange", readyStateChange); +attachCustomEventListener(document, "DOMContentLoaded", DOMContentLoaded); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug347174_write.html b/dom/html/test/test_bug347174_write.html new file mode 100644 index 0000000000..75912b59a9 --- /dev/null +++ b/dom/html/test/test_bug347174_write.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=347174 +--> +<head> + <title>Test for Bug 347174</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=347174">Mozilla Bug 347174</a> +<p id="display"></p> + +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 347174 **/ +// simple test of readyState during loading, DOMContentLoaded, and complete +// this test passes in IE7 +window.readyStateText = []; +window.loaded = false; +function attachCustomEventListener(element, eventName, command) { + if (window.addEventListener && !window.opera) + element.addEventListener(eventName, command, true); + else if (window.attachEvent) + element.attachEvent("on" + eventName, command); +} + +function showMessage(msg) { + window.readyStateText.push(msg); + document.getElementById("display").innerHTML = readyStateText.join("<br>"); +} + +function frameLoad() { + var doc = $('iframe').contentWindow.document; + is(doc.readyState, "complete", "frame document.readyState should be 'complete' on load"); + showMessage("frame load: " + doc.readyState); + if (window.loaded) SimpleTest.finish(); +} + +function load() { + window.loaded = true; + + var imgsrc = "<img onload ='window.parent.imgLoad()' src='image.png?noCache=" + + (new Date().getTime()) + "'>\n"; + var doc = $('iframe').contentWindow.document; + doc.writeln(imgsrc); + doc.close(); + showMessage("frame after document.write: " + doc.readyState); + isnot(doc.readyState, "complete", "frame document.readyState should not be 'complete' after document.write"); +} + +function imgLoad() { + var doc = $('iframe').contentWindow.document; + showMessage("frame after imgLoad: " + doc.readyState); + is(doc.readyState, "interactive", "frame document.readyState should still be 'interactive' after img loads"); +} + +window.onload=load; + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<iframe src="404doesnotexist" id="iframe" onload="frameLoad();"></iframe> +</body> +</html> diff --git a/dom/html/test/test_bug347174_xsl.html b/dom/html/test/test_bug347174_xsl.html new file mode 100644 index 0000000000..e396e8b721 --- /dev/null +++ b/dom/html/test/test_bug347174_xsl.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=347174 +--> +<head> + <title>Test for Bug 347174</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=347174">Mozilla Bug 347174</a> +<p id="display"></p> +<div id="content" style="display: none"> + <iframe src="347174transformable.xml" id="iframe"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 347174 **/ +// Test of readyState of XML document transformed via XSLT to HTML +// this test passes in IE7 +window.readyStateText = []; + +function showMessage(msg) { + window.readyStateText.push(msg); + document.getElementById("display").innerHTML = readyStateText.join("<br>"); +} + +function frameScriptTag(readyState) { + isnot(readyState, "complete", "document.readyState should not be 'complete' when scripts run initially"); + showMessage("script tag: " + readyState); +} + +function frameLoad(readyState) { + is(readyState, "complete", "document.readyState should be 'complete' on load"); + showMessage("load: " + readyState); + SimpleTest.finish(); +} + +function frameReadyStateChange(readyState) { + showMessage("readyStateChange: " + readyState); +} + +function frameDOMContentLoaded(readyState) { + is(readyState, "interactive", "document.readyState should be 'interactive' on DOMContentLoaded"); + showMessage("DOMContentLoaded: " + readyState); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug347174_xslp.html b/dom/html/test/test_bug347174_xslp.html new file mode 100644 index 0000000000..313e96d1d0 --- /dev/null +++ b/dom/html/test/test_bug347174_xslp.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=347174 +--> +<head> + <title>Test for Bug 347174</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=347174">Mozilla Bug 347174</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 347174 **/ +// verifies that documents created with createDocument are born in "complete" state +// (so we don't accidentally leave them in "interactive" state) +window.readyStateText = []; + +function runTest() { + var xhr = new XMLHttpRequest(); + xhr.responseType = "document"; + xhr.open("GET", "347174transform.xsl"); + xhr.send(); + xhr.onload = function() { + var xslDoc = xhr.responseXML.documentElement; + + var processor = new XSLTProcessor(); + processor.importStylesheet(xslDoc); + + window.transformedDoc = processor.transformToDocument(xmlDoc); + + showMessage("loaded: " + xmlDoc.readyState); + is(xmlDoc.readyState, "complete", "XML document.readyState should be 'complete' after transform"); + SimpleTest.finish(); + }; +} + +var xmlDoc = document.implementation.createDocument("", "test", null); +showMessage("createDocument: " + xmlDoc.readyState); +is(xmlDoc.readyState, "complete", "created document readyState should be 'complete' before being associated with a parser"); + +runTest(); + +function showMessage(msg) { + window.readyStateText.push(msg); + $("display").innerHTML = readyStateText.join("<br>"); +} + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug353415-1.html b/dom/html/test/test_bug353415-1.html new file mode 100644 index 0000000000..5cb93e0577 --- /dev/null +++ b/dom/html/test/test_bug353415-1.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<iframe name="submit_frame"></iframe> +<form method="get" id="form1" target="submit_frame" action="../../../../../blah"> +<input type="text" name="field1" value="teststring"><br> +<input type="radio" name="field2" value="0" checked> 0 +<input type="radio" name="field3" value="1"> 1<br> +<input type="checkbox" name="field4" value="1" checked> 1 +<input type="checkbox" name="field5" value="2"> 2 +<input type="checkbox" name="field6" value="3" checked> 3 +<select name="field7"> +<option value="1">1</option> +<option value="2" selected>2</option> +<option value="3">3</option> +<option value="4">4</option> +</select> +<input name="field8" value="8"> +<input name="field9" value="9"> +<input type="image" name="field10"> +<label name="field11"> +<input name="field12"> +<input type="button" name="field13" value="button"> +</form> +<script> + SimpleTest.waitForExplicitFinish(); + + addLoadEvent(function() { + document.getElementsByName('submit_frame')[0].onload = function() { + is(frames.submit_frame.location.href, `${location.origin}/blah?field1=teststring&field2=0&field4=1&field6=3&field7=2&field8=8&field9=9&field12=`, "Submit string was correct."); + SimpleTest.finish(); + }; + + document.forms[0].submit(); + }); +</script> +</body> +</html> diff --git a/dom/html/test/test_bug353415-2.html b/dom/html/test/test_bug353415-2.html new file mode 100644 index 0000000000..d27480dff4 --- /dev/null +++ b/dom/html/test/test_bug353415-2.html @@ -0,0 +1,67 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<iframe name="submit_frame"></iframe> +<form method="get" id="form1" target="submit_frame" action="../../../../../blah"> +<table> +<tr><td> +<input type="text" name="field1" value="teststring"><br> +<input type="radio" name="field2" value="0" checked> 0 +<input type="radio" name="field3" value="1"> 1<br> +<input type="checkbox" name="field4" value="1" checked> 1 +<input type="checkbox" name="field5" value="2"> 2 +<input type="checkbox" name="field6" value="3" checked> 3 +<select name="field7"> +<option value="1">1</option> +<option value="2" selected>2</option> +<option value="3">3</option> +<option value="4">4</option> +</select> +<input name="field8" value="8"> +<input name="field9" value="9"> +<input type="image" name="field10"> +<label name="field11"></label> +<input name="field12"> +<input type="button" name="field13" value="button"> +<input type="hidden" name="field14" value="14"> +</td> +<input type="text" name="field1-2" value="teststring"><br> +<input type="radio" name="field2-2" value="0" checked> 0 +<input type="radio" name="field3-2" value="1"> 1<br> +<input type="checkbox" name="field4-2" value="1" checked> 1 +<input type="checkbox" name="field5-2" value="2"> 2 +<input type="checkbox" name="field6-2" value="3" checked> 3 +<select name="field7-2"> +<option value="1">1</option> +<option value="2" selected>2</option> +<option value="3">3</option> +<option value="4">4</option> +</select> +<input name="field8-2" value="8"> +<input name="field9-2" value="9"> +<input type="image" name="field10-2"> +<label name="field11-2"></label> +<input name="field12-2"> +<input type="button" name="field13-2" value="button"> +<input type="hidden" name="field14-2" value="14"> +</tr> +</table> +</form> +<script> + SimpleTest.waitForExplicitFinish(); + + addLoadEvent(function() { + document.getElementsByName('submit_frame')[0].onload = function() { + is(frames.submit_frame.location.href, `${location.origin}/blah?field1-2=teststring&field2-2=0&field4-2=1&field6-2=3&field7-2=2&field8-2=8&field9-2=9&field12-2=&field1=teststring&field2=0&field4=1&field6=3&field7=2&field8=8&field9=9&field12=&field14=14&field14-2=14`, "Submit string was correct."); + SimpleTest.finish(); + }; + + document.forms[0].submit(); + }); +</script> +</body> +</html> diff --git a/dom/html/test/test_bug359657.html b/dom/html/test/test_bug359657.html new file mode 100644 index 0000000000..fe24eace2e --- /dev/null +++ b/dom/html/test/test_bug359657.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=359657 +--> +<head> + <title>Test for Bug 359657</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=359657">Mozilla Bug 359657</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +/** Test for Bug 359657 **/ +function runTest() { + var span = document.createElement("span"); + $("test").insertBefore(span, $("test").firstChild); + ok(true, "Reachability, we should get here without crashing"); + is($("test").firstChild, span, "First child is correct"); + SimpleTest.finish(); +} +</script> +<div> + <iframe src=""></iframe> + <!-- Important: This test needs to run async at this point. The actual test + is not crashing while running this test! --> + <script type="text/javascript" src="data:text/javascript,runTest()"></script> +</div> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug369370.html b/dom/html/test/test_bug369370.html new file mode 100644 index 0000000000..c39e6e3243 --- /dev/null +++ b/dom/html/test/test_bug369370.html @@ -0,0 +1,153 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=369370 +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <title>Test for Bug 369370</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <script type="text/javascript"> + /* + * Test strategy: + */ + function makeClickFor(x, y) { + var event = kidDoc.createEvent("mouseevent"); + event.initMouseEvent("click", + true, true, kidWin, 1, // bubbles, cancelable, view, single-click + x, y, x, y, // screen X/Y, client X/Y + false, false, false, false, // no key modifiers + 0, null); // left click, not relatedTarget + return event; + } + + function childLoaded() { + kidDoc = kidWin.document; + ok(true, "Child window loaded"); + + var elements = kidDoc.getElementsByTagName("img"); + is(elements.length, 1, "looking for imagedoc img"); + var img = elements[0]; + + // Need to use innerWidth/innerHeight of the window + // since the containing image is absolutely positioned, + // causing clientHeight to be zero. + is(kidWin.innerWidth, 400, "Checking doc width"); + is(kidWin.innerHeight, 300, "Checking doc height"); + + // Image just loaded and is scaled to window size. + is(img.width, 400, "image width"); + is(img.height, 300, "image height"); + is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft"); + is(kidDoc.body.scrollTop, 0, "Checking scrollTop"); + + // ========== test 1 ========== + // Click in the upper left to zoom in + var event = makeClickFor(25,25); + img.dispatchEvent(event); + ok(true, "----- click 1 -----"); + + is(img.width, 800, "image width"); + is(img.height, 600, "image height"); + is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft"); + is(kidDoc.body.scrollTop, 0, "Checking scrollTop"); + + // ========== test 2 ========== + // Click there again to zoom out + event = makeClickFor(25,25); + img.dispatchEvent(event); + ok(true, "----- click 2 -----"); + + is(img.width, 400, "image width"); + is(img.height, 300, "image height"); + is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft"); + is(kidDoc.body.scrollTop, 0, "Checking scrollTop"); + + // ========== test 3 ========== + // Click in the lower right to zoom in + event = makeClickFor(350, 250); + img.dispatchEvent(event); + ok(true, "----- click 3 -----"); + + is(img.width, 800, "image width"); + is(img.height, 600, "image height"); + is(kidDoc.body.scrollLeft, + kidDoc.body.scrollLeftMax, "Checking scrollLeft"); + is(kidDoc.body.scrollTop, + kidDoc.body.scrollTopMax, "Checking scrollTop"); + + // ========== test 4 ========== + // Click there again to zoom out + event = makeClickFor(350, 250); + img.dispatchEvent(event); + ok(true, "----- click 4 -----"); + + is(img.width, 400, "image width"); + is(img.height, 300, "image height"); + is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft"); + is(kidDoc.body.scrollTop, 0, "Checking scrollTop"); + + // ========== test 5 ========== + // Click in the upper left to zoom in again + event = makeClickFor(25, 25); + img.dispatchEvent(event); + ok(true, "----- click 5 -----"); + is(img.width, 800, "image width"); + is(img.height, 600, "image height"); + is(kidDoc.body.scrollLeft, 0, "Checking scrollLeft"); + is(kidDoc.body.scrollTop, 0, "Checking scrollTop"); + is(img.getBoundingClientRect().top, 0, "Image is in view vertically"); + + // ========== test 6 ========== + // Now try resizing the window so the image fits vertically. + function test6() { + kidWin.addEventListener("resize", function() { + // Give the image document time to respond + SimpleTest.executeSoon(function() { + is(img.height, 600, "image height"); + var bodyHeight = kidDoc.documentElement.scrollHeight; + var imgRect = img.getBoundingClientRect(); + is(imgRect.top, bodyHeight - imgRect.bottom, "Image is vertically centered"); + test7(); + }); + }, {once: true}); + + var decorationSize = kidWin.outerHeight - kidWin.innerHeight; + kidWin.resizeTo(400, 600 + 50 + decorationSize); + } + + // ========== test 7 ========== + // Now try resizing the window so the image no longer fits vertically. + function test7() { + kidWin.addEventListener("resize", function() { + // Give the image document time to respond + SimpleTest.executeSoon(function() { + is(img.height, 600, "image height"); + is(img.getBoundingClientRect().top, 0, "Image is at top again"); + kidWin.close(); + SimpleTest.finish(); + }); + }, {once: true}); + + var decorationSize = kidWin.outerHeight - kidWin.innerHeight; + kidWin.resizeTo(400, 300 + decorationSize); + } + + test6(); + } + var kidWin; + var kidDoc; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set":[["browser.enable_automatic_image_resizing", true]]}, function() { + kidWin = window.open("bug369370-popup.png", "bug369370", "width=400,height=300"); + // will init onload + ok(kidWin, "opened child window"); + kidWin.onload = childLoaded; + }); + </script> +</body> +</html> diff --git a/dom/html/test/test_bug371375.html b/dom/html/test/test_bug371375.html new file mode 100644 index 0000000000..1cd0865293 --- /dev/null +++ b/dom/html/test/test_bug371375.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=371375 +--> +<head> + <title>Test for Bug 371375</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=371375">Mozilla Bug 371375</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + /** Test for Bug 371375 **/ + var load1Called = false; + var error1Called = false; + var s = document.createElement('script'); + s.type = 'text/javascript'; + s.onload = function() { load1Called = true; }; + s.onerror = function(event) { error1Called = true; event.stopPropagation(); }; + s.src = 'about:cache-entry?client=image&sb=0&key=http://www.google.com'; + document.body.appendChild(s); + + var load2Called = false; + var error2Called = false; + var s2 = document.createElement('script'); + s2.type = 'text/javascript'; + s2.onload = function() { load2Called = true; }; + s2.onerror = function(event) { error2Called = true; event.stopPropagation(); }; + s2.src = 'data:text/plain, var x = 1;' + document.body.appendChild(s2); + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + is(load1Called, false, "Load handler should not be called"); + is(error1Called, true, "Error handler should be called"); + is(load2Called, true, "Load handler for valid script should be called"); + is(error2Called, false, + "Error handler for valid script should not be called"); + SimpleTest.finish(); + }); +</script> +</body> +</html> + + + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug372098.html b/dom/html/test/test_bug372098.html new file mode 100644 index 0000000000..900b20100d --- /dev/null +++ b/dom/html/test/test_bug372098.html @@ -0,0 +1,68 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=372098 +--> +<head> + <title>Test for Bug 372098</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <base target="bug372098"></base> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=372098">Mozilla Bug 372098</a> + <p id="display"></p> + <div id="content" style="display:none;"> + <iframe name="bug372098"></iframe> + <a id="a" href="bug372098-link-target.html?a" target="">link</a> + <map> + <area id="area" shape="default" href="bug372098-link-target.html?area" target=""/> + </map> + </div> + <pre id="test"> + <script class="testbody" type="text/javascript"> + +var a_passed = false; +var area_passed = false; + +/* Start the test */ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(handle_load); + +function handle_load() +{ + sendMouseEvent({type:'click'}, 'a'); +} + +/* Finish the test */ + +function finish_test() +{ + ok(a_passed, "The 'a' element used the correct target."); + ok(area_passed, "The 'area' element used the correct target."); + SimpleTest.finish(); +} + +/* Callback function used by the linked document */ + +function callback(tag) +{ + switch (tag) { + case 'a': + a_passed = true; + sendMouseEvent({type:'click'}, 'area'); + return; + case 'area': + area_passed = true; + finish_test(); + return; + } + throw new Error("Eh??? We only test the 'a', 'link' and 'area' elements."); +} + + </script> + </pre> + +</body> +</html> diff --git a/dom/html/test/test_bug373589.html b/dom/html/test/test_bug373589.html new file mode 100644 index 0000000000..f494370f3d --- /dev/null +++ b/dom/html/test/test_bug373589.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=373589
+-->
+<head>
+ <title>Test for Bug 373589</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=373589">Mozilla Bug 373589</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 373589 **/
+ var docElem = document.documentElement;
+ var body = document.body;
+ var numChildren = docElem.childNodes.length;
+ docElem.removeChild(body);
+ ok(numChildren > docElem.childNodes.length, "body was removed");
+ body.link;
+ ok(true, "didn't crash");
+ docElem.appendChild(body);
+ is(numChildren, docElem.childNodes.length, "body re-added");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/html/test/test_bug375003-1.html b/dom/html/test/test_bug375003-1.html new file mode 100644 index 0000000000..2a5e6b911e --- /dev/null +++ b/dom/html/test/test_bug375003-1.html @@ -0,0 +1,157 @@ +<!DOCTYPE HTML> +<html id="html"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=375003 +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <title>Test 1 for bug 375003</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <style type="text/css"> + + html,body { + color:black; background-color:white; font-size:16px; padding:0; margin:0; + } + + .s { display:block; width:20px; height:20px; background-color:lime; } + table { background:pink; } + #td5,#td6 { border:7px solid blue;} + </style> + +<script> +var x = [ 'Left','Top','Width','Height' ]; +function test(id,s,expected) { + var el = document.getElementById(id); + for(var i = 0; i < x.length; ++i) { + // eslint-disable-next-line no-eval + var actual = eval('el.'+s+x[i]); + if (expected[i] != -1 && s+x[i]!='scrollHeight') + is(actual, expected[i], id+"."+s+x[i]); + } +} +function t3(id,c,o,s,pid) { + test(id,'client',c); + test(id,'offset',o); + test(id,'scroll',s); + var p = document.getElementById(id).offsetParent; + is(p.id, pid, id+".offsetParent"); +} + +function run_test() { + t3('span1',[0,0,20,20],[12,12,20,20],[0,0,20,20],'td1'); + t3('td1' ,[1,1,69,44],[16,16,71,46],[0,0,69,46],'table1'); + t3('tr1' ,[0,0,71,46],[16,16,71,46],[0,0,71,44],'table1'); + t3('span2',[10,0,20,20],[27,12,30,20],[0,0,20,20],'td2'); + t3('table1',[0,0,103,131],[10,10,103,131],[0,0,103,131],'body'); + t3('div1',[10,10,-1,131],[0,0,-1,151],[0,0,-1,85],'body'); + + t3('span2b',[10,0,20,20],[25,-1,30,20],[0,0,20,20],'body'); + // XXX not sure how to make reliable cross-platform tests for replaced-inline, inline + // t3('span2c',[10,2,18,2],[25,-1,30,6],[0,0,30,20],'body'); + // t3('span2d',[0,0,0,0],[25,-1,10,19],[0,0,10,20],'body'); + + t3('span3' ,[0,0,20,20],[15,0,20,20],[0,0,20,20],'td3'); + t3('td3' ,[0,0,35,20],[0,0,35,20],[0,0,35,20],'table3'); + t3('tr3' ,[0,0,35,20],[0,0,35,20],[0,0,35,22],'table3'); + t3('span4' ,[0,0,20,20],[0,0,20,20],[0,0,20,20],'td4'); + t3('table3',[0,0,35,40],[0,0,35,40],[0,0,35,40],'div3'); + t3('div3',[10,10,-1,40],[0,151,-1,60],[0,0,-1,70],'body'); + + t3('span5' ,[0,0,20,20],[1,1,20,20],[0,0,20,20],'td5'); + t3('td5' ,[7,7,22,22],[2,2,36,36],[0,0,22,36],'table5'); + t3('tr5' ,[0,0,36,36],[2,2,36,36],[0,0,36,22],'table5'); + t3('span6' ,[0,0,20,20],[20,58,20,20],[0,0,20,20],'div5'); + t3('table5',[0,0,40,78],[0,0,40,78],[0,0,40,78],'div5'); + t3('div5',[10,10,-1,78],[0,211,-1,98],[0,0,-1,70],'body'); + + t3('span7' ,[0,0,20,20],[1,1,20,20],[0,0,20,20],'td7'); + t3('td7' ,[1,1,37,22],[2,2,39,24],[0,0,37,22],'table7'); + t3('tr7' ,[0,0,39,24],[2,2,39,24],[0,0,39,22],'table7'); + t3('span8' ,[0,0,20,20],[19,30,20,20],[0,0,20,20],'table7'); + t3('table7',[0,0,57,68],[10,319,57,68],[0,0,57,68],'body'); + t3('div7',[10,10,-1,68],[0,309,-1,88],[0,0,-1,70],'body'); + + t3('span9' ,[0,0,20,20],[1,1,20,20],[0,0,20,20],'td9'); + t3('td9' ,[1,1,22,22],[2,2,24,24],[0,0,22,24],'table9'); + t3('tr9' ,[0,0,24,24],[2,2,24,24],[0,0,24,22],'table9'); + t3('span10' ,[0,0,20,20],[17,43,20,20],[0,0,20,20],'table9'); + t3('table9',[0,0,54,60],[10,407,54,60],[0,0,54,60],'body'); + t3('div9',[10,10,-1,0],[0,397,-1,20],[0,0,-1,70],'body'); + + t3('span11' ,[0,0,20,20],[1,1,20,20],[0,0,20,20],'td11'); + t3('td11' ,[0,0,22,22],[2,2,22,22],[0,0,22,22],'table11'); + t3('tr11' ,[0,0,22,22],[2,2,22,22],[0,0,22,22],'table11'); + t3('span12' ,[0,0,20,20],[28,454,20,20],[0,0,20,20],'body'); + t3('table11',[0,0,26,30],[10,427,26,30],[0,0,26,30],'body'); + t3('div11',[10,10,-1,30],[0,417,-1,50],[0,0,-1,70],'body'); +} +</script> +</head> +<body id="body"> + +<div id="content"> +<div id="div1" style="border:10px solid black"> +<table id="table1" cellspacing="7" cellpadding="12" border="9"> + <tbody id="tbody1"><tr id="tr1"><td id="td1"><div class="s" id="span1"></div></td></tr></tbody> + <tbody id="tbody2"><tr id="tr2"><td id="td2"><div class="s" id="span2" style="margin-left:15px; border-left:10px solid blue;"></div></td></tr></tbody> +</table> +</div> + +<div id="div3" style="border:10px solid black; position:relative"> +<table id="table3" cellpadding="0" cellspacing="0" border="0"> + <tbody id="tbody3"><tr id="tr3"><td id="td3"><div class="s" id="span3" style="margin-left:15px"></div></td></tr></tbody> + <tbody id="tbody4"><tr id="tr4"><td id="td4"><div class="s" id="span4"></div></td></tr></tbody> +</table> +</div> + +<div id="div5" style="border:10px solid black; position:relative"> +<table id="table5"> + <tbody id="tbody5"><tr id="tr5"><td id="td5"><div class="s" id="span5"></div></td></tr></tbody> + <tbody id="tbody6"><tr id="tr6"><td id="td6"><div class="s" id="span6" style="left:10px; top:10px; position:relative"></div></td></tr></tbody> +</table> +</div> + +<div id="div7" style="border:10px solid black;"> +<table id="table7" style="position:relative" border=7> + <tbody id="tbody7"><tr id="tr7"><td id="td7"><div class="s" id="span7"></div></td></tr></tbody> + <tbody id="tbody8"><tr id="tr8"><td id="td8"><div class="s" id="span8" style="position:relative; margin-left:15px"></div></td></tr></tbody> +</table> +</div> + +<div id="div9" style="border:10px solid black;"> +<table id="table9" style="position:absolute" border="13"> + <tbody id="tbody9"><tr id="tr9"><td id="td9"><div class="s" id="span9"></div></td></tr></tbody> + <tbody id="tbody10"><tr id="tr10"><td id="td10"><div class="s" id="span10" style="position:absolute"></div></td></tr></tbody> +</table> +</div> + +<div id="div11" style="border:10px solid black; "> +<table id="table11"> + <tbody id="tbody11"><tr id="tr11"><td id="td11"><div class="s" id="span11"></div></td></tr></tbody> + <tbody id="tbody12"><tr id="tr12"><td id="td12"><div class="s" id="span12" style="position:absolute;margin-left:15px"></div></td></tr></tbody> +</table> +</div> + +<div style="border:10px solid black"> +<div class="s" id="span2b" style="margin-left:15px; border-left:10px solid blue;"></div></div> + +<div style="border:10px solid black"> +<button id="span2c" style="margin-left:15px; border-left:10px solid blue;"></button></div> + +<div style="border:10px solid black"> +<span id="span2d" style="margin-left:15px; border-left:10px solid blue;"></span></div> +</div> + +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=375003">Mozilla Bug 375003</a> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +run_test(); +</script> +</pre> + +</body> +</html> diff --git a/dom/html/test/test_bug375003-2.html b/dom/html/test/test_bug375003-2.html new file mode 100644 index 0000000000..b8b985846c --- /dev/null +++ b/dom/html/test/test_bug375003-2.html @@ -0,0 +1,110 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=375003 +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <title>Test 2 for bug 375003</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <style type="text/css"> + + html { + padding:0; margin:0; + } + body { + color:black; background-color:white; font-size:12px; padding:10px; margin:0; + } + + #div1,#abs1,#table1 { + border: 20px solid lime; + padding: 30px; + width: 100px; + height: 60px; + overflow:scroll; + } + #abs1,#table2parent { + position:absolute; + left:500px; + } + #table3parent { + position:fixed; + left:300px; + top:100px; + } + .content { + display:block; + width:200px; + height:200px; + background:yellow; + border: 0px dotted black; + } +</style> + + +<script type="text/javascript"> +var x = [ 'Left','Top','Width','Height' ]; +function test(id,s,expected) { + var el = document.getElementById(id); + for(var i = 0; i < x.length; ++i) { + // eslint-disable-next-line no-eval + var actual = eval('el.'+s+x[i]); + if (expected[i] != -1 && s+x[i]!='scrollHeight') + is(actual, expected[i], id+"."+s+x[i]); + } +} +function t3(id,c,o,s,pid) { + test(id,'client',c); + test(id,'offset',o); + test(id,'scroll',s); + var p = document.getElementById(id).offsetParent; + is(p.id, pid, id+".offsetParent"); +} + +function run_test() { + // XXX how test clientWidth/clientHeight (the -1 below) in cross-platform manner + // without hard-coding the scrollbar width? + t3('div1',[20,20,-1,-1],[10,10,200,160],[0,0,230,20],'body'); + t3('abs1',[20,20,-1,-1],[500,170,200,160],[0,0,230,20],'body'); + t3('table1',[0,0,306,306],[10,170,306,306],[0,0,306,306],'body'); + t3('table2',[0,0,206,206],[0,0,206,206],[0,0,206,20],'table2parent'); + t3('table3',[0,0,228,228],[0,0,228,228],[0,0,228,228],'table3parent'); + t3('table3parent',[0,0,228,228],[300,100,228,228],[0,0,228,228],'body'); +} +</script> + +</head> +<body id="body"> +<div id="content"> +<div id="div1parent"> + <div id="div1"><span class="content">DIV</span></div> +</div> + +<div id="abs1parent"> + <div id="abs1"><span class="content">abs.pos.DIV</span></div> +</div> + +<div id="table1parent"> + <table id="table1"><tbody><tr><td id="td1"><span class="content">TABLE</span></td></tr></tbody></table> +</div> + +<div id="table2parent"> + <table id="table2"><tbody><tr><td id="td2"><span class="content">TABLE in abs</span></td></tr></tbody></table> +</div> + +<div id="table3parent"> + <table id="table3" border="10"><tbody><tr><td id="td3"><span class="content">TABLE in fixed</span></td></tr></tbody></table> +</div> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +run_test(); +</script> +</pre> + +</body> +</html> diff --git a/dom/html/test/test_bug377624.html b/dom/html/test/test_bug377624.html new file mode 100644 index 0000000000..d385708c5e --- /dev/null +++ b/dom/html/test/test_bug377624.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=377624 +--> +<head> + <title>Test for Bug 377624</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=377624">Mozilla Bug 377624</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 377624 **/ + +var input = document.createElement('input'); +ok("accept" in input, "'accept' is a valid input property"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug380383.html b/dom/html/test/test_bug380383.html new file mode 100644 index 0000000000..4ec632c0d6 --- /dev/null +++ b/dom/html/test/test_bug380383.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=380383 +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1"> + <title>Test for Bug 380383</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=380383">Mozilla Bug 380383</a> +<p id="display"> + <iframe id="f1" name="f1"></iframe> + <iframe id="f2" name="f2"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + /** Test for Bug 380383 **/ + is($("f1").contentDocument.characterSet, "UTF-8", + "Unexpected charset for f1"); + + function runTest() { + is($("f2").contentDocument.characterSet, "UTF-8", + "Unexpected charset for f2"); + } + + addLoadEvent(runTest); + addLoadEvent(SimpleTest.finish); + SimpleTest.waitForExplicitFinish(); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug383383.html b/dom/html/test/test_bug383383.html new file mode 100644 index 0000000000..a518f426cf --- /dev/null +++ b/dom/html/test/test_bug383383.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=383383 +--> +<head> + <title>Test for Bug 383383</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=383383">Mozilla Bug 383383</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript" for=" window " event=" onload() "> + +var foo = "bar"; + +</script> + +<script class="testbody" type="text/javascript" for="object" event="handler"> + +// This script should fail to run +foo = "baz"; + +isnot(foo, "baz", "test failed"); + +</script> + +<script class="testbody" type="text/javascript"> + +ok(foo == "bar", "test passed"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug383383_2.xhtml b/dom/html/test/test_bug383383_2.xhtml new file mode 100644 index 0000000000..4dccd381c4 --- /dev/null +++ b/dom/html/test/test_bug383383_2.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for bug 383383</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <script> + SimpleTest.waitForExplicitFinish() + </script> + <script for="window" event="bar"> + // This script should not run, but should not cause a parse error either. + ok(false, "Script was unexpectedly run") + </script> + <script> + ok(true, "Script was run as it should") + SimpleTest.finish() + </script> +</body> +</html> diff --git a/dom/html/test/test_bug384419.html b/dom/html/test/test_bug384419.html new file mode 100644 index 0000000000..72ed15ef87 --- /dev/null +++ b/dom/html/test/test_bug384419.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=384419 +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> + <title>Test for bug 384419</title> + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + + <style type="text/css"> + html,body { + color:black; background-color:white; font-size:16px; padding:0; margin:0; + } + body { margin: 10px; } + table { border:15px solid black; margin-left:100px; } +</style> + + +<script type="text/javascript"> +function t3(id,expected,pid) { + var el = document.getElementById(id); + var actual = el.offsetLeft; + is(actual, expected, id+".offsetLeft"); + + var p = document.getElementById(id).offsetParent; + is(p.id, pid, id+".offsetParent"); +} + +function run_test() { + t3('rel384419',135,'body'); + t3('abs384419',135,'body'); + t3('fix384419',135,'body'); +} +</script> + +</head> +<body id="body"> +<!-- It's important for the test that the tables below are directly inside body --> +<table cellpadding="7" cellspacing="3"><tr><td width="100"><div id="rel384419" style="position:relative;border:1px solid blue">X</div> relative</table> +<table cellpadding="7" cellspacing="3"><tr><td width="100"><div id="abs384419" style="position:absolute;border:1px solid blue">X</div> absolute</table> +<table cellpadding="7" cellspacing="3"><tr><td width="100"><div id="fix384419" style="position:fixed;border:1px solid blue">X</div> fixed</table> + + +<pre id="test"> +<script class="testbody" type="text/javascript"> +run_test(); +</script> +</pre> + +<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=384419">bug 384419</a> + +</body> +</html> diff --git a/dom/html/test/test_bug386496.html b/dom/html/test/test_bug386496.html new file mode 100644 index 0000000000..47c48592e8 --- /dev/null +++ b/dom/html/test/test_bug386496.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=386496 +--> +<head> + <title>Test for Bug 386496</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <SCRIPT Type="text/javascript" src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=386496">Mozilla Bug 386496</a> +<p id="display"></p> +<div id="content"> + <iframe style='display: block;' id="testIframe" + srcdoc="<div><a id='a' href='http://a.invalid/'>Link</a></div>"> + </iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 386496 **/ + +var frame = document.getElementById("testIframe"); + +function testDesignMode() { + var unloadRequested = false; + + frame.contentDocument.designMode = "on"; + + frame.contentWindow.addEventListener("beforeunload", function() { + unloadRequested = true; + }); + + synthesizeMouseAtCenter(frame.contentDocument.getElementById("a"), {}, + frame.contentWindow); + + // The click has been sent. If 'beforeunload' event has been caught when we go + // back from the event loop that means the link has been activated. + setTimeout(function() { + ok(!unloadRequested, "The link should not be activated in designMode"); + SimpleTest.finish(); + }, 0); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(testDesignMode); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug386728.html b/dom/html/test/test_bug386728.html new file mode 100644 index 0000000000..5562ce6712 --- /dev/null +++ b/dom/html/test/test_bug386728.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=386728 +--> +<head> + <title>Test for Bug 386728</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=386728">Mozilla Bug 386728</a> +<p id="display"></p> +<div id="content"> + <div id="frameContent"> + <div id="edit">This text is editable</div> + <button id="button_on" onclick="document.getElementById('edit').setAttribute('contenteditable', 'true')"></button> + </div> + <iframe id="testIframe"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 386728 **/ + +var frame = document.getElementById("testIframe"); + +function testContentEditable() { + frame.style.display = 'block'; + var frameContent = frame.contentDocument.adoptNode(document.getElementById("frameContent")); + frame.contentDocument.body.appendChild(frameContent); + frame.contentDocument.getElementById("edit").contentEditable = "true"; + frame.contentDocument.getElementById("edit").contentEditable = "false"; + frame.contentDocument.getElementById("button_on").click(); + is(frame.contentDocument.getElementById("edit").contentEditable, "true"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(testContentEditable); +addLoadEvent(SimpleTest.finish); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug386996.html b/dom/html/test/test_bug386996.html new file mode 100644 index 0000000000..a3068c2c2e --- /dev/null +++ b/dom/html/test/test_bug386996.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=386996 +--> +<head> + <title>Test for Bug 386996</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=386996">Mozilla Bug 386996</a> +<p id="display"></p> +<div id="content"> + <input id="input1"><input disabled><input id="input2"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 386996 **/ + +var frame = document.getElementById("testIframe"); + +function testContentEditable() { + var focusedElement; + document.getElementById("input1").onfocus = function() { focusedElement = this }; + document.getElementById("input2").onfocus = function() { focusedElement = this }; + + document.getElementById("input1").focus(); + synthesizeKey("KEY_Tab"); + + is(focusedElement.id, "input2"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(testContentEditable); +addLoadEvent(SimpleTest.finish); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug388558.html b/dom/html/test/test_bug388558.html new file mode 100644 index 0000000000..a86bab8d1a --- /dev/null +++ b/dom/html/test/test_bug388558.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=388558 +--> +<head> + <title>Test for Bug 388558</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=388558">Mozilla Bug 388558</a> +<p id="display"></p> +<div id="content"> + <input type="text" id="input" onchange="++inputChange;"> + <textarea id="textarea" onchange="++textareaChange;"></textarea> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 388558 **/ +var inputChange = 0; +var textareaChange = 0; + +function testUserInput() { + var input = document.getElementById("input"); + var textarea = SpecialPowers.wrap(document.getElementById("textarea")); + + input.focus(); + SpecialPowers.wrap(input).setUserInput("foo"); + input.blur(); + is(inputChange, 1, "Input element should have got one change event."); + + input.focus(); + input.value = "bar"; + input.blur(); + is(inputChange, 1, + "Change event dispatched when setting the value of the input element"); + + input.value = ""; + is(inputChange, 1, + "Change event dispatched when setting the value of the input element (2)."); + + SpecialPowers.wrap(input).setUserInput("foo"); + is(inputChange, 2, + "Change event dispatched when input element doesn't have focus."); + + textarea.focus(); + textarea.setUserInput("foo"); + textarea.blur(); + is(textareaChange, 1, "Textarea element should have got one change event."); + + textarea.focus(); + textarea.value = "bar"; + textarea.blur(); + is(textareaChange, 1, + "Change event dispatched when setting the value of the textarea element."); + + textarea.value = ""; + is(textareaChange, 1, + "Change event dispatched when setting the value of the textarea element (2)."); + + textarea.setUserInput("foo"); + is(textareaChange, 1, + "Change event dispatched when textarea element doesn't have focus."); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(testUserInput); +addLoadEvent(SimpleTest.finish); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug388746.html b/dom/html/test/test_bug388746.html new file mode 100644 index 0000000000..38c73ee0b5 --- /dev/null +++ b/dom/html/test/test_bug388746.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=388746 +--> +<head> + <title>Test for Bug 388746</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=388746">Mozilla Bug 388746</a> +<p id="display"></p> +<div id="content"> + <input> + <textarea></textarea> + <select> + <option>option1</option> + <optgroup label="optgroup"> + <option>option2</option> + </optgroup> + </select> + <button>Button</button> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 388746 **/ + +var previousEventTarget = ""; + +function handler(evt) { + if (evt.eventPhase == 2) { + previousEventTarget = evt.target.localName.toLowerCase(); + } +} + +function testElementType(type) { + var el = document.getElementsByTagName(type)[0]; + el.addEventListener("DOMAttrModified", handler, true); + el.setAttribute("foo", "bar"); + ok(previousEventTarget == type, + type + " element should have got DOMAttrModified event."); +} + +function test() { + testElementType("input"); + testElementType("textarea"); + testElementType("select"); + testElementType("option"); + testElementType("optgroup"); + testElementType("button"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(test); +addLoadEvent(SimpleTest.finish); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug388794.html b/dom/html/test/test_bug388794.html new file mode 100644 index 0000000000..06388547d7 --- /dev/null +++ b/dom/html/test/test_bug388794.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=388794 +--> +<head> + <title>Test for Bug 388794</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input { padding: 0; margin: 0; border: none; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=388794">Mozilla Bug 388794</a> +<p id="display"> + <form action="dummy_page.html" target="test1" method="GET"> + <input id="test1image" type="image" name="testImage"> + </form> + <form action="dummy_page.html" target="test2" method="GET"> + <input id="test2image" type="image"> + </form> + <form action="dummy_page.html" target="test3" method="GET"> + <input id="test3image" type="image" src="nnc_lockup.gif" name="testImage"> + </form> + <form action="dummy_page.html" target="test4" method="GET"> + <input id="test4image" type="image" src="nnc_lockup.gif"> + </form> + <form action="dummy_page.html" target="test5" method="GET"> + <input id="test5image" type="image" src="nnc_lockup.gif" name="testImage"> + </form> + <form action="dummy_page.html" target="test6" method="GET"> + <input id="test6image" type="image" src="nnc_lockup.gif"> + </form> + <iframe name="test1" id="test1"></iframe> + <iframe name="test2" id="test2"></iframe> + <iframe name="test3" id="test3"></iframe> + <iframe name="test4" id="test4"></iframe> + <iframe name="test5" id="test5"></iframe> + <iframe name="test6" id="test6"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 388794 **/ +SimpleTest.waitForExplicitFinish(); + +var pendingLoads = 0; +/* Use regex due to rounding error in Fennec with C++APZ enabled */ +var hrefs = { + test1: /\?testImage\.x=0&testImage\.y=0/, + test2: /\?x=0&y=0/, + test3: /\?testImage\.x=0&testImage\.y=0/, + test4: /\?x=0&y=0/, + test5: /\?testImage\.x=[4-6]&testImage\.y=[4-6]/, + test6: /\?x=[4-6]&y=[4-6]/, +}; + +function submitForm(idNum) { + $("test"+idNum).setAttribute("onload", "frameLoaded(this)"); + $("test" + idNum + "image").focus(); + sendKey("return"); +} + +function submitFormMouse(idNum) { + $("test"+idNum).setAttribute("onload", "frameLoaded(this)"); + // Use 4.99 instead of 5 to guard against the possibility that the + // image's 'top' is exactly N + 0.5 pixels from the root. In that case + // we'd round up the widget mouse coordinate to N + 6, which relative + // to the image would be 5.5, which would get rounded up to 6 when + // submitting the form. Instead we round the widget mouse coordinate to + // N + 5, which relative to the image would be 4.5 which gets rounded up + // to 5. + synthesizeMouse($("test" + idNum + "image"), 4.99, 4.99, {}); +} + +addLoadEvent(function() { + // Need the timeout so painting has a chance to be unsuppressed. + setTimeout(function() { + submitForm(++pendingLoads); + submitForm(++pendingLoads); + submitForm(++pendingLoads); + submitForm(++pendingLoads); + submitFormMouse(++pendingLoads); + submitFormMouse(++pendingLoads); + }, 0); +}); + +function frameLoaded(frame) { + ok( + hrefs[frame.name].test(frame.contentWindow.location.href), + "Unexpected href for frame " + frame.name + " - " + + "expected to match: " + hrefs[frame.name].toString() + " got: " + frame.contentWindow.location.href + ); + if (--pendingLoads == 0) { + SimpleTest.finish(); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug389797.html b/dom/html/test/test_bug389797.html new file mode 100644 index 0000000000..701d6e65c9 --- /dev/null +++ b/dom/html/test/test_bug389797.html @@ -0,0 +1,243 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=389797 +--> +<head> + <title>Test for Bug 389797</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=389797">Mozilla Bug 389797</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 389797 **/ +var allTags = []; +var classInfos = {}; +var interfaces = {}; +var interfacesNonClassinfo = {}; + +function getClassName(tag) { + return "HTML" + classInfos[tag] + "Element"; +} + +function HTML_TAG(aTagName, aImplClass) { + allTags.push(aTagName); + classInfos[aTagName] = aImplClass; + interfaces[aTagName] = []; + + // Some interfaces don't appear in classinfo because other interfaces that + // inherit from them do. + interfacesNonClassinfo[aTagName] = [ ]; + + if (arguments.length > 2) { + for (var i = 0; i < arguments[2].length; ++i) { + interfaces[aTagName].push(arguments[2][i]); + } + } + + if (arguments.length > 3) { + for (i = 0; i < arguments[3].length; ++i) { + interfacesNonClassinfo[aTagName].push(arguments[3][i]); + } + } +} + +const objectIfaces = [ + "nsIRequestObserver", + "nsIStreamListener", + "nsIObjectLoadingContent", + "nsIChannelEventSink", +]; + +/* List copy/pasted from nsHTMLTagList.h, with the second field modified to the + correct classinfo (instead of the impl class) in the following cases: + + base + blockquote + dir + dl + embed + menu + ol + param + q + ul + wbr + head + html + */ + +HTML_TAG("a", "Anchor"); +HTML_TAG("abbr", ""); +HTML_TAG("acronym", ""); +HTML_TAG("address", ""); +HTML_TAG("area", "Area"); +HTML_TAG("article", ""); +HTML_TAG("aside", ""); +HTML_TAG("b", ""); +HTML_TAG("base", "Base"); +HTML_TAG("bdi", "") +HTML_TAG("bdo", ""); +HTML_TAG("bgsound", "Unknown"); +HTML_TAG("big", ""); +HTML_TAG("blockquote", "Quote"); +HTML_TAG("body", "Body"); +HTML_TAG("br", "BR"); +HTML_TAG("button", "Button"); +HTML_TAG("canvas", "Canvas"); +HTML_TAG("caption", "TableCaption"); +HTML_TAG("center", ""); +HTML_TAG("cite", ""); +HTML_TAG("code", ""); +HTML_TAG("col", "TableCol"); +HTML_TAG("colgroup", "TableCol"); +HTML_TAG("data", "Data"); +HTML_TAG("datalist", "DataList"); +HTML_TAG("dd", ""); +HTML_TAG("del", "Mod"); +HTML_TAG("dfn", ""); +HTML_TAG("dir", "Directory"); +HTML_TAG("div", "Div"); +HTML_TAG("dl", "DList"); +HTML_TAG("dt", ""); +HTML_TAG("em", ""); +HTML_TAG("embed", "Embed", [], objectIfaces); +HTML_TAG("fieldset", "FieldSet"); +HTML_TAG("figcaption", "") +HTML_TAG("figure", "") +HTML_TAG("font", "Font"); +HTML_TAG("footer", "") +HTML_TAG("form", "Form"); +HTML_TAG("frame", "Frame", [ "nsIDOMMozBrowserFrame" ]); +HTML_TAG("frameset", "FrameSet"); +HTML_TAG("h1", "Heading"); +HTML_TAG("h2", "Heading"); +HTML_TAG("h3", "Heading"); +HTML_TAG("h4", "Heading"); +HTML_TAG("h5", "Heading"); +HTML_TAG("h6", "Heading"); +HTML_TAG("head", "Head"); +HTML_TAG("header", "") +HTML_TAG("hgroup", "") +HTML_TAG("hr", "HR"); +HTML_TAG("html", "Html"); +HTML_TAG("i", ""); +HTML_TAG("iframe", "IFrame", [ "nsIDOMMozBrowserFrame" ]); +HTML_TAG("image", ""); +HTML_TAG("img", "Image", [ "nsIImageLoadingContent" ], []); +HTML_TAG("input", "Input", [], [ "imgINotificationObserver", + "nsIImageLoadingContent" ]); +HTML_TAG("ins", "Mod"); +HTML_TAG("kbd", ""); +HTML_TAG("keygen", "Unknown"); +HTML_TAG("label", "Label"); +HTML_TAG("legend", "Legend"); +HTML_TAG("li", "LI"); +HTML_TAG("link", "Link"); +HTML_TAG("listing", "Pre"); +HTML_TAG("main", ""); +HTML_TAG("map", "Map"); +HTML_TAG("mark", ""); +HTML_TAG("marquee", "Marquee"); +HTML_TAG("menu", "Menu"); +HTML_TAG("meta", "Meta"); +HTML_TAG("meter", "Meter"); +HTML_TAG("multicol", "Unknown"); +HTML_TAG("nav", "") +HTML_TAG("nobr", ""); +HTML_TAG("noembed", ""); +HTML_TAG("noframes", ""); +HTML_TAG("noscript", ""); +HTML_TAG("object", "Object", [], objectIfaces); +HTML_TAG("ol", "OList"); +HTML_TAG("optgroup", "OptGroup"); +HTML_TAG("option", "Option"); +HTML_TAG("p", "Paragraph"); +HTML_TAG("param", "Param"); +HTML_TAG("plaintext", ""); +HTML_TAG("pre", "Pre"); +HTML_TAG("q", "Quote"); +HTML_TAG("rb", ""); +HTML_TAG("rp", ""); +HTML_TAG("rt", ""); +HTML_TAG("rtc", ""); +HTML_TAG("ruby", ""); +HTML_TAG("s", ""); +HTML_TAG("samp", ""); +HTML_TAG("script", "Script", [ "nsIScriptLoaderObserver" ], []); +HTML_TAG("section", "") +HTML_TAG("select", "Select"); +HTML_TAG("small", ""); +HTML_TAG("span", "Span"); +HTML_TAG("strike", ""); +HTML_TAG("strong", ""); +HTML_TAG("style", "Style"); +HTML_TAG("sub", ""); +HTML_TAG("sup", ""); +HTML_TAG("table", "Table"); +HTML_TAG("tbody", "TableSection"); +HTML_TAG("td", "TableCell"); +HTML_TAG("textarea", "TextArea"); +HTML_TAG("tfoot", "TableSection"); +HTML_TAG("th", "TableCell"); +HTML_TAG("thead", "TableSection"); +HTML_TAG("template", "Template"); +HTML_TAG("time", "Time"); +HTML_TAG("title", "Title"); +HTML_TAG("tr", "TableRow"); +HTML_TAG("tt", ""); +HTML_TAG("u", ""); +HTML_TAG("ul", "UList"); +HTML_TAG("var", ""); +HTML_TAG("wbr", ""); +HTML_TAG("xmp", "Pre"); + +function tagName(aTag) { + return "<" + aTag + ">"; +} + +for (var tag of allTags) { + var node = document.createElement(tag); + + // Have to use the proto's toString(), since HTMLAnchorElement and company + // override toString(). + var nodeString = HTMLElement.prototype.toString.apply(node); + + // Debug builds have extra info, so chop off after "Element" if it's followed + // by ' ' or ']' + nodeString = nodeString.replace(/Element[\] ].*/, "Element"); + + var classInfoString = getClassName(tag); + is(nodeString, "[object " + classInfoString, + "Unexpected classname for " + tagName(tag)); + is(node instanceof window[classInfoString], true, + tagName(tag) + " not an instance of " + classInfos[tag]); + + if (classInfoString != 'HTMLUnknownElement') { + is(node instanceof HTMLUnknownElement, false, + tagName(tag) + " is an instance of HTMLUnknownElement"); + } else { + is(node instanceof HTMLUnknownElement, true, + tagName(tag) + " is an instance of HTMLUnknownElement"); + } + + // Check that each node QIs to all the things we expect it to QI to + for (var iface of interfaces[tag].concat(interfacesNonClassinfo[tag])) { + is(iface in SpecialPowers.Ci, true, + iface + " not in Components.interfaces"); + is(node instanceof SpecialPowers.Ci[iface], true, + tagName(tag) + " does not QI to " + iface); + } +} +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug390975.html b/dom/html/test/test_bug390975.html new file mode 100644 index 0000000000..8a7e09b807 --- /dev/null +++ b/dom/html/test/test_bug390975.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=390975 +--> +<head> + <title>Test for Bug 390975</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=390975">Mozilla Bug 390975</a> +<p id="display"></p> +<div id="content" style="display: none"> + <table id="table1"> + <form id="form1"> + <input> + <input> + <tr><td> + <input> + <input> + <input> + </td></tr> + </form> + </table> + + <table id="table2"> + <form id="form2"> + <input> + <input> + <tr id="row2"><td> + <input> + <input> + <input> + </td></tr> + </form> + </table> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 390975 **/ +var form = $("form1"); +is(form.elements.length, 5, "Unexpected elements length"); + +$("table1").remove(); +is(form.elements.length, 3, "Should have lost control outside table"); + +form.remove(); +is(form.elements.length, 0, "Should have lost control outside form"); + +form = $("form2"); +is(form.elements.length, 5, "Unexpected elements length"); + +$("row2").remove(); +is(form.elements.length, 2, "Should have lost controls inside table row"); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug391994.html b/dom/html/test/test_bug391994.html new file mode 100644 index 0000000000..8dfa6cc772 --- /dev/null +++ b/dom/html/test/test_bug391994.html @@ -0,0 +1,184 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=391994 +--> +<head> + <title>Test for Bug 391994</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=391994">Mozilla Bug 391994</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 391994 **/ +var testNumber = 0; + +function assertSelected(aOption, aExpectDefaultSelected, aExpectSelected) { + ++testNumber; + is(aOption.defaultSelected, aExpectDefaultSelected, + "Asserting default-selected state for option " + testNumber); + is(aOption.selected, aExpectSelected, + "Asserting selected state for option " + testNumber); +} + +function assertSame(aSel1, aSel2Str, aTestNumber) { + var div = document.createElement("div"); + div.innerHTML = aSel2Str; + sel2 = div.firstChild; + is(aSel1.options.length, sel2.options.length, + "Length should be same in select test " + aTestNumber); + is(aSel1.selectedIndex, sel2.selectedIndex, + "Selected index should be same in select test " + aTestNumber); + for (var i = 0; i < aSel1.options.length; ++i) { + is(aSel1.options[i].selected, sel2.options[i].selected, + "Options[" + i + "].selected should be the same in select test " + + aTestNumber); + is(aSel1.options[i].defaultSelected, sel2.options[i].defaultSelected, + "Options[" + i + + "].defaultSelected should be the same in select test " + + aTestNumber); + } +} + +// Creation methods +var opt = document.createElement("option"); +assertSelected(opt, false, false); + +opt = new Option(); +assertSelected(opt, false, false); + +// Setting of defaultSelected +opt = new Option(); +opt.setAttribute("selected", "selected"); +assertSelected(opt, true, true); + +opt = new Option(); +opt.defaultSelected = true; +assertSelected(opt, true, true); +is(opt.hasAttribute("selected"), true, "Attribute should be set"); +is(opt.getAttribute("selected"), "", + "Attribute should be set to empty string"); + +// Setting of selected +opt = new Option(); +opt.selected = false; +assertSelected(opt, false, false); + +opt = new Option(); +opt.selected = true; +assertSelected(opt, false, true); + +// Interaction of selected and defaultSelected +opt = new Option(); +opt.selected; +opt.setAttribute("selected", "selected"); +assertSelected(opt, true, true); + +opt = new Option(); +opt.selected = false; +opt.setAttribute("selected", "selected"); +assertSelected(opt, true, false); + +opt = new Option(); +opt.setAttribute("selected", "selected"); +opt.selected = true; +opt.removeAttribute("selected"); +assertSelected(opt, false, true); + +// First test of putting things in a <select>: Adding default-selected option +// should select it. +var sel = document.createElement("select"); +sel.appendChild(new Option()); +is(sel.selectedIndex, 0, "First option should be selected"); +assertSelected(sel.firstChild, false, true); + +sel.appendChild(new Option()); +is(sel.selectedIndex, 0, "First option should still be selected"); +assertSelected(sel.firstChild, false, true); +assertSelected(sel.firstChild.nextSibling, false, false); + +opt = new Option(); +opt.defaultSelected = true; +sel.appendChild(opt); +assertSelected(sel.firstChild, false, false); +assertSelected(sel.firstChild.nextSibling, false, false); +assertSelected(opt, true, true); +is(opt, sel.firstChild.nextSibling.nextSibling, "What happened here?"); +is(sel.options[0], sel.firstChild, "Unexpected option 0"); +is(sel.options[1], sel.firstChild.nextSibling, "Unexpected option 1"); +is(sel.options[2], opt, "Unexpected option 2"); +is(sel.selectedIndex, 2, "Unexpected selectedIndex in select test 1"); + +assertSame(sel, "<select><option><option><option selected></select>", 1); + +// Second test of putting things in a <select>: Adding two default-selected +// options should select the second one. +sel = document.createElement("select"); +sel.appendChild(new Option()); +sel.appendChild(new Option()); +opt = new Option(); +opt.defaultSelected = true; +sel.appendChild(opt); +opt = new Option(); +opt.defaultSelected = true; +sel.appendChild(opt); +assertSelected(sel.options[0], false, false); +assertSelected(sel.options[1], false, false); +assertSelected(sel.options[2], true, false); +assertSelected(sel.options[3], true, true); +is(sel.selectedIndex, 3, "Unexpected selectedIndex in select test 2"); + +assertSame(sel, + "<select><option><option><option selected><option selected></select>", 2); + +// Third test of putting things in a <select>: adding a selected option earlier +// than another selected option should make the new option selected. +sel = document.createElement("select"); +sel.appendChild(new Option()); +sel.appendChild(new Option()); +opt = new Option(); +opt.defaultSelected = true; +sel.appendChild(opt); +opt = new Option(); +opt.defaultSelected = true; +sel.options[0] = opt; +assertSelected(sel.options[0], true, true); +assertSelected(sel.options[1], false, false); +assertSelected(sel.options[2], true, false); +is(sel.selectedIndex, 0, "Unexpected selectedIndex in select test 3"); + +// Fourth test of putting things in a <select>: Just like second test, but with +// a <select multiple> +sel = document.createElement("select"); +sel.multiple = true; +sel.appendChild(new Option()); +sel.appendChild(new Option()); +opt = new Option(); +opt.defaultSelected = true; +sel.appendChild(opt); +opt = new Option(); +opt.defaultSelected = true; +sel.appendChild(opt); +assertSelected(sel.options[0], false, false); +assertSelected(sel.options[1], false, false); +assertSelected(sel.options[2], true, true); +assertSelected(sel.options[3], true, true); +is(sel.selectedIndex, 2, "Unexpected selectedIndex in select test 4"); + +assertSame(sel, + "<select multiple><option><option>" + + "<option selected><option selected></select>", + 4); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug394700.html b/dom/html/test/test_bug394700.html new file mode 100644 index 0000000000..fb6a54421b --- /dev/null +++ b/dom/html/test/test_bug394700.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=394700 +--> +<head> + <title>Test for Bug 394700</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=394700">Mozilla Bug 394700</a> +<p id="display"></p> +<div id="content"> + <select><option id="A">A</option><option id="B">B</option></select> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 394700 **/ + +function remove(q1) { q1.remove(); } + +function testSelectedIndex() +{ + document.addEventListener("DOMNodeRemoved", foo); + remove(document.getElementById("B")); + document.removeEventListener("DOMNodeRemoved", foo); + + function foo() + { + document.removeEventListener("DOMNodeRemoved", foo); + remove(document.getElementById("A")); + } + var selectElement = document.getElementsByTagName("select")[0]; + is(selectElement.selectedIndex, -1, "Wrong selected index!"); + is(selectElement.length, 0, "Select shouldn't have any options!"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(testSelectedIndex); +addLoadEvent(SimpleTest.finish); + + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug395107.html b/dom/html/test/test_bug395107.html new file mode 100644 index 0000000000..8273cd6c6e --- /dev/null +++ b/dom/html/test/test_bug395107.html @@ -0,0 +1,108 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=395107 +--> +<head> + <title>Test for Bug 395107</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=395107">Mozilla Bug 395107</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 395107 **/ +var testNumber = 0; + +function assertSelected(aOption, aExpectDefaultSelected, aExpectSelected) { + ++testNumber; + is(aOption.defaultSelected, aExpectDefaultSelected, + "Asserting default-selected state for option " + testNumber); + is(aOption.selected, aExpectSelected, + "Asserting selected state for option " + testNumber); +} + +function assertSame(aSel1, aSel2Str, aTestNumber) { + var div = document.createElement("div"); + div.innerHTML = aSel2Str; + sel2 = div.firstChild; + is(aSel1.options.length, sel2.options.length, + "Length should be same in select test " + aTestNumber); + is(aSel1.selectedIndex, sel2.selectedIndex, + "Selected index should be same in select test " + aTestNumber); + for (var i = 0; i < aSel1.options.length; ++i) { + is(aSel1.options[i].selected, sel2.options[i].selected, + "Options[" + i + "].selected should be the same in select test " + + aTestNumber); + is(aSel1.options[i].defaultSelected, sel2.options[i].defaultSelected, + "Options[" + i + + "].defaultSelected should be the same in select test " + + aTestNumber); + } +} + +// In a single-select, setting an option selected should deselect an +// existing selected option. +var sel = document.createElement("select"); +sel.appendChild(new Option()); +is(sel.selectedIndex, 0, "First option should be selected"); +assertSelected(sel.firstChild, false, true); +sel.appendChild(new Option()); +is(sel.selectedIndex, 0, "First option should still be selected"); +assertSelected(sel.firstChild, false, true); +assertSelected(sel.firstChild.nextSibling, false, false); + +opt = new Option(); +sel.appendChild(opt); +opt.defaultSelected = true; +assertSelected(sel.firstChild, false, false); +assertSelected(sel.firstChild.nextSibling, false, false); +assertSelected(opt, true, true); +is(opt, sel.firstChild.nextSibling.nextSibling, "What happened here?"); +is(sel.options[0], sel.firstChild, "Unexpected option 0"); +is(sel.options[1], sel.firstChild.nextSibling, "Unexpected option 1"); +is(sel.options[2], opt, "Unexpected option 2"); +is(sel.selectedIndex, 2, "Unexpected selectedIndex in select test 1"); + +assertSame(sel, "<select><option><option><option selected></select>", 1); + +// Same, but with the option that gets set selected earlier in the select +sel = document.createElement("select"); +sel.appendChild(new Option()); +sel.appendChild(new Option()); +opt = new Option(); +opt.defaultSelected = true; +sel.appendChild(opt); +opt = new Option(); +sel.options[0] = opt; +opt.defaultSelected = true; +assertSelected(sel.options[0], true, true); +assertSelected(sel.options[1], false, false); +assertSelected(sel.options[2], true, false); +is(sel.selectedIndex, 0, "Unexpected selectedIndex in select test 2"); + +// And now try unselecting options +sel = document.createElement("select"); +sel.appendChild(new Option()); +opt = new Option(); +opt.defaultSelected = true; +sel.appendChild(opt); +sel.appendChild(new Option()); +opt.defaultSelected = false; + +assertSelected(sel.options[0], false, true); +assertSelected(sel.options[1], false, false); +assertSelected(sel.options[2], false, false); +is(sel.selectedIndex, 0, "Unexpected selectedIndex in select test 2"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug401160.xhtml b/dom/html/test/test_bug401160.xhtml new file mode 100644 index 0000000000..bb9fe47111 --- /dev/null +++ b/dom/html/test/test_bug401160.xhtml @@ -0,0 +1,27 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=401160 +--> +<head> + <title>Test for Bug 401160</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=401160">Mozilla Bug 401160</a> +<label id="label" contenteditable="true"><legend></legend><div></div></label> + +<pre id="test"> +<script type="text/javascript"> + +function do_test() { + document.getElementById('label').focus(); + ok(true, "This is crash test - the test succeeded if we reach this line") +} + +do_test(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug402680.html b/dom/html/test/test_bug402680.html new file mode 100644 index 0000000000..517942772e --- /dev/null +++ b/dom/html/test/test_bug402680.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=402680 +--> +<head> + <title>Test for Bug 402680</title> + <script> + var activeElementIsNull = (document.activeElement == null); + </script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=402680">Mozilla Bug 402680</a> +<p id="display"></p> +<div id="content"> + <input type="text"> + <textarea></textarea> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 402680 **/ + +ok(activeElementIsNull, + "Before document has body, active element should be null"); + +function testActiveElement() { + ok(document.body == document.activeElement, + "After page load body element should be the active element!"); + var input = document.getElementsByTagName("input")[0]; + input.focus(); + ok(document.activeElement == input, + "Input element isn't the active element!"); + var textarea = document.getElementsByTagName("textarea")[0]; + textarea.focus(); + ok(document.activeElement == textarea, + "Textarea element isn't the active element!"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(testActiveElement); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug403868.html b/dom/html/test/test_bug403868.html new file mode 100644 index 0000000000..43118a0683 --- /dev/null +++ b/dom/html/test/test_bug403868.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=403868 +--> +<head> + <title>Test for Bug 403868</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=403868">Mozilla Bug 403868</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 403868 **/ +function createSpan(id, insertionPoint) { + var s = document.createElement("span"); + s.id = id; + $("content").insertBefore(s, insertionPoint); + return s; +} + +var s1a = createSpan("test1", null); +is(document.getElementById("test1"), s1a, + "Only one span with id=test1 in the tree; should work!"); + +var s2a = createSpan("test1", null); +is(document.getElementById("test1"), s1a, + "Appending span with id=test1 doesn't change which one comes first"); + +var s3a = createSpan("test1", s2a); +is(document.getElementById("test1"), s1a, + "Inserting span with id=test1 not at the beginning; doesn't matter"); + +var s4a = createSpan("test1", s1a); +is(document.getElementById("test1"), s4a, + "Inserting span with id=test1 at the beginning changes which one is first"); + +s4a.remove(); +is(document.getElementById("test1"), s1a, + "First-created span with id=test1 is first again"); + +s1a.remove(); +is(document.getElementById("test1"), s3a, + "Third-created span with id=test1 is first now"); + +// Start the id hashtable +for (var i = 0; i < 256; ++i) { + document.getElementById("no-such-id-in-the-document" + i); +} + +var s1b = createSpan("test2", null); +is(document.getElementById("test2"), s1b, + "Only one span with id=test2 in the tree; should work!"); + +var s2b = createSpan("test2", null); +is(document.getElementById("test2"), s1b, + "Appending span with id=test2 doesn't change which one comes first"); + +var s3b = createSpan("test2", s2b); +is(document.getElementById("test2"), s1b, + "Inserting span with id=test2 not at the beginning; doesn't matter"); + +var s4b = createSpan("test2", s1b); +is(document.getElementById("test2"), s4b, + "Inserting span with id=test2 at the beginning changes which one is first"); + +s4b.remove(); +is(document.getElementById("test2"), s1b, + "First-created span with id=test2 is first again"); + +s1b.remove(); +is(document.getElementById("test2"), s3b, + "Third-created span with id=test2 is first now"); + + + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug403868.xhtml b/dom/html/test/test_bug403868.xhtml new file mode 100644 index 0000000000..53c2a24d57 --- /dev/null +++ b/dom/html/test/test_bug403868.xhtml @@ -0,0 +1,86 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=403868 +--> +<head> + <title>Test for Bug 403868</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=403868">Mozilla Bug 403868</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +/** Test for Bug 403868 **/ +function createSpan(id, insertionPoint) { + var s = document.createElementNS("http://www.w3.org/1999/xhtml", "span"); + s.id = id; + $("content").insertBefore(s, insertionPoint); + return s; +} + +var s1a = createSpan("test1", null); +is(document.getElementById("test1"), s1a, + "Only one span with id=test1 in the tree; should work!"); + +var s2a = createSpan("test1", null); +is(document.getElementById("test1"), s1a, + "Appending span with id=test1 doesn't change which one comes first"); + +var s3a = createSpan("test1", s2a); +is(document.getElementById("test1"), s1a, + "Inserting span with id=test1 not at the beginning; doesn't matter"); + +var s4a = createSpan("test1", s1a); +is(document.getElementById("test1"), s4a, + "Inserting span with id=test1 at the beginning changes which one is first"); + +s4a.remove(); +is(document.getElementById("test1"), s1a, + "First-created span with id=test1 is first again"); + +s1a.remove(); +is(document.getElementById("test1"), s3a, + "Third-created span with id=test1 is first now"); + +// Start the id hashtable +for (var i = 0; i < 256; ++i) { + document.getElementById("no-such-id-in-the-document" + i); +} + +var s1b = createSpan("test2", null); +is(document.getElementById("test2"), s1b, + "Only one span with id=test2 in the tree; should work!"); + +var s2b = createSpan("test2", null); +is(document.getElementById("test2"), s1b, + "Appending span with id=test2 doesn't change which one comes first"); + +var s3b = createSpan("test2", s2b); +is(document.getElementById("test2"), s1b, + "Inserting span with id=test2 not at the beginning; doesn't matter"); + +var s4b = createSpan("test2", s1b); +is(document.getElementById("test2"), s4b, + "Inserting span with id=test2 at the beginning changes which one is first"); + +s4b.remove(); +is(document.getElementById("test2"), s1b, + "First-created span with id=test2 is first again"); + +s1b.remove(); +is(document.getElementById("test2"), s3b, + "Third-created span with id=test2 is first now"); + +]]> +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug405242.html b/dom/html/test/test_bug405242.html new file mode 100644 index 0000000000..b8999dc9f6 --- /dev/null +++ b/dom/html/test/test_bug405242.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=405242 +--> +<head> + <title>Test for Bug 405252</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=405242">Mozilla Bug 405242</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/** Test for Bug 405242 **/ +var sel = document.createElement("select"); +sel.appendChild(new Option()); +sel.appendChild(new Option()); +sel.appendChild(new Option()); +opt = new Option(); +opt.value = 10; +sel.appendChild(opt); +sel.options.remove(0); +sel.options.remove(1000); +sel.options.remove(-1); +is(sel.length, 3, "Unexpected option collection length"); +is(sel[2].value, "10", "Unexpected remained option"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug406596.html b/dom/html/test/test_bug406596.html new file mode 100644 index 0000000000..6886d078be --- /dev/null +++ b/dom/html/test/test_bug406596.html @@ -0,0 +1,83 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=406596 +--> +<head> + <title>Test for Bug 406596</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=406596">Mozilla Bug 406596</a> +<div id="content"> + <div id="edit" contenteditable="true">This text is editable, you can change its content <a href="#" id="a" tabindex="0">ABCDEFGHIJKLMNOPQRSTUV</a> <input type="submit" value="abcd" id="b"></input> <img src="foo.png" id="c"></div> + <div tabindex="0">This text is not editable but is focusable</div> + <div tabindex="0">This text is not editable but is focusable</div> + <a href="#" id="d" contenteditable="true">ABCDEFGHIJKLMNOPQRSTUV</a> + <div tabindex="0">This text is not editable but is focusable</div> + <div tabindex="0">This text is not editable but is focusable</div> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 406596 **/ + +function testTabbing(click, focus, selectionOffset) { + var wu = SpecialPowers.getDOMWindowUtils(window); + + var elem = document.getElementById(click); + var rect = elem.getBoundingClientRect(); + var selection = window.getSelection(); + + var x = (rect.left + rect.right) / 4; + var y = (rect.top + rect.bottom) / 2; + wu.sendMouseEvent("mousedown", x, y, 0, 1, 0); + wu.sendMouseEvent("mousemove", x + selectionOffset, y, 0, 1, 0); + wu.sendMouseEvent("mouseup", x + selectionOffset, y, 0, 1, 0); + if (selectionOffset) { + is(selection.rangeCount, 1, "there should be one range in the selection"); + var range = selection.getRangeAt(0); + } + var focusedElement = document.activeElement; + is(focusedElement, document.getElementById(focus), + "clicking should move focus to the contentEditable node"); + synthesizeKey("KEY_Tab"); + synthesizeKey("KEY_Tab"); + synthesizeKey("KEY_Tab", {shiftKey: true}); + synthesizeKey("KEY_Tab", {shiftKey: true}); + is(document.activeElement, focusedElement, + "tab/shift-tab should move focus back to the contentEditable node"); + if (selectionOffset) { + is(selection.rangeCount, 1, + "there should still be one range in the selection"); + var newRange = selection.getRangeAt(0); + is(newRange.compareBoundaryPoints(Range.START_TO_START, range), 0, + "the selection should be the same as before the tabbing"); + is(newRange.compareBoundaryPoints(Range.END_TO_END, range), 0, + "the selection should be the same as before the tabbing"); + } +} + +function test() { + window.getSelection().removeAllRanges(); + testTabbing("edit", "edit", 0); + testTabbing("a", "edit", 0); + testTabbing("d", "d", 0); + testTabbing("edit", "edit", 10); + testTabbing("a", "edit", 10); + testTabbing("d", "d", 10); + + SimpleTest.finish(); +} + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + setTimeout(test, 0); +}; + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug417760.html b/dom/html/test/test_bug417760.html new file mode 100644 index 0000000000..52a3c1b425 --- /dev/null +++ b/dom/html/test/test_bug417760.html @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=417760 +--> +<head> + <title>cannot focus() img with tabindex="-1"</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <style type="text/css"> + img { + border: 5px solid white; + } + img:focus { + border: 5px solid black; + } + </style> + + + <script src="/tests/SimpleTest/SimpleTest.js"></script> + + <script type="text/javascript"> + function checkFocus(aExpected, aTabIndex) + { + elemCurr = document.activeElement.getAttribute("id"); + is(elemCurr, aExpected, "Element with tabIndex " + aTabIndex + + " did not receive focus!"); + } + + function doTest() + { + // First, test img with tabindex = 0 + document.getElementById("img-tabindex-0").focus(); + checkFocus("img-tabindex-0", 0); + + // now test the img with tabindex = -1 + document.getElementById("img-tabindex-minus-1").focus(); + checkFocus("img-tabindex-minus-1", -1); + + // now test the img without tabindex, should NOT receive focus! + document.getElementById("img-no-tabindex").focus(); + checkFocus("img-tabindex-minus-1", null); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=417760">Mozilla Bug 417760</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <br>img tabindex="0": + <img id="img-tabindex-0" + src="file_bug417760.png" + alt="MoCo logo" tabindex="0"/> + <br>img tabindex="-1": + <img id="img-tabindex-minus-1" + src="file_bug417760.png" + alt="MoCo logo" tabindex="-1"/> + <br>img without tabindex: + <img id="img-no-tabindex" + src="file_bug417760.png" + alt="MoCo logo"/> +</body> +</html> diff --git a/dom/html/test/test_bug421640.html b/dom/html/test/test_bug421640.html new file mode 100644 index 0000000000..c63d026d1f --- /dev/null +++ b/dom/html/test/test_bug421640.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=421640 +--> +<head> + <title>Test for Bug 421640</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=421640">Mozilla Bug 421640</a> +<div id="content"> + <div id="edit" contenteditable="true">This text is editable</div> + <div><button id="button">Test</button></div> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 421640 **/ + +function test(click, focus, nextFocus) { + var wu = SpecialPowers.getDOMWindowUtils(window); + + var selection = window.getSelection(); + var edit = document.getElementById("edit"); + var text = edit.firstChild; + + selection.removeAllRanges(); + + var rect = edit.getBoundingClientRect(); + wu.sendMouseEvent("mousedown", rect.left + 1, rect.top + 1, 0, 1, 0); + wu.sendMouseEvent("mousemove", rect.right - 1, rect.top + 1, 0, 1, 0); + wu.sendMouseEvent("mouseup", rect.right - 1, rect.top + 1, 0, 1, 0); + + is(selection.anchorNode, text, ""); + + rect = document.getElementById("button").getBoundingClientRect(); + wu.sendMouseEvent("mousedown", rect.left + 10, rect.top + 1, 0, 1, 0); + wu.sendMouseEvent("mouseup", rect.left + 10, rect.top + 1, 0, 1, 0); + + is(selection.anchorNode, text, ""); + + SimpleTest.finish(); +} + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + setTimeout(test, 0); +}; + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug424698.html b/dom/html/test/test_bug424698.html new file mode 100644 index 0000000000..b59190e53d --- /dev/null +++ b/dom/html/test/test_bug424698.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=424698 +--> +<head> + <title>Test for Bug 424698</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=424698">Mozilla Bug 424698</a> +<p id="display"> +<input id="i1"> +<input id="target"> +<textarea id="i2"></textarea> +<textarea id="target2"></textarea> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 424698 **/ +var i = $("i1"); +is(i.value, "", "Value should be empty string"); +i.defaultValue = "test"; +is(i.value, "test", "Setting defaultValue should work"); +i.defaultValue = "test2"; +is(i.value, "test2", "Setting defaultValue multiple times should work"); + +// Now let's hide and reshow things +i.style.display = "none"; +is(i.offsetWidth, 0, "Input didn't hide?"); +i.style.display = ""; +isnot(i.offsetWidth, 0, "Input didn't show?"); +is(i.value, "test2", "Hiding/showing should not affect value"); +i.defaultValue = "test3"; +is(i.value, "test3", "Setting defaultValue after hide/show should work"); + +// Make sure typing works ok +i = $("target"); +i.focus(); // Otherwise editor gets confused when we send the key events +is(i.value, "", "Value should be empty string in second control"); +sendString("2test2"); +is(i.value, "2test2", 'We just typed the string "2test2"'); +i.defaultValue = "2test3"; +is(i.value, "2test2", "Setting defaultValue after typing should not work"); +i.style.display = "none"; +is(i.offsetWidth, 0, "Second input didn't hide?"); +i.style.display = ""; +isnot(i.offsetWidth, 0, "Second input didn't show?"); +is(i.value, "2test2", "Hiding/showing second input should not affect value"); +i.defaultValue = "2test4"; +is(i.value, "2test2", "Setting defaultValue after hide/show should not work if we typed"); + +i = $("i2"); +is(i.value, "", "Textarea value should be empty string"); +i.defaultValue = "test"; +is(i.value, "test", "Setting textarea defaultValue should work"); +i.defaultValue = "test2"; +is(i.value, "test2", "Setting textarea defaultValue multiple times should work"); + +// Now let's hide and reshow things +i.style.display = "none"; +is(i.offsetWidth, 0, "Textarea didn't hide?"); +i.style.display = ""; +isnot(i.offsetWidth, 0, "Textarea didn't show?"); +is(i.value, "test2", "Hiding/showing textarea should not affect value"); +i.defaultValue = "test3"; +is(i.value, "test3", "Setting textarea defaultValue after hide/show should work"); + +// Make sure typing works ok +i = $("target2"); +i.focus(); // Otherwise editor gets confused when we send the key events +is(i.value, "", "Textarea value should be empty string in second control"); +sendString("2test2"); +is(i.value, "2test2", 'We just typed the string "2test2"'); +i.defaultValue = "2test3"; +is(i.value, "2test2", "Setting textarea defaultValue after typing should not work"); +i.style.display = "none"; +is(i.offsetWidth, 0, "Second textarea didn't hide?"); +i.style.display = ""; +isnot(i.offsetWidth, 0, "Second textarea didn't show?"); +is(i.value, "2test2", "Hiding/showing second textarea should not affect value"); +i.defaultValue = "2test4"; +is(i.value, "2test2", "Setting textarea defaultValue after hide/show should not work if we typed"); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug428135.xhtml b/dom/html/test/test_bug428135.xhtml new file mode 100644 index 0000000000..ce269e2f8c --- /dev/null +++ b/dom/html/test/test_bug428135.xhtml @@ -0,0 +1,156 @@ +<?xml version="1.0"?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=428135 +--> +<head> + <title>Test for Bug 428135</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=428135">Mozilla Bug 428135</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +/** Test for Bug 428135 **/ + +var expectedCurrentTargets = new Array(); + +function d(el, ename) { + var e = document.createEvent("Events"); + e.initEvent(ename, true, true); + el.dispatchEvent(e); +} + +function testListener(e) { + e.preventDefault(); + var expected = expectedCurrentTargets.shift(); + ok(expected == e.currentTarget, + "Unexpected current target [" + e.currentTarget + "], event=" + e.type + + ", phase=" + e.eventPhase + ", target should have been " + expected); +} + +function getAndAddListeners(elname) { + var el = document; + if (elname) { + el = document.getElementById(elname); + } + el.addEventListener("submit", testListener, true); + el.addEventListener("submit", testListener); + el.addEventListener("reset", testListener, true); + el.addEventListener("reset", testListener); + el.addEventListener("fooEvent", testListener, true); + el.addEventListener("fooEvent", testListener); + return el; +} + +function testSubmitResetEvents() { + getAndAddListeners(null); + var outerForm = getAndAddListeners("outerForm"); + var outerSubmit = getAndAddListeners("outerSubmit"); + var outerReset = getAndAddListeners("outerReset"); + var outerSubmitDispatcher = getAndAddListeners("outerSubmitDispatcher"); + var outerResetDispatcher = getAndAddListeners("outerResetDispatcher"); + var outerChild = getAndAddListeners("outerChild"); + var innerForm = getAndAddListeners("innerForm"); + var innerSubmit = getAndAddListeners("innerSubmit"); + var innerReset = getAndAddListeners("innerReset"); + var innerSubmitDispatcher = getAndAddListeners("innerSubmitDispatcher"); + var innerResetDispatcher = getAndAddListeners("innerResetDispatcher"); + + expectedCurrentTargets = new Array(document, outerForm, outerForm, document); + outerSubmit.click(); + ok(!expectedCurrentTargets.length, + "(1) expectedCurrentTargets isn't empty!"); + + expectedCurrentTargets = new Array(document, outerForm, outerForm, document); + outerReset.click(); + ok(!expectedCurrentTargets.length, + "(2) expectedCurrentTargets isn't empty!"); + + // Because of bug 428135, submit shouldn't propagate + // back to outerForm and document! + expectedCurrentTargets = + new Array(document, outerForm, outerSubmitDispatcher, outerSubmitDispatcher); + outerSubmitDispatcher.click(); + ok(!expectedCurrentTargets.length, + "(3) expectedCurrentTargets isn't empty!"); + + // Because of bug 428135, reset shouldn't propagate + // back to outerForm and document! + expectedCurrentTargets = + new Array(document, outerForm, outerResetDispatcher, outerResetDispatcher); + outerResetDispatcher.click(); + ok(!expectedCurrentTargets.length, + "(4) expectedCurrentTargets isn't empty!"); + + // Because of bug 428135, submit shouldn't propagate + // back to outerForm and document! + expectedCurrentTargets = + new Array(document, outerForm, outerChild, innerForm, innerForm, outerChild); + innerSubmit.click(); + ok(!expectedCurrentTargets.length, + "(5) expectedCurrentTargets isn't empty!"); + + // Because of bug 428135, reset shouldn't propagate + // back to outerForm and document! + expectedCurrentTargets = + new Array(document, outerForm, outerChild, innerForm, innerForm, outerChild); + innerReset.click(); + ok(!expectedCurrentTargets.length, + "(6) expectedCurrentTargets isn't empty!"); + + // Because of bug 428135, submit shouldn't propagate + // back to inner/outerForm or document! + expectedCurrentTargets = + new Array(document, outerForm, outerChild, innerForm, innerSubmitDispatcher, + innerSubmitDispatcher); + innerSubmitDispatcher.click(); + ok(!expectedCurrentTargets.length, + "(7) expectedCurrentTargets isn't empty!"); + + // Because of bug 428135, reset shouldn't propagate + // back to inner/outerForm or document! + expectedCurrentTargets = + new Array(document, outerForm, outerChild, innerForm, innerResetDispatcher, + innerResetDispatcher); + innerResetDispatcher.click(); + ok(!expectedCurrentTargets.length, + "(8) expectedCurrentTargets isn't empty!"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(testSubmitResetEvents); +addLoadEvent(SimpleTest.finish); + + +]]> +</script> +</pre> +<form id="outerForm"> + <input type="submit" value="outer" id="outerSubmit"/> + <input type="reset" value="reset outer" id="outerReset"/> + <input type="button" value="dispatch submit" onclick="d(this, 'submit')" + id="outerSubmitDispatcher"/> + <input type="button" value="dispatch reset" onclick="d(this, 'reset')" + id="outerResetDispatcher"/> + <div id="outerChild"> + <form id="innerForm"> + <input type="submit" value="inner" id="innerSubmit"/> + <input type="reset" value="reset inner" id="innerReset"/> + <input type="button" value="dispatch submit" onclick="d(this, 'submit')" + id="innerSubmitDispatcher"/> + <input type="button" value="dispatch reset" onclick="d(this, 'reset')" + id="innerResetDispatcher"/> + </form> + </div> +</form> +</body> +</html> + diff --git a/dom/html/test/test_bug430351.html b/dom/html/test/test_bug430351.html new file mode 100644 index 0000000000..8cee4fe24f --- /dev/null +++ b/dom/html/test/test_bug430351.html @@ -0,0 +1,523 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=430351 +--> +<head> + <title>Test for Bug 430351</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=430351">Mozilla Bug 430351</a> +<p id="display"></p> +<div id="content"> + <div id="parent"></div> + <div id="editableParent" contenteditable="true"></div> + <iframe id="frame"></iframe> + <map name="map"><area></map> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 430351 **/ + +var focusableElements = [ + "<a tabindex=\"-1\"></a>", + "<a tabindex=\"0\"></a>", + "<a tabindex=\"0\" disabled></a>", + "<a tabindex=\"1\"></a>", + "<a contenteditable=\"true\"></a>", + + "<a href=\"#\"></a>", + "<a href=\"#\" tabindex=\"-1\"></a>", + "<a href=\"#\" tabindex=\"0\"></a>", + "<a href=\"#\" tabindex=\"0\" disabled></a>", + "<a href=\"#\" tabindex=\"1\"></a>", + "<a href=\"#\" contenteditable=\"true\"></a>", + "<a href=\"#\" disabled></a>", + + "<button></button>", + "<button tabindex=\"-1\"></button>", + "<button tabindex=\"0\"></button>", + "<button tabindex=\"1\"></button>", + "<button contenteditable=\"true\"></button>", + + "<button type=\"reset\"></button>", + "<button type=\"reset\" tabindex=\"-1\"></button>", + "<button type=\"reset\" tabindex=\"0\"></button>", + "<button type=\"reset\" tabindex=\"1\"></button>", + "<button type=\"reset\" contenteditable=\"true\"></button>", + + "<button type=\"submit\"></button>", + "<button type=\"submit\" tabindex=\"-1\"></button>", + "<button type=\"submit\" tabindex=\"0\"></button>", + "<button type=\"submit\" tabindex=\"1\"></button>", + "<button type=\"submit\" contenteditable=\"true\"></button>", + + "<div tabindex=\"-1\"></div>", + "<div tabindex=\"0\"></div>", + "<div tabindex=\"1\"></div>", + "<div contenteditable=\"true\"></div>", + "<div tabindex=\"0\" disabled></div>", + + "<embed>", + "<embed tabindex=\"-1\">", + "<embed tabindex=\"0\">", + "<embed tabindex=\"0\" disabled>", + "<embed tabindex=\"1\">", + "<embed disabled>", + "<embed contenteditable=\"true\">", + + "<iframe contenteditable=\"true\"></iframe>", + + "<iframe src=\"about:blank\"></iframe>", + "<iframe src=\"about:blank\" disabled></iframe>", + "<iframe src=\"about:blank\" tabindex=\"-1\"></iframe>", + "<iframe src=\"about:blank\" tabindex=\"0\"></iframe>", + "<iframe src=\"about:blank\" tabindex=\"0\" disabled></iframe>", + "<iframe src=\"about:blank\" tabindex=\"1\"></iframe>", + "<iframe src=\"about:blank\" contenteditable=\"true\"></iframe>", + + "<iframe></iframe>", + "<iframe tabindex=\"-1\"></iframe>", + "<iframe tabindex=\"0\"></iframe>", + "<iframe tabindex=\"0\" disabled></iframe>", + "<iframe tabindex=\"1\"></iframe>", + "<iframe disabled></iframe>", + + "<img tabindex=\"-1\">", + "<img tabindex=\"0\">", + "<img tabindex=\"0\" disabled>", + "<img tabindex=\"1\">", + + "<input>", + "<input tabindex=\"-1\">", + "<input tabindex=\"0\">", + "<input tabindex=\"1\">", + "<input contenteditable=\"true\">", + + "<input type=\"button\">", + "<input type=\"button\" tabindex=\"-1\">", + "<input type=\"button\" tabindex=\"0\">", + "<input type=\"button\" tabindex=\"1\">", + "<input type=\"button\" contenteditable=\"true\">", + + "<input type=\"checkbox\">", + "<input type=\"checkbox\" tabindex=\"-1\">", + "<input type=\"checkbox\" tabindex=\"0\">", + "<input type=\"checkbox\" tabindex=\"1\">", + "<input type=\"checkbox\" contenteditable=\"true\">", + + "<input type=\"image\">", + "<input type=\"image\" tabindex=\"-1\">", + "<input type=\"image\" tabindex=\"0\">", + "<input type=\"image\" tabindex=\"1\">", + "<input type=\"image\" contenteditable=\"true\">", + + "<input type=\"password\">", + "<input type=\"password\" tabindex=\"-1\">", + "<input type=\"password\" tabindex=\"0\">", + "<input type=\"password\" tabindex=\"1\">", + "<input type=\"password\" contenteditable=\"true\">", + + "<input type=\"radio\">", + "<input type=\"radio\" tabindex=\"-1\">", + "<input type=\"radio\" tabindex=\"0\">", + "<input type=\"radio\" tabindex=\"1\">", + "<input type=\"radio\" contenteditable=\"true\">", + "<input type=\"radio\" checked>", + "<form><input type=\"radio\" name=\"foo\"></form>", + + "<input type=\"reset\">", + "<input type=\"reset\" tabindex=\"-1\">", + "<input type=\"reset\" tabindex=\"0\">", + "<input type=\"reset\" tabindex=\"1\">", + "<input type=\"reset\" contenteditable=\"true\">", + + "<input type=\"submit\">", + "<input type=\"submit\" tabindex=\"-1\">", + "<input type=\"submit\" tabindex=\"0\">", + "<input type=\"submit\" tabindex=\"1\">", + "<input type=\"submit\" contenteditable=\"true\">", + + "<input type=\"text\">", + "<input type=\"text\" tabindex=\"-1\">", + "<input type=\"text\" tabindex=\"0\">", + "<input type=\"text\" tabindex=\"1\">", + "<input type=\"text\" contenteditable=\"true\">", + + "<input type=\"number\">", + "<input type=\"number\" tabindex=\"-1\">", + "<input type=\"number\" tabindex=\"0\">", + "<input type=\"number\" tabindex=\"1\">", + "<input type=\"number\" contenteditable=\"true\">", + + "<object tabindex=\"-1\"></object>", + "<object tabindex=\"0\"></object>", + "<object tabindex=\"1\"></object>", + "<object contenteditable=\"true\"></object>", + + "<object classid=\"java:a\"></object>", + "<object classid=\"java:a\" tabindex=\"-1\"></object>", + "<object classid=\"java:a\" tabindex=\"0\"></object>", + "<object classid=\"java:a\" tabindex=\"0\" disabled></object>", + "<object classid=\"java:a\" tabindex=\"1\"></object>", + "<object classid=\"java:a\" disabled></object>", + "<object classid=\"java:a\" contenteditable=\"true\"></object>", + + "<select></select>", + "<select tabindex=\"-1\"></select>", + "<select tabindex=\"0\"></select>", + "<select tabindex=\"1\"></select>", + "<select contenteditable=\"true\"></select>", + + "<option tabindex='-1'></option>", + "<option tabindex='0'></option>", + "<option tabindex='1'></option>", + "<option contenteditable></option>", + + "<optgroup tabindex='-1'></optgroup>", + "<optgroup tabindex='0'></optgroup>", + "<optgroup tabindex='1'></optgroup>", + "<optgroup contenteditable></optgroup>" +]; + +var nonFocusableElements = [ + "<a></a>", + "<a disabled></a>", + + "<button tabindex=\"0\" disabled></button>", + "<button disabled></button>", + + "<button type=\"reset\" tabindex=\"0\" disabled></button>", + "<button type=\"reset\" disabled></button>", + + "<button type=\"submit\" tabindex=\"0\" disabled></button>", + "<button type=\"submit\" disabled></button>", + + "<div></div>", + "<div disabled></div>", + + "<img>", + "<img disabled>", + "<img contenteditable=\"true\">", + + "<img usemap=\"#map\">", + "<img usemap=\"#map\" tabindex=\"-1\">", + "<img usemap=\"#map\" tabindex=\"0\">", + "<img usemap=\"#map\" tabindex=\"0\" disabled>", + "<img usemap=\"#map\" tabindex=\"1\">", + "<img usemap=\"#map\" disabled>", + "<img usemap=\"#map\" contenteditable=\"true\">", + + "<input tabindex=\"0\" disabled>", + "<input disabled>", + + "<input type=\"button\" tabindex=\"0\" disabled>", + "<input type=\"button\" disabled>", + + "<input type=\"checkbox\" tabindex=\"0\" disabled>", + "<input type=\"checkbox\" disabled>", + + "<input type=\"file\" tabindex=\"0\" disabled>", + "<input type=\"file\" disabled>", + + "<input type=\"hidden\">", + "<input type=\"hidden\" tabindex=\"-1\">", + "<input type=\"hidden\" tabindex=\"0\">", + "<input type=\"hidden\" tabindex=\"0\" disabled>", + "<input type=\"hidden\" tabindex=\"1\">", + "<input type=\"hidden\" disabled>", + "<input type=\"hidden\" contenteditable=\"true\">", + + "<input type=\"image\" tabindex=\"0\" disabled>", + "<input type=\"image\" disabled>", + + "<input type=\"password\" tabindex=\"0\" disabled>", + "<input type=\"password\" disabled>", + + "<input type=\"radio\" tabindex=\"0\" disabled>", + "<input type=\"radio\" disabled>", + + "<input type=\"reset\" tabindex=\"0\" disabled>", + "<input type=\"reset\" disabled>", + + "<input type=\"submit\" tabindex=\"0\" disabled>", + "<input type=\"submit\" disabled>", + + "<input type=\"text\" tabindex=\"0\" disabled>", + "<input type=\"text\" disabled>", + + "<object></object>", + + "<select tabindex=\"0\" disabled></select>", + "<select disabled></select>", + + "<option></option>", + "<option tabindex='1' disabled></option>", + + "<optgroup></optgroup>", + "<optgroup tabindex='1' disabled></optgroup>" +]; + +var focusableInContentEditable = [ + "<button></button>", + "<button tabindex=\"-1\"></button>", + "<button tabindex=\"0\"></button>", + "<button tabindex=\"1\"></button>", + "<button contenteditable=\"true\"></button>", + + "<button type=\"reset\"></button>", + "<button type=\"reset\" tabindex=\"-1\"></button>", + "<button type=\"reset\" tabindex=\"0\"></button>", + "<button type=\"reset\" tabindex=\"1\"></button>", + "<button type=\"reset\" contenteditable=\"true\"></button>", + + "<button type=\"submit\"></button>", + "<button type=\"submit\" tabindex=\"-1\"></button>", + "<button type=\"submit\" tabindex=\"0\"></button>", + "<button type=\"submit\" tabindex=\"1\"></button>", + "<button type=\"submit\" contenteditable=\"true\"></button>", + + "<div tabindex=\"-1\"></div>", + "<div tabindex=\"0\"></div>", + "<div tabindex=\"1\"></div>", + "<div tabindex=\"0\" disabled></div>", + + "<embed>", + "<embed tabindex=\"-1\">", + "<embed tabindex=\"0\">", + "<embed tabindex=\"0\" disabled>", + "<embed tabindex=\"1\">", + "<embed disabled>", + "<embed contenteditable=\"true\">", + + "<iframe src=\"about:blank\"></iframe>", + "<iframe></iframe>", + "<iframe src=\"about:blank\" disabled></iframe>", + "<iframe disabled></iframe>", + "<iframe src=\"about:blank\" tabindex=\"-1\"></iframe>", + "<iframe tabindex=\"-1\"></iframe>", + "<iframe src=\"about:blank\" tabindex=\"0\"></iframe>", + "<iframe tabindex=\"0\"></iframe>", + "<iframe src=\"about:blank\" tabindex=\"0\" disabled></iframe>", + "<iframe tabindex=\"0\" disabled></iframe>", + "<iframe src=\"about:blank\" tabindex=\"1\"></iframe>", + "<iframe tabindex=\"1\"></iframe>", + "<iframe src=\"about:blank\" contenteditable=\"true\"></iframe>", + "<iframe contenteditable=\"true\"></iframe>", + + "<img tabindex=\"-1\">", + "<img tabindex=\"0\">", + "<img tabindex=\"0\" disabled>", + "<img tabindex=\"1\">", + + "<input>", + "<input tabindex=\"-1\">", + "<input tabindex=\"0\">", + "<input tabindex=\"1\">", + "<input contenteditable=\"true\">", + + "<input type=\"button\">", + "<input type=\"button\" tabindex=\"-1\">", + "<input type=\"button\" tabindex=\"0\">", + "<input type=\"button\" tabindex=\"1\">", + "<input type=\"button\" contenteditable=\"true\">", + + "<input type=\"file\">", + "<input type=\"file\" tabindex=\"-1\">", + "<input type=\"file\" tabindex=\"0\">", + "<input type=\"file\" tabindex=\"1\">", + "<input type=\"file\" contenteditable=\"true\">", + + "<input type=\"checkbox\">", + "<input type=\"checkbox\" tabindex=\"-1\">", + "<input type=\"checkbox\" tabindex=\"0\">", + "<input type=\"checkbox\" tabindex=\"1\">", + "<input type=\"checkbox\" contenteditable=\"true\">", + + "<input type=\"image\">", + "<input type=\"image\" tabindex=\"-1\">", + "<input type=\"image\" tabindex=\"0\">", + "<input type=\"image\" tabindex=\"1\">", + "<input type=\"image\" contenteditable=\"true\">", + + "<input type=\"password\">", + "<input type=\"password\" tabindex=\"-1\">", + "<input type=\"password\" tabindex=\"0\">", + "<input type=\"password\" tabindex=\"1\">", + "<input type=\"password\" contenteditable=\"true\">", + + "<input type=\"radio\">", + "<input type=\"radio\" tabindex=\"-1\">", + "<input type=\"radio\" tabindex=\"0\">", + "<input type=\"radio\" tabindex=\"1\">", + "<input type=\"radio\" contenteditable=\"true\">", + "<input type=\"radio\" checked>", + "<form><input type=\"radio\" name=\"foo\"></form>", + + "<input type=\"reset\">", + "<input type=\"reset\" tabindex=\"-1\">", + "<input type=\"reset\" tabindex=\"0\">", + "<input type=\"reset\" tabindex=\"1\">", + "<input type=\"reset\" contenteditable=\"true\">", + + "<input type=\"submit\">", + "<input type=\"submit\" tabindex=\"-1\">", + "<input type=\"submit\" tabindex=\"0\">", + "<input type=\"submit\" tabindex=\"1\">", + "<input type=\"submit\" contenteditable=\"true\">", + + "<input type=\"text\">", + "<input type=\"text\" tabindex=\"-1\">", + "<input type=\"text\" tabindex=\"0\">", + "<input type=\"text\" tabindex=\"1\">", + "<input type=\"text\" contenteditable=\"true\">", + + "<input type=\"number\">", + "<input type=\"number\" tabindex=\"-1\">", + "<input type=\"number\" tabindex=\"0\">", + "<input type=\"number\" tabindex=\"1\">", + "<input type=\"number\" contenteditable=\"true\">", + + "<object tabindex=\"-1\"></object>", + "<object tabindex=\"0\"></object>", + "<object tabindex=\"1\"></object>", + + // Disabled doesn't work for <object>. + "<object tabindex=\"0\" disabled></object>", + "<object disabled></object>", + + "<select></select>", + "<select tabindex=\"-1\"></select>", + "<select tabindex=\"0\"></select>", + "<select tabindex=\"1\"></select>", + "<select contenteditable=\"true\"></select>", + + "<option tabindex='-1'></option>", + "<option tabindex='0'></option>", + "<option tabindex='1'></option>", + + "<optgroup tabindex='-1'></optgroup>", + "<optgroup tabindex='0'></optgroup>", + "<optgroup tabindex='1'></optgroup>" +]; + +var focusableInDesignMode = [ + "<embed>", + "<embed tabindex=\"-1\">", + "<embed tabindex=\"0\">", + "<embed tabindex=\"0\" disabled>", + "<embed tabindex=\"1\">", + "<embed disabled>", + "<embed contenteditable=\"true\">", + + "<img tabindex=\"-1\">", + "<img tabindex=\"0\">", + "<img tabindex=\"0\" disabled>", + "<img tabindex=\"1\">", +]; + +// Can't currently test these, need a plugin. +var focusableElementsTODO = [ + "<object classid=\"java:a\"></object>", + "<object classid=\"java:a\" tabindex=\"-1\"></object>", + "<object classid=\"java:a\" tabindex=\"0\"></object>", + "<object classid=\"java:a\" tabindex=\"0\" disabled></object>", + "<object classid=\"java:a\" tabindex=\"1\"></object>", + "<object classid=\"java:a\" disabled></object>", + "<object classid=\"java:a\" contenteditable=\"true\"></object>", +]; + +var serializer = new XMLSerializer(); + +function testElements(parent, tags, shouldBeFocusable) +{ + var focusable, errorSuffix = ""; + if (parent.ownerDocument.designMode == "on") { + focusable = focusableInDesignMode; + errorSuffix = " in a document with designMode=on"; + } + else if (parent.contentEditable == "true") { + focusable = focusableInContentEditable; + } + + for (var tag of tags) { + parent.ownerDocument.body.focus(); + + if (focusableElementsTODO.indexOf(tag) > -1) { + todo_is(parent.ownerDocument.activeElement, parent.firstChild, + tag + " should be focusable" + errorSuffix); + continue; + } + + parent.innerHTML = tag; + + // Focus the deepest descendant. + var descendant = parent; + while ((descendant = descendant.firstChild)) + element = descendant; + + if (element.nodeName == "IFRAME") + var foo = element.contentDocument; + + element.focus(); + + var errorPrefix = serializer.serializeToString(element) + " in " + + serializer.serializeToString(parent); + + try { + // Make sure activeElement doesn't point to a + // native anonymous element. + parent.ownerDocument.activeElement.localName; + } catch (ex) { + ok(false, ex + errorPrefix + errorSuffix); + } + if (focusable ? focusable.indexOf(tag) > -1 : shouldBeFocusable) { + is(parent.ownerDocument.activeElement, element, + errorPrefix + " should be focusable" + errorSuffix); + } + else { + isnot(parent.ownerDocument.activeElement, element, + errorPrefix + " should not be focusable" + errorSuffix); + } + + parent.innerHTML = ""; + } +} + +function test() +{ + var parent = document.getElementById("parent"); + var editableParent = document.getElementById("editableParent"); + + testElements(parent, focusableElements, true); + testElements(parent, nonFocusableElements, false); + + testElements(editableParent, focusableElements, true); + testElements(editableParent, nonFocusableElements, false); + + var frame = document.getElementById("frame"); + frame.contentDocument.body.innerHTML = document.getElementById("content").innerHTML; + frame.contentDocument.designMode = "on"; + parent = frame.contentDocument.getElementById("parent"); + editableParent = frame.contentDocument.getElementById("editableParent"); + + testElements(parent, focusableElements, false); + testElements(parent, nonFocusableElements, false); + + testElements(editableParent, focusableElements, false); + testElements(editableParent, nonFocusableElements, false); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(test); +addLoadEvent(SimpleTest.finish); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug435128.html b/dom/html/test/test_bug435128.html new file mode 100644 index 0000000000..0f4cf7cdb0 --- /dev/null +++ b/dom/html/test/test_bug435128.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=435128 +--> +<head> + <title>Test for Bug 435128</title> + <script type="application/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=435128">Mozilla Bug 435128</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<iframe id="content" src="data:text/html;charset=utf-8,%3Chtml%3E%3Chead%3E%3C/head%3E%3Cbody%3E%0A%3Ciframe%20id%3D%22a%22%3E%3C/iframe%3E%0A%3Cscript%3E%0Afunction%20doe%28%29%20%7B%0Avar%20x%20%3D%20window.frames%5B0%5D.document%3B%0A%0Avar%20y%3Ddocument.getElementById%28%27a%27%29%3B%0Ay.parentNode.removeChild%28y%29%3B%0A%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0Atry%20%7Bx.write%28%27t%27%29%3B%7D%20catch%28e%29%20%7B%7D%0A%7D%0AsetTimeout%28%27doe%28%29%27%2C20%29%3B%0A%3C/script%3E%0A%3C/body%3E%3C/html%3E" style="width: 1000px; height: 200px;"></iframe> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 435128 **/ + +SimpleTest.waitForExplicitFinish(); + +setTimeout(finish, 60000); + +function doe2() { + document.getElementById('content').src = document.getElementById('content').src; +} +setInterval(doe2, 400); + +function finish() +{ + ok(true, "This is a mochikit version of a crash test. To complete is to pass."); + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug441930.html b/dom/html/test/test_bug441930.html new file mode 100644 index 0000000000..dcb7926734 --- /dev/null +++ b/dom/html/test/test_bug441930.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=441930 +--> +<head> + <title>Test for Bug 441930</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=441930">Mozilla Bug 441930</a> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 441930: see bug441930_iframe.html **/ + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +<p id="display"> + <iframe src="bug441930_iframe.html"></iframe> +</p> +<div id="content" style="display: none"> +</div> +</body> +</html> + diff --git a/dom/html/test/test_bug442801.html b/dom/html/test/test_bug442801.html new file mode 100644 index 0000000000..1a93d94f14 --- /dev/null +++ b/dom/html/test/test_bug442801.html @@ -0,0 +1,63 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=442801 +--> +<head> + <title>Test for Bug 442801</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=442801">Mozilla Bug 442801</a> +<p id="display"></p> +<div id="content" style="display: none"> + +<div contenteditable="true"> +<p id="ce_true" contenteditable="true">contenteditable true</p> +</div> + +<div contenteditable="true"> +<p id="ce_false" contenteditable="false">contenteditable false</p> +</div> + +<div contenteditable="true"> +<p id="ce_empty" contenteditable="">contenteditable empty</p> +</div> + +<div contenteditable="true"> +<p id="ce_inherit" contenteditable="inherit">contenteditable inherit</p> +</div> + +<div contenteditable="true"> +<p id="ce_none" >contenteditable none</p> +</div> + + + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 442801 **/ + +is(window.getComputedStyle($("ce_true")).getPropertyValue("-moz-user-modify"), + "read-write", + "parent contenteditable is true, contenteditable is true; user-modify should be read-write"); +is(window.getComputedStyle($("ce_false")).getPropertyValue("-moz-user-modify"), + "read-only", + "parent contenteditable is true, contenteditable is false; user-modify should be read-only"); +is(window.getComputedStyle($("ce_empty")).getPropertyValue("-moz-user-modify"), + "read-write", + "parent contenteditable is true, contenteditable is empty; user-modify should be read-write"); +is(window.getComputedStyle($("ce_inherit")).getPropertyValue("-moz-user-modify"), + "read-write", + "parent contenteditable is true, contenteditable is inherit; user-modify should be read-write"); +is(window.getComputedStyle($("ce_none")).getPropertyValue("-moz-user-modify"), + "read-write", + "parent contenteditable is true, contenteditable is none; user-modify should be read-write"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug445004.html b/dom/html/test/test_bug445004.html new file mode 100644 index 0000000000..02fc79f425 --- /dev/null +++ b/dom/html/test/test_bug445004.html @@ -0,0 +1,138 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=445004 +--> +<head> + <title>Test for Bug 445004</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=445004">Mozilla Bug 445004</a> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 445004 **/ +is(window.location.hostname, "mochi.test", "Unexpected hostname"); +is(window.location.port, "8888", "Unexpected port; fix testcase"); + +SimpleTest.waitForExplicitFinish(); + +var loads = 1; + +function loadStarted() { + ++loads; +} +function loadEnded() { + --loads; + if (loads == 0) { + doTest(); + } +} + +window.onload = loadEnded; + +function getMessage(evt) { + ok(evt.data == "start" || evt.data == "end", "Must have start or end"); + if (evt.data == "start") + loadStarted(); + else + loadEnded(); +} + +window.addEventListener("message", getMessage); + +function checkURI(uri, name, type) { + var host = uri.match(/^http:\/\/([a-z.0-9]*)/)[1]; + var file = uri.match(/([^\/]*).png$/)[1]; + is(host, file, "Unexpected base URI for test " + name + + " when testing " + type); +} + +function checkFrame(num) { + // Just snarf our data + var outer = SpecialPowers.wrap(window.frames[num]); + name = outer.name; + + is(outer.document.baseURI, + "http://example.org/tests/dom/html/test/bug445004-outer.html", + "Unexpected base URI for " + name); + + var iswrite = name.match(/write/); + + var inner = outer.frames[0]; + if (iswrite) { + is(inner.document.baseURI, + "http://example.org/tests/dom/html/test/bug445004-outer.html", + "Unexpected inner base URI for " + name); + } else { + is(inner.document.baseURI, + "http://test1.example.org/tests/dom/html/test/bug445004-inner.html", + "Unexpected inner base URI for " + name); + } + + var isrel = name.match(/rel/); + var offsite = name.match(/offsite/); + + if (!iswrite) { + if ((isrel && !offsite) || (!isrel && offsite)) { + is(inner.location.hostname, outer.location.hostname, + "Unexpected hostnames for " + name); + } else { + isnot(inner.location.hostname, outer.location.hostname, + "Unexpected hostnames for " + name); + } + } + + checkURI(inner.frames[0].location.href, name, "direct location"); + checkURI(inner.frames[1].document.getElementsByTagName("img")[0].src, + name, "direct write"); + if (!iswrite) { + is(inner.frames[1].location.hostname, inner.location.hostname, + "Incorrect hostname for " + name + " direct write") + } + checkURI(inner.frames[2].location.href, name, "indirect location"); + checkURI(inner.frames[3].document.getElementsByTagName("img")[0].src, + name, "indirect write"); + if (!iswrite) { + is(inner.frames[3].location.hostname, outer.location.hostname, + "Incorrect hostname for " + name + " indirect write") + } + checkURI(inner.document.getElementsByTagName("img")[0].src, + name, "direct image load"); +} + + +function doTest() { + for (var num = 0; num < 5; ++num) { + checkFrame(num); + } + + SimpleTest.finish(); +} + +</script> +</pre> +<p id="display"> + <iframe + src="http://example.org/tests/dom/html/test/bug445004-outer-rel.html" + name="bug445004-outer-rel.html"></iframe> + <iframe + src="http://test1.example.org/tests/dom/html/test/bug445004-outer-rel.html" + name="bug445004-outer-rel.html offsite"></iframe> + <iframe + src="http://example.org/tests/dom/html/test/bug445004-outer-abs.html" + name="bug445004-outer-abs.html"></iframe> + <iframe + src="http://test1.example.org/tests/dom/html/test/bug445004-outer-abs.html" + name="bug445004-outer-abs.html offsite"></iframe> + <iframe + src="http://example.org/tests/dom/html/test/bug445004-outer-write.html" + name="bug445004-outer-write.html"></iframe> +</p> +</body> +</html> diff --git a/dom/html/test/test_bug446483.html b/dom/html/test/test_bug446483.html new file mode 100644 index 0000000000..9821670da7 --- /dev/null +++ b/dom/html/test/test_bug446483.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=446483 +--> +<head> + <title>Test for Bug 446483</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=446483">Mozilla Bug 446483</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 446483 **/ + +function gc() { + SpecialPowers.gc(); +} + +function runTest() { + document.getElementById('display').innerHTML = + '<iframe src="bug446483-iframe.html"><\/iframe>\n' + + '<iframe src="bug446483-iframe.html"><\/iframe>\n'; + + setInterval(gc, 1000); + + setTimeout(function() { + document.getElementById('display').innerHTML = ''; + ok(true, ''); + SimpleTest.finish(); + }, 4000); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +addLoadEvent(runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug448166.html b/dom/html/test/test_bug448166.html new file mode 100644 index 0000000000..45b47fcb3f --- /dev/null +++ b/dom/html/test/test_bug448166.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=448166 +--> +<head> + <meta charset="utf-8" /> + <title>Test for Bug 448166</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=448166">Mozilla Bug 448166</a> +<p id="display"> + <a id="test" href="http://www.moz�illa.org">should not be Mozilla</a> + <a id="control" href="http://www.mozilla.org">should not be Mozilla</a> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 448166 **/ +isnot($("test").href, "http://www.mozilla.org/", + "Should notice unpaired surrogate"); +is($("test").href, "http://www.moz�illa.org", + "URL parser fails. Href returns original input string"); + +SimpleTest.doesThrow(() => { new URL($("test").href);}, "URL parser rejects input"); + +is($("control").href, "http://www.mozilla.org/", + "Just making sure .href works"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug448564.html b/dom/html/test/test_bug448564.html new file mode 100644 index 0000000000..bfb61af8dd --- /dev/null +++ b/dom/html/test/test_bug448564.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=448564 +--> +<head> + <title>Test for Bug 448564</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=448564">Mozilla Bug 448564</a> +<p id="display"> + <iframe src="bug448564-iframe-1.html"></iframe> + <iframe src="bug448564-iframe-2.html"></iframe> + <iframe src="bug448564-iframe-3.html"></iframe> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 448564 **/ + +/** + * The three iframes are going to be loaded with some dirty constructed forms. + * Each of them will be submitted before the load event and a SJS will replace + * the frame content with the query string. + * Then, on the load event, our test file will check the content of each iframes + * and check if the query string were correctly formatted (implying that all + * iframes were correctly submitted. + */ + +function checkQueryString(frame) { + var queryString = frame.document.body.textContent; + is(queryString.split("&").sort().join("&"), + "a=aval&b=bval&c=cval&d=dval", + "Not all form fields were properly submitted."); +} + +SimpleTest.waitForExplicitFinish(); + +addLoadEvent(function() { + checkQueryString(frames[0]); + checkQueryString(frames[1]); + checkQueryString(frames[2]); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug456229.html b/dom/html/test/test_bug456229.html new file mode 100644 index 0000000000..c9d6c36054 --- /dev/null +++ b/dom/html/test/test_bug456229.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=456229 +--> +<head> + <title>Test for Bug 456229</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=456229">Mozilla Bug 456229</a> +<p id="display"></p> +<div id="content" style="display: none"> + <input id='i' type="search"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 456229 **/ + +// More checks are done in test_bug551670.html. + +var i = document.getElementById('i'); +is(i.type, 'search', "Search state should be recognized"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug458037.xhtml b/dom/html/test/test_bug458037.xhtml new file mode 100644 index 0000000000..c8ae3e1191 --- /dev/null +++ b/dom/html/test/test_bug458037.xhtml @@ -0,0 +1,112 @@ +<html xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=458037 +--> +<head> + <title>Test for Bug 458037</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=458037">Mozilla Bug 458037</a> +<p id="display"></p> +<div id="content" > +<div id="a"></div> +<div id="b" contenteditable="true"></div> +<div id="c" contenteditable="false"></div> +<div id="d" contenteditable="inherit"></div> +<div contenteditable="true"> + <div id="e"></div> +</div> +<div contenteditable="false"> + <div id="f"></div> +</div> +<div contenteditable="true"> + <div id="g" contenteditable="false"></div> +</div> +<div contenteditable="false"> + <div id="h" contenteditable="true"></div> +</div> +<div contenteditable="true"> + <div id="i" contenteditable="inherit"></div> +</div> +<div contenteditable="false"> + <div id="j" contenteditable="inherit"></div> +</div> +<div contenteditable="true"> + <xul:box> + <div id="k"></div> + </xul:box> +</div> +<div contenteditable="false"> + <xul:box> + <div id="l"></div> + </xul:box> +</div> +<div contenteditable="true"> + <xul:box> + <div id="m" contenteditable="inherit"></div> + </xul:box> +</div> +<div contenteditable="false"> + <xul:box> + <div id="n" contenteditable="inherit"></div> + </xul:box> +</div> +<div id="x"></div> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 458037 **/ + +function test(id, expected) { + is(document.getElementById(id).isContentEditable, expected, + "Element " + id + " should " + (expected ? "" : "not ") + "be editable"); +} + +document.addEventListener("DOMContentLoaded", function() { + test("a", false); + test("b", true); + test("c", false); + test("d", false); + test("e", true); + test("f", false); + test("g", false); + test("h", true); + test("i", true); + test("j", false); + test("k", true); + test("l", false); + test("m", true); + test("n", false); + + var d = document.getElementById("x"); + test("x", false); + d.setAttribute("contenteditable", "true"); + test("x", true); + d.setAttribute("contenteditable", "false"); + test("x", false); + d.setAttribute("contenteditable", "inherit"); + test("x", false); + d.removeAttribute("contenteditable"); + test("x", false); + d.contentEditable = "true"; + test("x", true); + d.contentEditable = "false"; + test("x", false); + d.contentEditable = "inherit"; + test("x", false); + + // Make sure that isContentEditable is read-only + var origValue = d.isContentEditable; + d.isContentEditable = !origValue; + is(d.isContentEditable, origValue, "isContentEditable should be read only"); +}); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug460568.html b/dom/html/test/test_bug460568.html new file mode 100644 index 0000000000..db379e6fcc --- /dev/null +++ b/dom/html/test/test_bug460568.html @@ -0,0 +1,144 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=460568 +--> +<head> + <title>Test for Bug 460568</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=460568">Mozilla Bug 460568</a> +<p id="display"><a href="" id="anchor">a[href]</a></p> +<div id="editor"> + <a href="" id="anchorInEditor">a[href] in editor</a> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 460568 **/ + +function runTest() +{ + var editor = document.getElementById("editor"); + var anchor = document.getElementById("anchor"); + var anchorInEditor = document.getElementById("anchorInEditor"); + + var focused; + anchorInEditor.onfocus = function() { focused = true; }; + + function isReallyEditable() + { + editor.focus(); + var range = document.createRange(); + range.selectNodeContents(editor); + var prevStr = range.toString(); + + var docShell = SpecialPowers.wrap(window).docShell; + var controller = + docShell.QueryInterface(SpecialPowers.Ci.nsIInterfaceRequestor) + .getInterface(SpecialPowers.Ci.nsISelectionDisplay) + .QueryInterface(SpecialPowers.Ci.nsISelectionController); + var sel = controller.getSelection(controller.SELECTION_NORMAL); + sel.collapse(anchorInEditor, 0); + sendString("a"); + range.selectNodeContents(editor); + return prevStr != range.toString(); + } + + focused = false; + anchor.focus(); + editor.setAttribute("contenteditable", "true"); + anchorInEditor.focus(); + is(focused, false, "focus moved to element in contenteditable=true"); + is(isReallyEditable(), true, "cannot edit by a key event"); + + // for bug 502273 + focused = false; + anchor.focus(); + editor.setAttribute("dummy", "dummy"); + editor.removeAttribute("dummy"); + anchorInEditor.focus(); + is(focused, false, "focus moved to element in contenteditable=true (after dummy attribute was removed)"); + is(isReallyEditable(), true, "cannot edit by a key event"); + + focused = false; + anchor.focus(); + editor.setAttribute("contenteditable", "false"); + anchorInEditor.focus(); + is(focused, true, "focus didn't move to element in contenteditable=false"); + is(isReallyEditable(), false, "can edit by a key event"); + + // for bug 502273 + focused = false; + anchor.focus(); + editor.setAttribute("dummy", "dummy"); + editor.removeAttribute("dummy"); + anchorInEditor.focus(); + is(focused, true, "focus moved to element in contenteditable=true (after dummy attribute was removed)"); + is(isReallyEditable(), false, "cannot edit by a key event"); + + focused = false; + anchor.focus(); + editor.setAttribute("contenteditable", "true"); + anchorInEditor.focus(); + is(focused, false, "focus moved to element in contenteditable=true"); + is(isReallyEditable(), true, "cannot edit by a key event"); + + // for bug 502273 + focused = false; + anchor.focus(); + editor.setAttribute("dummy", "dummy"); + editor.removeAttribute("dummy"); + anchorInEditor.focus(); + is(focused, false, "focus moved to element in contenteditable=true (after dummy attribute was removed)"); + is(isReallyEditable(), true, "cannot edit by a key event"); + + focused = false; + anchor.focus(); + editor.removeAttribute("contenteditable"); + anchorInEditor.focus(); + is(focused, true, "focus didn't move to element in contenteditable removed element"); + is(isReallyEditable(), false, "can edit by a key event"); + + focused = false; + anchor.focus(); + editor.contentEditable = true; + anchorInEditor.focus(); + is(focused, false, "focus moved to element in contenteditable=true by property"); + is(isReallyEditable(), true, "cannot edit by a key event"); + + focused = false; + anchor.focus(); + editor.contentEditable = false; + anchorInEditor.focus(); + is(focused, true, "focus didn't move to element in contenteditable=false by property"); + is(isReallyEditable(), false, "can edit by a key event"); + + focused = false; + anchor.focus(); + editor.setAttribute("contenteditable", "true"); + anchorInEditor.focus(); + is(focused, false, "focus moved to element in contenteditable=true"); + is(isReallyEditable(), true, "cannot edit by a key event"); + + // for bug 502273 + focused = false; + anchor.focus(); + editor.setAttribute("dummy", "dummy"); + editor.removeAttribute("dummy"); + anchorInEditor.focus(); + is(focused, false, "focus moved to element in contenteditable=true (after dummy attribute was removed)"); + is(isReallyEditable(), true, "cannot edit by a key event"); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +addLoadEvent(SimpleTest.finish); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug463104.html b/dom/html/test/test_bug463104.html new file mode 100644 index 0000000000..c44419120d --- /dev/null +++ b/dom/html/test/test_bug463104.html @@ -0,0 +1,25 @@ +<!DOCTYPE html>
+<html>
+<head>
+<title>Noninteger coordinates test</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<div id="a" style="position: fixed; left: 5.5px; top: 5.5px; width: 100px; height: 100px; background: blue"></div>
+<p style="margin-top: 110px">
+<script>
+var a = document.getElementById("a");
+isnot(a, document.elementFromPoint(5, 5), "a shouldn't be found");
+isnot(a, document.elementFromPoint(5.25, 5.25), "a shouldn't be found");
+is(a, document.elementFromPoint(5.5, 5.5), "a should be found");
+is(a, document.elementFromPoint(5.75, 5.75), "a should be found");
+is(a, document.elementFromPoint(6, 6), "a should be found");
+is(a, document.elementFromPoint(105, 105), "a should be found");
+is(a, document.elementFromPoint(105.25, 105.25), "a should be found");
+isnot(a, document.elementFromPoint(105.5, 105.5), "a shouldn't be found");
+isnot(a, document.elementFromPoint(105.75, 105.75), "a shouldn't be found");
+isnot(a, document.elementFromPoint(106, 106), "a shouldn't be found");
+</script>
+</body>
+</html>
diff --git a/dom/html/test/test_bug478251.html b/dom/html/test/test_bug478251.html new file mode 100644 index 0000000000..e33e7b04e2 --- /dev/null +++ b/dom/html/test/test_bug478251.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=478251 +--> +<head> + <title>Test for Bug 478251</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=478251">Mozilla Bug 478251</a> +<p id="display"><iframe id="t"></iframe></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 478251 **/ +var doc = $("t").contentDocument; +doc.open(); +doc.write(); +doc.close(); +is(doc.documentElement.textContent, "", "Writing || failed"); + +doc.open(); +doc.write(null); +doc.close(); +is(doc.documentElement.textContent, "null", "Writing |null| failed"); + +doc.open(); +doc.write(null, null); +doc.close(); +is(doc.documentElement.textContent, "nullnull", "Writing |null, null| failed"); + +doc.open(); +doc.write(undefined); +doc.close(); +is(doc.documentElement.textContent, "undefined", "Writing |undefined| failed"); + +doc.open(); +doc.write(undefined, undefined); +doc.close(); +is(doc.documentElement.textContent, "undefinedundefined", "Writing |undefined, undefined| failed"); + +doc.open(); +doc.writeln(); +doc.close(); +ok(doc.documentElement.textContent == "\n" || doc.documentElement.textContent == "", "Writing |\\n| failed"); + +doc.open(); +doc.writeln(null); +doc.close(); +is(doc.documentElement.textContent, "null\n", "Writing |null\\n| failed"); + +doc.open(); +doc.writeln(null, null); +doc.close(); +is(doc.documentElement.textContent, "nullnull\n", "Writing |null, null\\n| failed"); + +doc.open(); +doc.writeln(undefined); +doc.close(); +is(doc.documentElement.textContent, "undefined\n", "Writing |undefined\\n| failed"); + +doc.open(); +doc.writeln(undefined, undefined); +doc.close(); +is(doc.documentElement.textContent, "undefinedundefined\n", "Writing |undefined, undefined\\n| failed"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug481335.xhtml b/dom/html/test/test_bug481335.xhtml new file mode 100644 index 0000000000..8fdd145222 --- /dev/null +++ b/dom/html/test/test_bug481335.xhtml @@ -0,0 +1,122 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=481335 +--> +<head> + <title>Test for Bug 481335</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style type="text/css"> + a { color:blue; } + a:visited { color:red; } + </style> + <base href="https://example.com/" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=481335">Mozilla Bug 481335</a> +<p id="display"> + <a id="t">A link</a> + <iframe id="i"></iframe> +</p> +<p id="newparent"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +<script type="application/javascript"> +<![CDATA[ + +/** Test for Bug 481335 **/ +SimpleTest.waitForExplicitFinish(); +var rand = Date.now() + "-" + Math.random(); + +is($("t").href, "", + "Unexpected href before set"); +is($("t").href, "", + "Unexpected cached href before set"); + +$("t").setAttribute("href", rand); +is($("t").href, "https://example.com/" + rand, + "Unexpected href after set"); +is($("t").href, "https://example.com/" + rand, + "Unexpected cached href after set"); +const unvisitedColor = "rgb(0, 0, 255)"; +const visitedColor = "rgb(255, 0, 0)"; + +let tests = testIterator(); +function continueTest() { + tests.next(); +} + +function checkLinkColor(aElmId, aExpectedColor, aMessage) { + // Because link coloring is asynchronous, we wait until we get the right + // result, or we will time out (resulting in a failure). + function getColor() { + var utils = SpecialPowers.getDOMWindowUtils(window); + return utils.getVisitedDependentComputedStyle($(aElmId), "", "color"); + } + while (getColor() != aExpectedColor) { + requestIdleCallback(continueTest); + return false; + } + is(getColor(), aExpectedColor, aMessage); + return true; +} + +let win; + +function* testIterator() { + // After first load + $("newparent").appendChild($("t")); + is($("t").href, "https://example.com/" + rand, + "Unexpected href after move"); + is($("t").href, "https://example.com/" + rand, + "Unexpected cached href after move"); + while (!checkLinkColor("t", unvisitedColor, "Should be unvisited now")) + yield undefined; + + win.close(); + win = window.open($("t").href, "_blank"); + + // After second load + while (!checkLinkColor("t", visitedColor, "Should be visited now")) + yield undefined; + $("t").pathname = rand; + while (!checkLinkColor("t", visitedColor, + "Should still be visited after setting pathname to its existing value")) { + yield undefined; + } + + /* TODO uncomment this test with the landing of bug 534526. See + * https://bugzilla.mozilla.org/show_bug.cgi?id=461199#c167 + $("t").pathname += "x"; + while (!checkLinkColor("t", unvisitedColor, + "Should not be visited after changing pathname")) { + yield undefined; + } + $("t").pathname = $("t").pathname; + while (!checkLinkColor("t", unvisitedColor, + "Should not be visited after setting unvisited pathname to existing value")) { + yield undefined; + } + */ + + win.close(); + win = window.open($("t").href, "_blank"); + + // After third load + while (!checkLinkColor("t", visitedColor, + "Should be visited now after third load")) { + yield undefined; + } + win.close(); + SimpleTest.finish(); +} + +addLoadEvent(function() { + win = window.open($("t").href, "_blank"); + requestIdleCallback(continueTest); +}); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug481440.html b/dom/html/test/test_bug481440.html new file mode 100644 index 0000000000..ab26b63e97 --- /dev/null +++ b/dom/html/test/test_bug481440.html @@ -0,0 +1,30 @@ +<!--Test must be in quirks mode for document.all to work--> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=481440 +--> +<head> + <title>Test for Bug 481440</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=481440">Mozilla Bug 481440</a> +<p id="display"></p> +<div id="content" style="display: none"> + <input name="x" id="y"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 481440 **/ +// Do a bunch of getElementById calls to catch hashtables auto-going live +for (var i = 0; i < 500; ++i) { + document.getElementById(i); +} +is(document.all.x, document.getElementById("y"), + "Unexpected node"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug481647.html b/dom/html/test/test_bug481647.html new file mode 100644 index 0000000000..b74fb7997e --- /dev/null +++ b/dom/html/test/test_bug481647.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=481647 +--> +<head> + <title>Test for Bug 481647</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=481647">Mozilla Bug 481647</a> +<p id="display"> + <iframe src="javascript:'aaa'"></iframe> + <iframe src="javascript:document.write('aaa'); document.close();"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 481647 **/ +SimpleTest.waitForExplicitFinish() + +function testFrame(num) { + is(window.frames[num].document.baseURI, document.baseURI, + "Unexpected base URI in frame " + num); +} + +addLoadEvent(function() { + for (var i = 0; i < 2; ++i) { + testFrame(i); + } + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug482659.html b/dom/html/test/test_bug482659.html new file mode 100644 index 0000000000..df2c66a747 --- /dev/null +++ b/dom/html/test/test_bug482659.html @@ -0,0 +1,64 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=482659 +--> +<head> + <title>Test for Bug 482659</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=482659">Mozilla Bug 482659</a> +<p id="display"> + <iframe></iframe> + <iframe src="about:blank"></iframe> + <iframe></iframe> + <iframe src="about:blank"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 482659 **/ +SimpleTest.waitForExplicitFinish() + +function testFrame(num) { + is(window.frames[num].document.baseURI, document.baseURI, + "Unexpected base URI in frame " + num); + is(window.frames[num].document.documentURI, "about:blank", + "Unexpected document URI in frame " + num); +} + +function appendScript(doc) { + var s = doc.createElement("script"); + s.textContent = "document.write('executed'); document.close()"; + doc.body.appendChild(s); +} + +function verifyScriptRan(num) { + is(window.frames[num].document.documentElement.textContent, "executed", + "write didn't happen in frame " + num); +} + +addLoadEvent(function() { +/* document.write part of test disabled due to bug 483818 + appendScript(window.frames[2].document); + appendScript(window.frames[3].document); + + verifyScriptRan(2); + verifyScriptRan(3); +*/ + for (var i = 0; i < 4; ++i) { + testFrame(i); + } + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug486741.html b/dom/html/test/test_bug486741.html new file mode 100644 index 0000000000..a20cd44e5e --- /dev/null +++ b/dom/html/test/test_bug486741.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=486741 +--> +<head> + <title>Test for Bug 486741</title> + <script type="application/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=486741">Mozilla Bug 486741</a> +<p id="display"><iframe id="f"></iframe></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 486741 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var d = $("f").contentDocument; + var root = d.documentElement; + is(root.tagName, "HTML", "Unexpected root"); + + d.open(); + isnot(d.documentElement, root, "Shouldn't have the old root element"); + + d.write("Test"); + d.close(); + + isnot(d.documentElement, root, "Still shouldn't have the old root element"); + is(d.documentElement.tagName, "HTML", "Unexpected new root after write"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug489532.html b/dom/html/test/test_bug489532.html new file mode 100644 index 0000000000..ac28c35482 --- /dev/null +++ b/dom/html/test/test_bug489532.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=489532 +--> +<head> + <title>Test for Bug 489532</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=489532">Mozilla Bug 489532</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script> +/** Test for Bug 489532 **/ +try { + document.createElement("<div>"); + ok(false, "Should throw.") +} catch (e) { + is(e.name, "InvalidCharacterError", + "Expected InvalidCharacterError."); + ok(e instanceof DOMException, "Expected DOMException."); + is(e.code, DOMException.INVALID_CHARACTER_ERR, + "Expected INVALID_CHARACTER_ERR."); +} +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug497242.xhtml b/dom/html/test/test_bug497242.xhtml new file mode 100644 index 0000000000..943c46ddc9 --- /dev/null +++ b/dom/html/test/test_bug497242.xhtml @@ -0,0 +1,41 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=497242 +--> +<head> + <title>Test for Bug 497242</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=497242">Mozilla Bug 497242</a> +<p id="display"></p> +<div id="content" style="display: none"> + <form name="foo"/> + <form name="foo"/> + <form name="bar"/> + <form name="bar" xmlns=""/> +</div> +<pre id="test"> +<script type="application/javascript"> +<![CDATA[ + +/** Test for Bug 497242 **/ +is(document.getElementsByName("foo").length, 2, + "Should find both forms with name 'foo'"); +is(document.getElementsByName("foo")[0], + document.getElementsByTagName("form")[0], + "Unexpected first foo"); +is(document.getElementsByName("foo")[1], + document.getElementsByTagName("form")[1], + "Unexpected second foo"); +is(document.getElementsByName("bar").length, 1, + "Should find only the HTML form with name 'bar'"); +is(document.getElementsByName("bar")[0], + document.getElementsByTagName("form")[2], + "Unexpected bar"); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug499092.html b/dom/html/test/test_bug499092.html new file mode 100644 index 0000000000..d5d019bc54 --- /dev/null +++ b/dom/html/test/test_bug499092.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=499092 +--> +<head> + <title>Test for Bug 499092</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=499092">Mozilla Bug 499092</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 499092 **/ +SimpleTest.waitForExplicitFinish(); +var content = document.getElementById("content"); + +function testHtml() { + is(this.contentDocument.title, "HTML OK"); + SimpleTest.finish(); +} + +function testXml() { + is(this.contentDocument.title, "XML OK"); + var iframeHtml = document.createElement("iframe"); + iframeHtml.onload = testHtml; + iframeHtml.src = "bug499092.html"; + content.appendChild(iframeHtml); +} + +var iframeXml = document.createElement("iframe"); +iframeXml.onload = testXml; +iframeXml.src = "bug499092.xml"; +content.appendChild(iframeXml); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug500885.html b/dom/html/test/test_bug500885.html new file mode 100644 index 0000000000..3ab9225a4c --- /dev/null +++ b/dom/html/test/test_bug500885.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=500885 +--> +<head> + <title>Test for Bug 500885</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/paint_listener.js"></script> + <script type="text/javascript" src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=500885">Mozilla Bug 500885</a> +<div> + <input id="file" type="file" /> +</div> +<script type="text/javascript"> + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); +MockFilePicker.returnValue = MockFilePicker.returnOK; + +async function test() { + // SpecialPowers.DOMWindowUtils doesn't appear to fire mouseEvents correctly + var wu = SpecialPowers.getDOMWindowUtils(window); + + try { + var domActivateEvents; + var fileInput = document.getElementById("file"); + var rect = fileInput.getBoundingClientRect(); + + fileInput.addEventListener ("DOMActivate", function (e) { + ok("detail" in e, "DOMActivate should have .detail"); + is(e.detail, 1, ".detail should be 1"); + domActivateEvents++; + }); + + fileInput.scrollIntoView({ behaviour: "smooth" }); + await promiseApzFlushedRepaints(); + + domActivateEvents = 0; + wu.sendMouseEvent("mousedown", rect.left + 5, rect.top + 5, 0, 1, 0); + wu.sendMouseEvent("mouseup", rect.left + 5, rect.top + 5, 0, 1, 0); + is(domActivateEvents, 1, "click on button should fire 1 DOMActivate event"); + + domActivateEvents = 0; + wu.sendMouseEvent("mousedown", rect.right - 5, rect.top + 5, 0, 1, 0); + wu.sendMouseEvent("mouseup", rect.right - 5, rect.top + 5, 0, 1, 0); + is(domActivateEvents, 1, "click on text field should fire 1 DOMActivate event"); + } finally { + SimpleTest.executeSoon(function() { + MockFilePicker.cleanup(); + SimpleTest.finish(); + }); + } +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(test); + +</script> +</body> + +</html> diff --git a/dom/html/test/test_bug512367.html b/dom/html/test/test_bug512367.html new file mode 100644 index 0000000000..35af18a5a1 --- /dev/null +++ b/dom/html/test/test_bug512367.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=512367 +--> +<head> + <title>Test for Bug 512367</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=512367">Mozilla Bug 512367</a> +<p id="display"> + <iframe src="bug369370-popup.png" id="i" style="width:200px; height:200px"></iframe> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +var frame = document.getElementById("i"); + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + SpecialPowers.setFullZoom(frame.contentWindow, 1.5); + + setTimeout(function() { + synthesizeMouse(frame, 30, 30, {}); + + is(SpecialPowers.getFullZoom(frame.contentWindow), 1.5, "Zoom in the image frame should not have been reset"); + + SimpleTest.finish(); + }, 0); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug514856.html b/dom/html/test/test_bug514856.html new file mode 100644 index 0000000000..77b8feecbd --- /dev/null +++ b/dom/html/test/test_bug514856.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=514856 +--> +<head> + <title>Test for Bug 514856</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=514856">Mozilla Bug 514856</a> +<p id="display"></p> +<div id="content"> + <iframe id="testFrame" src="bug514856_iframe.html"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 514856 **/ + +function beginTest() { + var ifr = document.getElementById("testFrame"); + var win = ifr.contentWindow; + + // After the click, the load event should be fired. + ifr.addEventListener('load', function() { + testDone(); + }); + + // synthesizeMouse adds getBoundingClientRect left and top to the offsets but + // in that particular case, we don't want that. + var rect = ifr.getBoundingClientRect(); + var left = rect.left; + var top = rect.top; + + synthesizeMouse(ifr, 10 - left, 10 - top, { type: "mousemove" }, win); + synthesizeMouse(ifr, 12 - left, 12 - top, { type: "mousemove" }, win); + synthesizeMouse(ifr, 14 - left, 14 - top, { type: "mousemove" }, win); + synthesizeMouse(ifr, 16 - left, 16 - top, { }, win); +} + +function testDone() { + var ifr = document.getElementById("testFrame"); + var url = new String(ifr.contentWindow.location); + + is(url.indexOf("?10,10"), -1, "Shouldn't have ?10,10 in the URL!"); + is(url.indexOf("?12,12"), -1, "Shouldn't have ?12,12 in the URL!"); + is(url.indexOf("?14,14"), -1, "Shouldn't have ?14,14 in the URL!"); + isnot(url.indexOf("?16,16"), -1, "Should have ?16,16 in the URL!"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(beginTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug518122.html b/dom/html/test/test_bug518122.html new file mode 100644 index 0000000000..acb9d78d0a --- /dev/null +++ b/dom/html/test/test_bug518122.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=518122 +--> +<head> + <title>Test for Bug 518122</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=518122">Mozilla Bug 518122</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 518122 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTests); + +var simple_tests = [ ["foo", "foo"], + ["", ""], + [null, ""], + [undefined , "undefined"], + ["\n", "\n"], + ["\r", "\n"], + ["\rfoo", "\nfoo"], + ["foo\r", "foo\n"], + ["foo\rbar", "foo\nbar"], + ["foo\rbar\r", "foo\nbar\n"], + ["\r\n", "\n"], + ["\r\nfoo", "\nfoo"], + ["foo\r\n", "foo\n"], + ["foo\r\nbar", "foo\nbar"], + ["foo\r\nbar\r\n", "foo\nbar\n"] ]; + +var value_append_tests = [ ["foo", "bar", "foobar"], + ["foo", "foo", "foofoo"], + ["foobar", "bar", "foobarbar"], + ["foobar", "foo", "foobarfoo"], + ["foo\n", "foo", "foo\nfoo"], + ["foo\r", "foo", "foo\nfoo"], + ["foo\r\n", "foo", "foo\nfoo"], + ["\n", "\n", "\n\n"], + ["\r", "\r", "\n\n"], + ["\r\n", "\r\n", "\n\n"], + ["\r", "\r\n", "\n\n"], + ["\r\n", "\r", "\n\n"], + [null, null, "null"], + [null, undefined, "undefined"], + ["", "", ""] + ]; + + +var simple_tests_for_input = [ ["foo", "foo"], + ["", ""], + [null, ""], + [undefined , "undefined"], + ["\n", ""], + ["\r", ""], + ["\rfoo", "foo"], + ["foo\r", "foo"], + ["foo\rbar", "foobar"], + ["foo\rbar\r", "foobar"], + ["\r\n", ""], + ["\r\nfoo", "foo"], + ["foo\r\n", "foo"], + ["foo\r\nbar", "foobar"], + ["foo\r\nbar\r\n", "foobar"] ]; + +var value_append_tests_for_input = [ ["foo", "bar", "foobar"], + ["foo", "foo", "foofoo"], + ["foobar", "bar", "foobarbar"], + ["foobar", "foo", "foobarfoo"], + ["foo\n", "foo", "foofoo"], + ["foo\r", "foo", "foofoo"], + ["foo\r\n", "foo", "foofoo"], + ["\n", "\n", ""], + ["\r", "\r", ""], + ["\r\n", "\r\n", ""], + ["\r", "\r\n", ""], + ["\r\n", "\r", ""], + [null, null, "null"], + [null, undefined, "undefined"], + ["", "", ""] + ]; +function runTestsFor(el, simpleTests, appendTests) { + for(var i = 0; i < simpleTests.length; ++i) { + el.value = simpleTests[i][0]; + is(el.value, simpleTests[i][1], "Wrong value (wrap=" + el.getAttribute('wrap') + ", simple_test=" + i + ")"); + } + for (var j = 0; j < appendTests.length; ++j) { + el.value = appendTests[j][0]; + el.value += appendTests[j][1]; + is(el.value, appendTests[j][2], "Wrong value (wrap=" + el.getAttribute('wrap') + ", value_append_test=" + j + ")"); + } +} + +function runTests() { + var textareas = document.getElementsByTagName("textarea"); + for (var i = 0; i < textareas.length; ++i) { + runTestsFor(textareas[i], simple_tests, value_append_tests); + } + var input = document.getElementsByTagName("input")[0]; + runTestsFor(input, simple_tests_for_input, value_append_tests_for_input); + // initialize the editor + input.focus(); + input.blur(); + runTestsFor(input, simple_tests_for_input, value_append_tests_for_input); + SimpleTest.finish(); +} + + +</script> +</pre> +<textarea cols="30" rows="7" wrap="none"></textarea> +<textarea cols="30" rows="7" wrap="off"></textarea><br> +<textarea cols="30" rows="7" wrap="soft"></textarea> +<textarea cols="30" rows="7" wrap="hard"></textarea> +<input type="text"> +</body> +</html> diff --git a/dom/html/test/test_bug519987.html b/dom/html/test/test_bug519987.html new file mode 100644 index 0000000000..875368c9b0 --- /dev/null +++ b/dom/html/test/test_bug519987.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=519987 +--> +<head> + <title>Test for Bug 519987</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=519987">Mozilla Bug 519987</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 519987 **/ +var xmlns = 'http://www.w3.org/1999/xhtml'; +is((new Image()).namespaceURI, xmlns, "Unexpected namespace for new Image()"); +is((new Audio()).namespaceURI, xmlns, "Unexpected namespace for new Audio()"); +var titles = document.getElementsByTagName("title"); +var t = titles[0]; +t.remove(); +document.title = "abcdefg"; +is(titles[0].namespaceURI, xmlns, "Unexpected namespace for new <title>"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug523771.html b/dom/html/test/test_bug523771.html new file mode 100644 index 0000000000..9f6af3de76 --- /dev/null +++ b/dom/html/test/test_bug523771.html @@ -0,0 +1,106 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=523771 +--> +<head> + <title>Test for Bug 523771</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=523771">Mozilla Bug 523771</a> +<p id="display"></p> +<iframe name="target_iframe" id="target_iframe"></iframe> +<form action="form_submit_server.sjs" target="target_iframe" id="form" +method="POST" enctype="multipart/form-data"> + <input id=singleFile name=singleFile type=file> + <input id=multiFile name=multiFile type=file multiple> +</form> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +singleFileInput = document.getElementById('singleFile'); +multiFileInput = document.getElementById('multiFile'); +var input1File = { name: "523771_file1", type: "", body: "file1 contents"}; +var input2Files = + [{ name: "523771_file2", type: "", body: "second file contents" }, + { name: "523771_file3.txt", type: "text/plain", body: "123456" }, + { name: "523771_file4.html", type: "text/html", body: "<html>content</html>" } + ]; + +SimpleTest.waitForExplicitFinish(); + +function setFileInputs () { + var f = createFileWithData(input1File.name, input1File.body, input1File.type); + SpecialPowers.wrap(singleFileInput).mozSetFileArray([f]); + + var input2FileNames = []; + for (file of input2Files) { + f = createFileWithData(file.name, file.body, file.type); + input2FileNames.push(f); + } + SpecialPowers.wrap(multiFileInput).mozSetFileArray(input2FileNames); +} + +function createFileWithData(fileName, fileData, fileType) { + return new File([fileData], fileName, { type: fileType }); +} + +function cleanupFiles() { + singleFileInput.value = ""; + multiFileInput.value = ""; +} + +is(singleFileInput.files.length, 0, "single-file .files.length"); // bug 524421 +is(multiFileInput.files.length, 0, "multi-file .files.length"); // bug 524421 + +setFileInputs(); + +is(singleFileInput.multiple, false, "single-file input .multiple"); +is(multiFileInput.multiple, true, "multi-file input .multiple"); +is(singleFileInput.value, 'C:\\fakepath\\' + input1File.name, "single-file input .value"); +is(multiFileInput.value, 'C:\\fakepath\\' + input2Files[0].name, "multi-file input .value"); +is(singleFileInput.files[0].name, input1File.name, "single-file input .files[n].name"); +is(singleFileInput.files[0].size, input1File.body.length, "single-file input .files[n].size"); +is(singleFileInput.files[0].type, input1File.type, "single-file input .files[n].type"); +for(i = 0; i < input2Files.length; ++i) { + is(multiFileInput.files[i].name, input2Files[i].name, "multi-file input .files[n].name"); + is(multiFileInput.files[i].size, input2Files[i].body.length, "multi-file input .files[n].size"); + is(multiFileInput.files[i].type, input2Files[i].type, "multi-file input .files[n].type"); +} + +document.getElementById('form').submit(); +iframe = document.getElementById('target_iframe'); +iframe.onload = function() { + response = JSON.parse(iframe.contentDocument.documentElement.textContent); + is(response[0].headers["Content-Disposition"], + "form-data; name=\"singleFile\"; filename=\"" + input1File.name + + "\"", + "singleFile Content-Disposition"); + is(response[0].headers["Content-Type"], input1File.type || "application/octet-stream", + "singleFile Content-Type"); + is(response[0].body, input1File.body, "singleFile body"); + + for(i = 0; i < input2Files.length; ++i) { + is(response[i + 1].headers["Content-Disposition"], + "form-data; name=\"multiFile\"; filename=\"" + input2Files[i].name + + "\"", + "multiFile Content-Disposition"); + is(response[i + 1].headers["Content-Type"], input2Files[i].type || "application/octet-stream", + "multiFile Content-Type"); + is(response[i + 1].body, input2Files[i].body, "multiFile body"); + } + + cleanupFiles(); + + is(singleFileInput.files.length, 0, "single-file .files.length"); // bug 524421 + is(multiFileInput.files.length, 0, "multi-file .files.length"); // bug 524421 + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug529819.html b/dom/html/test/test_bug529819.html new file mode 100644 index 0000000000..4147a341b2 --- /dev/null +++ b/dom/html/test/test_bug529819.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=529819 +--> +<head> + <title>Test for Bug 529819</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529819">Mozilla Bug 529819</a> +<p id="display"></p> +<div id="content" style="display: none"> +<form id="form"> + <input name="foo" id="foo"> + <input name="bar" type="radio"> + <input name="bar" id="bar" type="radio"> +</form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 529819 **/ +is($("form").elements.foo instanceof HTMLInputElement, true, "Should have an element here"); +is($("form").elements.bar instanceof HTMLInputElement, false, "Should have a list here"); +is($("form").elements.bar.length, 2, "Should have a list with two elements here"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug529859.html b/dom/html/test/test_bug529859.html new file mode 100644 index 0000000000..9d607c8a22 --- /dev/null +++ b/dom/html/test/test_bug529859.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=529859 +--> +<head> + <title>Test for Bug 529859</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529859">Mozilla Bug 529859</a> +<div id="content"> + <iframe name="target_iframe" id="target_iframe"></iframe> + <form action="form_submit_server.sjs" target="target_iframe" id="form" + method="POST" enctype="multipart/form-data"> + <input id="emptyFileInput" name="emptyFileInput" type="file"> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 529859 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + $("target_iframe").onload = function() { + var response = JSON.parse(this.contentDocument.documentElement.textContent); + is(response.length, 1, "Unexpected number of inputs"); + is(response[0].headers["Content-Disposition"], + "form-data; name=\"emptyFileInput\"; filename=\"\"", + "Incorrect content-disposition"); + is(response[0].headers["Content-Type"], "application/octet-stream", + "Unexpected content-type"); + SimpleTest.finish(); + } + $("form").submit(); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug535043.html b/dom/html/test/test_bug535043.html new file mode 100644 index 0000000000..3eb046b3c5 --- /dev/null +++ b/dom/html/test/test_bug535043.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=535043 +--> +<head> + <title>Test for Bug 535043</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=535043">Mozilla Bug 535043</a> +<p id="display"></p> +<div id="content"> + <textarea></textarea> + <textarea maxlength="-1"></textarea> + <textarea maxlength="0"></textarea> + <textarea maxlength="2"></textarea> +</div> +<pre id="test"> +<script type="text/javascript"> + +/** Test for Bug 535043 **/ +function checkTextArea(textArea) { + textArea.value = ''; + textArea.focus(); + for (var j = 0; j < 3; j++) { + sendString("x"); + } + var htmlMaxLength = textArea.getAttribute('maxlength'); + var domMaxLength = textArea.maxLength; + if (htmlMaxLength == null) { + is(domMaxLength, -1, + 'maxlength is unset but maxLength DOM attribute is not -1'); + } else if (htmlMaxLength < 0) { + // Per the HTML5 spec, out-of-range values are supposed to translate to -1, + // not 0, but they don't? + is(domMaxLength, -1, + 'maxlength is out of range but maxLength DOM attribute is not -1'); + } else { + is(domMaxLength, parseInt(htmlMaxLength), + 'maxlength in DOM does not match provided value'); + } + if (textArea.maxLength == -1) { + is(textArea.value.length, 3, + 'textarea with maxLength -1 should have no length limit'); + } else { + is(textArea.value.length, textArea.maxLength, 'textarea has maxLength ' + + textArea.maxLength + ' but length ' + textArea.value.length ); + } +} + +SimpleTest.waitForFocus(function() { + var textAreas = document.getElementsByTagName('textarea'); + for (var i = 0; i < textAreas.length; i++) { + checkTextArea(textAreas[i]); + } + + textArea = textAreas[0]; + testNums = [-42, -1, 0, 2]; + for (var i = 0; i < testNums.length; i++) { + textArea.removeAttribute('maxlength'); + + var caught = false; + try { + textArea.maxLength = testNums[i]; + } catch (e) { + caught = true; + } + if (testNums[i] < 0) { + ok(caught, 'Setting negative maxLength should throw exception'); + } else { + ok(!caught, 'Setting nonnegative maxLength should not throw exception'); + } + checkTextArea(textArea); + + textArea.setAttribute('maxlength', testNums[i]); + checkTextArea(textArea); + } + + SimpleTest.finish(); +}); + +SimpleTest.waitForExplicitFinish(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug536891.html b/dom/html/test/test_bug536891.html new file mode 100644 index 0000000000..89bb93d1b0 --- /dev/null +++ b/dom/html/test/test_bug536891.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=536891 +--> +<head> + <title>Test for Bug 536891</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=536891">Mozilla Bug 536891</a> +<p id="display"></p> +<div id="content" style="display: none"> +<textarea id="t" maxlength="-2" minlength="-2"></textarea> +<input id="i" type="text" maxlength="-2" minlength="-2"> +<input id="p" type="password" maxlength="-2" minlength="-2"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 536891 **/ + +function checkNegativeMinMaxLength(element) +{ + for(let type of ["min", "max"]) { + /* value is set to -2 initially in the document, see above */ + is(element[type + "Length"], -1, "negative " + type + "Length should be considered invalid and represented as -1"); + + // changing the property to an negative value should throw (see bug 536895). + for(let value of [-15, -2147483648]) { // PR_INT32_MIN + let threw = false; + try { + element[type + "Length"] = value; + } catch(e) { + threw = true; + } + is(threw, true, "setting " + type + "Length property to " + value + " should throw"); + } + element[type + "Length"] = "non-numerical value"; + is(element[type + "Length"], 0, "setting " + type + "Length property to a non-numerical value should set it to zero"); + + + element.setAttribute(type + 'Length', -15); + is(element[type + "Length"], -1, "negative " + type + "Length is not processed correctly when set dynamically"); + is(element.getAttribute(type + 'Length'), "-15", type + "Length attribute doesn't return the correct value"); + + element.setAttribute(type + 'Length', 0); + is(element[type + "Length"], 0, "zero " + type + "Length is not processed correctly"); + element.setAttribute(type + 'Length', 2147483647); // PR_INT32_MAX + is(element[type + "Length"], 2147483647, "negative " + type + "Length is not processed correctly"); + element.setAttribute(type + 'Length', -2147483648); // PR_INT32_MIN + is(element[type + "Length"], -1, "negative " + type + "Length is not processed correctly"); + element.setAttribute(type + 'Length', 'non-numerical-value'); + is(element[type + "Length"], -1, "non-numerical value should be considered invalid and represented as -1"); + } +} + +/* TODO: correct behavior may be checked for email, telephone, url and search input types */ +checkNegativeMinMaxLength(document.getElementById('t')); +checkNegativeMinMaxLength(document.getElementById('i')); +checkNegativeMinMaxLength(document.getElementById('p')); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug536895.html b/dom/html/test/test_bug536895.html new file mode 100644 index 0000000000..c135432260 --- /dev/null +++ b/dom/html/test/test_bug536895.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=536895 +--> +<head> + <title>Test for Bug 536895</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=536895">Mozilla Bug 536895</a> +<p id="display"></p> +<div id="content" style="display: none"> +<textarea id="t"></textarea> +<input id="i" type="text"> +<input id="p" type="password"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 536895 **/ + +function checkNegativeMaxLengthException(element) +{ + caught = false; + try { + element.setAttribute('maxLength', -10); + } catch(e) { + caught = true; + } + ok(!caught, "Setting maxLength attribute to a negative value shouldn't throw an exception"); + + caught = false; + try { + element.maxLength = -20; + } catch(e) { + is(e.name, "IndexSizeError", "Should be an IndexSizeError exception"); + caught = true; + } + ok(caught, "Setting negative maxLength from the DOM should throw an exception"); + + is(element.getAttribute('maxLength'), "-10", "When the exception is raised, the maxLength attribute shouldn't change"); +} + +/* TODO: correct behavior may be checked for email, telephone, url and search input types */ +checkNegativeMaxLengthException(document.getElementById('t')); +checkNegativeMaxLengthException(document.getElementById('i')); +checkNegativeMaxLengthException(document.getElementById('p')); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug546995.html b/dom/html/test/test_bug546995.html new file mode 100644 index 0000000000..ff4d80ec45 --- /dev/null +++ b/dom/html/test/test_bug546995.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=546995 +--> +<head> + <title>Test for Bug 546995</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=546995">Mozilla Bug 546995</a> +<p id="display"></p> +<div id="content" style="display: none"> + <select id='s'></select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 546995 **/ + +/* This test in only testing IDL reflection, another one is testing the behavior */ + +function checkAutofocusIDLAttribute(element) +{ + ok('autofocus' in element, "Element has the autofocus IDL attribute"); + ok(!element.autofocus, "autofocus default value is false"); + element.setAttribute('autofocus', 'autofocus'); + ok(element.autofocus, "autofocus should be enabled"); + element.removeAttribute('autofocus'); + ok(!element.autofocus, "autofocus should be disabled"); +} + +// TODO: keygen should be added when correctly implemented, see bug 101019. +checkAutofocusIDLAttribute(document.getElementById('s')); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug547850.html b/dom/html/test/test_bug547850.html new file mode 100644 index 0000000000..a2e0323ec8 --- /dev/null +++ b/dom/html/test/test_bug547850.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=547850 +--> +<head> + <title>Test for Bug 547850</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=547850">Mozilla Bug 547850</a> +<script> +document.write("<div id=content><f\u00c5></f\u00c5><r\u00e5></r\u00e5>"); +document.write("<span g\u00c5=a1 t\u00e5=a2></span></div>"); +</script> +<pre id="test"> +<script class="testbody" type="text/javascript"> +var ch = $('content').childNodes; +is(ch[0].localName, "f\u00c5", "upper case localName"); +is(ch[1].localName, "r\u00e5", "lower case localName"); +is(ch[0].nodeName, "F\u00c5", "upper case nodeName"); +is(ch[1].nodeName, "R\u00e5", "lower case nodeName"); +is(ch[0].tagName, "F\u00c5", "upper case tagName"); +is(ch[1].tagName, "R\u00e5", "lower case tagName"); +is(ch[2].getAttribute("g\u00c5"), "a1", "upper case attr name"); +is(ch[2].getAttribute("t\u00e5"), "a2", "lower case attr name"); +is(ch[2].getAttribute("G\u00c5"), "a1", "upper case attr name"); +is(ch[2].getAttribute("T\u00e5"), "a2", "lower case attr name"); +is(ch[2].getAttribute("g\u00e5"), null, "wrong lower case attr name"); +is(ch[2].getAttribute("t\u00c5"), null, "wrong upper case attr name"); +is($('content').getElementsByTagName("f\u00c5")[0], ch[0], "gEBTN upper case"); +is($('content').getElementsByTagName("f\u00c5").length, 1, "gEBTN upper case length"); +is($('content').getElementsByTagName("r\u00e5")[0], ch[1], "gEBTN lower case"); +is($('content').getElementsByTagName("r\u00e5").length, 1, "gEBTN lower case length"); +is($('content').getElementsByTagName("F\u00c5")[0], ch[0], "gEBTN upper case"); +is($('content').getElementsByTagName("F\u00c5").length, 1, "gEBTN upper case length"); +is($('content').getElementsByTagName("R\u00e5")[0], ch[1], "gEBTN lower case"); +is($('content').getElementsByTagName("R\u00e5").length, 1, "gEBTN lower case length"); +is($('content').getElementsByTagName("f\u00e5").length, 0, "gEBTN wrong upper case"); +is($('content').getElementsByTagName("r\u00c5").length, 0, "gEBTN wrong lower case"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug551846.html b/dom/html/test/test_bug551846.html new file mode 100644 index 0000000000..4950b1e452 --- /dev/null +++ b/dom/html/test/test_bug551846.html @@ -0,0 +1,164 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=551846 +--> +<head> + <title>Test for Bug 551846</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=551846">Mozilla Bug 551846</a> +<p id="display"></p> +<div id="content" style="display: none"> + <select id='s'> + <option>Tulip</option> + <option>Lily</option> + <option>Gagea</option> + <option>Snowflake</option> + <option>Ismene</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 551846 **/ + +function checkSizeReflection(element, defaultValue) +{ + is(element.size, defaultValue, "Default size should be " + defaultValue); + + element.setAttribute('size', -15); + is(element.size, defaultValue, + "The reflecting IDL attribute should return the default value when content attribute value is invalid"); + is(element.getAttribute('size'), "-15", + "The content attribute should containt the previously set value"); + + element.setAttribute('size', 0); + is(element.size, 0, + "0 should be considered as a valid value"); + is(element.getAttribute('size'), "0", + "The content attribute should containt the previously set value"); + + element.setAttribute('size', 2147483647); /* PR_INT32_MAX */ + is(element.size, 2147483647, + "PR_INT32_MAX should be considered as a valid value"); + is(element.getAttribute('size'), "2147483647", + "The content attribute should containt the previously set value"); + + element.setAttribute('size', -2147483648); /* PR_INT32_MIN */ + is(element.size, defaultValue, + "The reflecting IDL attribute should return the default value when content attribute value is invalid"); + is(element.getAttribute('size'), "-2147483648", + "The content attribute should containt the previously set value"); + + element.setAttribute('size', 'non-numerical-value'); + is(element.size, defaultValue, + "The reflecting IDL attribute should return the default value when content attribute value is invalid"); + is(element.getAttribute('size'), 'non-numerical-value', + "The content attribute should containt the previously set value"); + + element.setAttribute('size', 4294967294); /* PR_INT32_MAX * 2 */ + is(element.size, defaultValue, + "Value greater than PR_INT32_MAX should be considered as invalid"); + is(element.getAttribute('size'), "4294967294", + "The content attribute should containt the previously set value"); + + element.setAttribute('size', -4294967296); /* PR_INT32_MIN * 2 */ + is(element.size, defaultValue, + "The reflecting IDL attribute should return the default value when content attribute value is invalid"); + is(element.getAttribute('size'), "-4294967296", + "The content attribute should containt the previously set value"); + + element.size = defaultValue + 1; + element.removeAttribute('size'); + is(element.size, defaultValue, + "When the attribute is removed, the size should be the default size"); + + element.setAttribute('size', 'foobar'); + is(element.size, defaultValue, + "The reflecting IDL attribute should return the default value when content attribute value is invalid"); + element.removeAttribute('size'); + is(element.size, defaultValue, + "When the attribute is removed, the size should be the default size"); +} + +function checkSetSizeException(element) +{ + var caught = false; + + try { + element.size = 1; + } catch(e) { + caught = true; + } + ok(!caught, "Setting a positive size shouldn't throw an exception"); + + caught = false; + try { + element.size = 0; + } catch(e) { + caught = true; + } + ok(!caught, "Setting a size to 0 from the IDL shouldn't throw an exception"); + + element.size = 1; + + caught = false; + try { + element.size = -1; + } catch(e) { + caught = true; + } + ok(!caught, "Setting a negative size from the IDL shouldn't throw an exception"); + + is(element.size, 0, "The size should now be equal to the minimum non-negative value"); + + caught = false; + try { + element.setAttribute('size', -10); + } catch(e) { + caught = true; + } + ok(!caught, "Setting an invalid size in the content attribute shouldn't throw an exception"); + + // reverting to defalut + element.removeAttribute('size'); +} + +function checkSizeWhenChangeMultiple(element, aDefaultNonMultiple, aDefaultMultiple) +{ + s.setAttribute('size', -1) + is(s.size, aDefaultNonMultiple, "Size IDL attribute should be 1"); + + s.multiple = true; + is(s.size, aDefaultMultiple, "Size IDL attribute should be 4"); + + is(s.getAttribute('size'), "-1", "Size content attribute should be -1"); + + s.setAttribute('size', -2); + is(s.size, aDefaultMultiple, "Size IDL attribute should be 4"); + + s.multiple = false; + is(s.size, aDefaultNonMultiple, "Size IDL attribute should be 1"); + + is(s.getAttribute('size'), "-2", "Size content attribute should be -2"); +} + +var s = document.getElementById('s'); + +checkSizeReflection(s, 0); +checkSetSizeException(s); + +s.setAttribute('multiple', 'true'); +checkSizeReflection(s, 0); +checkSetSizeException(s); +s.removeAttribute('multiple'); + +checkSizeWhenChangeMultiple(s, 0, 0); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug555567.html b/dom/html/test/test_bug555567.html new file mode 100644 index 0000000000..0857955275 --- /dev/null +++ b/dom/html/test/test_bug555567.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=555567 +--> +<head> + <title>Test for Bug 555567</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=555567">Mozilla Bug 555567</a> +<div id='content' style="display: none"> + <form> + <fieldset> + <legend id="a"></legend> + </fieldset> + <legend id="b"></legend> + </form> + <legend id="c"></legend> +</div> +<pre id="test"> +<p id="display"></p> +<script type="application/javascript"> + +/** Test for Bug 555567 **/ + +var a = document.getElementById('a'); +var b = document.getElementById('b'); +var c = document.getElementById('c'); + +isnot(a.form, null, + "First legend element should have a not null form IDL attribute"); +is(b.form, null, + "Second legend element should have a null form IDL attribute"); +is(c.form, null, + "Third legend element should have a null form IDL attribute"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug556645.html b/dom/html/test/test_bug556645.html new file mode 100644 index 0000000000..3c308f9ef6 --- /dev/null +++ b/dom/html/test/test_bug556645.html @@ -0,0 +1,73 @@ +<html> +<head> + <title>Test for Bug 556645 and Bug 1848196</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script> +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(async () => { + const object = document.createElement("object"); + object.setAttribute("type", "text/html"); + object.setAttribute("width", "200"); + object.setAttribute("height", "200"); + document.body.appendChild(object); + const promiseLoadObject = new Promise(resolve => { + object.addEventListener("load", resolve, {once: true}); + }); + object.setAttribute("data", "object_bug556645.html"); + await promiseLoadObject; + runTest(object); + object.remove(); + + const embed = document.createElement("embed"); + embed.setAttribute("type", "text/html"); + embed.setAttribute("width", "200"); + embed.setAttribute("height", "200"); + document.body.appendChild(embed); + const promiseLoadEmbed = new Promise(resolve => { + embed.addEventListener("load", resolve, {once: true}); + }); + embed.setAttribute("src", "object_bug556645.html"); + await promiseLoadEmbed; + runTest(embed); + embed.remove(); + + SimpleTest.finish(); +}); + +function runTest(aObjectOrEmbed) +{ + const desc = `<${aObjectOrEmbed.tagName.toLowerCase()}>`; + const childDoc = aObjectOrEmbed.contentDocument || aObjectOrEmbed.getSVGDocument(); + const body = childDoc.body; + is(document.activeElement, document.body, `${desc}: focus in parent before`); + is(childDoc.activeElement, body, `${desc}: focus in child before`); + + const button = childDoc.querySelector("button"); + button.focus(); + childDoc.defaultView.focus(); + is(document.activeElement, aObjectOrEmbed, `${desc}: focus in parent after focus()`); + is(childDoc.activeElement, button, `${desc}: focus in child after focus()`); + + button.blur(); + const pbutton = document.getElementById("pbutton"); + pbutton.focus(); + + synthesizeKey("KEY_Tab"); + is(document.activeElement, aObjectOrEmbed, `${desc}: focus in parent after tab`); + is(childDoc.activeElement, childDoc.documentElement, `${desc}: focus in child after tab`); + + synthesizeKey("KEY_Tab"); + is(document.activeElement, aObjectOrEmbed, `${desc}: focus in parent after tab 2`); + is(childDoc.activeElement, button, `${desc}: focus in child after tab 2`); +} + +</script> + +<button id="pbutton">Parent</button> + +</body> +</html> diff --git a/dom/html/test/test_bug557087-1.html b/dom/html/test/test_bug557087-1.html new file mode 100644 index 0000000000..9bd2068e8d --- /dev/null +++ b/dom/html/test/test_bug557087-1.html @@ -0,0 +1,129 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=557087 +--> +<head> + <title>Test for Bug 557087</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script> + +/** Test for Bug 557087 **/ + +function checkDisabledAttribute(aFieldset) +{ + ok('disabled' in aFieldset, + "fieldset elements should have the disabled attribute"); + + ok(!aFieldset.disabled, + "fieldset elements disabled attribute should be disabled"); + is(aFieldset.getAttribute('disabled'), null, + "fieldset elements disabled attribute should be disabled"); + + aFieldset.disabled = true; + ok(aFieldset.disabled, + "fieldset elements disabled attribute should be enabled"); + isnot(aFieldset.getAttribute('disabled'), null, + "fieldset elements disabled attribute should be enabled"); + + aFieldset.removeAttribute('disabled'); + aFieldset.setAttribute('disabled', ''); + ok(aFieldset.disabled, + "fieldset elements disabled attribute should be enabled"); + isnot(aFieldset.getAttribute('disabled'), null, + "fieldset elements disabled attribute should be enabled"); + + aFieldset.removeAttribute('disabled'); + ok(!aFieldset.disabled, + "fieldset elements disabled attribute should be disabled"); + is(aFieldset.getAttribute('disabled'), null, + "fieldset elements disabled attribute should be disabled"); +} + +function checkDisabledPseudoClass(aFieldset) +{ + is(document.querySelector(":disabled"), null, + "no elements should have :disabled applied to them"); + + aFieldset.disabled = true; + is(document.querySelector(":disabled"), aFieldset, + ":disabled should apply to fieldset elements"); + + aFieldset.disabled = false; + is(document.querySelector(":disabled"), null, + "no elements should have :disabled applied to them"); +} + +function checkEnabledPseudoClass(aFieldset) +{ + is(document.querySelector(":enabled"), aFieldset, + ":enabled should apply to fieldset elements"); + + aFieldset.disabled = true; + is(document.querySelector(":enabled"), null, + "no elements should have :enabled applied to them"); + + aFieldset.disabled = false; + is(document.querySelector(":enabled"), aFieldset, + ":enabled should apply to fieldset elements"); +} + +function checkFocus(aFieldset) +{ + aFieldset.disabled = true; + aFieldset.setAttribute('tabindex', 1); + + aFieldset.focus(); + + isnot(document.activeElement, aFieldset, + "fieldset can't be focused when disabled"); + aFieldset.removeAttribute('tabindex'); + aFieldset.disabled = false; +} + +function checkClickEvent(aFieldset) +{ + var clickHandled = false; + + aFieldset.disabled = true; + + aFieldset.addEventListener("click", function(aEvent) { + clickHandled = true; + }, {once: true}); + + sendMouseEvent({type:'click'}, aFieldset); + SimpleTest.executeSoon(function() { + ok(clickHandled, "When disabled, fieldset should not prevent click events"); + SimpleTest.finish(); + }); +} + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({ + set: [["dom.forms.fieldset_disable_only_descendants.enabled", true]] +}).then(() => { + var fieldset = document.createElement("fieldset"); + var content = document.getElementById('content'); + content.appendChild(fieldset); + + checkDisabledAttribute(fieldset); + checkDisabledPseudoClass(fieldset); + checkEnabledPseudoClass(fieldset); + checkFocus(fieldset); + checkClickEvent(fieldset); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug557087-2.html b/dom/html/test/test_bug557087-2.html new file mode 100644 index 0000000000..435e924f84 --- /dev/null +++ b/dom/html/test/test_bug557087-2.html @@ -0,0 +1,363 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=557087 +--> +<head> + <title>Test for Bug 557087</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a> +<p id="display"></p> +<div id="content" style="display:none;"> +</div> +<pre id="test"> +<script> + +/** Test for Bug 557087 **/ + +SimpleTest.waitForExplicitFinish(); + +var elementsPreventingClick = [ "input", "button", "select", "textarea" ]; +var elementsWithClick = [ "option", "optgroup", "output", "label", "object", "fieldset" ]; +var gHandled = 0; + +function clickShouldNotHappenHandler(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldNotHappenHandler); + ok(false, "click event should be prevented! (test1)"); + if (++gHandled >= elementsWithClick.length) { + test2(); + } +} + +function clickShouldNotHappenHandler2(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldNotHappenHandler3); + ok(false, "click event should be prevented! (test2)"); + if (++gHandled >= elementsWithClick.length) { + test3(); + } +} + +function clickShouldNotHappenHandler5(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldNotHappenHandler5); + ok(false, "click event should be prevented! (test5)"); + if (++gHandled >= elementsWithClick.length) { + test6(); + } +} + +function clickShouldNotHappenHandler7(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldNotHappenHandler7); + ok(false, "click event should be prevented! (test7)"); + if (++gHandled >= elementsWithClick.length) { + test8(); + } +} + +function clickShouldHappenHandler(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldHappenHandler); + ok(true, "click event has been correctly received (test1)"); + if (++gHandled >= elementsWithClick.length) { + test2(); + } +} + +function clickShouldHappenHandler2(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldHappenHandler2); + ok(true, "click event has been correctly received (test2)"); + if (++gHandled >= elementsWithClick.length) { + test3(); + } +} + +function clickShouldHappenHandler3(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldHappenHandler3); + ok(true, "click event has been correctly received (test3)"); + if (++gHandled >= (elementsWithClick.length + + elementsPreventingClick.length)) { + test4(); + } +} + +function clickShouldHappenHandler4(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldHappenHandler4); + ok(true, "click event has been correctly received (test4)"); + if (++gHandled >= (elementsWithClick.length + + elementsPreventingClick.length)) { + test5(); + } +} + +function clickShouldHappenHandler5(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldHappenHandler5); + ok(true, "click event has been correctly received (test5)"); + if (++gHandled >= elementsWithClick.length) { + test6(); + } +} + +function clickShouldHappenHandler6(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldHappenHandler6); + ok(true, "click event has been correctly received (test6)"); + if (++gHandled >= (elementsWithClick.length + + elementsPreventingClick.length)) { + test7(); + } +} + +function clickShouldHappenHandler7(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldHappenHandler7); + ok(true, "click event has been correctly received (test5)"); + if (++gHandled >= elementsWithClick.length) { + test8(); + } +} + +function clickShouldHappenHandler8(aEvent) +{ + aEvent.target.removeEventListener("click", clickShouldHappenHandler8); + ok(true, "click event has been correctly received (test8)"); + if (++gHandled >= (elementsWithClick.length + + elementsPreventingClick.length)) { + SimpleTest.finish(); + } +} + +var fieldset1 = document.createElement("fieldset"); +var fieldset2 = document.createElement("fieldset"); +var legendA = document.createElement("legend"); +var legendB = document.createElement("legend"); +var content = document.getElementById('content'); +fieldset1.disabled = true; +content.appendChild(fieldset1); +fieldset1.appendChild(fieldset2); + +function clean() +{ + var count = fieldset2.children.length; + for (var i=0; i<count; ++i) { + if (fieldset2.children[i] != legendA && + fieldset2.children[i] != legendB) { + fieldset2.removeChild(fieldset2.children[i]); + } + } +} + +function test1() +{ + gHandled = 0; + + // Initialize children without click expected. + for (var name of elementsPreventingClick) { + var element = document.createElement(name); + fieldset2.appendChild(element); + element.addEventListener("click", clickShouldNotHappenHandler); + sendMouseEvent({type:'click'}, element); + } + + // Initialize children with click expected. + for (var name of elementsWithClick) { + var element = document.createElement(name); + fieldset2.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler); + sendMouseEvent({type:'click'}, element); + } +} + +function test2() +{ + gHandled = 0; + fieldset1.disabled = false; + fieldset2.disabled = true; + + // Initialize children without click expected. + for (var name of elementsPreventingClick) { + var element = document.createElement(name); + fieldset2.appendChild(element); + element.addEventListener("click", clickShouldNotHappenHandler2); + sendMouseEvent({type:'click'}, element); + } + + // Initialize children with click expected. + for (var name of elementsWithClick) { + var element = document.createElement(name); + fieldset2.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler2); + sendMouseEvent({type:'click'}, element); + } +} + +function test3() +{ + gHandled = 0; + fieldset1.disabled = false; + fieldset2.disabled = false; + + // All elements should accept the click. + for (var name of elementsPreventingClick) { + var element = document.createElement(name); + fieldset2.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler3); + sendMouseEvent({type:'click'}, element); + } + + // Initialize children with click expected. + for (var name of elementsWithClick) { + var element = document.createElement(name); + fieldset2.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler3); + sendMouseEvent({type:'click'}, element); + } +} + +function test4() +{ + gHandled = 0; + fieldset1.disabled = false; + fieldset2.disabled = true; + + fieldset2.appendChild(legendA); + + // All elements should accept the click. + for (var name of elementsPreventingClick) { + var element = document.createElement(name); + legendA.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler4); + sendMouseEvent({type:'click'}, element); + } + + // Initialize children with click expected. + for (var name of elementsWithClick) { + var element = document.createElement(name); + legendA.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler4); + sendMouseEvent({type:'click'}, element); + } +} + +function test5() +{ + gHandled = 0; + fieldset2.insertBefore(legendB, legendA); + + // Initialize children without click expected. + for (var name of elementsPreventingClick) { + var element = document.createElement(name); + legendA.appendChild(element); + element.addEventListener("click", clickShouldNotHappenHandler5); + sendMouseEvent({type:'click'}, element); + } + + // Initialize children with click expected. + for (var name of elementsWithClick) { + var element = document.createElement(name); + legendA.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler5); + sendMouseEvent({type:'click'}, element); + } +} + +function test6() +{ + gHandled = 0; + fieldset2.removeChild(legendB); + fieldset1.disabled = true; + fieldset2.disabled = false; + + fieldset1.appendChild(legendA); + legendA.appendChild(fieldset2); + + // All elements should accept the click. + for (var name of elementsPreventingClick) { + var element = document.createElement(name); + fieldset2.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler6); + sendMouseEvent({type:'click'}, element); + } + + // Initialize children with click expected. + for (var name of elementsWithClick) { + var element = document.createElement(name); + fieldset2.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler6); + sendMouseEvent({type:'click'}, element); + } +} + +function test7() +{ + gHandled = 0; + fieldset1.disabled = true; + fieldset2.disabled = false; + + fieldset1.appendChild(fieldset2); + fieldset2.appendChild(legendA); + + // All elements should accept the click. + for (var name of elementsPreventingClick) { + var element = document.createElement(name); + legendA.appendChild(element); + element.addEventListener("click", clickShouldNotHappenHandler7); + sendMouseEvent({type:'click'}, element); + } + + // Initialize children with click expected. + for (var name of elementsWithClick) { + var element = document.createElement(name); + legendA.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler7); + sendMouseEvent({type:'click'}, element); + } +} + +function test8() +{ + gHandled = 0; + fieldset1.disabled = true; + fieldset2.disabled = true; + + fieldset1.appendChild(legendA); + legendA.appendChild(fieldset2); + fieldset2.appendChild(legendB); + + // All elements should accept the click. + for (var name of elementsPreventingClick) { + var element = document.createElement(name); + legendB.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler8); + sendMouseEvent({type:'click'}, element); + } + + // Initialize children with click expected. + for (var name of elementsWithClick) { + var element = document.createElement(name); + legendB.appendChild(element); + element.addEventListener("click", clickShouldHappenHandler8); + sendMouseEvent({type:'click'}, element); + } +} + +SpecialPowers.pushPrefEnv({ + set: [["dom.forms.fieldset_disable_only_descendants.enabled", true]] +}).then(() => { + test1(); +}) + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug557087-3.html b/dom/html/test/test_bug557087-3.html new file mode 100644 index 0000000000..98d0b0de4b --- /dev/null +++ b/dom/html/test/test_bug557087-3.html @@ -0,0 +1,215 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=557087 +--> +<head> + <title>Test for Bug 557087</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 557087 **/ + +function checkValueMissing(aElement, aExpected) +{ + var msg = aExpected ? aElement.tagName + " should suffer from value missing" + : aElement.tagName + " should not suffer from value missing" + is(aElement.validity.valueMissing, aExpected, msg); +} + +function checkCandidateForConstraintValidation(aElement, aExpected) +{ + var msg = aExpected ? aElement.tagName + " should be candidate for constraint validation" + : aElement.tagName + " should not be candidate for constraint validation" + is(aElement.willValidate, aExpected, msg); +} + +function checkDisabledPseudoClass(aElement, aDisabled) +{ + var disabledElements = document.querySelectorAll(":disabled"); + var found = false; + + for (var e of disabledElements) { + if (aElement == e) { + found = true; + break; + } + } + + var msg = aDisabled ? aElement.tagName + " should have :disabled applying" + : aElement.tagName + " should not have :disabled applying"; + ok(aDisabled ? found : !found, msg); +} + +function checkEnabledPseudoClass(aElement, aEnabled) +{ + var enabledElements = document.querySelectorAll(":enabled"); + var found = false; + + for (var e of enabledElements) { + if (aElement == e) { + found = true; + break; + } + } + + var msg = aEnabled ? aElement.tagName + " should have :enabled applying" + : aElement.tagName + " should not have :enabled applying"; + ok(aEnabled ? found : !found, msg); +} + +function checkFocus(aElement, aExpected) +{ + aElement.setAttribute('tabindex', 1); + + // We use the focus manager so we can test <label>. + var fm = SpecialPowers.Cc["@mozilla.org/focus-manager;1"] + .getService(SpecialPowers.Ci.nsIFocusManager); + fm.setFocus(aElement, 0); + + if (aExpected) { + is(document.activeElement, aElement, "element should be focused"); + } else { + isnot(document.activeElement, aElement, "element should not be focused"); + } + + aElement.blur(); + aElement.removeAttribute('tabindex'); +} + +var elements = [ "input", "button", "select", "textarea", "fieldset", "option", + "optgroup", "label", "output", "object" ]; + +var testData = { +/* tag name | affected by disabled | test focus | test pseudo-classes | test willValidate */ + "INPUT": [ true, true, true, true, true ], + "BUTTON": [ true, true, true, false, false ], + "SELECT": [ true, true, true, true, false ], + "TEXTAREA": [ true, true, true, true, true ], + "FIELDSET": [ true, true, true, false, false ], + "OPTION": [ false, true, true, false, false ], + "OPTGROUP": [ false, true, true, false, false ], + "OBJECT": [ false, true, false, false, false ], + "LABEL": [ false, true, false, false, false ], + "OUTPUT": [ false, true, false, false, false ], +}; + +/** + * For not candidate elements without disabled attribute and not submittable, + * we only have to check that focus and click works even inside a disabled + * fieldset. + */ +function checkElement(aElement, aDisabled) +{ + var data = testData[aElement.tagName]; + var expected = data[0] ? !aDisabled : true; + + if (data[1]) { + checkFocus(aElement, expected); + } + + if (data[2]) { + checkEnabledPseudoClass(aElement, data[0] ? !aDisabled : true); + checkDisabledPseudoClass(aElement, data[0] ? aDisabled : false); + } + + if (data[3]) { + checkCandidateForConstraintValidation(aElement, expected); + } + + if (data[4]) { + checkValueMissing(aElement, expected); + } +} + +var fieldset1 = document.createElement("fieldset"); +var fieldset2 = document.createElement("fieldset"); +var legendA = document.createElement("legend"); +var legendB = document.createElement("legend"); +var content = document.getElementById('content'); +content.appendChild(fieldset1); +fieldset1.appendChild(fieldset2); +fieldset2.disabled = true; + +for (var data of elements) { + var element = document.createElement(data); + + if (data[4]) { + element.required = true; + } + + fieldset1.disabled = false; + fieldset2.appendChild(element); + + checkElement(element, fieldset2.disabled); + + // Make sure changes are correctly managed. + fieldset2.disabled = false; + checkElement(element, fieldset2.disabled); + fieldset2.disabled = true; + checkElement(element, fieldset2.disabled); + + // Make sure if a fieldset which is not the first fieldset is disabled, the + // elements inside the second fielset are disabled. + fieldset2.disabled = false; + fieldset1.disabled = true; + checkElement(element, fieldset1.disabled); + + // Make sure the state change of the inner fieldset will not confuse. + fieldset2.disabled = true; + fieldset2.disabled = false; + checkElement(element, fieldset1.disabled); + + + /* legend tests */ + + // elements in the first legend of a disabled fieldset should not be disabled. + fieldset2.disabled = true; + fieldset1.disabled = false; + legendA.appendChild(element); + fieldset2.appendChild(legendA); + checkElement(element, false); + + // elements in the second legend should be disabled + fieldset2.insertBefore(legendB, legendA); + checkElement(element, fieldset2.disabled); + fieldset2.removeChild(legendB); + + // Elements in the first legend of a fieldset disabled by another fieldset + // should be disabled. + fieldset1.disabled = true; + checkElement(element, fieldset1.disabled); + + // Elements inside a fieldset inside the first legend of a disabled fieldset + // should not be diasbled. + fieldset2.disabled = false; + fieldset1.appendChild(legendA); + legendA.appendChild(fieldset2); + fieldset2.appendChild(element); + checkElement(element, false); + + // Elements inside the first legend of a disabled fieldset inside the first + // legend of a disabled fieldset should not be disabled. + fieldset2.disabled = false; + fieldset2.appendChild(legendB); + legendB.appendChild(element); + checkElement(element, false); + fieldset2.removeChild(legendB); + fieldset1.appendChild(fieldset2); + + element.remove(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug557087-4.html b/dom/html/test/test_bug557087-4.html new file mode 100644 index 0000000000..72aa0f1dc2 --- /dev/null +++ b/dom/html/test/test_bug557087-4.html @@ -0,0 +1,90 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=557087 +--> +<head> + <title>Test for Bug 557087</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a> +<p id="display"></p> +<div id="content"> + <iframe name='f'></iframe> + <form target='f' action="data:text/html"> + <input type='text' id='a'> + <input type='checkbox' id='b'> + <input type='radio' id='c'> + <fieldset disabled> + <fieldset> + <input type='submit' id='s'> + </fieldset> + </fieldset> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 557087 **/ + +SimpleTest.waitForExplicitFinish(); + +var gExpectedSubmits = 6; +var gSubmitReceived = 0; +var gEnd = false; + +var fieldsets = document.getElementsByTagName("fieldset"); +var form = document.forms[0]; + +form.addEventListener("submit", function() { + ok(gEnd, gEnd ? "expected submit" : "non expected submit"); + if (++gSubmitReceived >= gExpectedSubmits) { + form.removeEventListener("submit", arguments.callee); + SimpleTest.finish(); + } +}); + +var inputs = [ + document.getElementById('a'), + document.getElementById('b'), + document.getElementById('c'), +]; + +function doSubmit() +{ + for (e of inputs) { + e.focus(); + synthesizeKey("KEY_Enter"); + } +} + +SimpleTest.waitForFocus(function() { + doSubmit(); + + fieldsets[1].disabled = true; + fieldsets[0].disabled = false; + doSubmit(); + + fieldsets[0].disabled = false; + fieldsets[1].disabled = false; + + gEnd = true; + doSubmit(); + + // Simple check that we can submit from inside a legend even if the fieldset + // is disabled. + var legend = document.createElement("legend"); + fieldsets[0].appendChild(legend); + fieldsets[0].disabled = true; + legend.appendChild(document.getElementById('s')); + + doSubmit(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug557087-5.html b/dom/html/test/test_bug557087-5.html new file mode 100644 index 0000000000..9d58e0ba93 --- /dev/null +++ b/dom/html/test/test_bug557087-5.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=557087 +--> +<head> + <title>Test for Bug 557087</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a> +<p id="display"></p> +<div id="content"> + <iframe name='t'></iframe> + <form target='t' action="dummy_page.html"> + <fieldset disabled> + <fieldset> + <input name='i' value='i'> + <textarea name='t'>t</textarea> + <select name='s'><option>s</option></select> + </fieldset> + </fieldset> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 557087 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +const BASE_URI = `${location.origin}/tests/dom/html/test/dummy_page.html`; +var testResults = [ + BASE_URI + "?", + BASE_URI + "?", + BASE_URI + "?i=i&t=t&s=s", + BASE_URI + "?i=i&t=t&s=s", +]; +var gTestCount = 0; + +var form = document.forms[0]; +var iframe = document.getElementsByTagName('iframe')[0]; +var fieldsets = document.getElementsByTagName('fieldset'); + +function runTest() +{ + iframe.addEventListener("load", function() { + is(iframe.contentWindow.location.href, testResults[gTestCount], + testResults[gTestCount] + " should have been loaded"); + + switch (++gTestCount) { + case 1: + fieldsets[1].disabled = true; + fieldsets[0].disabled = false; + form.submit(); + SimpleTest.executeSoon(function() { + form.submit() + }); + break; + case 2: + fieldsets[0].disabled = false; + fieldsets[1].disabled = false; + SimpleTest.executeSoon(function() { + form.submit() + }); + break; + case 3: + // Elements inside the first legend of a disabled fieldset are submittable. + fieldsets[0].disabled = true; + fieldsets[1].disabled = true; + var legend = document.createElement("legend"); + fieldsets[0].appendChild(legend); + while (fieldsets[1].firstChild) { + legend.appendChild(fieldsets[1].firstChild); + } + SimpleTest.executeSoon(function() { + form.submit() + }); + break; + default: + iframe.removeEventListener("load", arguments.callee); + SimpleTest.executeSoon(SimpleTest.finish); + } + }); + + form.submit(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug557087-6.html b/dom/html/test/test_bug557087-6.html new file mode 100644 index 0000000000..8d7bf08e5c --- /dev/null +++ b/dom/html/test/test_bug557087-6.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=557087 +--> +<head> + <title>Test for Bug 557087</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557087">Mozilla Bug 557087</a> +<p id="display"></p> +<div id="content" style="display: none"> + <fieldset disabled> + <input> + </fieldset> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 557087 **/ + +// Testing random stuff following review comments. + +var fieldset = document.getElementsByTagName("fieldset")[0]; + +is(fieldset.elements.length, 1, + "there should be one element inside the fieldset"); +is(fieldset.elements[0], document.getElementsByTagName("input")[0], + "input should be the element inside the fieldset"); + +document.body.removeChild(document.getElementById('content')); +is(fieldset.querySelector("input:disabled"), fieldset.elements[0], + "the input should still be disabled"); + +fieldset.disabled = false; +is(fieldset.querySelector("input:enabled"), fieldset.elements[0], + "the input should be enabled"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug557620.html b/dom/html/test/test_bug557620.html new file mode 100644 index 0000000000..9a05988963 --- /dev/null +++ b/dom/html/test/test_bug557620.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=557620 +--> +<head> + <title>Test for Bug 557620</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=557620">Mozilla Bug 557620</a> +<p id="display"></p> +<div id="content" style="display: none"> + <input type="tel" id='i'> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 557620 **/ + +// More checks are done in test_bug551670.html. + +var tel = document.getElementById('i'); +is(tel.type, 'tel', "input with type='tel' should return 'tel'"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug558788-1.html b/dom/html/test/test_bug558788-1.html new file mode 100644 index 0000000000..5dc4a1f34b --- /dev/null +++ b/dom/html/test/test_bug558788-1.html @@ -0,0 +1,212 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=558788 +--> +<head> + <title>Test for Bug 558788</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input, textarea { background-color: rgb(0,0,0) !important; } + :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; } + :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=558788">Mozilla Bug 558788</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 558788 **/ + +/** + * This test checks the behavior of :valid and :invalid pseudo-classes + * when the user is typing/interacting with the element. + * Only <input> and <textarea> elements can have there validity changed by an + * user input. + */ + +var gContent = document.getElementById('content'); + +function checkValidApplies(elmt) +{ + is(window.getComputedStyle(elmt).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); +} + +function checkInvalidApplies(elmt, aTodo) +{ + if (aTodo) { + todo_is(window.getComputedStyle(elmt).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); + return; + } + is(window.getComputedStyle(elmt).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); +} + +function checkMissing(elementName) +{ + var element = document.createElement(elementName); + element.required = true; + gContent.appendChild(element); + checkInvalidApplies(element); + + element.focus(); + + sendString("a"); + checkValidApplies(element); + + synthesizeKey("KEY_Backspace"); + checkInvalidApplies(element); + + gContent.removeChild(element); +} + +function checkTooLong(elementName) +{ + var element = document.createElement(elementName); + element.value = "foo"; + element.maxLength = 2; + gContent.appendChild(element); + checkInvalidApplies(element, true); + + element.focus(); + + synthesizeKey("KEY_Backspace"); + checkValidApplies(element); + gContent.removeChild(element); +} + +function checkTextAreaMissing() +{ + checkMissing('textarea'); +} + +function checkTextAreaTooLong() +{ + checkTooLong('textarea'); +} + +function checkTextArea() +{ + checkTextAreaMissing(); + checkTextAreaTooLong(); +} + +function checkInputMissing() +{ + checkMissing('input'); +} + +function checkInputTooLong() +{ + checkTooLong('input'); +} + +function checkInputEmail() +{ + var element = document.createElement('input'); + element.type = 'email'; + gContent.appendChild(element); + checkValidApplies(element); + + element.focus(); + + sendString("a"); + checkInvalidApplies(element); + + sendString("@b.c"); + checkValidApplies(element); + + synthesizeKey("KEY_Backspace"); + for (var i=0; i<4; ++i) { + if (i == 1) { + // a@b is a valid value. + checkValidApplies(element); + } else { + checkInvalidApplies(element); + } + synthesizeKey("KEY_Backspace"); + } + checkValidApplies(element); + + gContent.removeChild(element); +} + +function checkInputURL() +{ + var element = document.createElement('input'); + element.type = 'url'; + gContent.appendChild(element); + checkValidApplies(element); + + element.focus(); + + sendString("h"); + checkInvalidApplies(element); + + sendString("ttp://mozilla.org"); + checkValidApplies(element); + + for (var i=0; i<10; ++i) { + synthesizeKey("KEY_Backspace"); + checkValidApplies(element); + } + + synthesizeKey("KEY_Backspace"); + // "http://" is now invalid + for (var i=0; i<7; ++i) { + checkInvalidApplies(element); + synthesizeKey("KEY_Backspace"); + } + checkValidApplies(element); + + gContent.removeChild(element); +} + +function checkInputPattern() +{ + var element = document.createElement('input'); + element.pattern = "[0-9]*" + gContent.appendChild(element); + checkValidApplies(element); + + element.focus(); + + sendString("0"); + checkValidApplies(element); + + sendString("a"); + checkInvalidApplies(element); + + synthesizeKey("KEY_Backspace"); + checkValidApplies(element); + + synthesizeKey("KEY_Backspace"); + checkValidApplies(element); + + gContent.removeChild(element); +} + +function checkInput() +{ + checkInputMissing(); + checkInputTooLong(); + checkInputEmail(); + checkInputURL(); + checkInputPattern(); +} + +checkTextArea(); +checkInput(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug558788-2.html b/dom/html/test/test_bug558788-2.html new file mode 100644 index 0000000000..8224159d38 --- /dev/null +++ b/dom/html/test/test_bug558788-2.html @@ -0,0 +1,174 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=558788 +--> +<head> + <title>Test for Bug 558788</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=558788">Mozilla Bug 558788</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 558788 **/ + +var validElementsDescription = [ + /* element type value required pattern maxlength minlength */ + /* <input> */ + [ "input", null, null, null, null, null, null ], + /* <input required value='foo'> */ + [ "input", null, "foo", true, null, null, null ], + /* <input type='email'> */ + [ "input", "email", null, null, null, null, null ], + /* <input type='email' value='foo@mozilla.org'> */ + [ "input", "email", "foo@mozilla.org", null, null, null, null ], + /* <input type='url'> */ + [ "input", "url", null, null, null, null, null ], + /* <input type='url' value='http://mozilla.org'> */ + [ "input", "url", "http://mozilla.org", null, null, null, null ], + /* <input pattern='\\d\\d'> */ + [ "input", null, null, null, "\\d\\d", null, null ], + /* <input pattern='\\d\\d' value='42'> */ + [ "input", null, "42", null, "\\d\\d", null, null ], + /* <input maxlength='3'> - still valid until user interaction */ + [ "input", null, null, null, null, "3", null ], + /* <input maxlength='3'> */ + [ "input", null, "fooo", null, null, "3", null ], + /* <input minlength='3'> - still valid until user interaction */ + [ "input", null, null, null, null, null, "3" ], + /* <input minlength='3'> */ + [ "input", null, "fo", null, null, null, "3" ], + /* <textarea></textarea> */ + [ "textarea", null, null, null, null, null, null ], + /* <textarea required>foo</textarea> */ + [ "textarea", null, "foo", true, null, null, null ] +]; + +var invalidElementsDescription = [ + /* element type value required pattern maxlength minlength valid-value */ + /* <input required> */ + [ "input", null, null, true, null, null, null, "foo" ], + /* <input type='email' value='foo'> */ + [ "input", "email", "foo", null, null, null, null, "foo@mozilla.org" ], + /* <input type='url' value='foo'> */ + [ "input", "url", "foo", null, null, null, null, "http://mozilla.org" ], + /* <input pattern='\\d\\d' value='foo'> */ + [ "input", null, "foo", null, "\\d\\d", null, null, "42" ], + /* <input maxlength='3'> - still valid until user interaction */ + [ "input", null, "foooo", null, null, "3", null, "foo" ], + /* <input minlength='3'> - still valid until user interaction */ + [ "input", null, "foo", null, null, null, "3", "foo" ], + /* <textarea required></textarea> */ + [ "textarea", null, null, true, null, null, null, "foo" ], +]; + +var validElements = []; +var invalidElements = []; + +function appendElements(aElementsDesc, aElements) +{ + var content = document.getElementById('content'); + var length = aElementsDesc.length; + + for (var i=0; i<length; ++i) { + var e = document.createElement(aElementsDesc[i][0]); + if (aElementsDesc[i][1]) { + e.type = aElementsDesc[i][1]; + } + if (aElementsDesc[i][2]) { + e.value = aElementsDesc[i][2]; + } + if (aElementsDesc[i][3]) { + e.required = true; + } + if (aElementsDesc[i][4]) { + e.pattern = aElementsDesc[i][4]; + } + if (aElementsDesc[i][5]) { + e.maxLength = aElementsDesc[i][5]; + } + if (aElementsDesc[i][6]) { + e.minLength = aElementsDesc[i][6]; + } + + content.appendChild(e); + + // Adding the element to the appropriate list. + aElements.push(e); + } +} + +function compareArrayWithSelector(aElements, aSelector) +{ + var aSelectorElements = document.querySelectorAll(aSelector); + + is(aSelectorElements.length, aElements.length, + aSelector + " selector should return the correct number of elements"); + + if (aSelectorElements.length != aElements.length) { + return; + } + + var length = aElements.length; + for (var i=0; i<length; ++i) { + is(aSelectorElements[i], aElements[i], + aSelector + " should return the correct elements"); + } +} + +function makeMinMaxLengthElementsActuallyInvalid(aInvalidElements, + aInvalidElementsDesc) +{ + // min/maxlength elements are not invalid until user edits them + var length = aInvalidElementsDesc.length; + + for (var i=0; i<length; ++i) { + var e = aInvalidElements[i]; + if (aInvalidElementsDesc[i][5]) { // maxlength + e.focus(); + synthesizeKey("KEY_Backspace"); + } else if (aInvalidElementsDesc[i][6]) { // minlength + e.focus(); + synthesizeKey("KEY_Backspace"); + } + } +} + +function makeInvalidElementsValid(aInvalidElements, + aInvalidElementsDesc, + aValidElements) +{ + var length = aInvalidElementsDesc.length; + + for (var i=0; i<length; ++i) { + var e = aInvalidElements.shift(); + e.value = aInvalidElementsDesc[i][7]; + aValidElements.push(e); + } +} + +appendElements(validElementsDescription, validElements); +appendElements(invalidElementsDescription, invalidElements); + +makeMinMaxLengthElementsActuallyInvalid(invalidElements, invalidElementsDescription); + +compareArrayWithSelector(validElements, ":valid"); +compareArrayWithSelector(invalidElements, ":invalid"); + +makeInvalidElementsValid(invalidElements, invalidElementsDescription, + validElements); + +compareArrayWithSelector(validElements, ":valid"); +compareArrayWithSelector(invalidElements, ":invalid"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug560112.html b/dom/html/test/test_bug560112.html new file mode 100644 index 0000000000..48aaad8dc5 --- /dev/null +++ b/dom/html/test/test_bug560112.html @@ -0,0 +1,211 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=560112 +--> +<head> + <title>Test for Bug 560112</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=560112">Mozilla Bug 560112</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 560112 **/ + +/** + * Sets dataset property. Checks data attribute "attr". + * Gets dataset property. Checks data attribute "attr". + * Overwrites dataset property Checks data attribute "attr". + * Deletes dataset property. Checks data attribute "attr". + */ +function SetGetOverwriteDel(attr, prop) +{ + var el = document.createElement('div'); + + // Set property. + is(prop in el.dataset, false, 'Property should not be in dataset before setting.'); + el.dataset[prop] = "zzzzzz"; + is(prop in el.dataset, true, 'Property should be in dataset after setting.'); + ok(el.hasAttribute(attr), 'Element should have data attribute for dataset property "' + prop + '".'); + + // Get property. + is(el.dataset[prop], "zzzzzz", 'Dataset property "' + prop + '" should have value "zzzzzz".'); + is(el.getAttribute(attr), "zzzzzz", 'Attribute "' + attr + '" should have value "zzzzzz".'); + + // Overwrite property. + el.dataset[prop] = "yyyyyy"; + is(el.dataset[prop], "yyyyyy", 'Dataset property "' + prop + '" should have value "yyyyyy".'); + is(el.getAttribute(attr), "yyyyyy", 'Attribute "' + attr + '" should have value "yyyyyy".'); + + // Delete property. + delete el.dataset[prop]; + ok(!el.hasAttribute(attr), 'Element should not have data attribute for dataset property "' + prop + '".'); + is(prop in el.dataset, false, 'Deleted property should not be in dataset.'); +} + +/** + * Sets dataset property and expects exception. + */ +function SetExpectException(prop) +{ + var el = document.createElement('div'); + + try { + el.dataset[prop] = "xxxxxx"; + ok(false, 'Exception should have been thrown.'); + } catch (ex) { + ok(true, 'Exception should have been thrown.'); + } +} + +/** + * Adds attributes in "attrList" to element. + * Deletes attributes in "delList" from element. + * Checks for "numProp" properties in dataset. + */ +function DelAttrEnumerate(attrList, delList, numProp) +{ + var el = document.createElement('div'); + + // Adds attributes in "attrList". + for (var i = 0; i < attrList.length; ++i) { + el.setAttribute(attrList[i], "aaaaaa"); + } + + // Remove attributes in "delList". + for (var i = 0; i < delList.length; ++i) { + el.removeAttribute(delList[i]); + } + + var numPropCounted = 0; + + for (var prop in el.dataset) { + if (el.dataset[prop] == "aaaaaa") { + ++numPropCounted; + } + } + + is(numPropCounted, numProp, 'Number of enumerable dataset properties is incorrent after attribute removal.'); +} + +/** + * Adds attributes in "attrList" to element. + * Checks for "numProp" properties in dataset. + */ +function Enumerate(attrList, numProp) +{ + var el = document.createElement('div'); + + // Adds attributes in "attrList" to element. + for (var i = 0; i < attrList.length; ++i) { + el.setAttribute(attrList[i], "aaaaaa"); + } + + var numPropCounted = 0; + + for (var prop in el.dataset) { + if (el.dataset[prop] == "aaaaaa") { + ++numPropCounted; + } + } + + is(numPropCounted, numProp, 'Number of enumerable dataset properties is incorrect.'); +} + +/** + * Adds dataset property then removes attribute from element and check for presence of + * properties using the "in" operator. + */ +function AddPropDelAttr(attr, prop) +{ + var el = document.createElement('div'); + + el.dataset[prop] = 'dddddd'; + is(prop in el.dataset, true, 'Operator "in" should return true after setting property.'); + el.removeAttribute(attr); + is(prop in el.dataset, false, 'Operator "in" should return false for removed attribute.'); +} + +/** + * Adds then removes attribute from element and check for presence of properties using the + * "in" operator. + */ +function AddDelAttr(attr, prop) +{ + var el = document.createElement('div'); + + el.setAttribute(attr, 'dddddd'); + is(prop in el.dataset, true, 'Operator "in" should return true after setting attribute.'); + el.removeAttribute(attr); + is(prop in el.dataset, false, 'Operator "in" should return false for removed attribute.'); +} + +// Typical use case. +SetGetOverwriteDel('data-property', 'property'); +SetGetOverwriteDel('data-a-longer-property', 'aLongerProperty'); + +AddDelAttr('data-property', 'property'); +AddDelAttr('data-a-longer-property', 'aLongerProperty'); + +AddPropDelAttr('data-property', 'property'); +AddPropDelAttr('data-a-longer-property', 'aLongerProperty'); + +// Empty property name. +SetGetOverwriteDel('data-', ''); + +// Leading dash characters. +SetGetOverwriteDel('data--', '-'); +SetGetOverwriteDel('data--d', 'D'); +SetGetOverwriteDel('data---d', '-D'); + +// Trailing dash characters. +SetGetOverwriteDel('data-d-', 'd-'); +SetGetOverwriteDel('data-d--', 'd--'); +SetGetOverwriteDel('data-d-d-', 'dD-'); + +// "data-" in attribute name. +SetGetOverwriteDel('data-data-', 'data-'); +SetGetOverwriteDel('data-data-data-', 'dataData-'); + +// Longer attribute. +SetGetOverwriteDel('data-long-long-long-long-long-long-long-long-long-long-long-long-long', 'longLongLongLongLongLongLongLongLongLongLongLongLong'); + +var longAttr = 'data-long'; +var longProp = 'long'; +for (var i = 0; i < 30000; ++i) { + // Create really long attribute and property names. + longAttr += '-long'; + longProp += 'Long'; +} + +SetGetOverwriteDel(longAttr, longProp); + +// Syntax error in setting dataset property (dash followed by lower case). +SetExpectException('-a'); +SetExpectException('a-a'); +SetExpectException('a-a-a'); + +// Invalid character. +SetExpectException('a a'); + +// Enumeration over dataset properties. +Enumerate(['data-a-big-fish'], 1); +Enumerate(['dat-a-big-fish'], 0); +Enumerate(['data-'], 1); +Enumerate(['data-', 'data-more-data'], 2); +Enumerate(['daaata-', 'data-more-data'], 1); + +// Delete data attributes and enumerate properties. +DelAttrEnumerate(['data-one', 'data-two'], ['data-one'], 1); +DelAttrEnumerate(['data-one', 'data-two'], ['data-three'], 2); +DelAttrEnumerate(['data-one', 'data-two'], ['one'], 2); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug561634.html b/dom/html/test/test_bug561634.html new file mode 100644 index 0000000000..1eab90508f --- /dev/null +++ b/dom/html/test/test_bug561634.html @@ -0,0 +1,126 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=561634 +--> +<head> + <title>Test for Bug 561634</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=561634">Mozilla Bug 561634</a> +<p id="display"></p> +<div id="content" style="display: none;"> + <form> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 561634 **/ + +function checkEmptyForm() +{ + ok(document.forms[0].checkValidity(), "An empty form is valid"); +} + +function checkBarredFromConstraintValidation() +{ + var f = document.forms[0]; + var fs = document.createElement('fieldset'); + var i = document.createElement('input'); + + f.appendChild(fs); + i.type = 'hidden'; + f.appendChild(i); + + fs.setCustomValidity("foo"); + i.setCustomValidity("foo"); + + ok(f.checkValidity(), + "A form with invalid element barred from constraint validation should be valid"); + + f.removeChild(i); + f.removeChild(fs); +} + +function checkValid() +{ + var f = document.forms[0]; + var i = document.createElement('input'); + f.appendChild(i); + + ok(f.checkValidity(), "A form with valid elements is valid"); + + f.removeChild(i); +} + +function checkInvalid() +{ + var f = document.forms[0]; + var i = document.createElement('input'); + f.appendChild(i); + + i.setCustomValidity("foo"); + ok(!f.checkValidity(), "A form with invalid elements is invalid"); + + var i2 = document.createElement('input'); + f.appendChild(i2); + ok(!f.checkValidity(), + "A form with at least one invalid element is invalid"); + + f.removeChild(i2); + f.removeChild(i); +} + +function checkInvalidEvent() +{ + var f = document.forms[0]; + var i = document.createElement('input'); + f.appendChild(i); + var i2 = document.createElement('input'); + f.appendChild(i2); + + i.setCustomValidity("foo"); + + var invalidEventForInvalidElement = false; + var invalidEventForValidElement = false; + + i.addEventListener("invalid", function (e) { + invalidEventForInvalidElement = true; + ok(e.cancelable, "invalid event should be cancelable"); + ok(!e.bubbles, "invalid event should not bubble"); + }); + + i2.addEventListener("invalid", function (e) { + invalidEventForValidElement = true; + }); + + f.checkValidity(); + + setTimeout(function() { + ok(invalidEventForInvalidElement, + "invalid event should be fired on invalid elements"); + ok(!invalidEventForValidElement, + "invalid event should not be fired on valid elements"); + + f.removeChild(i2); + f.removeChild(i); + + SimpleTest.finish(); + }, 0); +} + +SimpleTest.waitForExplicitFinish(); + +checkEmptyForm(); +checkBarredFromConstraintValidation(); +checkValid(); +checkInvalid(); +checkInvalidEvent(); // will call finish(). + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug561636.html b/dom/html/test/test_bug561636.html new file mode 100644 index 0000000000..da51b07607 --- /dev/null +++ b/dom/html/test/test_bug561636.html @@ -0,0 +1,99 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=561636 +--> +<head> + <title>Test for Bug 561636</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=561636">Mozilla Bug 561636</a> +<p id="display"></p> +<iframe style='width:50px; height: 50px;' name='t'></iframe> +<iframe style='width:50px; height: 50px;' name='t2' id='i'></iframe> +<div id="content"> + <form target='t' action='data:text/html,'> + <input required> + <input id='a' type='submit'> + </form> + <form target='t' action='data:text/html,'> + <input type='checkbox' required> + <button id='b' type='submit'></button> + </form> + <form target='t' action='data:text/html,'> + <input id='c' required> + </form> + <form target='t' action='data:text/html,'> + <input> + <input id='s2' type='submit'> + </form> + <form target='t2' action='data:text/html,'> + <input required> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 561636 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); + +function runTest() +{ + var formSubmitted = [ false, false ]; + var invalidHandled = false; + + // Initialize + document.forms[0].addEventListener('submit', function(aEvent) { + formSubmitted[0] = true; + }, {once: true}); + + document.forms[1].addEventListener('submit', function(aEvent) { + formSubmitted[1] = true; + }, {once: true}); + + document.forms[2].addEventListener('submit', function(aEvent) { + formSubmitted[2] = true; + }, {once: true}); + + document.forms[3].addEventListener('submit', function(aEvent) { + formSubmitted[3] = true; + + ok(!formSubmitted[0], "Form 1 should not have been submitted because invalid"); + ok(!formSubmitted[1], "Form 2 should not have been submitted because invalid"); + ok(!formSubmitted[2], "Form 3 should not have been submitted because invalid"); + ok(formSubmitted[3], "Form 4 should have been submitted because valid"); + + // Next test. + document.forms[4].submit(); + }, {once: true}); + + document.forms[4].elements[0].addEventListener('invalid', function(aEvent) { + invalidHandled = true; + }, {once: true}); + + document.getElementById('i').addEventListener('load', function(aEvent) { + SimpleTest.executeSoon(function () { + ok(true, "Form 5 should have been submitted because submit() has been used even if invalid"); + ok(!invalidHandled, "Invalid event should not have been sent"); + + SimpleTest.finish(); + }); + }, {once: true}); + + document.getElementById('a').click(); + document.getElementById('b').click(); + var c = document.getElementById('c'); + c.focus(); + synthesizeKey("KEY_Enter"); + document.getElementById('s2').click(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug561640.html b/dom/html/test/test_bug561640.html new file mode 100644 index 0000000000..ed7bf84126 --- /dev/null +++ b/dom/html/test/test_bug561640.html @@ -0,0 +1,72 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=561640 +--> +<head> + <title>Test for Bug 561640</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + input, textarea { background-color: rgb(0,0,0) !important; } + :-moz-any(input,textarea):valid { background-color: rgb(0,255,0) !important; } + :-moz-any(input,textarea):invalid { background-color: rgb(255,0,0) !important; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=561640">Mozilla Bug 561640</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 561640 **/ + +var elements = [ 'input', 'textarea' ]; +var content = document.getElementById('content'); + +function checkValid(elmt) +{ + ok(!elmt.validity.tooLong, "element should not be too long"); + is(window.getComputedStyle(elmt).getPropertyValue('background-color'), + "rgb(0, 255, 0)", ":valid pseudo-class should apply"); +} + +function checkInvalid(elmt) +{ + todo(elmt.validity.tooLong, "element should be too long"); + todo_is(window.getComputedStyle(elmt).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); +} + +for (var elmtName of elements) { + var elmt = document.createElement(elmtName); + content.appendChild(elmt); + + if (elmtName == 'textarea') { + elmt.textContent = 'foo'; + } else { + elmt.setAttribute('value', 'foo'); + } + elmt.maxLength = 2; + checkValid(elmt); + + elmt.value = 'a'; + checkValid(elmt); + + if (elmtName == 'textarea') { + elmt.textContent = 'f'; + } else { + elmt.setAttribute('value', 'f'); + } + elmt.value = 'bar'; + checkInvalid(elmt); + + content.removeChild(elmt); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug564001.html b/dom/html/test/test_bug564001.html new file mode 100644 index 0000000000..3815ac8cf9 --- /dev/null +++ b/dom/html/test/test_bug564001.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=564001 +--> +<head> + <title>Test for Bug 564001</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=564001">Mozilla Bug 564001</a> +<p id="display"><img usemap=#map src=image.png></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script> +/** Test for Bug 564001 **/ +SimpleTest.waitForExplicitFinish(); + +var wrongArea = document.createElement("area"); +wrongArea.shape = "default"; +wrongArea.href = "#FAIL"; +var wrongMap = document.createElement("map"); +wrongMap.name = "map"; +wrongMap.appendChild(wrongArea); +document.body.appendChild(wrongMap); + +var rightArea = document.createElement("area"); +rightArea.shape = "default"; +rightArea.href = "#PASS"; +var rightMap = document.createElement("map"); +rightMap.name = "map"; +rightMap.appendChild(rightArea); +document.body.insertBefore(rightMap, wrongMap); + +var images = document.getElementsByTagName("img"); +onhashchange = function() { + is(location.hash, "#PASS", "Should get the first map in tree order."); + SimpleTest.finish(); +}; +SimpleTest.waitForFocus(() => synthesizeMouse(images[0], 50, 50, {})); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug566046.html b/dom/html/test/test_bug566046.html new file mode 100644 index 0000000000..88c59a4e61 --- /dev/null +++ b/dom/html/test/test_bug566046.html @@ -0,0 +1,200 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=566046 +--> +<head> + <title>Test for Bug 566046</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <base> + <base target='frame2'> + <base target=''> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566046">Mozilla Bug 566046</a> +<p id="display"></p> +<style> + iframe { width: 130px; height: 100px;} +</style> +<iframe name='frame1' id='frame1'></iframe> +<iframe name='frame2' id='frame2'></iframe> +<iframe name='frame3' id='frame3'></iframe> +<iframe name='frame4' id='frame4'></iframe> +<iframe name='frame5' id='frame5'></iframe> +<iframe name='frame5bis' id='frame5bis'></iframe> +<iframe name='frame6' id='frame6'></iframe> +<iframe name='frame7' id='frame7'></iframe> +<iframe name='frame8' id='frame8'></iframe> +<iframe name='frame9' id='frame9'></iframe> +<div id="content"> + <form target='frame1' action="dummy_page.html" method="GET"> + <input name='foo' value='foo'> + </form> + <form action="dummy_page.html" method="GET"> + <input name='bar' value='bar'> + </form> + <form target=""> + </form> + + <!-- submit controls with formtarget that are validated with a CLICK --> + <form target="tulip" action="dummy_page.html" method="GET"> + <input name='tulip' value='tulip'> + <input type='submit' id='is' formtarget='frame3'> + </form> + <form action="dummy_page.html" method="GET"> + <input name='foobar' value='foobar'> + <input type='image' id='ii' formtarget='frame4'> + </form> + <form action="dummy_page.html" method="GET"> + <input name='tulip2' value='tulip2'> + <button type='submit' id='bs' formtarget='frame5'>submit</button> + </form> + <form action="dummy_page.html" method="GET"> + <input name='tulip3' value='tulip3'> + <button type='submit' id='bsbis' formtarget='frame5bis'>submit</button> + </form> + + <!-- submit controls with formtarget that are validated with ENTER --> + <form target="tulip" action="dummy_page.html" method="GET"> + <input name='footulip' value='footulip'> + <input type='submit' id='is2' formtarget='frame6'> + </form> + <form action="dummy_page.html" method="GET"> + <input name='tulipfoobar' value='tulipfoobar'> + <input type='image' id='ii2' formtarget='frame7'> + </form> + <form action="dummy_page.html" method="GET"> + <input name='tulipbar' value='tulipbar'> + <button type='submit' id='bs2' formtarget='frame8'>submit</button> + </form> + + <!-- check that a which is not a submit control do not use @formtarget --> + <form target='frame9' action="dummy_page.html" method="GET"> + <input id='enter' name='input' value='enter' formtarget='frame6'> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 566046 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + setTimeout(runTests, 0); +}); + +const BASE_URI = `${location.origin}/tests/dom/html/test/dummy_page.html`; +var gTestResults = { + frame1: BASE_URI + "?foo=foo", + frame2: BASE_URI + "?bar=bar", + frame3: BASE_URI + "?tulip=tulip", + frame4: BASE_URI + "?foobar=foobar&x=0&y=0", + frame5: BASE_URI + "?tulip2=tulip2", + frame5bis: BASE_URI + "?tulip3=tulip3", + frame6: BASE_URI + "?footulip=footulip", + frame7: BASE_URI + "?tulipfoobar=tulipfoobar&x=0&y=0", + frame8: BASE_URI + "?tulipbar=tulipbar", + frame9: BASE_URI + "?input=enter", +}; + +var gPendingLoad = 0; // Has to be set after depending on the frames number. + +function runTests() +{ + // Check the target IDL attribute. + for (var i=0; i<document.forms.length; ++i) { + var testValue = document.forms[i].getAttribute('target'); + is(document.forms[i].target, testValue ? testValue : "", + "target IDL attribute should reflect the target content attribute"); + } + + // We add a load event for the frames which will be called when the forms + // will be submitted. + var frames = [ document.getElementById('frame1'), + document.getElementById('frame2'), + document.getElementById('frame3'), + document.getElementById('frame4'), + document.getElementById('frame5'), + document.getElementById('frame5bis'), + document.getElementById('frame6'), + document.getElementById('frame7'), + document.getElementById('frame8'), + document.getElementById('frame9'), + ]; + gPendingLoad = frames.length; + + for (var i=0; i<frames.length; i++) { + frames[i].setAttribute('onload', "frameLoaded(this);"); + } + + // Submitting only the forms with a valid target. + document.forms[0].submit(); + document.forms[1].submit(); + + /** + * We are going to focus each element before interacting with either for + * simulating the ENTER key (synthesizeKey) or a click (synthesizeMouse) or + * using .click(). This because it may be needed (ENTER) and because we want + * to have the element visible in the iframe. + * + * Focusing the first element (id='is') is launching the tests. + */ + document.getElementById('is').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('is'), 5, 5, {}); + document.getElementById('ii').focus(); + }, {once: true}); + + document.getElementById('ii').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('ii'), 5, 5, {}); + document.getElementById('bs').focus(); + }, {once: true}); + + document.getElementById('bs').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('bs'), 5, 5, {}); + document.getElementById('bsbis').focus(); + }, {once: true}); + + document.getElementById('bsbis').addEventListener('focus', function(aEvent) { + document.getElementById('bsbis').click(); + document.getElementById('is2').focus(); + }, {once: true}); + + document.getElementById('is2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('ii2').focus(); + }, {once: true}); + + document.getElementById('ii2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('bs2').focus(); + }, {once: true}); + + document.getElementById('bs2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('enter').focus(); + }, {once: true}); + + document.getElementById('enter').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + }, {once: true}); + + document.getElementById('is').focus(); +} + +function frameLoaded(aFrame) { + // Check if when target is unspecified, it fallback correctly to the base + // element target attribute. + is(aFrame.contentWindow.location.href, gTestResults[aFrame.name], + "the target attribute doesn't have the correct behavior"); + + if (--gPendingLoad == 0) { + SimpleTest.finish(); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug567938-1.html b/dom/html/test/test_bug567938-1.html new file mode 100644 index 0000000000..0c5d78f1c0 --- /dev/null +++ b/dom/html/test/test_bug567938-1.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=567938 +--> +<head> + <title>Test for Bug 567938</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="runTests();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567938">Mozilla Bug 567938</a> +<p id="display"></p> +<iframe id='iframe' name="submit_frame" style="visibility: hidden;"></iframe> +<div id="content" style="display: none"> + <form id='f' method='get' target='submit_frame'> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 567938 **/ + +SimpleTest.waitForExplicitFinish(); + +var gTestData = ["submit", "image"]; +var gCurrentTest = 0; + +function initializeNextTest() +{ + var form = document.forms[0]; + + // Cleaning-up. + form.textContent = ""; + + // Add the new element. + var element = document.createElement("input"); + element.id = 'i'; + element.type = gTestData[gCurrentTest]; + element.onclick = function() { form.submit(); return false; }; + form.action = gTestData[gCurrentTest]; + form.appendChild(element); + + sendMouseEvent({type: 'click'}, 'i'); +} + +function runTests() +{ + document.getElementById('iframe').addEventListener('load', function(aEvent) { + is(frames.submit_frame.location.href, + `${location.origin}/tests/dom/html/test/${gTestData[gCurrentTest]}?`, + "The form should have been submitted"); + gCurrentTest++; + if (gCurrentTest < gTestData.length) { + initializeNextTest(); + } else { + aEvent.target.removeEventListener('load', arguments.callee); + SimpleTest.finish(); + } + }); + + initializeNextTest(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug567938-2.html b/dom/html/test/test_bug567938-2.html new file mode 100644 index 0000000000..4a97516805 --- /dev/null +++ b/dom/html/test/test_bug567938-2.html @@ -0,0 +1,70 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=567938 +--> +<head> + <title>Test for Bug 567938</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567938">Mozilla Bug 567938</a> +<p id="display"></p> +<iframe id='iframe' name="submit_frame" style="visibility: hidden;"></iframe> +<div id="content" style="display: none"> + <form id='f' method='get' target='submit_frame'> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 567938 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTests); + +var gTestData = ["submit", "image"]; +var gCurrentTest = 0; + +function initializeNextTest() +{ + var form = document.forms[0]; + + // Cleaning-up. + form.textContent = ""; + + // Add the new element. + var element = document.createElement("input"); + element.id = 'i'; + element.type = gTestData[gCurrentTest]; + element.onclick = function() { form.submit(); element.type='text'; }; + form.action = gTestData[gCurrentTest]; + form.appendChild(element); + + sendMouseEvent({type: 'click'}, 'i'); +} + +function runTests() +{ + document.getElementById('iframe').addEventListener('load', function(aEvent) { + is(frames.submit_frame.location.href, + `${location.origin}/tests/dom/html/test/${gTestData[gCurrentTest]}?`, + "The form should have been submitted"); + gCurrentTest++; + if (gCurrentTest < gTestData.length) { + initializeNextTest(); + } else { + aEvent.target.removeEventListener('load', arguments.callee); + SimpleTest.finish(); + } + }); + + initializeNextTest(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug567938-3.html b/dom/html/test/test_bug567938-3.html new file mode 100644 index 0000000000..3c23129466 --- /dev/null +++ b/dom/html/test/test_bug567938-3.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=567938 +--> +<head> + <title>Test for Bug 567938</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567938">Mozilla Bug 567938</a> +<p id="display"></p> +<iframe id='iframe' name="submit_frame" style="visibility: hidden;"></iframe> +<div id="content" style="display: none"> + <form id='f' method='get' target='submit_frame'> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 567938 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTests); + +var gTestData = ["submit", "image"]; +var gCurrentTest = 0; + +function initializeNextTest() +{ + var form = document.forms[0]; + + // Cleaning-up. + form.textContent = ""; + + // Add the new element. + var element = document.createElement("input"); + element.id = 'i'; + element.type = gTestData[gCurrentTest]; + // eslint-disable-next-line no-implied-eval + element.onclick = function() { setTimeout("document.forms[0].submit();",0); return false; }; + form.appendChild(element); + form.action = gTestData[gCurrentTest]; + + sendMouseEvent({type: 'click'}, 'i'); +} + +function runTests() +{ + document.getElementById('iframe').addEventListener('load', function(aEvent) { + is(frames.submit_frame.location.href, + `${location.origin}/tests/dom/html/test/${gTestData[gCurrentTest]}?`, + "The form should have been submitted"); + gCurrentTest++; + if (gCurrentTest < gTestData.length) { + initializeNextTest(); + } else { + aEvent.target.removeEventListener('load', arguments.callee); + SimpleTest.finish(); + } + }); + + initializeNextTest(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug567938-4.html b/dom/html/test/test_bug567938-4.html new file mode 100644 index 0000000000..f04ac27f5f --- /dev/null +++ b/dom/html/test/test_bug567938-4.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=567938 +--> +<head> + <title>Test for Bug 567938</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567938">Mozilla Bug 567938</a> +<p id="display"></p> +<div id="content" style="display: none"> + <input id='i' type='checkbox' onclick="return false;"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 567938 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTests); + +function runTests() +{ + document.getElementById('i').checked = false; + + document.getElementById('i').addEventListener('click', function(aEvent) { + SimpleTest.executeSoon(function() { + ok(!aEvent.target.checked, "the input should not be checked"); + SimpleTest.finish(); + }); + }, {once: true}); + + sendMouseEvent({type: 'click'}, 'i'); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug569955.html b/dom/html/test/test_bug569955.html new file mode 100644 index 0000000000..0ed5ce88c7 --- /dev/null +++ b/dom/html/test/test_bug569955.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=569955 +--> +<head> + <title>Test for Bug 569955</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=569955">Mozilla Bug 569955</a> +<p id="display"></p> +<div id="content" style="display: none"> + <input id='i' autofocus> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 569955 **/ + +function runTests() +{ + isnot(document.activeElement, document.getElementById('i'), + "not rendered elements can't be autofocused"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + setTimeout(runTests, 0); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug573969.html b/dom/html/test/test_bug573969.html new file mode 100644 index 0000000000..f6020d1445 --- /dev/null +++ b/dom/html/test/test_bug573969.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=573969 +--> +<head> + <title>Test for Bug 573969</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=573969">Mozilla Bug 573969</a> +<p id="display"></p> +<div id="content" style="display: none"> + <xmp id='x'></xmp> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 573969 **/ + +var testData = [ + '<div>foo</div>', + '<div></div>', +]; + +var x = document.getElementById('x'); + +for (v of testData) { + x.innerHTML = v; + is(x.innerHTML, v, "innerHTML value should not be escaped"); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug57600.html b/dom/html/test/test_bug57600.html new file mode 100644 index 0000000000..fc7037bd32 --- /dev/null +++ b/dom/html/test/test_bug57600.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=57600 +--> +<head> + <title>Test for Bug 57600</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=57600">Mozilla Bug 57600</a> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 57600 **/ +SimpleTest.waitForExplicitFinish(); +var count = 0; +function disp(win) { + var d = win ? win.document : self.testname.document; + var str = 'You should see this'; + d.open(); + d.write(str); + d.close(); + is(d.documentElement.textContent, str, "Unexpected text"); + if (++count == 2) { + SimpleTest.finish(); + } +} +</script> +</pre> +<p id="display"> + <iframe src="javascript:'<body onload="this.onerror = parent.onerror; parent.disp(self)"></body>'"> + </iframe> + <iframe name="testname" src="javascript:'<body onload="this.onerror = parent.onerror; parent.disp()"></body>'"> + </iframe> +</p> +</body> +</html> diff --git a/dom/html/test/test_bug579079.html b/dom/html/test/test_bug579079.html new file mode 100644 index 0000000000..e5ec429226 --- /dev/null +++ b/dom/html/test/test_bug579079.html @@ -0,0 +1,40 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=579079 +--> +<head> + <title>Test for Bug 579079</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=579079">Mozilla Bug 579079</a> + +<div id="foo"> + <img name="img1"> + <form name="form1"></form> + <applet name="applet1"></applet> + <embed name="embed1"></embed> + <object name="object1"></object> +</div> + +<pre id="test"> +<script class="testbody" type="text/javascript"> +var img = document.img1; +var form = document.form1; +var embed = document.embed1; +var object = document.object1; +$("foo").innerHTML = $("foo").innerHTML; +isnot(document.img1, img); +ok(document.img1 instanceof HTMLImageElement); +isnot(document.form1, form); +ok(document.form1 instanceof HTMLFormElement); +isnot(document.embed1, embed); +ok(document.embed1 instanceof HTMLEmbedElement); +isnot(document.object1, object); +ok(document.object1 instanceof HTMLObjectElement); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug582412-1.html b/dom/html/test/test_bug582412-1.html new file mode 100644 index 0000000000..f1256e3d5a --- /dev/null +++ b/dom/html/test/test_bug582412-1.html @@ -0,0 +1,200 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=566160 +--> +<head> + <title>Test for Bug 566160</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566160">Mozilla Bug 566160</a> +<p id="display"></p> +<style> + iframe { width: 130px; height: 100px;} +</style> +<iframe name='frame1' id='frame1'></iframe> +<iframe name='frame2' id='frame2'></iframe> +<iframe name='frame3' id='frame3'></iframe> +<iframe name='frame3bis' id='frame3bis'></iframe> +<iframe name='frame4' id='frame4'></iframe> +<iframe name='frame5' id='frame5'></iframe> +<iframe name='frame6' id='frame6'></iframe> +<iframe name='frame7' id='frame7'></iframe> +<iframe name='frame8' id='frame8'></iframe> +<iframe name='frame9' id='frame9'></iframe> +<div id="content"> + <!-- submit controls with formaction that are validated with a CLICK --> + <form target="frame1" action="dummy_page.html" method="POST"> + <input name='foo' value='foo'> + <input type='submit' id='is' formmethod="GET"> + </form> + <form target="frame2" action="dummy_page.html" method="POST"> + <input name='bar' value='bar'> + <input type='image' id='ii' formmethod="GET"> + </form> + <form target="frame3" action="dummy_page.html" method="POST"> + <input name='tulip' value='tulip'> + <button type='submit' id='bs' formmethod="GET">submit</button> + </form> + <form target="frame3bis" action="dummy_page.html" method="POST"> + <input name='tulipbis' value='tulipbis'> + <button type='submit' id='bsbis' formmethod="GET">submit</button> + </form> + + <!-- submit controls with formaction that are validated with ENTER --> + <form target="frame4" action="dummy_page.html" method="POST"> + <input name='footulip' value='footulip'> + <input type='submit' id='is2' formmethod="GET"> + </form> + <form target="frame5" action="dummy_page.html" method="POST"> + <input name='foobar' value='foobar'> + <input type='image' id='ii2' formmethod="GET"> + </form> + <form target="frame6" action="dummy_page.html" method="POST"> + <input name='tulip2' value='tulip2'> + <button type='submit' id='bs2' formmethod="GET">submit</button> + </form> + + <!-- check that when submitting a from from an element + which is not a submit control, @formaction isn't used --> + <form target='frame7' action="dummy_page.html" method="GET"> + <input id='enter' name='input' value='enter' formmethod="POST"> + </form> + + <!-- If formmethod isn't set, it's default value shouldn't be used --> + <form target="frame8" action="dummy_page.html" method="POST"> + <input name='tulip8' value='tulip8'> + <input type='submit' id='i8'> + </form> + + <!-- If formmethod is set but has an invalid value, the default value should + be used. --> + <form target="frame9" action="dummy_page.html" method="POST"> + <input name='tulip9' value='tulip9'> + <input type='submit' id='i9' formmethod=""> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 566160 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + setTimeout(runTests, 0); +}); + +const BASE_URI = `${location.origin}/tests/dom/html/test/dummy_page.html`; +var gTestResults = { + frame1: BASE_URI + "?foo=foo", + frame2: BASE_URI + "?bar=bar&x=0&y=0", + frame3: BASE_URI + "?tulip=tulip", + frame3bis: BASE_URI + "?tulipbis=tulipbis", + frame4: BASE_URI + "?footulip=footulip", + frame5: BASE_URI + "?foobar=foobar&x=0&y=0", + frame6: BASE_URI + "?tulip2=tulip2", + frame7: BASE_URI + "?input=enter", + frame8: BASE_URI + "", + frame9: BASE_URI + "?tulip9=tulip9", +}; + +var gPendingLoad = 0; // Has to be set after depending on the frames number. + +function runTests() +{ + // We add a load event for the frames which will be called when the forms + // will be submitted. + var frames = [ document.getElementById('frame1'), + document.getElementById('frame2'), + document.getElementById('frame3'), + document.getElementById('frame3bis'), + document.getElementById('frame4'), + document.getElementById('frame5'), + document.getElementById('frame6'), + document.getElementById('frame7'), + document.getElementById('frame8'), + document.getElementById('frame9'), + ]; + gPendingLoad = frames.length; + + for (var i=0; i<frames.length; i++) { + frames[i].setAttribute('onload', "frameLoaded(this);"); + } + + /** + * We are going to focus each element before interacting with either for + * simulating the ENTER key (synthesizeKey) or a click (synthesizeMouse) or + * using .click(). This because it may be needed (ENTER) and because we want + * to have the element visible in the iframe. + * + * Focusing the first element (id='is') is launching the tests. + */ + document.getElementById('is').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('is'), 5, 5, {}); + document.getElementById('ii').focus(); + }, {once: true}); + + document.getElementById('ii').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('ii'), 5, 5, {}); + document.getElementById('bs').focus(); + }, {once: true}); + + document.getElementById('bs').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('bs'), 5, 5, {}); + document.getElementById('bsbis').focus(); + }, {once: true}); + + document.getElementById('bsbis').addEventListener('focus', function(aEvent) { + document.getElementById('bsbis').click(); + document.getElementById('is2').focus(); + }, {once: true}); + + document.getElementById('is2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('ii2').focus(); + }, {once: true}); + + document.getElementById('ii2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('bs2').focus(); + }, {once: true}); + + document.getElementById('bs2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('enter').focus(); + }, {once: true}); + + document.getElementById('enter').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('i8').focus(); + }, {once: true}); + + document.getElementById('i8').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('i9').focus(); + }, {once: true}); + + document.getElementById('i9').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + }, {once: true}); + + document.getElementById('is').focus(); +} + +function frameLoaded(aFrame) { + // Check if formaction/action has the correct behavior. + is(aFrame.contentWindow.location.href, gTestResults[aFrame.name], + "the method/formmethod attribute doesn't have the correct behavior"); + + if (--gPendingLoad == 0) { + SimpleTest.finish(); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug582412-2.html b/dom/html/test/test_bug582412-2.html new file mode 100644 index 0000000000..b5ff8fc81e --- /dev/null +++ b/dom/html/test/test_bug582412-2.html @@ -0,0 +1,199 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=566160 +--> +<head> + <title>Test for Bug 566160</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566160">Mozilla Bug 566160</a> +<p id="display"></p> +<style> + iframe { width: 130px; height: 100px;} +</style> +<iframe name='frame1' id='frame1'></iframe> +<iframe name='frame2' id='frame2'></iframe> +<iframe name='frame3' id='frame3'></iframe> +<iframe name='frame3bis' id='frame3bis'></iframe> +<iframe name='frame4' id='frame4'></iframe> +<iframe name='frame5' id='frame5'></iframe> +<iframe name='frame6' id='frame6'></iframe> +<iframe name='frame7' id='frame7'></iframe> +<iframe name='frame8' id='frame8'></iframe> +<iframe name='frame9' id='frame9'></iframe> +<div id="content"> + <!-- submit controls with formaction that are validated with a CLICK --> + <form target="frame1" action="form_submit_server.sjs" method="POST"> + <input name='foo' value='foo'> + <input type='submit' id='is' formenctype='multipart/form-data'> + </form> + <form target="frame2" action="form_submit_server.sjs" method="POST"> + <input name='bar' value='bar'> + <input type='image' id='ii' formenctype='multipart/form-data'> + </form> + <form target="frame3" action="form_submit_server.sjs" method="POST"> + <input name='tulip' value='tulip'> + <button type='submit' id='bs' formenctype="multipart/form-data">submit</button> + </form> + <form target="frame3bis" action="form_submit_server.sjs" method="POST"> + <input name='tulipbis' value='tulipbis'> + <button type='submit' id='bsbis' formenctype="multipart/form-data">submit</button> + </form> + + <!-- submit controls with formaction that are validated with ENTER --> + <form target="frame4" action="form_submit_server.sjs" method="POST"> + <input name='footulip' value='footulip'> + <input type='submit' id='is2' formenctype="multipart/form-data"> + </form> + <form target="frame5" action="form_submit_server.sjs" method="POST"> + <input name='foobar' value='foobar'> + <input type='image' id='ii2' formenctype="multipart/form-data"> + </form> + <form target="frame6" action="form_submit_server.sjs" method="POST"> + <input name='tulip2' value='tulip2'> + <button type='submit' id='bs2' formenctype="multipart/form-data">submit</button> + </form> + + <!-- check that when submitting a from from an element + which is not a submit control, @formaction isn't used --> + <form target='frame7' action="form_submit_server.sjs" method="POST"> + <input id='enter' name='input' value='enter' formenctype="multipart/form-data"> + </form> + + <!-- If formenctype isn't set, it's default value shouldn't be used --> + <form target="frame8" action="form_submit_server.sjs" method="POST" enctype="multipart/form-data"> + <input name='tulip8' value='tulip8'> + <input type='submit' id='i8'> + </form> + + <!-- If formenctype is set but has an invalid value, the default value should + be used. --> + <form target="frame9" action="form_submit_server.sjs" method="POST" enctype="multipart/form-data"> + <input name='tulip9' value='tulip9'> + <input type='submit' id='i9' formenctype=""> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 566160 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + setTimeout(runTests, 0); +}); + +var gTestResults = { + frame1: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"foo\\\"\"},\"body\":\"foo\"}]', + frame2: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"bar\\\"\"},\"body\":\"bar\"},{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"x\\\"\"},\"body\":\"0\"},{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"y\\\"\"},\"body\":\"0\"}]', + frame3: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"tulip\\\"\"},\"body\":\"tulip\"}]', + frame3bis: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"tulipbis\\\"\"},\"body\":\"tulipbis\"}]', + frame4: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"footulip\\\"\"},\"body\":\"footulip\"}]', + frame5: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"foobar\\\"\"},\"body\":\"foobar\"},{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"x\\\"\"},\"body\":\"0\"},{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"y\\\"\"},\"body\":\"0\"}]', + frame6: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"tulip2\\\"\"},\"body\":\"tulip2\"}]', + frame7: '[]', + frame8: '[{\"headers\":{\"Content-Disposition\":\"form-data; name=\\\"tulip8\\\"\"},\"body\":\"tulip8\"}]', + frame9: '[]', +}; + +var gPendingLoad = 0; // Has to be set after depending on the frames number. + +function runTests() +{ + // We add a load event for the frames which will be called when the forms + // will be submitted. + var frames = [ document.getElementById('frame1'), + document.getElementById('frame2'), + document.getElementById('frame3'), + document.getElementById('frame3bis'), + document.getElementById('frame4'), + document.getElementById('frame5'), + document.getElementById('frame6'), + document.getElementById('frame7'), + document.getElementById('frame8'), + document.getElementById('frame9'), + ]; + gPendingLoad = frames.length; + + for (var i=0; i<frames.length; i++) { + frames[i].setAttribute('onload', "frameLoaded(this);"); + } + + /** + * We are going to focus each element before interacting with either for + * simulating the ENTER key (synthesizeKey) or a click (synthesizeMouse) or + * using .click(). This because it may be needed (ENTER) and because we want + * to have the element visible in the iframe. + * + * Focusing the first element (id='is') is launching the tests. + */ + document.getElementById('is').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('is'), 5, 5, {}); + document.getElementById('ii').focus(); + }, {once: true}); + + document.getElementById('ii').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('ii'), 5, 5, {}); + document.getElementById('bs').focus(); + }, {once: true}); + + document.getElementById('bs').addEventListener('focus', function(aEvent) { + synthesizeMouse(document.getElementById('bs'), 5, 5, {}); + document.getElementById('bsbis').focus(); + }, {once: true}); + + document.getElementById('bsbis').addEventListener('focus', function(aEvent) { + document.getElementById('bsbis').click(); + document.getElementById('is2').focus(); + }, {once: true}); + + document.getElementById('is2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('ii2').focus(); + }, {once: true}); + + document.getElementById('ii2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('bs2').focus(); + }, {once: true}); + + document.getElementById('bs2').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('enter').focus(); + }, {once: true}); + + document.getElementById('enter').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('i8').focus(); + }, {once: true}); + + document.getElementById('i8').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + document.getElementById('i9').focus(); + }, {once: true}); + + document.getElementById('i9').addEventListener('focus', function(aEvent) { + synthesizeKey("KEY_Enter"); + }, {once: true}); + + document.getElementById('is').focus(); +} + +function frameLoaded(aFrame) { + // Check if formaction/action has the correct behavior. + is(aFrame.contentDocument.documentElement.textContent, gTestResults[aFrame.name], + "the enctype/formenctype attribute doesn't have the correct behavior"); + + if (--gPendingLoad == 0) { + SimpleTest.finish(); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug583514.html b/dom/html/test/test_bug583514.html new file mode 100644 index 0000000000..a3dead89aa --- /dev/null +++ b/dom/html/test/test_bug583514.html @@ -0,0 +1,71 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=583514 +--> +<head> + <title>Test for Bug 583514</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=583514">Mozilla Bug 583514</a> +<p id="display"></p> +<div id="content"> + <input> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 583514 **/ + +var gExpectDivClick = false; +var gExpectInputClick = false; + +var div = document.getElementById('content'); +var input = document.getElementsByTagName('input')[0]; + +div.addEventListener('click', function() { + ok(gExpectDivClick, "click event received on div and expected status was: " + + gExpectDivClick); +}); + +input.addEventListener('click', function() { + ok(gExpectInputClick, "click event received on input and expected status was: " + + gExpectInputClick); +}); + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + var body = document.body; + + body.addEventListener('click', function(aEvent) { + if (aEvent.target == input) { + body.removeEventListener('click', arguments.callee); + } + + ok(true, "click event received on body"); + + SimpleTest.executeSoon(function() { + isnot(document.activeElement, input, "input shouldn't have been focused"); + isnot(document.activeElement, div, "div shouldn't have been focused"); + + if (aEvent.target == input) { + SimpleTest.finish(); + } else { + gExpectDivClick = true; + gExpectInputClick = true; + input.click(); + } + }); + }); + + gExpectDivClick = true; + div.click(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug583533.html b/dom/html/test/test_bug583533.html new file mode 100644 index 0000000000..c0b8c92e95 --- /dev/null +++ b/dom/html/test/test_bug583533.html @@ -0,0 +1,81 @@ +<!DOCTYPE HTML> +<html> + <!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=583533 +--> + <head> + <title>Test for Bug 583514</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + </head> + <body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=583533">Mozilla Bug 583533</a> + <p id="display"></p> + <div id="content"> + <div id="e" accesskey="a"> + </div> +</div> +<pre id="test"> +<script type="application/javascript"> + + /** Test for Bug 583533 **/ + + var sbs = SpecialPowers.Cc['@mozilla.org/intl/stringbundle;1']. + getService(SpecialPowers.Ci.nsIStringBundleService); + var bundle = sbs.createBundle("chrome://global-platform/locale/platformKeys.properties"); + + var shiftText = bundle.GetStringFromName("VK_SHIFT"); + var altText = bundle.GetStringFromName("VK_ALT"); + var controlText = bundle.GetStringFromName("VK_CONTROL"); + var metaText = bundle.GetStringFromName("VK_COMMAND_OR_WIN"); + var separatorText = bundle.GetStringFromName("MODIFIER_SEPARATOR"); + + var modifier = SpecialPowers.getIntPref("ui.key.contentAccess"); + + var isShift; + var isAlt; + var isControl; + var isMeta; + + is(modifier < 16 && modifier >= 0, true, "Modifier in range"); + + // There are no consts for the mask of this prefs. + if (modifier & 8) { + isMeta = true; + } + if (modifier & 1) { + isShift = true; + } + if (modifier & 2) { + isControl = true; + } + if (modifier & 4) { + isAlt = true; + } + + var label = ""; + + if (isControl) + label += controlText + separatorText; + if (isMeta) + label += metaText + separatorText; + if (isAlt) + label += altText + separatorText; + if (isShift) + label += shiftText + separatorText; + + label += document.getElementById("e").accessKey; + + is(label, document.getElementById("e").accessKeyLabel, "JS and C++ agree on accessKeyLabel"); + + /** Test for Bug 808964 **/ + + var div = document.createElement("div"); + document.body.appendChild(div); + + is(div.accessKeyLabel, "", "accessKeyLabel should be empty string"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug586763.html b/dom/html/test/test_bug586763.html new file mode 100644 index 0000000000..b396cb8bc9 --- /dev/null +++ b/dom/html/test/test_bug586763.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=586763 +--> +<title>Test for Bug 586763</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=586763">Mozilla Bug 586763</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script> +/** Test for Bug 586763 **/ +var tests = [ + ["ol", "start", 1], + ["li", "value", 0], + ["object", "hspace", 0], + ["object", "vspace", 0], + ["img", "hspace", 0], + ["img", "vspace", 0], + ["video", "height", 0], + ["video", "width", 0], + ["pre", "width", 0], + ["textarea", "cols", 20], + ["textarea", "rows", 2], + ["span", "tabIndex", -1], + ["frame", "tabIndex", 0], + ["a", "tabIndex", 0], + ["area", "tabIndex", 0], + ["button", "tabIndex", 0], + ["input", "tabIndex", 0], + ["object", "tabIndex", 0], + ["select", "tabIndex", 0], + ["textarea", "tabIndex", 0], +]; + +for (var i = 0; i < tests.length; i++) { + is(document.createElement(tests[i][0])[tests[i][1]], tests[i][2], "Reflected attribute " + tests[i][0] + "." + tests[i][1] + " should default to " + tests[i][2]); +} +</script> +</pre> diff --git a/dom/html/test/test_bug586786.html b/dom/html/test/test_bug586786.html new file mode 100644 index 0000000000..3cfa7fa4b4 --- /dev/null +++ b/dom/html/test/test_bug586786.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=586786 +--> +<head> + <title>Test for Bug 586786</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=586786">Mozilla Bug 586786</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 586786 **/ + +var elements = ["col", "colgroup", "tbody", "tfoot", "thead", "tr", "td", "th"]; + +for(var i = 0; i < elements.length; i++) +{ + reflectString({ + element: document.createElement(elements[i]), + attribute: "align", + otherValues: [ "left", "right", "center", "justify", "char" ] + }); + + reflectString({ + element: document.createElement(elements[i]), + attribute: "vAlign", + otherValues: [ "top", "middle", "bottom", "baseline" ] + }); + + reflectString({ + element: document.createElement(elements[i]), + attribute: {idl: "ch", content: "char"} + }); +} + +// table.border, table.width +reflectString({ + element: document.createElement("table"), + attribute: "border" +}); + +reflectString({ + element: document.createElement("table"), + attribute: "width" +}); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug587469.html b/dom/html/test/test_bug587469.html new file mode 100644 index 0000000000..b0e941296a --- /dev/null +++ b/dom/html/test/test_bug587469.html @@ -0,0 +1,41 @@ +<!-- Quirks --> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=587469 +--> +<head> + <title>Test for Bug 587469</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=587469">Mozilla Bug 587469</a> +<p id="display"> +<map name=a></map> +<map name=a><area shape=rect coords=25,25,75,75 href=#fail></map> +<img usemap=#a src=image.png> +</p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 587469 **/ +SimpleTest.waitForExplicitFinish(); +function finish() { + is(location.hash, "", "Should not have changed the hash."); + SimpleTest.finish(); +} +SimpleTest.waitForFocus(function() { + synthesizeMouse(document.getElementsByTagName("img")[0], 50, 50, {}); + // Hit the event loop twice before doing the test + setTimeout(function() { + setTimeout(finish, 0); + }, 0); +}); + + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug589.html b/dom/html/test/test_bug589.html new file mode 100644 index 0000000000..3a4ac666a7 --- /dev/null +++ b/dom/html/test/test_bug589.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=589 +--> +<head> + <title>Test for Bug 589</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<style type="text/css"> +.letters {list-style-type: upper-alpha;} +.numbers {list-style-type: decimal;} +</style> + +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=589">Mozilla Bug 589</a> +<p id="display"></p> +<div id="content" > + +<OL id="thelist" class="letters"> +<LI id="liA">This list should feature... +<LI id="liB">...letters for each item... +<LI id="li3" class="numbers">...except this one. +</OL> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 589 **/ + +is(computedStyle($("liA"),"list-style-type"),"upper-alpha"); +is(computedStyle($("liB"),"list-style-type"),"upper-alpha"); +is(computedStyle($("li3"),"list-style-type"),"decimal"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug590353-1.html b/dom/html/test/test_bug590353-1.html new file mode 100644 index 0000000000..af79317f36 --- /dev/null +++ b/dom/html/test/test_bug590353-1.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=590353 +--> +<head> + <title>Test for Bug 590353</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=590353">Mozilla Bug 590353</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 590353 **/ + +var testData = ['checkbox', 'radio']; + +for (var data of testData) { + var e = document.createElement('input'); + e.type = data; + e.checked = true; + e.value = "foo"; + + is(e.value, "foo", "foo should be the new " + data + "value"); + is(e.getAttribute('value'), "foo", "foo should be the new " + data + + " value attribute value"); + ok(e.checked, data + " should still be checked"); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug590353-2.html b/dom/html/test/test_bug590353-2.html new file mode 100644 index 0000000000..9ef6e40e23 --- /dev/null +++ b/dom/html/test/test_bug590353-2.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=590353 +--> +<head> + <title>Test for Bug 590353</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=590353">Mozilla Bug 590353</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 590353 **/ + +var testData = [ + [ "text", "foo", "" ], + [ "email", "foo@bar.com", "" ], + [ "url", "http:///foo.com", "" ], + [ "tel", "555 555 555 555", "" ], + [ "search", "foo", "" ], + [ "password", "secret", "" ], + [ "hidden", "foo", "foo" ], + [ "button", "foo", "foo" ], + [ "reset", "foo", "foo" ], + [ "submit", "foo", "foo" ], + [ "checkbox", true, false ], + [ "radio", true, false ], + [ "file", "590353_file", "" ], +]; + +function createFileWithData(fileName, fileData) { + return new File([new Blob([fileData], { type: "text/plain" })], fileName); +} + +var content = document.getElementById('content'); +var form = document.createElement('form'); +content.appendChild(form); + +for (var data of testData) { + var e = document.createElement('input'); + e.type = data[0]; + + if (data[0] == 'checkbox' || data[0] == 'radio') { + e.checked = data[1]; + } else if (data[0] == 'file') { + var file = createFileWithData(data[1], "file content"); + SpecialPowers.wrap(e).mozSetFileArray([file]); + } else { + e.value = data[1]; + } + + form.appendChild(e); +} + +form.reset(); + +var size = form.elements.length; +for (var i=0; i<size; ++i) { + var e = form.elements[i]; + + if (e.type == 'radio' || e.type == 'checkbox') { + is(e.checked, testData[i][2], + "the element checked value should be " + testData[i][2]); + } else { + is(e.value, testData[i][2], + "the element value should be " + testData[i][2]); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug590363.html b/dom/html/test/test_bug590363.html new file mode 100644 index 0000000000..de12079a71 --- /dev/null +++ b/dom/html/test/test_bug590363.html @@ -0,0 +1,133 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=590363 +--> +<head> + <title>Test for Bug 590363</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=590363">Mozilla Bug 590363</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 590363 **/ + +var testData = [ + /* type to test | is the value reset when changing to file then reverting */ + [ "button", false ], + [ "checkbox", false ], + [ "hidden", false ], + [ "reset", false ], + [ "image", false ], + [ "radio", false ], + [ "submit", false ], + [ "tel", true ], + [ "text", true ], + [ "url", true ], + [ "email", true ], + [ "search", true ], + [ "password", true ], + [ "number", true ], + [ "date", true ], + [ "time", true ], + [ "range", true ], + [ "color", true ], + [ 'month', true ], + [ 'week', true ], + [ 'datetime-local', true ] + // 'file' is treated separatly. +]; + +var nonTrivialSanitizing = [ 'number', 'date', 'time', 'color', 'month', 'week', + 'datetime-local' ]; + +var length = testData.length; +for (var i=0; i<length; ++i) { + for (var j=0; j<length; ++j) { + var e = document.createElement('input'); + e.type = testData[i][0]; + + var expectedValue; + + // range will sanitize its value to 50 (the default) if it isn't a valid + // number. We need to handle that specially. + if (testData[j][0] == 'range' || testData[i][0] == 'range') { + if (testData[j][0] == 'date' || testData[j][0] == 'time' || + testData[j][0] == 'month' || testData[j][0] == 'week' || + testData[j][0] == 'datetime-local') { + expectedValue = ''; + } else if (testData[j][0] == 'color') { + expectedValue = '#000000'; + } else { + expectedValue = '50'; + } + } else if (testData[i][0] == 'color' || testData[j][0] == 'color') { + if (testData[j][0] == 'number' || testData[j][0] == 'date' || + testData[j][0] == 'time' || testData[j][0] == 'month' || + testData[j][0] == 'week' || testData[j][0] == 'datetime-local') { + expectedValue = '' + } else { + expectedValue = '#000000'; + } + } else if (nonTrivialSanitizing.includes(testData[i][0]) && + nonTrivialSanitizing.includes(testData[j][0])) { + expectedValue = ''; + } else if (testData[i][0] == 'number' || testData[j][0] == 'number') { + expectedValue = '42'; + } else if (testData[i][0] == 'date' || testData[j][0] == 'date') { + expectedValue = '2012-12-21'; + } else if (testData[i][0] == 'time' || testData[j][0] == 'time') { + expectedValue = '21:21'; + } else if (testData[i][0] == 'month' || testData[j][0] == 'month') { + expectedValue = '2013-03'; + } else if (testData[i][0] == 'week' || testData[j][0] == 'week') { + expectedValue = '2016-W35'; + } else if (testData[i][0] == 'datetime-local' || + testData[j][0] == 'datetime-local') { + expectedValue = '2016-11-07T16:40'; + } else { + expectedValue = "foo"; + } + e.value = expectedValue; + + e.type = testData[j][0]; + is(e.value, expectedValue, ".value should still return the same value after " + + "changing type from " + testData[i][0] + " to " + testData[j][0]); + } +} + +// For type='file' .value doesn't behave the same way. +// We are just going to check that we do not loose the value. +for (var data of testData) { + var e = document.createElement('input'); + e.type = data[0]; + e.value = 'foo'; + e.type = 'file'; + e.type = data[0]; + + if (data[0] == 'range') { + is(e.value, '50', ".value should still return the same value after " + + "changing type from " + data[0] + " to 'file' then reverting to " + data[0]); + } else if (data[0] == 'color') { + is(e.value, '#000000', ".value should have been reset to the default color after " + + "changing type from " + data[0] + " to 'file' then reverting to " + data[0]); + } else if (data[1]) { + is(e.value, '', ".value should have been reset to the empty string after " + + "changing type from " + data[0] + " to 'file' then reverting to " + data[0]); + } else { + is(e.value, 'foo', ".value should still return the same value after " + + "changing type from " + data[0] + " to 'file' then reverting to " + data[0]); + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug592802.html b/dom/html/test/test_bug592802.html new file mode 100644 index 0000000000..e8b30d84c8 --- /dev/null +++ b/dom/html/test/test_bug592802.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=592802 +--> +<head> + <title>Test for Bug 592802</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=592802">Mozilla Bug 592802</a> +<p id="display"></p> +<div id="content"> + <input id='a' type='file'> + <input id='a2' type='file'> +</div> +<button id='b' onclick="document.getElementById('a').click();">Show Filepicker</button> +<button id='b2' onclick="document.getElementById('a2').click();">Show Filepicker</button> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 592802 **/ + +SimpleTest.waitForExplicitFinish(); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +var testData = [ +/* visibility | display | multiple */ + [ "", "", false ], + [ "hidden", "", false ], + [ "", "none", false ], + [ "", "", true ], + [ "hidden", "", true ], + [ "", "none", true ], +]; + +var testCounter = 0; +var testNb = testData.length; + +function finished() +{ + MockFilePicker.cleanup(); + SimpleTest.finish(); +} + +SimpleTest.waitForFocus(function() { + // mockFilePicker will simulate a cancel for the first time the file picker will be shown. + MockFilePicker.returnValue = MockFilePicker.returnCancel; + + var b2 = document.getElementById('b2'); + b2.focus(); // Be sure the element is visible. + document.getElementById('b2').addEventListener("change", function(aEvent) { + ok(false, "When cancel is received, change should not fire"); + }, {once: true}); + b2.click(); + + // Now, we can launch tests when file picker isn't canceled. + MockFilePicker.useBlobFile(); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + var b = document.getElementById('b'); + b.focus(); // Be sure the element is visible. + + document.getElementById('a').addEventListener("change", function(aEvent) { + ok(true, "change event correctly sent"); + ok(aEvent.bubbles, "change event should bubble"); + ok(!aEvent.cancelable, "change event should not be cancelable"); + testCounter++; + + if (testCounter >= testNb) { + aEvent.target.removeEventListener("change", arguments.callee); + SimpleTest.executeSoon(finished); + } else { + var data = testData[testCounter]; + var a = document.getElementById('a'); + a.style.visibility = data[0]; + a.style.display = data[1]; + a.multiple = data[2]; + + SimpleTest.executeSoon(function() { + b.click(); + }); + } + }); + + b.click(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug593689.html b/dom/html/test/test_bug593689.html new file mode 100644 index 0000000000..14cfa8f0fb --- /dev/null +++ b/dom/html/test/test_bug593689.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=593689 +--> +<head> + <title>Test for Bug 593689</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=593689">Mozilla Bug 593689</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 593689 **/ +function testWidth(w) { + var img = new Image(w); + is(img.width, w|0, "Unexpected handling of '" + w + "' width"); +} + +testWidth(1); +testWidth(0); +testWidth("xxx"); +testWidth(null); +testWidth(undefined); +testWidth({}); +testWidth({ valueOf() { return 10; } }); + +function testHeight(h) { + var img = new Image(100, h); + is(img.height, h|0, "Unexpected handling of '" + h + "' height"); +} + +testHeight(1); +testHeight(0); +testHeight("xxx"); +testHeight(null); +testHeight(undefined); +testHeight({}); +testHeight({ valueOf() { return 10; } }); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug595429.html b/dom/html/test/test_bug595429.html new file mode 100644 index 0000000000..9a9215bd6c --- /dev/null +++ b/dom/html/test/test_bug595429.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=595429 +--> +<head> + <title>Test for Bug 595429</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=595429">Mozilla Bug 595429</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 595429 **/ + +var fieldset = document.createElement("fieldset"); + +ok("name" in fieldset, "<fieldset> should have a name IDL attribute"); + +var testData = [ + "", + " ", + "foo", + "foo bar", +]; + +is(fieldset.getAttribute("name"), null, + "By default, name content attribute should be null"); +is(fieldset.name, "", + "By default, name IDL attribute should be the empty string"); + +for (var data of testData) { + fieldset.setAttribute("name", data); + is(fieldset.getAttribute("name"), data, + "name content attribute should be " + data); + is(fieldset.name, data, "name IDL attribute should be " + data); + + fieldset.setAttribute("name", ""); + fieldset.name = data; + is(fieldset.getAttribute("name"), data, + "name content attribute should be " + data); + is(fieldset.name, data, "name IDL attribute should be " + data); +} + +fieldset.removeAttribute("name"); +is(fieldset.getAttribute("name"), null, + "name content attribute should be null"); +is(fieldset.name, "", "name IDL attribute should be the empty string"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug595447.html b/dom/html/test/test_bug595447.html new file mode 100644 index 0000000000..e647364879 --- /dev/null +++ b/dom/html/test/test_bug595447.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=595447 +--> +<head> + <title>Test for Bug 595447</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=595447">Mozilla Bug 595447</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 595447 **/ + +var fieldset = document.createElement("fieldset"); + +ok("type" in fieldset, "fieldset element should have a type IDL attribute"); +is(fieldset.type, "fieldset", "fieldset.type should return 'fieldset'"); +fieldset.type = "foo"; +is(fieldset.type, "fieldset", "fieldset.type is readonly"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug595449.html b/dom/html/test/test_bug595449.html new file mode 100644 index 0000000000..5649b246ae --- /dev/null +++ b/dom/html/test/test_bug595449.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=595449 +--> +<head> + <title>Test for Bug 595449</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=595449">Mozilla Bug 595449</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 595449 **/ + +var fieldset = document.createElement("fieldset"); + +ok("elements" in fieldset, + "fieldset element should have an 'elements' IDL attribute"); + +ok(fieldset.elements instanceof HTMLCollection, + "fieldset.elements should be an instance of HTMLCollection"); + +// https://www.w3.org/Bugs/Public/show_bug.cgi?id=23356 +todo(fieldset.elements instanceof HTMLFormControlsCollection, + "fieldset.elements should be an instance of HTMLFormControlsCollection"); + +is(fieldset.elements.length, 0, "Nothing should be in fieldset.elements"); + +var oldElements = fieldset.elements; + +is(fieldset.elements, oldElements, + "fieldset.elements should always return the same object"); + +var tmpElement = document.createElement("input"); + +fieldset.appendChild(tmpElement); + +is(fieldset.elements.length, 1, + "fieldset.elements should now contain one element"); + +is(fieldset.elements[0], tmpElement, + "fieldset.elements[0] should be the input element"); + +tmpElement.name = "foo"; +is(fieldset.elements.foo, tmpElement, + "we should be able to access to an element using it's name as a property on .elements"); + +is(fieldset.elements, oldElements, + "fieldset.elements should always return the same object"); + +fieldset.removeChild(tmpElement); + +var testData = [ + [ "<input>", 1 , [ HTMLInputElement ] ], + [ "<button></button>", 1, [ HTMLButtonElement ] ], + [ "<button><input></button>", 2, [ HTMLButtonElement, HTMLInputElement ] ], + [ "<object>", 1, [ HTMLObjectElement ] ], + [ "<output></output>", 1, [ HTMLOutputElement ] ], + [ "<select></select>", 1, [ HTMLSelectElement ] ], + [ "<select><option>foo</option></select>", 1, [ HTMLSelectElement ] ], + [ "<select><option>foo</option><input></select>", 2, [ HTMLSelectElement, HTMLInputElement ] ], + [ "<textarea></textarea>", 1, [ HTMLTextAreaElement ] ], + [ "<label>foo</label>", 0 ], + [ "<progress>", 0 ], + [ "<meter>", 0 ], + [ "<keygen>", 0 ], + [ "<legend></legend>", 0 ], + [ "<legend><input></legend>", 1, [ HTMLInputElement ] ], + [ "<legend><input></legend><legend><input></legend>", 2, [ HTMLInputElement, HTMLInputElement ] ], + [ "<legend><input></legend><input>", 2, [ HTMLInputElement, HTMLInputElement ] ], + [ "<fieldset></fieldset>", 1, [ HTMLFieldSetElement ] ], + [ "<fieldset><input></fieldset>", 2, [ HTMLFieldSetElement, HTMLInputElement ] ], + [ "<fieldset><fieldset><input></fieldset></fieldset>", 3, [ HTMLFieldSetElement, HTMLFieldSetElement, HTMLInputElement ] ], + [ "<button></button><fieldset></fieldset><input><keygen><object><output></output><select></select><textarea></textarea>", 7, [ HTMLButtonElement, HTMLFieldSetElement, HTMLInputElement, HTMLObjectElement, HTMLOutputElement, HTMLSelectElement, HTMLTextAreaElement ] ], +]; + +for (var data of testData) { + fieldset.innerHTML = data[0]; + is(fieldset.elements.length, data[1], + "fieldset.elements should contain " + data[1] + " elements"); + + for (var i=0; i<data[1]; ++i) { + ok(fieldset.elements[i] instanceof data[2][i], + "fieldset.elements[" + i + "] should be instance of " + data[2][i]) + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug596350.html b/dom/html/test/test_bug596350.html new file mode 100644 index 0000000000..72e2f7ce73 --- /dev/null +++ b/dom/html/test/test_bug596350.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=596350 +--> +<head> + <title>Test for Bug 596350</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596350">Mozilla Bug 596350</a> +<p id="display"></p> +<div id="content"> + <object></object> + <object data="iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMsALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0vr4MkhoXe0rZigAAAABJRU5ErkJggg=="></object> + <object data="data:text/html,foo"></object> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 596350 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTests); + +var testData = [ +// Object 0 + [ 0, null, 0 ], + [ 0, "1", 1 ], + [ 0, "-1", -1 ], + [ 0, "0", 0 ], + [ 0, "foo", 0 ], +// Object 1 + [ 1, null, 0 ], + [ 1, "1", 1 ], +// Object 2 + [ 2, null, 0 ], + [ 2, "1", 1 ], + [ 2, "-1", -1 ], +]; + +var objects = document.getElementsByTagName("object"); + +function runTests() +{ + for (var data of testData) { + var obj = objects[data[0]]; + + if (data[1]) { + obj.setAttribute("tabindex", data[1]); + } + + is(obj.tabIndex, data[2], "tabIndex value should be " + data[2]); + + obj.removeAttribute("tabindex"); + } + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug596511.html b/dom/html/test/test_bug596511.html new file mode 100644 index 0000000000..42b93e4632 --- /dev/null +++ b/dom/html/test/test_bug596511.html @@ -0,0 +1,237 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=596511 +--> +<head> + <title>Test for Bug 596511</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + select:valid { background-color: green; } + select:invalid { background-color: red; } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=596511">Mozilla Bug 596511</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script> + +/** Test for Bug 596511 **/ + +function checkNotSufferingFromBeingMissing(element, aTodo) +{ + if (aTodo) { + ok = todo; + is = todo_is; + } + + ok(!element.validity.valueMissing, + "Element should not suffer from value missing"); + ok(element.validity.valid, "Element should be valid"); + ok(element.checkValidity(), "Element should be valid"); + + is(element.validationMessage, "", + "Validation message should be the empty string"); + + ok(element.matches(":valid"), ":valid pseudo-class should apply"); + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(0, 128, 0)", ":valid pseudo-class should apply"); + + if (aTodo) { + ok = SimpleTest.ok; + is = SimpleTest.is; + } +} + +function checkSufferingFromBeingMissing(element, aTodo) +{ + if (aTodo) { + ok = todo; + is = todo_is; + } + + ok(element.validity.valueMissing, "Element should suffer from value missing"); + ok(!element.validity.valid, "Element should not be valid"); + ok(!element.checkValidity(), "Element should not be valid"); + + is(element.validationMessage, "Please select an item in the list.", + "Validation message is wrong"); + + is(window.getComputedStyle(element).getPropertyValue('background-color'), + "rgb(255, 0, 0)", ":invalid pseudo-class should apply"); + + if (aTodo) { + ok = SimpleTest.ok; + is = SimpleTest.is; + } +} + +function checkRequiredAttribute(element) +{ + ok('required' in element, "select should have a required attribute"); + + ok(!element.required, "select required attribute should be disabled"); + is(element.getAttribute('required'), null, + "select required attribute should be disabled"); + + element.required = true; + ok(element.required, "select required attribute should be enabled"); + isnot(element.getAttribute('required'), null, + "select required attribute should be enabled"); + + element.removeAttribute('required'); + element.setAttribute('required', ''); + ok(element.required, "select required attribute should be enabled"); + isnot(element.getAttribute('required'), null, + "select required attribute should be enabled"); + + element.removeAttribute('required'); + ok(!element.required, "select required attribute should be disabled"); + is(element.getAttribute('required'), null, + "select required attribute should be disabled"); +} + +function checkRequiredAndOptionalSelectors(element) +{ + is(document.querySelector("select:optional"), element, + "select should be optional"); + is(document.querySelector("select:required"), null, + "select shouldn't be required"); + + element.required = true; + + is(document.querySelector("select:optional"), null, + "select shouldn't be optional"); + is(document.querySelector("select:required"), element, + "select should be required"); + + element.required = false; +} + +function checkInvalidWhenValueMissing(element) +{ + checkNotSufferingFromBeingMissing(select); + + element.required = true; + checkSufferingFromBeingMissing(select); + + /** + * Non-multiple and size=1. + */ + select.appendChild(new Option()); + checkSufferingFromBeingMissing(select); + + // When removing the required attribute, element should not be invalid. + element.required = false; + checkNotSufferingFromBeingMissing(select); + + element.required = true; + select.options[0].textContent = "foo"; + // TODO: having that working would require us to add a mutation observer on + // the select element. + checkNotSufferingFromBeingMissing(select, true); + + select.remove(0); + checkSufferingFromBeingMissing(select); + + select.add(new Option("foo", "foo"), null); + checkNotSufferingFromBeingMissing(select); + + select.add(new Option(), null); + checkNotSufferingFromBeingMissing(select); + + // The placeholder label can only be the first option, so a selected empty second option is valid + select.options[1].selected = true; + checkNotSufferingFromBeingMissing(select); + + select.selectedIndex = 0; + checkNotSufferingFromBeingMissing(select); + + select.add(select.options[0]); + select.selectedIndex = 0; + checkSufferingFromBeingMissing(select); + + select.remove(0); + checkNotSufferingFromBeingMissing(select); + + select.options[0].disabled = true; + // TODO: having that working would require us to add a mutation observer on + // the select element. + checkSufferingFromBeingMissing(select, true); + + select.options[0].disabled = false + select.remove(0); + checkSufferingFromBeingMissing(select); + + var option = new Option("foo", "foo"); + option.disabled = true; + select.add(option, null); + select.add(new Option("bar"), null); + option.selected = true; + checkNotSufferingFromBeingMissing(select); + + select.remove(0); + select.remove(0); + + /** + * Non-multiple and size > 1. + * Everything should be the same except moving the selection. + */ + select.multiple = false; + select.size = 4; + checkSufferingFromBeingMissing(select); + + // Setting defaultSelected to true should not make the option selected + select.add(new Option("", "", true), null); + checkSufferingFromBeingMissing(select); + select.remove(0); + + select.add(new Option("", "", true, true), null); + checkNotSufferingFromBeingMissing(select); + + select.add(new Option("foo", "foo"), null); + select.remove(0); + checkSufferingFromBeingMissing(select); + + select.options[0].selected = true; + checkNotSufferingFromBeingMissing(select); + + select.remove(0); + + /** + * Multiple, any size. + * We can select more than one element and at least needs a value. + */ + select.multiple = true; + select.size = 4; + checkSufferingFromBeingMissing(select); + + select.add(new Option("", "", true), null); + checkSufferingFromBeingMissing(select); + + select.add(new Option("", "", true), null); + checkSufferingFromBeingMissing(select); + + select.add(new Option("foo"), null); + checkSufferingFromBeingMissing(select); + + select.options[2].selected = true; + checkNotSufferingFromBeingMissing(select); +} + +var select = document.createElement("select"); +var content = document.getElementById('content'); +content.appendChild(select); + +checkRequiredAttribute(select); +checkRequiredAndOptionalSelectors(select); +checkInvalidWhenValueMissing(select); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug598643.html b/dom/html/test/test_bug598643.html new file mode 100644 index 0000000000..12f91fdec4 --- /dev/null +++ b/dom/html/test/test_bug598643.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=598643 +--> +<head> + <title>Test for Bug 598643</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=598643">Mozilla Bug 598643</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 598643 **/ + +function createFileWithData(fileName, fileData) +{ + return new File([new Blob([fileData], { type: "text/plain" })], fileName); +} + +function testFileControl(aElement) +{ + aElement.type = 'file'; + + var file = createFileWithData("file_bug598643", "file content"); + SpecialPowers.wrap(aElement).mozSetFileArray([file]); + + ok(aElement.validity.valid, "the file control should be valid"); + ok(!aElement.validity.tooLong, + "the file control shouldn't suffer from being too long"); +} + +var types = [ + // These types can be too long. + [ "text", "email", "password", "url", "search", "tel" ], + // These types can't be too long. + [ "radio", "checkbox", "submit", "button", "reset", "image", "hidden", + 'number', 'range', 'date', 'time', 'color', 'month', 'week', + 'datetime-local' ], +]; + +var input = document.createElement("input"); +input.maxLength = 1; +input.value = "foo"; + +// Too long types. +for (type of types[0]) { + input.type = type + if (type == 'email') { + input.value = "foo@bar.com"; + } else if (type == 'url') { + input.value = 'http://foo.org'; + } + + todo(!input.validity.valid, "the element should be invalid [type=" + type + "]"); + todo(input.validity.tooLong, + "the element should suffer from being too long [type=" + type + "]"); + + if (type == 'email' || type == 'url') { + input.value = 'foo'; + } +} + +// Not too long types. +for (type of types[1]) { + input.type = type + ok(input.validity.valid, "the element should be valid [type=" + type + "]"); + ok(!input.validity.tooLong, + "the element shouldn't suffer from being too long [type=" + type + "]"); +} + +testFileControl(input); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug598833-1.html b/dom/html/test/test_bug598833-1.html new file mode 100644 index 0000000000..5fbbe221e5 --- /dev/null +++ b/dom/html/test/test_bug598833-1.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=598833 +--> +<head> + <title>Test for Bug 598833</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=598833">Mozilla Bug 598833</a> +<p id="display"> + <fieldset disabled> + <select id="s" multiple required> + <option>one</option> + </select> + </fieldset> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 598833 **/ +var s = $("s"); +is(s.matches(":invalid"), false, "Disabled select should not be invalid"); +is(s.matches(":valid"), false, "Disabled select should not be valid"); +var p = s.parentNode; +p.removeChild(s); +is(s.matches(":invalid"), true, + "Required valueless select not in tree should be invalid"); +is(s.matches(":valid"), false, + "Required valueless select not in tree should not be valid"); +p.appendChild(s); +p.disabled = false; +is(s.matches(":invalid"), true, + "Required valueless select should be invalid"); +is(s.matches(":valid"), false, + "Required valueless select should not be valid"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug600155.html b/dom/html/test/test_bug600155.html new file mode 100644 index 0000000000..893dfe31f1 --- /dev/null +++ b/dom/html/test/test_bug600155.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=600155 +--> +<head> + <title>Test for Bug 600155</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=600155">Mozilla Bug 600155</a> +<p id="display"></p> +<div id='content' style='display:none;'> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 600155 **/ + +var subjectForConstraintValidation = [ "input", "select", "textarea" ]; +var content = document.getElementById('content'); + +for (var eName of subjectForConstraintValidation) { + var e = document.createElement(eName); + content.appendChild(e); + e.setCustomValidity("foo"); + if ("required" in e) { + e.required = true; + } else { + e.setCustomValidity("bar"); + } + + // At this point, the element is invalid. + is(e.validationMessage, "foo", + "the validation message should be the author one"); + + content.removeChild(e); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug601030.html b/dom/html/test/test_bug601030.html new file mode 100644 index 0000000000..f9a277f471 --- /dev/null +++ b/dom/html/test/test_bug601030.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=601030 +--> +<head> + <title>Test for Bug 601030</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=601030">Mozilla Bug 601030</a> +<p id="display"></p> +<div id="content"> + <iframe src="data:text/html,<input autofocus>"></iframe> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 601030 **/ + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var f = document.createElement("iframe"); + var content = document.getElementById('content'); + + f.addEventListener("load", function() { + SimpleTest.executeSoon(function() { + isnot(document.activeElement, f, + "autofocus should not work when another frame is inserted in the document"); + + content.removeChild(f); + content.removeChild(document.getElementsByTagName('iframe')[0]); + f = document.createElement('iframe'); + f.addEventListener("load", function() { + isnot(document.activeElement, f, + "autofocus should not work in a frame if the top document is already loaded"); + SimpleTest.finish(); + }, {once: true}); + f.src = "data:text/html,<input autofocus>"; + content.appendChild(f); + }); + }, {once: true}); + + f.src = "data:text/html,<input autofocus>"; + content.appendChild(f); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug605124-1.html b/dom/html/test/test_bug605124-1.html new file mode 100644 index 0000000000..d252987ee9 --- /dev/null +++ b/dom/html/test/test_bug605124-1.html @@ -0,0 +1,98 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=605124 +--> +<head> + <title>Test for Bug 605124</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=605124">Mozilla Bug 605124</a> +<p id="display"></p> +<div id="content"> + <form> + <textarea required></textarea> + <input required> + <select required></select> + <button type='submit'></button> + </form> + + <table> + <form> + <tr> + <textarea required></textarea> + <input required> + <select required></select> + <button type='submit'></button> + </tr> + </form> + </table> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 605124 **/ + +function checkPseudoClass(aElement, aExpected) +{ + is(aElement.matches(":user-invalid"), aExpected, + "matches(':user-invalid') should return " + aExpected + " for " + aElement); +} + +var content = document.getElementById('content'); +var textarea = document.getElementsByTagName('textarea')[0]; +var input = document.getElementsByTagName('input')[0]; +var select = document.getElementsByTagName('select')[0]; +var button = document.getElementsByTagName('button')[0]; +var form = document.forms[0]; + +checkPseudoClass(textarea, false); +checkPseudoClass(input, false); +checkPseudoClass(select, false); + +// Try to submit. +button.click(); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); + +// No longer in the form. +content.appendChild(textarea); +content.appendChild(input); +content.appendChild(select); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); + +// Back in the form. +form.appendChild(textarea); +form.appendChild(input); +form.appendChild(select); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); + +/* Case when elements get orphaned. */ +var textarea = document.getElementsByTagName('textarea')[1]; +var input = document.getElementsByTagName('input')[1]; +var select = document.getElementsByTagName('select')[1]; +var button = document.getElementsByTagName('button')[1]; +var form = document.forms[1]; + +// Try to submit. +button.click(); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); + +// Remove the form. +document.getElementsByTagName('table')[0].removeChild(form); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug605124-2.html b/dom/html/test/test_bug605124-2.html new file mode 100644 index 0000000000..07e3e5afcd --- /dev/null +++ b/dom/html/test/test_bug605124-2.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=605124 +--> +<head> + <title>Test for Bug 605124</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=605124">Mozilla Bug 605124</a> +<p id="display"></p> +<div id="content"> + <input required> + <textarea required></textarea> + <select required> + <option value="">foo</option> + <option>bar</option> + </select> + <select multiple required> + <option value="">foo</option> + <option>bar</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 605124 **/ + +function checkPseudoClass(aElement, aExpected) +{ + is(aElement.matches(":-moz-ui-invalid"), aExpected, + "matches(':-moz-ui-invalid') should return " + aExpected + " for " + aElement); +} + +function checkElement(aElement) +{ + checkPseudoClass(aElement, false); + + // Focusing while :-moz-ui-invalid doesn't apply, + // the pseudo-class should not apply while typing. + aElement.focus(); + checkPseudoClass(aElement, false); + // with keys + sendString("f"); + checkPseudoClass(aElement, false); + synthesizeKey("KEY_Backspace"); + checkPseudoClass(aElement, false); + // with .value + aElement.value = 'f'; + checkPseudoClass(aElement, false); + aElement.value = ''; + checkPseudoClass(aElement, false); + + aElement.blur(); + checkPseudoClass(aElement, true); + + // Focusing while :-moz-ui-invalid applies, + // the pseudo-class should apply while typing if appropriate. + aElement.focus(); + checkPseudoClass(aElement, true); + // with keys + sendString("f"); + checkPseudoClass(aElement, false); + synthesizeKey("KEY_Backspace"); + checkPseudoClass(aElement, true); + // with .value + aElement.value = 'f'; + checkPseudoClass(aElement, false); + aElement.value = ''; + checkPseudoClass(aElement, true); +} + +function checkSelectElement(aElement) +{ + checkPseudoClass(aElement, false); + + // Focusing while :-moz-ui-invalid doesn't apply, + // the pseudo-class should not apply while changing selection. + aElement.focus(); + checkPseudoClass(aElement, false); + + aElement.selectedIndex = 1; + checkPseudoClass(aElement, false); + aElement.selectedIndex = 0; + checkPseudoClass(aElement, false); + + aElement.blur(); + checkPseudoClass(aElement, false); + + // Focusing while :-moz-ui-invalid applies, + // the pseudo-class should apply while changing selection if appropriate. + aElement.focus(); + checkPseudoClass(aElement, false); + + aElement.selectedIndex = 1; + checkPseudoClass(aElement, false); + aElement.selectedIndex = 0; + checkPseudoClass(aElement, false); + aElement.selectedIndex = 1; + checkPseudoClass(aElement, false); + + aElement.blur(); + checkPseudoClass(aElement, false); +} + +checkElement(document.getElementsByTagName('input')[0]); +checkElement(document.getElementsByTagName('textarea')[0]); +checkSelectElement(document.getElementsByTagName('select')[0]); +checkSelectElement(document.getElementsByTagName('select')[1]); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug605125-1.html b/dom/html/test/test_bug605125-1.html new file mode 100644 index 0000000000..8b49b9bbc1 --- /dev/null +++ b/dom/html/test/test_bug605125-1.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=605125 +--> +<head> + <title>Test for Bug 605125</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=605125">Mozilla Bug 605125</a> +<p id="display"></p> +<div id="content"> + <form id='f1'> + <textarea></textarea> + <input> + <button type='submit'></button> + <select></select> + </form> + + <table> + <form id='f2'> + <tr> + <textarea></textarea> + <input> + <button type='submit'></button> + <select></select> + </tr> + </form> + </table> + <input form='f1' required> + <input form='f2' required> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 605125 **/ + +/** + * NOTE: this test is very similar to 605124-1.html. + */ + +function checkPseudoClass(aElement, aExpected) +{ + is(aElement.matches(":-moz-ui-valid"), aExpected, + "matches(':-moz-ui-valid') should return " + aExpected + " for " + aElement); +} + +var content = document.getElementById('content'); +var textarea = document.getElementsByTagName('textarea')[0]; +var input = document.getElementsByTagName('input')[0]; +var button = document.getElementsByTagName('button')[0]; +var select = document.getElementsByTagName('select')[0]; +var form = document.forms[0]; + +checkPseudoClass(textarea, false); +checkPseudoClass(input, false); +checkPseudoClass(select, false); + +// Try to submit. +button.click(); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); + +// No longer in the form. +content.appendChild(textarea); +content.appendChild(input); +content.appendChild(select); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); + +// Back in the form. +form.appendChild(textarea); +form.appendChild(input); +form.appendChild(select); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); + +/* Case when elements get orphaned. */ +var textarea = document.getElementsByTagName('textarea')[1]; +var input = document.getElementsByTagName('input')[1]; +var button = document.getElementsByTagName('button')[1]; +var select = document.getElementsByTagName('select')[1]; +var form = document.forms[1]; + +// Try to submit. +button.click(); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); + +// Remove the form. +document.getElementsByTagName('table')[0].removeChild(form); +checkPseudoClass(textarea, true); +checkPseudoClass(input, true); +checkPseudoClass(select, true); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug605125-2.html b/dom/html/test/test_bug605125-2.html new file mode 100644 index 0000000000..ea1195e189 --- /dev/null +++ b/dom/html/test/test_bug605125-2.html @@ -0,0 +1,149 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=605125 +--> +<head> + <title>Test for Bug 605125</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=605125">Mozilla Bug 605125</a> +<p id="display"></p> +<div id="content"> + <input> + <textarea></textarea> + <select> + <option value="">foo</option> + <option>bar</option> + </select> + <select multiple> + <option value="">foo</option> + <option>bar</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 605125 **/ + +function checkPseudoClass(aElement, aExpected) +{ + is(aElement.matches(":user-valid"), aExpected, + "matches(':user-valid') should return " + aExpected + " for " + aElement.outerHTML); +} + +function checkElement(aElement) +{ + checkPseudoClass(aElement, false); + + // Focusing while :user-valid doesn't apply, + // the pseudo-class should not apply while typing. + aElement.focus(); + checkPseudoClass(aElement, false); + // with keys + sendString("f"); + checkPseudoClass(aElement, false); + synthesizeKey("KEY_Backspace"); + checkPseudoClass(aElement, false); + // with .value + aElement.value = 'f'; + checkPseudoClass(aElement, false); + aElement.value = ''; + checkPseudoClass(aElement, false); + + aElement.blur(); + checkPseudoClass(aElement, true); + + // Focusing while :user-valid applies, + // the pseudo-class should apply while typing if appropriate. + aElement.focus(); + checkPseudoClass(aElement, true); + // with keys + sendString("f"); + checkPseudoClass(aElement, true); + synthesizeKey("KEY_Backspace"); + checkPseudoClass(aElement, true); + // with .value + aElement.value = 'f'; + checkPseudoClass(aElement, true); + aElement.value = ''; + checkPseudoClass(aElement, true); + + aElement.blur(); + aElement.required = true; + checkPseudoClass(aElement, false); + + // Focusing while :user-invalid applies, + // the pseudo-class should apply while typing if appropriate. + aElement.focus(); + checkPseudoClass(aElement, false); + // with keys + sendString("f"); + checkPseudoClass(aElement, true); + synthesizeKey("KEY_Backspace"); + checkPseudoClass(aElement, false); + // with .value + aElement.value = 'f'; + checkPseudoClass(aElement, true); + aElement.value = ''; + checkPseudoClass(aElement, false); +} + +function checkSelectElement(aElement) +{ + checkPseudoClass(aElement, false); + + if (!aElement.multiple && navigator.platform.startsWith("Mac")) { + // Arrow key on macOS opens the popup. + return; + } + + // Focusing while :user-valid doesn't apply, + // the pseudo-class should not apply while changing selection. + aElement.focus(); + checkPseudoClass(aElement, false); + + synthesizeKey("KEY_ArrowDown"); + checkPseudoClass(aElement, true); + + // Focusing while :user-valid applies, + // the pseudo-class should apply while changing selection if appropriate. + aElement.focus(); + checkPseudoClass(aElement, true); + + aElement.selectedIndex = 1; + checkPseudoClass(aElement, true); + aElement.selectedIndex = 0; + checkPseudoClass(aElement, true); + + aElement.blur(); + aElement.required = true; + // select set with multiple is only invalid if no option is selected + if (aElement.multiple) { + aElement.selectedIndex = -1; + } + checkPseudoClass(aElement, false); + + // Focusing while :user-invalid applies, + // the pseudo-class should apply while changing selection if appropriate. + aElement.focus(); + checkPseudoClass(aElement, false); + + synthesizeKey("KEY_ArrowDown"); + checkPseudoClass(aElement, true); + aElement.selectedIndex = 0; + checkPseudoClass(aElement, aElement.multiple); +} + +checkElement(document.getElementsByTagName('input')[0]); +checkElement(document.getElementsByTagName('textarea')[0]); +checkSelectElement(document.getElementsByTagName('select')[0]); +checkSelectElement(document.getElementsByTagName('select')[1]); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug606817.html b/dom/html/test/test_bug606817.html new file mode 100644 index 0000000000..4564753a93 --- /dev/null +++ b/dom/html/test/test_bug606817.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=606817 +--> +<head> + <title>Test for Bug 606817</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=606817">Mozilla Bug 606817</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 606817 **/ + +var messageMaxLength = 256; + +function checkMessage(aInput, aMsg, aWithTerminalPeriod) +{ + ok(aInput.validationMessage != aMsg, + "Original content-defined message should have been truncate"); + is(aInput.validationMessage.length - aInput.validationMessage.indexOf("_42_"), + aWithTerminalPeriod ? messageMaxLength+1 : messageMaxLength, + "validation message should be 256 characters length"); +} + +var input = document.createElement("input"); + +var msg = ""; +for (var i=0; i<75; ++i) { + msg += "_42_"; +} +// msg is now 300 chars long + +// Testing with setCustomValidity(). +input.setCustomValidity(msg); +checkMessage(input, msg, false); + +// Cleaning. +input.setCustomValidity(""); + +// Testing with pattern and titl. +input.pattern = "[0-9]*"; +input.value = "foo"; +input.title = msg; +checkMessage(input, msg, true); + +// Cleaning. +input.removeAttribute("pattern"); +input.removeAttribute("title"); +input.value = ""; + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug607145.html b/dom/html/test/test_bug607145.html new file mode 100644 index 0000000000..28ab3fe8ba --- /dev/null +++ b/dom/html/test/test_bug607145.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=607145 +--> +<head> + <title>Test for Bug 607145</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=607145">Mozilla Bug 607145</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +var xoriginParams; +/** Test for Bug 607145 **/ + +/** + * This is not really reflecting an URL as the HTML5 specs want to. + * It's how .action is reflected in Gecko (might change later). + * + * If this changes, add reflectURL for "formAction" in + * dom/html/test/forms/test_input_attributes_reflection.html and + * "action" in + * dom/html/test/forms/test_form_attributes_reflection.html + */ +function reflectURL(aElement, aAttr) +{ + var idl = aAttr; + var attr = aAttr.toLowerCase(); + var elmtName = aElement.tagName.toLowerCase(); + + var url = location.href.replace(/\?.*/, ""); + var dir = url.replace(/test_bug607145.html[^\/]*$/, ""); + var parentDir = dir.replace(/test\/$/, ""); + ok(idl in aElement, idl + " should be available in " + elmtName); + + // Default values. + is(aElement[idl].split("?")[0], url, "." + idl + " default value should be the document's URL"); + is(aElement.getAttribute(attr), null, + "@" + attr + " default value should be null"); + + var values = [ + /* value to set, resolved value */ + [ "foo.html", dir + "foo.html" ], + [ "data:text/html,<html></html>", "data:text/html,<html></html>" ], + [ "http://example.org/", "http://example.org/" ], + [ "//example.org/", "http://example.org/" ], + [ "?foo=bar", url + "?foo=bar" ], + [ "#foo", url + "#foo" ], + [ "", url ], + [ " ", url ], + [ "../", parentDir ], + [ "...", dir + "..." ], + // invalid URL + [ "http://a b/", "http://a b/" ], // TODO: doesn't follow the specs, should be "". + ]; + + for (var value of values) { + aElement[idl] = value[0]; + is(aElement[idl].replace(xoriginParams, ""), value[1], "." + idl + " value should be " + value[1]); + is(aElement.getAttribute(attr), value[0], + "@" + attr + " value should be " + value[0]); + } + + for (var value of values) { + aElement.setAttribute(attr, value[0]); + is(aElement[idl].replace(xoriginParams, ""), value[1], "." + idl + " value should be " + value[1]); + is(aElement.getAttribute(attr), value[0], + "@" + attr + " value should be " + value[0]); + } +} + + +xoriginParams = window.location.search; + +reflectURL(document.createElement("form"), "action"); +reflectURL(document.createElement("input"), "formAction"); +reflectURL(document.createElement("button"), "formAction"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug610212.html b/dom/html/test/test_bug610212.html new file mode 100644 index 0000000000..69838f7e4d --- /dev/null +++ b/dom/html/test/test_bug610212.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=610212 +--> +<head> + <title>Test for Bug 610212</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=610212">Mozilla Bug 610212</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 610212 **/ + +var canvas = document.createElement('canvas'); + +reflectUnsignedInt({ + element: canvas, + attribute: "width", + nonZero: false, + defaultValue: 300, +}); + +reflectUnsignedInt({ + element: canvas, + attribute: "height", + nonZero: false, + defaultValue: 150, +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug610687.html b/dom/html/test/test_bug610687.html new file mode 100644 index 0000000000..fd6950cce4 --- /dev/null +++ b/dom/html/test/test_bug610687.html @@ -0,0 +1,195 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=610687 +--> +<head> + <title>Test for Bug 610687</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=610687">Mozilla Bug 610687</a> +<p id="display"></p> +<div id="content"> + <form> + <input type='radio' name='a'> + <input type='radio' name='a'> + <input type='radio' name='b'> + </form> + <input type='radio' name='a'> + <input type='radio' name='a'> + <input type='radio' name='b'> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 610687 **/ + +function checkPseudoClasses(aElement, aValid, aValidUI, aInvalidUI) +{ + if (aValid) { + ok(aElement.matches(":valid"), ":valid should apply"); + } else { + ok(aElement.matches(":invalid"), ":invalid should apply"); + } + + is(aElement.matches(":user-valid"), aValidUI, + aValid ? ":user-valid should apply" : ":user-valid should not apply"); + + is(aElement.matches(":user-invalid"), aInvalidUI, + aInvalidUI ? ":user-invalid should apply" : ":user-invalid should not apply"); + + if (aInvalidUI && (aValid || aValidUI)) { + ok(false, ":invalid can't apply with :valid or :user-valid"); + } +} + +/** + * r1 and r2 should be in the same group. + * r3 should be in another group. + * form can be null. + */ +function checkRadios(r1, r2, r3, form) +{ + // Default state. + checkPseudoClasses(r1, true, false, false); + checkPseudoClasses(r2, true, false, false); + checkPseudoClasses(r3, true, false, false); + + // Suffering from being missing (without ui-invalid). + r1.required = true; + checkPseudoClasses(r1, false, false, false); + checkPseudoClasses(r2, false, false, false); + checkPseudoClasses(r3, true, false, false); + + // Do not suffer from being missing (with ui-valid). + r1.click(); + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, true, false, false); + checkPseudoClasses(r3, true, false, false); + + // Do not suffer from being missing (with ui-valid). + r1.checked = false; + r1.required = false; + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, true, false, false); + checkPseudoClasses(r3, true, false, false); + + // Suffering from being missing (with ui-invalid) with required set on one radio + // and the checked state changed on another. + r1.required = true; + r2.checked = false; + checkPseudoClasses(r1, false, false, true); + checkPseudoClasses(r2, false, false, false); + checkPseudoClasses(r3, true, false, false); + + // Do not suffer from being missing (with ui-valid) by checking the radio which + // hasn't the required attribute. + r2.checked = true; + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, true, false, false); + checkPseudoClasses(r3, true, false, false); + + // .setCustomValidity() should not affect the entire group. + r1.checked = false; r2.checked = false; r3.checked = false; + r1.required = false; + r1.setCustomValidity('foo'); + checkPseudoClasses(r1, false, false, true); + checkPseudoClasses(r2, true, false, false); + checkPseudoClasses(r3, true, false, false); + + r1.setCustomValidity(''); + r2.setCustomValidity('foo'); + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, false, false, false); + checkPseudoClasses(r3, true, false, false); + + r2.setCustomValidity(''); + r3.setCustomValidity('foo'); + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, true, false, false); + checkPseudoClasses(r3, false, false, false); + + // Removing the radio with the required attribute should make the group valid. + r1.setCustomValidity(''); + r2.setCustomValidity(''); + r1.required = false; + r2.required = true; + r1.checked = r2.checked = false; + checkPseudoClasses(r1, false, false, true); + checkPseudoClasses(r2, false, false, false); + + var p = r2.parentNode; + p.removeChild(r2); + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, false, false, false); + + p.appendChild(r2); + checkPseudoClasses(r1, false, false, true); + checkPseudoClasses(r2, false, false, false); + + // Adding a radio element to an invalid group should make it invalid. + p.removeChild(r1); + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, false, false, false); + + p.appendChild(r1); + checkPseudoClasses(r1, false, false, true); + checkPseudoClasses(r2, false, false, false); + + // Adding a checked radio element to an invalid group should make it valid. + p.removeChild(r1); + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, false, false, false); + + r1.checked = true; + p.appendChild(r1); + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, true, false, false); + r1.checked = false; + + // Adding an invalid radio element by changing the name attribute. + r2.name = 'c'; + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, false, false, false); + + r2.name = 'a'; + checkPseudoClasses(r1, false, false, true); + checkPseudoClasses(r2, false, false, false); + + // Adding an element to an invalid radio group by changing the name attribute. + r1.name = 'c'; + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, false, false, false); + + r1.name = 'a'; + checkPseudoClasses(r1, false, false, true); + checkPseudoClasses(r2, false, false, false); + + // Adding a checked element to an invalid radio group with the name attribute. + r1.name = 'c'; + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, false, false, false); + + r1.checked = true; + r1.name = 'a'; + checkPseudoClasses(r1, true, true, false); + checkPseudoClasses(r2, true, false, false); + r1.checked = false; +} + +var r1 = document.getElementsByTagName('input')[0]; +var r2 = document.getElementsByTagName('input')[1]; +var r3 = document.getElementsByTagName('input')[2]; +checkRadios(r1, r2, r3); + +r1 = document.getElementsByTagName('input')[3]; +r2 = document.getElementsByTagName('input')[4]; +r3 = document.getElementsByTagName('input')[5]; +checkRadios(r1, r2, r3); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug611189.html b/dom/html/test/test_bug611189.html new file mode 100644 index 0000000000..d798fd4393 --- /dev/null +++ b/dom/html/test/test_bug611189.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=611189 +--> +<head> + <title>Test for Bug 611189</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <script src="/tests/SimpleTest/WindowSnapshot.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=611189">Mozilla Bug 611189</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 611189 **/ +SimpleTest.waitForExplicitFinish(); +addLoadEvent(async function() { + var i = document.createElement("input"); + var b = document.getElementById("content"); + b.appendChild(i); + b.clientWidth; // bind to frame + i.focus(); // initialize editor + var before = await snapshotWindow(window, true); + i.value = "L"; // set the value + i.style.display = "none"; + b.clientWidth; // unbind from frame + i.value = ""; // set the value without a frame + i.style.display = ""; + b.clientWidth; // rebind to frame + is(i.value, "", "Input's value should be correctly updated"); + var after = await snapshotWindow(window, true); + ok(compareSnapshots(before, after, true), "The correct value should be rendered inside the control"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug612730.html b/dom/html/test/test_bug612730.html new file mode 100644 index 0000000000..ccdfc1241d --- /dev/null +++ b/dom/html/test/test_bug612730.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=612730 +--> +<head> + <title>Test for Bug 612730</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=612730">Mozilla Bug 612730</a> +<p id="display"></p> +<div id="content"> + <select multiple required> + <option value="">foo</option> + <option value="">bar</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 612730 **/ + +SimpleTest.waitForExplicitFinish(); + +function runTest() +{ + var select = document.getElementsByTagName('select')[0]; + + select.addEventListener("focus", function() { + isnot(select.selectedIndex, -1, "Something should have been selected"); + + ok(!select.matches(":-moz-ui-invalid"), + ":-moz-ui-invalid should not apply"); + ok(!select.matches(":-moz-ui-valid"), + ":-moz-ui-valid should not apply"); + + SimpleTest.finish(); + }, {once: true}); + + synthesizeMouse(select, 5, 5, {}); +} + +SimpleTest.waitForFocus(runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug613019.html b/dom/html/test/test_bug613019.html new file mode 100644 index 0000000000..3f96ce2542 --- /dev/null +++ b/dom/html/test/test_bug613019.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=613019 +--> +<head> + <title>Test for Bug 613019</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=613019">Mozilla Bug 613019</a> +<div id="content"> + <input type="text" maxlength="2" style="width:200px" value="Test"> + <textarea maxlength="2" style="width:200px">Test</textarea> + <input type="text" minlength="6" style="width:200px" value="Test"> + <textarea minlength="6" style="width:200px">Test</textarea> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 613019 **/ + +function testInteractivityOfMaxLength(elem) { + // verify that user interactivity is necessary for validity state to apply. + is(elem.value, "Test", "Element has incorrect starting value."); + is(elem.validity.tooLong, false, "Element should not be tooLong."); + + elem.setSelectionRange(elem.value.length, elem.value.length) + elem.focus(); + + synthesizeKey("KEY_Backspace"); + is(elem.value, "Tes", "Element value was not changed correctly."); + is(elem.validity.tooLong, true, "Element should still be tooLong."); + + synthesizeKey("KEY_Backspace"); + is(elem.value, "Te", "Element value was not changed correctly."); + is(elem.validity.tooLong, false, "Element should no longer be tooLong."); + + elem.value = "Test"; + is(elem.validity.tooLong, false, + "Element should not be tooLong after non-interactive value change."); +} + +function testInteractivityOfMinLength(elem) { + // verify that user interactivity is necessary for validity state to apply. + is(elem.value, "Test", "Element has incorrect starting value."); + is(elem.validity.tooLong, false, "Element should not be tooShort."); + + elem.setSelectionRange(elem.value.length, elem.value.length) + elem.focus(); + + sendString("e"); + is(elem.value, "Teste", "Element value was not changed correctly."); + is(elem.validity.tooShort, true, "Element should still be tooShort."); + + sendString("d"); + is(elem.value, "Tested", "Element value was not changed correctly."); + is(elem.validity.tooShort, false, "Element should no longer be tooShort."); + + elem.value = "Test"; + is(elem.validity.tooShort, false, + "Element should not be tooShort after non-interactive value change."); +} + +function test() { + window.getSelection().removeAllRanges(); + testInteractivityOfMaxLength(document.querySelector("input[type=text][maxlength]")); + testInteractivityOfMaxLength(document.querySelector("textarea[maxlength]")); + testInteractivityOfMinLength(document.querySelector("input[type=text][minlength]")); + testInteractivityOfMinLength(document.querySelector("textarea[minlength]")); + SimpleTest.finish(); +} + +window.onload = function() { + SimpleTest.waitForExplicitFinish(); + setTimeout(test, 0); +}; + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug613113.html b/dom/html/test/test_bug613113.html new file mode 100644 index 0000000000..3308246af0 --- /dev/null +++ b/dom/html/test/test_bug613113.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=613113 +--> +<head> + <title>Test for Bug 613113</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=613113">Mozilla Bug 613113</a> +<p id="display"></p> +<div id="content" style="display: none"> + <iframe name='f'></iframe> + <form target='f' action="data:text/html,"> + <output></output> + <button></button> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 613113 **/ + +SimpleTest.waitForExplicitFinish(); + +var invalidEvent = false; + +var form = document.forms[0]; +var button = document.getElementsByTagName('button')[0]; +var output = document.getElementsByTagName('output')[0]; + +output.addEventListener("invalid", function() { + ok(false, "invalid event should have been send"); +}); + +form.addEventListener("submit", function() { + ok(true, "submit has been caught"); + setTimeout(function() { + SimpleTest.finish(); + }, 0); +}); + +output.setCustomValidity("foo"); + +button.click(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug613722.html b/dom/html/test/test_bug613722.html new file mode 100644 index 0000000000..4fbead4507 --- /dev/null +++ b/dom/html/test/test_bug613722.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=613722 +--> +<head> + <title>Test for Bug 613722</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=613722">Mozilla Bug 613722</a> +<p id="display"></p> +<div id="content"> + <embed src="test_plugin.tst" hidden> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 613722 **/ + +var rect = document.getElementsByTagName('embed')[0].getBoundingClientRect(); + +var hasFrame = rect.left != 0 || rect.right != 0 || rect.top != 0 || + rect.bottom != 0; + +ok(hasFrame, "embed should have a frame with hidden set"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug613979.html b/dom/html/test/test_bug613979.html new file mode 100644 index 0000000000..40921d8064 --- /dev/null +++ b/dom/html/test/test_bug613979.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=613979 +--> +<head> + <title>Test for Bug 613979</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=613979">Mozilla Bug 613979</a> +<p id="display"></p> +<div id="content"> + <input required> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 613979 **/ + +var testNum = 0; +var input = document.getElementsByTagName('input')[0]; + +input.addEventListener("input", function() { + if (testNum == 0) { + ok(input.validity.valid, "input should be valid"); + testNum++; + SimpleTest.executeSoon(function() { + synthesizeKey("KEY_Backspace"); + }); + } else if (testNum == 1) { + ok(!input.validity.valid, "input should not be valid"); + input.removeEventListener("input", arguments.callee); + SimpleTest.finish(); + } +}); + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(function() { + input.focus(); + sendString("a"); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug615595.html b/dom/html/test/test_bug615595.html Binary files differnew file mode 100644 index 0000000000..4e3d002498 --- /dev/null +++ b/dom/html/test/test_bug615595.html diff --git a/dom/html/test/test_bug615833.html b/dom/html/test/test_bug615833.html new file mode 100644 index 0000000000..530603daee --- /dev/null +++ b/dom/html/test/test_bug615833.html @@ -0,0 +1,141 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=615697 +--> +<head> + <title>Test for Bug 615697</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=615697">Mozilla Bug 615697</a> +<p id="display"></p> +<div id="content"> + <input> + <textarea></textarea> + <input type='radio'> + <input type='checkbox'> + <select> + <option>foo</option> + <option>bar</option> + </select> + <select multiple size='1'> + <option>tulip</option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 615697 **/ + +/** + * This test is making all elements trigger 'change' event. + * You should read the test from bottom to top: + * events are registered from the last one to the first one. + * + * Sometimes, elements are focused before a click. This might sound useless + * but it guarantees to have the element visible before simulating the click. + */ + +var input = document.getElementsByTagName('input')[0]; +var textarea = document.getElementsByTagName('textarea')[0]; +var radio = document.getElementsByTagName('input')[1]; +var checkbox= document.getElementsByTagName('input')[2]; +var select = document.getElementsByTagName('select')[0]; +var selectMultiple = document.getElementsByTagName('select')[1]; + +function checkChangeEvent(aEvent) +{ + ok(aEvent.bubbles, "change event should bubble"); + ok(!aEvent.cancelable, "change event shouldn't be cancelable"); +} + +selectMultiple.addEventListener("change", function(aEvent) { + checkChangeEvent(aEvent); + SimpleTest.finish(); +}, {once: true}); + +selectMultiple.addEventListener("focus", function() { + SimpleTest.executeSoon(function () { + synthesizeMouseAtCenter(selectMultiple, {}); + }); +}, {once: true}); + +select.addEventListener("change", function(aEvent) { + checkChangeEvent(aEvent); + selectMultiple.focus(); +}, {once: true}); + +select.addEventListener("keyup", function() { + select.blur(); +}, {once: true}); + +select.addEventListener("focus", function() { + SimpleTest.executeSoon(function () { + synthesizeKey("KEY_ArrowDown"); + }); +}, {once: true}); + +checkbox.addEventListener("change", function(aEvent) { + checkChangeEvent(aEvent); + select.focus(); +}, {once: true}); + +checkbox.addEventListener("focus", function() { + SimpleTest.executeSoon(function () { + synthesizeMouseAtCenter(checkbox, {}); + }); +}, {once: true}); + +radio.addEventListener("change", function(aEvent) { + checkChangeEvent(aEvent); + checkbox.focus(); +}, {once: true}); + +radio.addEventListener("focus", function() { + SimpleTest.executeSoon(function () { + synthesizeMouseAtCenter(radio, {}); + }); +}, {once: true}); + +textarea.addEventListener("change", function(aEvent) { + checkChangeEvent(aEvent); + radio.focus(); +}, {once: true}); + +textarea.addEventListener("input", function() { + textarea.blur(); +}, {once: true}); + +textarea.addEventListener("focus", function() { + SimpleTest.executeSoon(function () { + sendString("f"); + }); +}, {once: true}); + +input.addEventListener("change", function(aEvent) { + checkChangeEvent(aEvent); + textarea.focus(); +}, {once: true}); + +input.addEventListener("input", function() { + input.blur(); +}, {once: true}); + +input.addEventListener("focus", function() { + SimpleTest.executeSoon(function () { + sendString("f"); + }); +}, {once: true}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + input.focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug618948.html b/dom/html/test/test_bug618948.html new file mode 100644 index 0000000000..04a9347261 --- /dev/null +++ b/dom/html/test/test_bug618948.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=618948 +--> +<head> + <title>Test for Bug 618948</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=618948">Mozilla Bug 618948</a> +<p id="display"></p> +<div id="content"> + <form> + <input type='email' id='i'> + <button>submit</button> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 618948 **/ + +var events = ["focus", "input", "change", "invalid" ]; + +var handled = ({}); + +function eventHandler(event) +{ + dump("\n" + event.type + "\n"); + handled[event.type] = true; +} + +function beginTest() +{ + for (var e of events) { + handled[e] = false; + } + + i.focus(); +} + +function endTest() +{ + for (var e of events) { + ok(handled[e], "on" + e + " should have been called"); + } + + SimpleTest.finish(); +} + +var i = document.getElementsByTagName('input')[0]; +var b = document.getElementsByTagName('button')[0]; + +i.onfocus = function(event) { + eventHandler(event); + sendString("f"); + i.onfocus = null; +}; + +i.oninput = function(event) { + eventHandler(event); + b.focus(); + i.oninput = null; +}; + +i.onchange = function(event) { + eventHandler(event); + i.onchange = null; + synthesizeMouseAtCenter(b, {}); +}; + +i.oninvalid = function(event) { + eventHandler(event); + i.oninvalid = null; + endTest(); +}; + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(beginTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug619278.html b/dom/html/test/test_bug619278.html new file mode 100644 index 0000000000..56f704c037 --- /dev/null +++ b/dom/html/test/test_bug619278.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=619278 +--> +<head> + <title>Test for Bug 619278</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=619278">Mozilla Bug 619278</a> +<p id="display"></p> +<div id="content"> + <form> + <input required><button>submit</button> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 619278 **/ + +function doElementMatchesSelector(aElement, aSelector) +{ + ok(aElement.matches(aSelector), + aSelector + " should match for " + aElement); +} + +var e = document.forms[0].elements[0]; + +e.addEventListener("invalid", function(event) { + e.addEventListener("invalid", arguments.callee); + + SimpleTest.executeSoon(function() { + doElementMatchesSelector(e, ":-moz-ui-invalid"); + SimpleTest.finish(); + }); +}); + +e.addEventListener("focus", function() { + SimpleTest.executeSoon(function() { + synthesizeKey("KEY_Enter"); + }); +}, {once: true}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + e.focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug622597.html b/dom/html/test/test_bug622597.html new file mode 100644 index 0000000000..ead4887a1d --- /dev/null +++ b/dom/html/test/test_bug622597.html @@ -0,0 +1,105 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=622597 +--> +<head> + <title>Test for Bug 622597</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=622597">Mozilla Bug 622597</a> +<p id="display"></p> +<div id="content"> + <form> + <input required> + <textarea required></textarea> + <select required><option value="">foo</option><option selected>bar</option></select> + <button>submit</button> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 622597 **/ + +var form = document.forms[0]; +var input = form.elements[0]; +var textarea = form.elements[1]; +var select = form.elements[2]; +var button = form.elements[3]; + +function checkPseudoClasses(aElement, aValid, aInvalid) +{ + is(aElement.matches(":-moz-ui-valid"), aValid, + aValid ? aElement + " should match :-moz-ui-valid" + : aElement + " should not match :-moz-ui-valid"); + is(aElement.matches(":-moz-ui-invalid"), aInvalid, + aInvalid ? aElement + " should match :-moz-ui-invalid" + : aElement + " should not match :-moz-ui-invalid"); + if (aValid && aInvalid) { + ok(false, + aElement + " should not match :-moz-ui-valid AND :-moz-ui-invalid"); + } +} + +select.addEventListener("focus", function() { + SimpleTest.executeSoon(function() { + form.noValidate = false; + SimpleTest.executeSoon(function() { + checkPseudoClasses(select, false, true); + SimpleTest.finish(); + }); + }); +}, {once: true}); + +textarea.addEventListener("focus", function() { + SimpleTest.executeSoon(function() { + form.noValidate = false; + SimpleTest.executeSoon(function() { + checkPseudoClasses(textarea, false, true); + form.noValidate = true; + select.selectedIndex = 0; + select.focus(); + }); + }); +}, {once: true}); + +input.addEventListener("invalid", function() { + input.addEventListener("focus", function() { + SimpleTest.executeSoon(function() { + form.noValidate = false; + SimpleTest.executeSoon(function() { + checkPseudoClasses(input, false, true); + form.noValidate = true; + textarea.value = ''; + textarea.focus(); + }); + }); + }, {once: true}); + + SimpleTest.executeSoon(function() { + form.noValidate = true; + input.blur(); + input.value = ''; + input.focus(); + }); +}, {once: true}); + +button.addEventListener("focus", function() { + SimpleTest.executeSoon(function() { + synthesizeKey("KEY_Enter"); + }); +}, {once: true}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + button.focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug623291.html b/dom/html/test/test_bug623291.html new file mode 100644 index 0000000000..c7b7ab7ea4 --- /dev/null +++ b/dom/html/test/test_bug623291.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=623291 +--> +<head> + <title>Test for Bug 623291</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=623291">Mozilla Bug 623291</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<input id="textField" onfocus="next()" onblur="done();"> +<button id="b">a button</button> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 623291 **/ + +function runTest() { + document.getElementById("textField").focus(); +} + +function next() { + synthesizeMouseAtCenter(document.getElementById('b'), {}, window); +} + +function done() { + isnot(document.activeElement, document.getElementById("textField"), + "TextField should not be active anymore!"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug6296.html b/dom/html/test/test_bug6296.html new file mode 100644 index 0000000000..6e74ce8ec2 --- /dev/null +++ b/dom/html/test/test_bug6296.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=6296 +--> +<head> + <title>Test for Bug 6296</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=6296">Mozilla Bug 6296</a> +<p id="display"></p> +<div id="content" style="display: none"> + <A HREF="../testdata/test.gif" id="foo" NAME="anchor1" ALT="this is a test of the image + attribute">Hi</A> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 6296 **/ +is($("foo").name, "anchor1", "accessing an anchor name should work, and not crash either!") + + + + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug629801.html b/dom/html/test/test_bug629801.html new file mode 100644 index 0000000000..073979a5fe --- /dev/null +++ b/dom/html/test/test_bug629801.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=629801 +--> +<head> + <title>Test for Bug 629801</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=629801">Mozilla Bug 629801</a> +<p id="display"></p> +<div id="content" style="display: none"> + +<div itemscope> + This tests itemValue on time elements, first with no datetime attribute, then with no text content, + then with both. + <time id="t1" itemprop="a">May 10th 2009</time> + <time id="t2" itemprop="b" datetime="2009-05-10"></time> + <time id="t3" itemprop="c" datetime="2009-05-10">May 10th 2009</time> +</div> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 629801 **/ + +var t1 = document.getElementById("t1"), + t2 = document.getElementById("t2"), + t3 = document.getElementById("t3"), + t4 = document.createElement("time"); + +// .dateTime IDL +is(t1.dateTime, "", "dateTime is properly set to empty string if datetime attributeis absent"); +is(t2.dateTime, "2009-05-10", "dateTime is properly set to datetime attribute with datetime and no text content"); +is(t3.dateTime, "2009-05-10", "dateTime is properly set to datetime attribute with datetime and text content"); + +// dateTime reflects datetime attribute +reflectString({ + element: t4, + attribute: "dateTime" +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug633058.html b/dom/html/test/test_bug633058.html new file mode 100644 index 0000000000..a92f1f9369 --- /dev/null +++ b/dom/html/test/test_bug633058.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=633058 +--> +<head> + <title>Test for Bug 633058</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=633058">Mozilla Bug 633058</a> +<p id="display"></p> +<div id="content"> + <input> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 633058 **/ + +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(startTest); + +function startTest() { + var nbExpectedKeyDown = 8; + var nbExpectedKeyPress = 1; + var inputGotKeyPress = 0; + var inputGotKeyDown = 0; + var divGotKeyPress = 0; + var divGotKeyDown = 0; + + var input = document.getElementsByTagName('input')[0]; + var content = document.getElementById('content'); + + content.addEventListener("keydown", () => { divGotKeyDown++; }); + content.addEventListener("keypress", () => { divGotKeyPress++; }); + input.addEventListener("keydown", () => { inputGotKeyDown++; }); + input.addEventListener("keypress", () => { inputGotKeyPress++; }); + + input.addEventListener('focus', function() { + SimpleTest.executeSoon(() => { + synthesizeKey('KEY_ArrowUp'); + synthesizeKey('KEY_ArrowLeft'); + synthesizeKey('KEY_ArrowRight'); + synthesizeKey('KEY_ArrowDown'); + synthesizeKey('KEY_Backspace'); + synthesizeKey('KEY_Delete'); + synthesizeKey('KEY_Escape'); + synthesizeKey('KEY_Enter'); // Will dispatch keypress event even in strict behavior. + + is(inputGotKeyDown, nbExpectedKeyDown, "input got all keydown events"); + is(inputGotKeyPress, nbExpectedKeyPress, "input got all keypress events"); + is(divGotKeyDown, nbExpectedKeyDown, "div got all keydown events"); + is(divGotKeyPress, nbExpectedKeyPress, "div got all keypress events"); + SimpleTest.finish(); + }); + }, {once: true}); + input.focus(); +} +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug636336.html b/dom/html/test/test_bug636336.html new file mode 100644 index 0000000000..314e941f84 --- /dev/null +++ b/dom/html/test/test_bug636336.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=636336 +--> +<head> + <title>Test for Bug 636336</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=636336">Mozilla Bug 636336</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 636336 **/ +function testIt(tag) { + var elem = document.createElement(tag); + elem.setAttribute("src", " "); + is(elem.getAttribute("src"), " ", + tag + " src attribute setter should not strip whitespace"); + elem.setAttribute("src", " test "); + is(elem.getAttribute("src"), " test ", + tag + " src attribute setter should not strip whitespace around non-whitespace"); + is(elem.src, window.location.href.replace(/\?.*/, "") + .replace(/test_bug636336\.html$/, "test"), + tag + ".src should strip whitespace as needed"); +} + +testIt("img"); +testIt("source"); +testIt("audio"); +testIt("video"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug641219.html b/dom/html/test/test_bug641219.html new file mode 100644 index 0000000000..44dc15cf41 --- /dev/null +++ b/dom/html/test/test_bug641219.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=641219 +--> +<head> + <title>Test for Bug 641219</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=641219">Mozilla Bug 641219</a> +<p id="display"></p> +<div id="content" style="display: none"> +<div id="div"> +<font></font> +<svg><font/></svg> +</div> +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 641219 **/ +var HTML = "http://www.w3.org/1999/xhtml", + SVG = "http://www.w3.org/2000/svg"; +var wrapper = document.getElementById("div"); +is(wrapper.getElementsByTagName("FONT").length, 1); +is(wrapper.getElementsByTagName("FONT")[0].namespaceURI, HTML); +is(wrapper.getElementsByTagName("font").length, 2); +is(wrapper.getElementsByTagName("font")[0].namespaceURI, HTML); +is(wrapper.getElementsByTagName("font")[1].namespaceURI, SVG); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug643051.html b/dom/html/test/test_bug643051.html new file mode 100644 index 0000000000..5719d1d2db --- /dev/null +++ b/dom/html/test/test_bug643051.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=643051 +--> +<head> + <title>Test for Bug 643051</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=643051">Mozilla Bug 643051</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +SpecialPowers.pushPrefEnv({ + "set": [ + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ] +}, () => { + /** Test for Bug 643051 **/ + document.cookie = "a=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; // clear cookie + document.cookie = "a2=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; // clear cookie + document.cookie = "a3=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; // clear cookie + + // single cookie, should work + document.cookie = "a=bar"; + is(document.cookie, "a=bar", "Can't read stored cookie!"); + + document.cookie = "a2=bar\na3=bar"; + is(document.cookie, "a=bar; a2=bar", "Wrong cookie value"); + + document.cookie = "a2=baz; a3=bar"; + is(document.cookie, "a=bar; a2=baz", "Wrong cookie value"); + + // clear cookies again to avoid affecting other tests + document.cookie = "a=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + document.cookie = "a2=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + document.cookie = "a3=; expires=Thu, 01-Jan-1970 00:00:01 GMT"; + + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug646157.html b/dom/html/test/test_bug646157.html new file mode 100644 index 0000000000..ea0cbedaf0 --- /dev/null +++ b/dom/html/test/test_bug646157.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=646157 +--> +<head> + <title>Test for Bug 646157</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=646157">Mozilla Bug 646157</a> +<p id="display"></p> +<div id="content"> + <label id="l1"/><input id="c1" type='checkbox'> + <label id="l2"/><input id="c2" type='checkbox'> + <label id="l3"/><input id="c3" type='checkbox'> + <label id="l4"/><input id="c4" type='checkbox'> + <label id="l5"/><input id="c5" type='checkbox'> + <label id="l6"/><input id="c6" type='checkbox'> + <label id="l7"/><input id="c7" type='checkbox'> + <label id="l8"/><input id="c8" type='checkbox'> + <label id="l9"/><input id="c9" type='checkbox'> + <label id="l10"/><input id="c10" type='checkbox'> +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 646157 **/ + +var expectedClicks = { + // [ Direct clicks, bubbled clicks, synthetic clicks] + l1: [0, 2, 1], + l2: [0, 2, 1], + l3: [0, 2, 1], + l4: [0, 2, 1], + l5: [0, 2, 1], + l6: [0, 2, 1], + l7: [0, 2, 1], + l8: [0, 2, 1], + l9: [0, 2, 1], + l10:[1, 2, 1], + c1: [0, 0, 0], + c2: [0, 0, 0], + c3: [0, 0, 0], + c4: [0, 0, 0], + c5: [0, 0, 0], + c6: [0, 0, 0], + c7: [0, 0, 0], + c8: [0, 0, 0], + c9: [0, 0, 0], + c10:[1, 1, 1] +}; + +function clickhandler(e) { + if (!e.currentTarget.clickCount) + e.currentTarget.clickCount = 1; + else + e.currentTarget.clickCount++; + + if (e.currentTarget === e.target) + e.currentTarget.directClickCount = 1; + + if (e.target != document.getElementById("l10")) { + if (!e.currentTarget.synthClickCount) + e.currentTarget.synthClickCount = 1; + else + e.currentTarget.synthClickCount++; + } +} + +for (var i = 1; i <= 10; i++) { + document.getElementById("l" + i).addEventListener('click', clickhandler); + document.getElementById("c" + i).addEventListener('click', clickhandler); +} + +document.getElementById("l10").click(); + +function check(thing) { + var expected = expectedClicks[thing.id]; + is(thing.directClickCount || 0, expected[0], "Wrong number of direct clicks"); + is(thing.clickCount || 0, expected[1], "Wrong number of clicks"); + is(thing.synthClickCount || 0, expected[2], "Wrong number of synthetic clicks"); +} + +// Compare them all +for (var i = 1; i <= 10; i++) { + check(document.getElementById("l" + i)); + check(document.getElementById("c" + i)); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug649134.html b/dom/html/test/test_bug649134.html new file mode 100644 index 0000000000..52d644fb14 --- /dev/null +++ b/dom/html/test/test_bug649134.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=649134 +--> +<head> + <title>Test for Bug 649134</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=649134">Mozilla Bug 649134</a> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 649134 **/ +SimpleTest.waitForExplicitFinish(); + +var calls = 0; +function finish() { + if (++calls == 4) + SimpleTest.finish(); +} +function verifyNoLoad(iframe) { + ok(iframe.contentDocument.body.offsetHeight > 0, + "HTTP Link stylesheet was ignored " + iframe.src); + finish(); +} +var verifyLoadCalls = 0; +function verifyLoad(iframe) { + if (++verifyLoadCalls == 2) { + ok(indexContent == iframe.contentDocument.body.innerHTML, + "bug649134/ loads bug649134/index.html " + iframe.src); + } + finish(); +} +function indexLoad(iframe) { + indexContent = iframe.contentDocument.body.innerHTML; + verifyLoad(iframe); +} + +</script> +</pre> +<p id="display"> +<!-- Note: the extra sub-directory is needed for the test, see bug 649134 comment 14 --> +<iframe onload="verifyNoLoad(this);" src="bug649134/file_bug649134-1.sjs"></iframe> +<iframe onload="verifyNoLoad(this);" src="bug649134/file_bug649134-2.sjs"></iframe> +<iframe onload="verifyLoad(this);" src="bug649134/"></iframe> <!-- verify that mochitest server loads index.html --> +<iframe onload="indexLoad(this);" src="bug649134/index.html"></iframe> +</p> +</body> +</html> diff --git a/dom/html/test/test_bug651956.html b/dom/html/test/test_bug651956.html new file mode 100644 index 0000000000..bd059bab94 --- /dev/null +++ b/dom/html/test/test_bug651956.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=651956 +--> +<head> + <title>Test for Bug 651956</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=651956">Mozilla Bug 651956</a> +<p id="display"></p> +<div id="content"> + <input> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 651956 **/ + +var input = document.getElementsByTagName('input')[0]; + +var gotInputEvent = false; + +input.addEventListener("input", function() { + gotInputEvent = true; +}, {once: true}); + +input.addEventListener("focus", function() { + synthesizeKey("KEY_Escape"); + + setTimeout(function() { + ok(!gotInputEvent, "No input event should have been sent."); + SimpleTest.finish(); + }); +}, {once: true}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + input.focus(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug658746.html b/dom/html/test/test_bug658746.html new file mode 100644 index 0000000000..260b345300 --- /dev/null +++ b/dom/html/test/test_bug658746.html @@ -0,0 +1,97 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=658746 +--> +<head> + <title>Test for Bug 658746</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=658746">Mozilla Bug 658746</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 658746 **/ + +/** + * Sets property, gets property and deletes property. + */ +function SetGetDelete(prop) +{ + var el = document.createElement('div'); + + el.dataset[prop] = 'aaaaaa'; + is(el.dataset[prop], 'aaaaaa', 'Dataset property "' + prop + '" should have been set.'); + + delete el.dataset[prop]; + is(el.dataset[prop], undefined, 'Dataset property"' + prop + '" should have been deleted.'); +} + +/** + * Gets, deletes and sets property. Expects exception while trying to set property. + */ +function SetExpectException(prop) +{ + var el = document.createElement('div'); + + is(el.dataset[prop], undefined, 'Dataset property "' + prop + '" should be undefined.'); + delete el.dataset[prop]; + + try { + el.dataset[prop] = "xxxxxx"; + ok(false, 'Exception should have been thrown when setting "' + prop + '".'); + } catch (ex) { + ok(true, 'Exception should have been thrown.'); + } +} + +// Numbers as properties. +SetGetDelete(-12345678901234567000); +SetGetDelete(-1); +SetGetDelete(0); +SetGetDelete(1); +SetGetDelete(12345678901234567000); + +// Floating point numbers as properties. +SetGetDelete(-1.1); +SetGetDelete(0.0); +SetGetDelete(1.1); + +// Hexadecimal numbers as properties. +SetGetDelete(0x3); +SetGetDelete(0xa); + +// Octal numbers as properties. +SetGetDelete(0o3); +SetGetDelete(0o7); + +// String numbers as properties. +SetGetDelete('0'); +SetGetDelete('01'); +SetGetDelete('0x1'); + +// Undefined as property. +SetGetDelete(undefined); + +// Empty arrays as properties. +SetGetDelete(new Array()); +SetGetDelete([]); + +// Non-empty array and object as properties. +SetExpectException(['a', 'b']); +SetExpectException({'a':'b'}); + +// Objects as properties. +SetExpectException(new Object()); +SetExpectException(document); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug659596.html b/dom/html/test/test_bug659596.html new file mode 100644 index 0000000000..79a55a7608 --- /dev/null +++ b/dom/html/test/test_bug659596.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=659596 +--> +<head> + <title>Test for Bug 659596</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=659596">Mozilla Bug 659596</a> +<p id="display"></p> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 659596 **/ + +function checkReflection(option, attribute) { + /** + * Getting. + */ + + // When attribute isn't present. + var tests = [ "", "foo" ]; + for (var test of tests) { + option.removeAttribute(attribute); + option.textContent = test; + is(option.getAttribute(attribute), null, + "option " + attribute + "'s value should be null"); + is(option[attribute], option.textContent, + "option." + attribute + " should reflect the text content when the attribute isn't set"); + } + + // When attribute is present. + tests = [ + [ "", "" ], + [ "", "foo" ], + [ "foo", "bar" ], + [ "foo", "" ], + ]; + for (var test of tests) { + option.setAttribute(attribute, test[0]); + option.textContent = test[1]; + is(option[attribute], option.getAttribute(attribute), + "option." + attribute + " should reflect the content attribute when it is set"); + } + + /** + * Setting. + */ + + // When attribute isn't present. + tests = [ + [ "", "new" ], + [ "foo", "new" ], + ]; + for (var test of tests) { + option.removeAttribute(attribute); + option.textContent = test[0]; + option[attribute] = test[1] + + is(option.getAttribute(attribute), test[1], + "when setting, the content attribute should change"); + is(option.textContent, test[0], + "when setting, the text content should not change"); + } + + // When attribute is present. + tests = [ + [ "", "", "new" ], + [ "", "foo", "new" ], + [ "foo", "bar", "new" ], + [ "foo", "", "new" ], + ]; + for (var test of tests) { + option.setAttribute(attribute, test[0]); + option.textContent = test[1]; + option[attribute] = test[2]; + + is(option.getAttribute(attribute), test[2], + "when setting, the content attribute should change"); + is(option.textContent, test[1], + "when setting, the text content should not change"); + } +} + +var option = document.createElement("option"); + +checkReflection(option, "value"); +checkReflection(option, "label"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug659743.xml b/dom/html/test/test_bug659743.xml new file mode 100644 index 0000000000..12236bdc02 --- /dev/null +++ b/dom/html/test/test_bug659743.xml @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=659743 +--> +<head> + <title>Test for Bug 659743</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=659743">Mozilla Bug 659743</a> +<p id="display"> +<map name="a"> +<area shape="rect" coords="25,25,75,75" href="#x"/> +</map> +<map id="b"> +<area shape="rect" coords="25,25,75,75" href="#y"/> +</map> +<map name="a"> +<area shape="rect" coords="25,25,75,75" href="#FAIL"/> +</map> +<map id="b"> +<area shape="rect" coords="25,25,75,75" href="#FAIL"/> +</map> + +<img usemap="#a" src="image.png"/> +<img usemap="#b" src="image.png"/> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 659743 **/ +SimpleTest.waitForExplicitFinish(); +var images = document.getElementsByTagName("img"); +var second = false; +onhashchange = function() { + if (!second) { + second = true; + is(location.hash, "#x", "First map"); + SimpleTest.waitForFocus(() => synthesizeMouse(images[1], 50, 50, {})); + } else { + is(location.hash, "#y", "Second map"); + SimpleTest.finish(); + } +}; +SimpleTest.waitForFocus(() => synthesizeMouse(images[0], 50, 50, {})); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug660663.html b/dom/html/test/test_bug660663.html new file mode 100644 index 0000000000..dcc208dfe5 --- /dev/null +++ b/dom/html/test/test_bug660663.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=660663 +--> +<head> + <title>Test for Bug 660663</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660663">Mozilla Bug 660663</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 660663 **/ +reflectLimitedEnumerated({ + element: document.createElement("div"), + attribute: "dir", + validValues: ["ltr", "rtl", "auto"], + invalidValues: ["cheesecake", ""] +}); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug660959-1.html b/dom/html/test/test_bug660959-1.html new file mode 100644 index 0000000000..930b31b4d3 --- /dev/null +++ b/dom/html/test/test_bug660959-1.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=660959 +--> +<head> + <title>Test for Bug 660959</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660959">Mozilla Bug 660959</a> +<p id="display"></p> +<div id="content" style="display: none"> + <a href="#" id="testa"></a> +</div> +<pre id="test"> +<script> + is($("content").querySelector(":link, :visited"), $("testa"), + "Should find a link even in a display:none subtree"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug660959-2.html b/dom/html/test/test_bug660959-2.html new file mode 100644 index 0000000000..a7dfb2e3d5 --- /dev/null +++ b/dom/html/test/test_bug660959-2.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=660959 +--> +<head> + <title>Test for Bug 660959</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <style> + :link, :visited { + color: red; + } + </style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660959">Mozilla Bug 660959</a> +<p id="display"></p> +<div id="content" style="display: none"> + <a href="#" id="a"></a> +</div> +<pre id="test"> +<script type="application/javascript"> + var a = document.getElementById("a"); + is(window.getComputedStyle(a).color, "rgb(255, 0, 0)", "Link is not right color?"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug660959-3.html b/dom/html/test/test_bug660959-3.html new file mode 100644 index 0000000000..cc39c1eb98 --- /dev/null +++ b/dom/html/test/test_bug660959-3.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=660959 +--> +<head> + <title>Test for Bug 660959</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=660959">Mozilla Bug 660959</a> +<p id="display"></p> +<div id="content" style="display: none"> + <a href="http://www.example.com"></a> + <div id="foo"> + <span id="test"></span> + </div> +</div> +<pre id="test"> +<script> + is($("foo").querySelector(":link + * span, :visited + * span"), $("test"), + "Should be able to find link siblings even in a display:none subtree"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug666200.html b/dom/html/test/test_bug666200.html new file mode 100644 index 0000000000..d68966f787 --- /dev/null +++ b/dom/html/test/test_bug666200.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=666200 +--> +<head> + <title>Test for Bug 666200</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=666200">Mozilla Bug 666200</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +/** Test for Bug 666200 **/ +var sel = document.createElement("select"); +var opt1 = new Option(); +var opt2 = new Option(); +var opt3 = new Option(); +var opt4 = new Option(); +var opt5 = new Option(); +opt1.value = 1; +opt2.value = 2; +opt3.value = 3; +opt4.value = 4; +opt5.value = 5; +sel.add(opt1); +sel.add(opt2, 0); +sel.add(opt3, 1000); +sel.options.add(opt4, opt3); +sel.add(opt5, undefined); +is(sel[0], opt2, "1st item should be 2"); +is(sel[1], opt1, "2nd item should be 1"); +is(sel[2], opt4, "3rd item should be 4"); +is(sel[3], opt3, "4th item should be 3"); +is(sel[4], opt5, "5th item should be 5"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug666666.html b/dom/html/test/test_bug666666.html new file mode 100644 index 0000000000..a3c22d4e0f --- /dev/null +++ b/dom/html/test/test_bug666666.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=666666 +--> +<head> + <title>Test for Bug 666666</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=666666">Mozilla Bug 666666</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 666666 **/ +["audio", "video"].forEach(function(element) { + reflectLimitedEnumerated({ + element: document.createElement(element), + attribute: "preload", + validValues: ["none", "metadata", "auto"], + invalidValues: ["cheesecake", ""] + }); +}); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug669012.html b/dom/html/test/test_bug669012.html new file mode 100644 index 0000000000..330286f33d --- /dev/null +++ b/dom/html/test/test_bug669012.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=669012 +--> +<head> + <title>Test for Bug 669012</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=669012">Mozilla Bug 669012</a> +<p id="display"></p> +<div id="content" style="display: none"> +<script> +var run = 0; +</script> +<svg> +<script> +run++; +ok(true, "Should run SVG script without attributes") +</script> +<script for=window event=onload> +run++; +ok(true, "Should run SVG script with for=window event=onload") +</script> +<script for=window event=foo> +run++; +ok(true, "Should run SVG script with for=window event=foo") +</script> +<script for=foo event=onload> +run++; +ok(true, "Should run SVG script with for=foo event=onload") +</script> +</svg> +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for Bug 669012 **/ +is(run, 4, "Should have run all tests") +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug674558.html b/dom/html/test/test_bug674558.html new file mode 100644 index 0000000000..ab9713bf35 --- /dev/null +++ b/dom/html/test/test_bug674558.html @@ -0,0 +1,287 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=674558 +--> +<head> + <title>Test for Bug 674558</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=674558">Mozilla Bug 674558</a> +<p id="display"></p> +<div id="content"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 674558 **/ +SimpleTest.waitForExplicitFinish(); + +SimpleTest.waitForFocus(startTest); + +function startTest() { + function textAreaCtor() { + return document.createElement("textarea"); + } + var ctors = [textAreaCtor]; + ["text", "password", "search"].forEach(function(type) { + ctors.push(function inputCtor() { + var input = document.createElement("input"); + input.type = type; + return input; + }); + }); + + for (var ctor in ctors) { + test(ctors[ctor]); + } + + SimpleTest.finish(); +} + +function test(ctor) { + var elem = ctor(); + ok(true, "Testing " + name(elem)); + + ok("selectionDirection" in elem, "elem should have the selectionDirection property"); + + is(elem.selectionStart, elem.value.length, "Default value"); + is(elem.selectionEnd, elem.value.length, "Default value"); + is(elem.selectionDirection, "forward", "Default value"); + + var content = document.getElementById("content"); + content.appendChild(elem); + + function flush() { document.body.clientWidth; } + function hide() { + content.style.display = "none"; + flush(); + } + function show() { + content.style.display = ""; + flush(); + } + + elem.value = "foobar"; + + is(elem.selectionStart, elem.value.length, "Default value"); + is(elem.selectionEnd, elem.value.length, "Default value"); + is(elem.selectionDirection, "forward", "Default value"); + + elem.setSelectionRange(1, 3); + is(elem.selectionStart, 1, "Correct value"); + is(elem.selectionEnd, 3, "Correct value"); + is(elem.selectionDirection, "forward", "If not set, should default to forward"); + + hide(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 3, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + show(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 3, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + // extend to right + elem.focus(); + synthesizeKey("VK_RIGHT", {shiftKey: true}); + + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Correct value"); + is(elem.selectionDirection, "forward", "Still forward"); + + hide(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + show(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + // change the direction + elem.selectionDirection = "backward"; + + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "backward", "Correct value"); + + hide(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + show(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + // extend to right again + synthesizeKey("VK_RIGHT", {shiftKey: true}); + + is(elem.selectionStart, 2, "Correct value"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "backward", "Still backward"); + + hide(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + show(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + elem.selectionEnd = 5; + + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Correct value"); + is(elem.selectionDirection, "backward", "Still backward"); + + hide(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + show(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + elem.selectionDirection = "none"; + + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "forward", "none not supported"); + + hide(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + show(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + elem.selectionDirection = "backward"; + + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Correct Value"); + + hide(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + show(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + elem.selectionDirection = "invalid"; + + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "forward", "Treated as none"); + + hide(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + show(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + elem.selectionDirection = "backward"; + + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Correct Value"); + + hide(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + show(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + elem.setSelectionRange(1, 4); + + is(elem.selectionStart, 1, "Correct value"); + is(elem.selectionEnd, 4, "Correct value"); + is(elem.selectionDirection, "forward", "Correct value"); + + hide(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + show(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + elem.setSelectionRange(1, 1); + synthesizeKey("VK_RIGHT", {shiftKey: true}); + synthesizeKey("VK_RIGHT", {shiftKey: true}); + synthesizeKey("VK_RIGHT", {shiftKey: true}); + + is(elem.selectionStart, 1, "Correct value"); + is(elem.selectionEnd, 4, "Correct value"); + is(elem.selectionDirection, "forward", "Correct value"); + + hide(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + show(); + is(elem.selectionStart, 1, "Value unchanged"); + is(elem.selectionEnd, 4, "Value unchanged"); + is(elem.selectionDirection, "forward", "Value unchanged"); + + elem.setSelectionRange(5, 5); + synthesizeKey("VK_LEFT", {shiftKey: true}); + synthesizeKey("VK_LEFT", {shiftKey: true}); + synthesizeKey("VK_LEFT", {shiftKey: true}); + + is(elem.selectionStart, 2, "Correct value"); + is(elem.selectionEnd, 5, "Correct value"); + is(elem.selectionDirection, "backward", "Correct value"); + + hide(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); + + show(); + is(elem.selectionStart, 2, "Value unchanged"); + is(elem.selectionEnd, 5, "Value unchanged"); + is(elem.selectionDirection, "backward", "Value unchanged"); +} + +function name(elem) { + var tag = elem.localName; + if (tag == "input") { + tag += "[type=" + elem.type + "]"; + } + return tag; +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug674927.html b/dom/html/test/test_bug674927.html new file mode 100644 index 0000000000..92af594530 --- /dev/null +++ b/dom/html/test/test_bug674927.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=674927 +--> +<title>Test for Bug 674927</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<p><span>Hello</span></p> +<div contenteditable>Contenteditable <i>is</i> splelchecked by default</div> +<textarea>Textareas are spellchekced by default</textarea> +<input value="Inputs are not spellcheckde by default"> +<script> +// Test the effect of setting spellcheck on various elements +[ + "html", + "body", + "p", + "span", + "div", + "i", + "textarea", + "input", +].forEach(function(query) { + var element = document.querySelector(query); + + // First check what happens if no attributes are set + var defaultSpellcheck; + if (element.isContentEditable || element.tagName == "TEXTAREA") { + defaultSpellcheck = true; + } else { + defaultSpellcheck = false; + } + is(element.spellcheck, defaultSpellcheck, + "Default spellcheck for <" + element.tagName.toLowerCase() + ">"); + + // Now try setting spellcheck on ancestors + var ancestor = element; + do { + testSpellcheck(ancestor, element); + ancestor = ancestor.parentNode; + } while (ancestor.nodeType == Node.ELEMENT_NODE); +}); + +function testSpellcheck(ancestor, element) { + ancestor.spellcheck = true; + is(element.spellcheck, true, + ".spellcheck on <" + element.tagName.toLowerCase() + "> with " + + "spellcheck=true on <" + ancestor.tagName.toLowerCase() + ">"); + ancestor.spellcheck = false; + is(element.spellcheck, false, + ".spellcheck on <" + element.tagName.toLowerCase() + "> with " + + "spellcheck=false on <" + ancestor.tagName.toLowerCase() + ">"); + ancestor.removeAttribute("spellcheck"); +} +</script> diff --git a/dom/html/test/test_bug677495-1.html b/dom/html/test/test_bug677495-1.html new file mode 100644 index 0000000000..be11d20fd6 --- /dev/null +++ b/dom/html/test/test_bug677495-1.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=677495 + +As mandated by the spec, the body of a media document must only contain one child. +--> +<head> + <title>Test for Bug 571981</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + function frameLoaded() { + var testframe = document.getElementById('testframe'); + var testframeChildren = testframe.contentDocument.body.childNodes; + is(testframeChildren.length, 1, "Body of video document has 1 child"); + is(testframeChildren[0].nodeName, "VIDEO", "Only child of body must be a <video> element"); + + SimpleTest.finish(); + } +</script> + +</head> +<body> + <p id="display"></p> + + <iframe id="testframe" name="testframe" onload="frameLoaded()" + src="file.webm"></iframe> + +</body> +</html> diff --git a/dom/html/test/test_bug677495.html b/dom/html/test/test_bug677495.html new file mode 100644 index 0000000000..2145d5899c --- /dev/null +++ b/dom/html/test/test_bug677495.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=677495 + +As mandated by the spec, the body of a media document must only contain one child. +--> +<head> + <title>Test for Bug 571981</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script type="application/javascript"> + SimpleTest.waitForExplicitFinish(); + + function frameLoaded() { + var testframe = document.getElementById('testframe'); + var testframeChildren = testframe.contentDocument.body.childNodes; + is(testframeChildren.length, 1, "Body of image document has 1 child"); + is(testframeChildren[0].nodeName, "IMG", "Only child of body must be an <img> element"); + + SimpleTest.finish(); + } +</script> + +</head> +<body> + <p id="display"></p> + + <iframe id="testframe" name="testframe" onload="frameLoaded()" + src="image.png"></iframe> + +</body> +</html> diff --git a/dom/html/test/test_bug677658.html b/dom/html/test/test_bug677658.html new file mode 100644 index 0000000000..79a4088b73 --- /dev/null +++ b/dom/html/test/test_bug677658.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=677658 +--> +<head> + <title>Test for Bug 677658</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body onload="test()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=677658">Mozilla Bug 677658</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"><!-- + +/** Test for Bug 677658 **/ + +SimpleTest.waitForExplicitFinish(); + +function testDone() { + ok(window.testPassed, "Script shouldn't have run!"); + SimpleTest.finish(); +} + +function test() { + window.testPassed = true; + document.getElementById("testtarget").innerHTML = + "<script async src='data:text/plain, window.testPassed = false;'></script>"; + SimpleTest.executeSoon(testDone); +} + +// --> +</script> +</pre> +<div id="testtarget"></div> +</body> +</html> diff --git a/dom/html/test/test_bug682886.html b/dom/html/test/test_bug682886.html new file mode 100644 index 0000000000..cb032738c9 --- /dev/null +++ b/dom/html/test/test_bug682886.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=682886 +--> +<head> + <title>Test for Bug 682886</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=682886">Mozilla Bug 682886</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 682886 **/ + + + var m = document.createElement("menu"); + var s = "<menuitem>foo</menuitem>"; + m.innerHTML = s; + is(m.innerHTML, s, "Wrong menuitem serialization!"); + + + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug691.html b/dom/html/test/test_bug691.html new file mode 100644 index 0000000000..f88df20a54 --- /dev/null +++ b/dom/html/test/test_bug691.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=691 +--> +<head> + <title>Test for Bug 691</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<script type="text/javascript"> + +function show(what) { + var stage = document.getElementById("stage"); + if (what == "modularity") { + var spaghetti = document.createElement("IMG",null); + spaghetti.setAttribute("SRC","nnc_lockup.gif"); + spaghetti.setAttribute("id","foo"); + stage.insertBefore(spaghetti,stage.firstChild); + } +} + +function remove() { + var stage = document.getElementById("stage"); + var body = document.getElementsByTagName("BODY")[0]; + while (stage.firstChild) { + stage.firstChild.remove(); + } +} + +</script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=691">Mozilla Bug 691</a> +<p id="display"></p> +<div id="content" > +<ul> +<li >foo</li> +</ul> +<div id="stage"> +</div> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 691 **/ + +show("modularity"); +remove(); +show("modularity"); +remove(); +show("modularity"); +remove(); +show("modularity"); + +ok($("foo"), "basic DOM manipulation doesn't crash"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug694.html b/dom/html/test/test_bug694.html new file mode 100644 index 0000000000..78eb054cfc --- /dev/null +++ b/dom/html/test/test_bug694.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=694 +--> +<head> + <title>Test for Bug 694</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=694">Mozilla Bug 694</a> +<p id="display"></p> +<div id="content" > +<img src="/missing_on_purpose" width=123 height=25 alt="Hello, "Quotes" how are you?" id="testimg"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 694 **/ + +is($("testimg").getAttribute("alt"), "Hello, \"Quotes\" how are you?", "entities in alt attribute works"); + + + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug694503.html b/dom/html/test/test_bug694503.html new file mode 100644 index 0000000000..4ff10feffc --- /dev/null +++ b/dom/html/test/test_bug694503.html @@ -0,0 +1,75 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=694503 +--> +<head> + <title>Test for Bug 694503</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=694503">Mozilla Bug 694503</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<div> +<map name="map1"> + <area onclick="++mapClickCount; event.preventDefault();" + coords="0,0,50,50" shape="rect"> +</map> +</div> + +<img id="img" + usemap="#map1" alt="Foo bar" src="about:logo"> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 694503 **/ + +var mapClickCount = 0; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var m = document.getElementsByTagName("map")[0]; + var img = document.getElementById('img'); + var origName = m.name; + + synthesizeMouse(img, 25, 25, {}); + is(mapClickCount, 1, "Wrong click count (1)"); + + m.name = "foo" + synthesizeMouse(img, 25, 25, {}); + is(mapClickCount, 1, "Wrong click count (2)"); + + m.removeAttribute("name"); + m.id = origName; + synthesizeMouse(img, 25, 25, {}); + is(mapClickCount, 2, "Wrong click count (3)"); + + // Back to original state + m.removeAttribute("id"); + m.name = origName; + synthesizeMouse(img, 25, 25, {}); + is(mapClickCount, 3, "Wrong click count (4)"); + + var p = m.parentNode; + p.removeChild(m); + synthesizeMouse(img, 25, 25, {}); + is(mapClickCount, 3, "Wrong click count (5)"); + + // Back to original state + p.appendChild(m); + synthesizeMouse(img, 25, 25, {}); + is(mapClickCount, 4, "Wrong click count (6)"); + + SimpleTest.finish(); +}); + + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug696.html b/dom/html/test/test_bug696.html new file mode 100644 index 0000000000..6b3c5d9561 --- /dev/null +++ b/dom/html/test/test_bug696.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=696 +--> +<head> + <title>Test for Bug 696</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=696">Mozilla Bug 696</a> +<p id="display"></p> +<div id="content" style="display: none"> + <table><tr id="mytr"><td>Foo</td><td>Bar</td></tr></table> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 696 **/ +var mytr = $("content").getElementsByTagName("TR")[0]; +is(mytr.getAttribute("ID"),"mytr","TR tags expose their ID attribute"); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug717819.html b/dom/html/test/test_bug717819.html new file mode 100644 index 0000000000..b2e04f17ba --- /dev/null +++ b/dom/html/test/test_bug717819.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=717819 +--> +<head> + <title>Test for Bug 717819</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=717819">Mozilla Bug 717819</a> +<p id="display"></p> +<div id="content"> + <table style="position: relative; top: 100px;"> + <tr> + <td> + <div id="test" style="position: absolute; top: 50px;"></div> + </td> + </tr> + </table> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 717819 **/ +var div = document.getElementById("test"); +is(div.offsetTop, 50, "The offsetTop must be calculated correctly"); +is(div.offsetParent, document.querySelector("table"), + "The offset should be calculated off of the correct parent"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug741266.html b/dom/html/test/test_bug741266.html new file mode 100644 index 0000000000..d61e5b6ab0 --- /dev/null +++ b/dom/html/test/test_bug741266.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=741266 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 741266</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=741266">Mozilla Bug 741266</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 741266 **/ +SimpleTest.waitForExplicitFinish(); + +var url = URL.createObjectURL(new Blob([""], { type: "text/html" })); +var w = window.open(url, "", "width=100,height=100"); +w.onload = function() { + is(w.innerHeight, 100, "Popup height should be 100 when opened with window.open"); + // XXXbz On at least some platforms, the innerWidth is off by the scrollbar + // width for some reason. So just make sure it's the same for both popups. + var width = w.innerWidth; + w.close(); + + w = document.open(url, "", "width=100,height=100"); + w.onload = function() { + is(w.innerHeight, 100, "Popup height should be 100 when opened with document.open"); + is(w.innerWidth, width, "Popup width should be the same when opened with document.open"); + w.close(); + SimpleTest.finish(); + }; +}; +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug742030.html b/dom/html/test/test_bug742030.html new file mode 100644 index 0000000000..28185e60db --- /dev/null +++ b/dom/html/test/test_bug742030.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=742030 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 742030</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=742030">Mozilla Bug 742030</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 742030 **/ +const str = " color: #ff0000 "; +var span = document.createElement("span"); +span.setAttribute("style", str); +is(span.getAttribute("style"), str, "Should have set properly"); +var span2 = span.cloneNode(false); +is(span2.getAttribute("style"), str, "Should have cloned properly"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug742549.html b/dom/html/test/test_bug742549.html new file mode 100644 index 0000000000..553493858e --- /dev/null +++ b/dom/html/test/test_bug742549.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=742549 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 742549</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=742549">Mozilla Bug 742549</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 742549 **/ +var els = [ document.createElement("script"), + document.createElementNS("http://www.w3.org/2000/svg", "script") ] + +for (var i = 0; i < els.length; ++i) { + reflectLimitedEnumerated({ + element: els[i], + attribute: { content: "crossorigin", idl: "crossOrigin" }, + // "" is a valid value per spec, but gets mapped to the "anonymous" state, + // just like invalid values, so just list it under invalidValues + validValues: [ "anonymous", "use-credentials" ], + invalidValues: [ + "", " aNOnYmous ", " UsE-CreDEntIALS ", "foobar", "FOOBAR", " fOoBaR " + ], + defaultValue: { invalid: "anonymous", missing: null }, + nullable: true, + }) +} + + + + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug745685.html b/dom/html/test/test_bug745685.html new file mode 100644 index 0000000000..c4544441e7 --- /dev/null +++ b/dom/html/test/test_bug745685.html @@ -0,0 +1,105 @@ +<!doctype html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=745685 +--> +<title>Test for Bug 745685</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=745685">Mozilla Bug 745685</a> +<font>Test text</font> +<font size=1>1</font> +<font size=2>2</font> +<font size=3>3</font> +<font size=4>4</font> +<font size=5>5</font> +<font size=6>6</font> +<font size=7>7</font> +<script> +/** Test for Bug 745685 **/ + +var referenceSizes = {}; +for (var i = 1; i <= 7; i++) { + referenceSizes[i] = + getComputedStyle(document.querySelector('[size="' + i + '"]')) + .fontSize; + if (i > 1) { + isnot(referenceSizes[i], referenceSizes[i - 1], + "Sanity check: different <font size>s give different .fontSize"); + } +} + +function testFontSize(input, expected) { + var font = document.querySelector("font"); + font.setAttribute("size", input); + is(font.getAttribute("size"), input, + "Setting doesn't round-trip (.getAttribute)"); + is(font.size, input, + "Setting doesn't round-trip (.size)"); + is(getComputedStyle(font).fontSize, referenceSizes[expected], + 'Incorrect size for "' + input + '" : expected the same as ' + expected); +} + +function testFontSizes(input, expected) { + testFontSize(input, expected); + // Leading whitespace + testFontSize(" " + input, expected); + testFontSize("\t" + input, expected); + testFontSize("\n" + input, expected); + testFontSize("\f" + input, expected); + testFontSize("\r" + input, expected); + // Trailing garbage + testFontSize(input + "abcd", expected); + testFontSize(input + ".5", expected); + testFontSize(input + "e2", expected); +} + +// Parse error +testFontSizes("", 3); + +// No sign +testFontSizes("0", 1); +testFontSizes("1", 1); +testFontSizes("2", 2); +testFontSizes("3", 3); +testFontSizes("4", 4); +testFontSizes("5", 5); +testFontSizes("6", 6); +testFontSizes("7", 7); +testFontSizes("8", 7); +testFontSizes("9", 7); +testFontSizes("10", 7); +testFontSizes("10000000000000000000000", 7); + +// Minus sign +testFontSizes("-0", 3); +testFontSizes("-1", 2); +testFontSizes("-2", 1); +testFontSizes("-3", 1); +testFontSizes("-4", 1); +testFontSizes("-5", 1); +testFontSizes("-6", 1); +testFontSizes("-7", 1); +testFontSizes("-8", 1); +testFontSizes("-9", 1); +testFontSizes("-10", 1); +testFontSizes("-10000000000000000000000", 1); + +// Plus sign +testFontSizes("+0", 3); +testFontSizes("+1", 4); +testFontSizes("+2", 5); +testFontSizes("+3", 6); +testFontSizes("+4", 7); +testFontSizes("+5", 7); +testFontSizes("+6", 7); +testFontSizes("+7", 7); +testFontSizes("+8", 7); +testFontSizes("+9", 7); +testFontSizes("+10", 7); +testFontSizes("+10000000000000000000000", 7); + +// Non-HTML5 whitespace +testFontSize("\b1", 3); +testFontSize("\v1", 3); +testFontSize("\0u00a01", 3); +</script> diff --git a/dom/html/test/test_bug763626.html b/dom/html/test/test_bug763626.html new file mode 100644 index 0000000000..11da9d1ad2 --- /dev/null +++ b/dom/html/test/test_bug763626.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=763626 +--> +<head> +<title>Test for Bug 763626</title> + +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function boom() +{ + var r = document.createElement("iframe").sandbox; + SpecialPowers.DOMWindowUtils.garbageCollect(); + is("" + r, "", "ToString should return empty string when element is gone"); + SimpleTest.finish(); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> + diff --git a/dom/html/test/test_bug765780.html b/dom/html/test/test_bug765780.html new file mode 100644 index 0000000000..9aee15ea6b --- /dev/null +++ b/dom/html/test/test_bug765780.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=765780 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 765780</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + /** Test for Bug 765780 **/ + SimpleTest.waitForExplicitFinish(); + window.onload = function() { + var f = $("f"); + var doc = f.contentDocument; + doc.designMode = "on"; + var s = doc.createElement("script"); + s.textContent = "parent.called = true;"; + + window.called = false; + doc.body.appendChild(s); + ok(called, "Script in designMode iframe should have run"); + + doc = doc.querySelector("iframe").contentDocument; + var s = doc.createElement("script"); + s.textContent = "parent.parent.called = true;"; + + window.called = false; + doc.body.appendChild(s); + ok(called, "Script in designMode iframe's child should have run"); + + SimpleTest.finish(); + } + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=765780">Mozilla Bug 765780</a> +<!-- Important: iframe needs to not be display: none --> +<p id="display"><iframe id="f" srcdoc="<iframe></iframe>"></iframe> </p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug780993.html b/dom/html/test/test_bug780993.html new file mode 100644 index 0000000000..14324e8e43 --- /dev/null +++ b/dom/html/test/test_bug780993.html @@ -0,0 +1,39 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test for bug 780993</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +test(function() { + var select = document.createElement("select"); + var option = document.createElement("option"); + select.appendChild(option); + assert_equals(select[0], option); + select[0] = null; + assert_equals(option.parentNode, null); + assert_equals(select[0], undefined); +}, "Should be able to set select[n] to null."); +test(function() { + var select = document.createElement("select"); + var option = document.createElement("option"); + var option2 = document.createElement("option"); + select.appendChild(option); + assert_equals(select[0], option); + select[0] = option2; + assert_equals(option.parentNode, null); + assert_equals(option2.parentNode, select); + assert_equals(select[0], option2); +}, "Should be able to set select[n] to an option element"); +test(function() { + var select = document.createElement("select"); + var option = document.createElement("option"); + select.appendChild(option); + assert_equals(select[0], option); + assert_throws(null, function() { + select[0] = 42; + }); + assert_equals(option.parentNode, select); + assert_equals(select[0], option); +}, "Should not be able to set select[n] to a primitive."); +</script> diff --git a/dom/html/test/test_bug787134.html b/dom/html/test/test_bug787134.html new file mode 100644 index 0000000000..59aee4e463 --- /dev/null +++ b/dom/html/test/test_bug787134.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=787134 +--> +<head> + <title>Test for Bug 787134</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=787134">Mozilla Bug 787134</a> +<p id="display"></p> +<p><a id="link-test1" href="example link">example link</a></p> +<pre id="test"> +<script> + var div = document.createElement('div'); + div.innerHTML = '<a href=#></a>'; + var a = div.firstChild; + ok(a.matches(':link'), "Should match a link not in a document"); + is(div.querySelector(':link'), a, "Should find a link not in a document"); + a = document.querySelector('#link-test1'); + ok(a.matches(':link'), "Should match a link in a document with an invalid URL"); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug797113.html b/dom/html/test/test_bug797113.html new file mode 100644 index 0000000000..6c246eb3c3 --- /dev/null +++ b/dom/html/test/test_bug797113.html @@ -0,0 +1,39 @@ +<!doctype html> +<meta charset=utf-8> +<title>Test for bug 780993</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +test(function() { + var select = document.createElement("select"); + var option = document.createElement("option"); + select.appendChild(option); + assert_equals(select.options[0], option); + select.options[0] = null; + assert_equals(option.parentNode, null); + assert_equals(select.options[0], undefined); +}, "Should be able to set select.options[n] to null."); +test(function() { + var select = document.createElement("select"); + var option = document.createElement("option"); + var option2 = document.createElement("option"); + select.appendChild(option); + assert_equals(select.options[0], option); + select.options[0] = option2; + assert_equals(option.parentNode, null); + assert_equals(option2.parentNode, select); + assert_equals(select.options[0], option2); +}, "Should be able to set select.options[n] to an option element"); +test(function() { + var select = document.createElement("select"); + var option = document.createElement("option"); + select.appendChild(option); + assert_equals(select.options[0], option); + assert_throws(null, function() { + select.options[0] = 42; + }); + assert_equals(option.parentNode, select); + assert_equals(select.options[0], option); +}, "Should not be able to set select.options[n] to a primitive."); +</script> diff --git a/dom/html/test/test_bug803677.html b/dom/html/test/test_bug803677.html new file mode 100644 index 0000000000..640f747528 --- /dev/null +++ b/dom/html/test/test_bug803677.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=803677 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 803677</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +<style> + .base { border:1px solid gray; } + .bad-table { display:table-cell; border:1px solid red; } +</style> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=803677">Mozilla Bug 803677</a> +<p id="display"></p> +<div id="content"> + <p class="base">1</p> + <p class="base">2</p> + <p class="base">3</p> + <p class="base bad-table">4</p> + <p class="base">7</p> + <p class="base">8</p> + <p class="base">9</p> +</div> +<pre id="test"> +<script type="application/javascript"> + var p = document.querySelectorAll(".base"); + var parent = document.querySelector("body"); + var prevOffset = 0; + for (var i = 0; i < p.length; i++) { + var t = 0, e = p[i]; + is(e.offsetParent, parent, "Offset parent of all paragraphs should be the body."); + while (e) { + t += e.offsetTop; + e = e.offsetParent; + } + p[i].innerHTML = t; + + ok(t > prevOffset, "Offset should increase down the page"); + prevOffset = t; + } +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug821307.html b/dom/html/test/test_bug821307.html new file mode 100644 index 0000000000..591018da17 --- /dev/null +++ b/dom/html/test/test_bug821307.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=821307 +--> +<head> + <title>Test for Bug 821307</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=821307">Mozilla Bug 821307</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<input id='dummy'></input> +<input type="password" id='input' value='11111111111111111' style="width:40em; font-size:40px;"></input> + +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + var dummy = document.getElementById('dummy'); + dummy.focus(); + is(document.activeElement, dummy, "Check dummy element is now focused"); + + var input = document.getElementById('input'); + var rect = input.getBoundingClientRect(); + synthesizeMouse(input, 100, rect.height/2, {}); + is(document.activeElement, input, "Check input element is now focused"); + + SimpleTest.finish(); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug827126.html b/dom/html/test/test_bug827126.html new file mode 100644 index 0000000000..c4cf28d44c --- /dev/null +++ b/dom/html/test/test_bug827126.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=827126 +--> +<head> + <title>Test for Bug 827126</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=827126">Mozilla Bug 827126</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test to ensure we reflect <img align> correctly **/ +reflectString({ + element: new Image(), + attribute: "align", + otherValues: [ "left", "right", "middle", "justify" ] +}); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug838582.html b/dom/html/test/test_bug838582.html new file mode 100644 index 0000000000..2d412a041b --- /dev/null +++ b/dom/html/test/test_bug838582.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=838582 +--> +<head> + <title>Test for Bug 838582</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=838582">Mozilla Bug 838582</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<textarea id="t">abc</textarea> +<script type="application/javascript"> + +/** Test for Bug 838582 **/ + +var textarea = document.getElementById("t"); + +is(t.textLength, 3, "Correct textLength for defaultValue"); +t.value = "abcdef"; +is(t.textLength, 6, "Correct textLength for value"); +ok(!("controllers" in t), "Don't have web-visible controllers property"); +ok("controllers" in SpecialPowers.wrap(t), "Have chrome-visible controllers property"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug839371.html b/dom/html/test/test_bug839371.html new file mode 100644 index 0000000000..5d434a1803 --- /dev/null +++ b/dom/html/test/test_bug839371.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=839371 +--> +<head> + <title>Test for Bug 839371</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=839371">Mozilla Bug 839371</a> +<p id="display"></p> +<div id="content" style="display: none"> + +<div itemscope> + <data id="d1" itemprop="product-id" value="9678AOU879">The Instigator 2000</data> +</div> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 839371 **/ + +var d1 = document.getElementById("d1"), + d2 = document.createElement("data"); + +// .value IDL +is(d1.value, "9678AOU879", "value property reflects content attribute"); +d1.value = "123"; +is(d1.value, "123", "value property can be set via setter"); + +// .value reflects value attribute +reflectString({ + element: d2, + attribute: "value" +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug839913.html b/dom/html/test/test_bug839913.html new file mode 100644 index 0000000000..7397fa3b6b --- /dev/null +++ b/dom/html/test/test_bug839913.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for HTMLAreaElement's stringifier</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +test(function() { + var area = document.createElement("area"); + area.href = "http://example.org/"; + assert_equals(area.href, "http://example.org/"); + assert_equals(String(area), "http://example.org/"); +}, "Area elements should stringify to the href attribute"); +</script> diff --git a/dom/html/test/test_bug841466.html b/dom/html/test/test_bug841466.html new file mode 100644 index 0000000000..98eb9a305e --- /dev/null +++ b/dom/html/test/test_bug841466.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=841466 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 841466</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"> + <script> + /** Test for Bug 841466 **/ +var els = ['button', 'fieldset', 'input', 'object', 'output', 'select', 'textarea']; +var code = "try { is(foo, 'bar', 'expected value bar from expando on element ' + localName); } catch (e) { ok(false, String(e)); }"; +els.forEach(function(el) { + var f = document.createElement("form"); + f.foo = "bar"; + f.innerHTML = '<' + el + ' onclick="' + code + '">'; + var e = f.firstChild + e.dispatchEvent(new Event("click")); +}) + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=841466">Mozilla Bug 841466</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug845057.html b/dom/html/test/test_bug845057.html new file mode 100644 index 0000000000..ef0d45d9ed --- /dev/null +++ b/dom/html/test/test_bug845057.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=845057 +--> +<head> + <title>Test for Bug 845057</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=845057">Mozilla Bug 845057</a> +<p id="display"></p> +<div id="content"> + <iframe id="iframe" sandbox="allow-scripts"></iframe> + +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + var iframe = document.getElementById("iframe"), + attr = iframe.sandbox; + // Security enforcement tests for iframe sandbox are in test_iframe_* + + function eq(a, b) { + // check if two attributes are qual modulo permutation + return ((a+'').split(" ").sort()+'') == ((b+'').split(" ").sort()+''); + } + + ok(attr instanceof DOMTokenList, + "Iframe sandbox attribute is instace of DOMTokenList"); + ok(eq(attr, "allow-scripts") && + eq(iframe.getAttribute("sandbox"), "allow-scripts"), + "Stringyfied sandbox attribute is same as that of the DOM element"); + + ok(attr.contains("allow-scripts") && !attr.contains("allow-same-origin"), + "Set membership of attribute elements is ok"); + + attr.add("allow-same-origin"); + + ok(attr.contains("allow-scripts") && attr.contains("allow-same-origin"), + "Attribute contains added atom"); + ok(eq(attr, "allow-scripts allow-same-origin") && + eq(iframe.getAttribute("sandbox"), "allow-scripts allow-same-origin"), + "Stringyfied attribute with new atom is correct"); + + attr.add("allow-forms"); + attr.remove("allow-scripts"); + + ok(!attr.contains("allow-scripts") && attr.contains("allow-forms") && + attr.contains("allow-same-origin"), + "Attribute does not contain removed atom"); + ok(eq(attr, "allow-forms allow-same-origin") && + eq(iframe.getAttribute("sandbox"), "allow-forms allow-same-origin"), + "Stringyfied attribute with removed atom is correct"); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_bug869040.html b/dom/html/test/test_bug869040.html new file mode 100644 index 0000000000..c7edcd89d9 --- /dev/null +++ b/dom/html/test/test_bug869040.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=869040 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 869040</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=869040">Mozilla Bug 869040</a> +<p id="display"></p> +<div id="content" style="display: none" data-foo="present1" data-bar="present2"> + +</div> +<pre id="test"> +</pre> + <script type="application/javascript"> + + /** Test for Bug 869040 **/ + var foo = "default1"; + var dataset = $("content").dataset; + for (var i = 0; i < 100000; ++i) + foo = dataset.foo; + + var bar = "default2"; + for (var j = 0; j < 100; ++j) + bar = dataset.bar; + + is(foo, "present1", "Our IC should work"); + is(bar, "present2", "Our non-IC case should work"); + </script> +</body> +</html> diff --git a/dom/html/test/test_bug870787.html b/dom/html/test/test_bug870787.html new file mode 100644 index 0000000000..d6f66dda32 --- /dev/null +++ b/dom/html/test/test_bug870787.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=870787 +--> +<head> + <title>Test for Bug 870787</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=870787">Mozilla Bug 870787</a> + +<p id="msg"></p> + +<form id="form0"></form> +<img name="img0" id="img0id"> + +<img name="img1" id="img1id" /> +<form id="form1"> + <img name="img2" id="img2id" /> +</form> +<img name="img3" id="img3id" /> + +<table> + <form id="form2"> + <tr><td> + <button name="input1" id="input1id" /> + <input name="input2" id="input2id" /> + </form> +</table> + +<table> + <form id="form3"> + <tr><td> + <img name="img4" id="img4id" /> + <img name="img5" id="img5id" /> + </form> +</table> + +<form id="form4"><img id="img6"></form> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 870787 **/ + +var form0 = document.getElementById("form0"); +ok(form0, "Form0 exists"); +ok(!form0.img0, "Form0.img0 doesn't exist"); +ok(!form0.img0id, "Form0.img0id doesn't exist"); + +var form1 = document.getElementById("form1"); +ok(form1, "Form1 exists"); +ok(!form1.img1, "Form1.img1 doesn't exist"); +ok(!form1.img1id, "Form1.img1id doesn't exist"); +is(form1.img2, document.getElementById("img2id"), "Form1.img2 exists"); +is(form1.img2id, document.getElementById("img2id"), "Form1.img2id exists"); +ok(!form1.img3, "Form1.img3 doesn't exist"); +ok(!form1.img3id, "Form1.img3id doesn't exist"); + +var form2 = document.getElementById("form2"); +ok(form2, "Form2 exists"); +is(form2.input1, document.getElementById("input1id"), "Form2.input1 exists"); +is(form2.input1id, document.getElementById("input1id"), "Form2.input1id exists"); +is(form2.input2, document.getElementById("input2id"), "Form2.input2 exists"); +is(form2.input2id, document.getElementById("input2id"), "Form2.input2id exists"); + +var form3 = document.getElementById("form3"); +ok(form3, "Form3 exists"); +is(form3.img4, document.getElementById("img4id"), "Form3.img4 doesn't exists"); +is(form3.img4id, document.getElementById("img4id"), "Form3.img4id doesn't exists"); +is(form3.img5, document.getElementById("img5id"), "Form3.img5 doesn't exists"); +is(form3.img5id, document.getElementById("img5id"), "Form3.img5id doesn't exists"); + +var form4 = document.getElementById("form4"); +ok(form4, "Form4 exists"); +is(Object.getOwnPropertyNames(form4.elements).indexOf("img6"), -1, "Form4.elements should not contain img6"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug871161.html b/dom/html/test/test_bug871161.html new file mode 100644 index 0000000000..c4512621b6 --- /dev/null +++ b/dom/html/test/test_bug871161.html @@ -0,0 +1,37 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=871161 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 871161</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 871161 **/ + SimpleTest.waitForExplicitFinish(); + + window.onmessage = function(e) { + is(e.data, "windows-1252", "Wrong charset"); + e.source.close(); + SimpleTest.finish(); + } + + function run() { + window.open("file_bug871161-1.html"); + } + + </script> +</head> +<body onload="run();"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=871161">Mozilla Bug 871161</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug874758.html b/dom/html/test/test_bug874758.html new file mode 100644 index 0000000000..fa77225ba6 --- /dev/null +++ b/dom/html/test/test_bug874758.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html data-expando-prop="xyz"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=874758 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 874758</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 874758 **/ + Object.prototype.expandoProp = 5; + is({}.expandoProp, 5, "Should see this on random objects"); + + is(document.head.dataset.expandoProp, 5, "Should see this on dataset too"); + is(document.documentElement.dataset.expandoProp, "xyz", + "But if the dataset has it, we should get it from there"); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=874758">Mozilla Bug 874758</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug879319.html b/dom/html/test/test_bug879319.html new file mode 100644 index 0000000000..692f880449 --- /dev/null +++ b/dom/html/test/test_bug879319.html @@ -0,0 +1,92 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=879319 +--> +<head> + <title>Test for Bug 879319</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=879319">Mozilla Bug 879319</a> + +<p id="msg"></p> + +<form id="form"> + <img id="img0" name="bar0" /> +</form> +<input id="input0" name="foo0" form="form" /> +<input id="input1" name="foo1" form="form" /> +<input id="input2" name="foo2" form="form" /> + +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 879319 **/ + +var input0 = document.getElementById("input0"); +ok(input0, "input0 exists"); + +var form = document.getElementById("form"); +ok(form, "form exists"); +is(form.foo0, input0, "Form.foo0 should exist"); + +ok("foo0" in form.elements, "foo0 in form.elements"); +is(input0.form, form, "input0.form is form"); + +input0.setAttribute("name", "tmp0"); +ok("tmp0" in form.elements, "tmp0 is in form.elements"); +ok(!("foo0" in form.elements), "foo0 is not in form.elements"); +is(form.tmp0, input0, "Form.tmp0 == input0"); +is(form.foo0, input0, "Form.foo0 is still here"); + +input0.setAttribute("name", "tmp1"); +ok("tmp1" in form.elements, "tmp1 is in form.elements"); +ok(!("tmp0" in form.elements), "tmp0 is not in form.elements"); +ok(!("foo0" in form.elements), "foo0 is not in form.elements"); +is(form.tmp0, input0, "Form.tmp0 == input0"); +is(form.tmp1, input0, "Form.tmp1 == input0"); +is(form.foo0, input0, "Form.foo0 is still here"); + +input0.setAttribute("form", ""); +ok(!("foo0" in form.elements), "foo0 is not in form.elements"); +is(form.foo0, undefined, "Form.foo0 should not still be here"); +is(form.tmp0, undefined, "Form.tmp0 should not still be here"); +is(form.tmp1, undefined, "Form.tmp1 should not still be here"); + +var input1 = document.getElementById("input1"); +ok(input1, "input1 exists"); +is(form.foo1, input1, "Form.foo1 should exist"); + +ok("foo1" in form.elements, "foo1 in form.elements"); +is(input1.form, form, "input1.form is form"); + +input1.setAttribute("name", "foo0"); +ok("foo0" in form.elements, "foo0 is in form.elements"); +is(form.foo0, input1, "Form.foo0 should be input1"); +is(form.foo1, input1, "Form.foo1 should be input1"); + +var input2 = document.getElementById("input2"); +ok(input2, "input2 exists"); +is(form.foo2, input2, "Form.foo2 should exist"); +input2.remove(); +ok(!("foo2" in form.elements), "foo2 is not in form.elements"); +is(form.foo2, undefined, "Form.foo2 should not longer be there"); + +var img0 = document.getElementById("img0"); +ok(img0, "img0 exists"); +is(form.bar0, img0, "Form.bar0 should exist"); + +img0.setAttribute("name", "old_bar0"); +is(form.old_bar0, img0, "Form.bar0 is still here"); +is(form.bar0, img0, "Form.bar0 is still here"); + +img0.remove(); +is(form.bar0, undefined, "Form.bar0 should not be here"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug885024.html b/dom/html/test/test_bug885024.html new file mode 100644 index 0000000000..96f1783910 --- /dev/null +++ b/dom/html/test/test_bug885024.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html data-expando-prop="xyz"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=885024 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 885024</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=885024">Mozilla Bug 885024</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> + +<img form="t"> + +<form id="form"> + <div id="div"></div> +</form> + +<pre id="test"> + <script type="application/javascript"> + var img = document.createElement('img'); + img.setAttribute('id', 'img'); + + var div = document.getElementById('div'); + div.appendChild(img); + + var form = document.getElementById('form'); + ok(form, "form exists"); + ok(form.img, "form.img exists"); + + var img2 = document.createElement('img'); + img2.setAttribute('id', 'img2'); + img2.setAttribute('form', 'blabla'); + ok(form, "form exists2"); + div.appendChild(img2); + ok(form.img2, "form.img2 exists"); + + </script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug893537.html b/dom/html/test/test_bug893537.html new file mode 100644 index 0000000000..5935529d87 --- /dev/null +++ b/dom/html/test/test_bug893537.html @@ -0,0 +1,45 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=893537 +--> + <head> +<title>Test for crash caused by unloading and reloading srcdoc iframes</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=893537">Mozilla Bug 893537</a> + +<iframe id="pframe" src="file_bug893537.html"></iframe> + +<pre id="test"> +<script> + <!-- Bug 895303 --> + SimpleTest.expectAssertions(0, 1); + + SimpleTest.waitForExplicitFinish(); + var pframe = $("pframe"); + + var loadState = 1; + pframe.contentWindow.addEventListener("load", function () { + + if (loadState == 1) { + var iframe = pframe.contentDocument.getElementById("iframe"); + iframe.removeAttribute("srcdoc"); + loadState = 2; + } + if (loadState == 2) { + SimpleTest.executeSoon(function () { pframe.contentWindow.location.reload() }); + loadState = 3; + } + if (loadState == 3) { + ok(true, "This is a mochitest implementation of a crashtest. To finish is to pass"); + SimpleTest.finish(); + } + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug95530.html b/dom/html/test/test_bug95530.html new file mode 100644 index 0000000000..c4a3078d3e --- /dev/null +++ b/dom/html/test/test_bug95530.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=95530 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 95530</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 95530 **/ + function run() { + is(document.compatMode, "CSS1Compat", "Ensure we are in standards mode, not quirks mode."); + + var body = document.getElementsByTagName("body"); + + is(computedStyle(body[0],"margin-top"), "100px", "Ensure margin-top matches topmargin"); + is(computedStyle(body[0],"margin-bottom"), "150px", "Ensure margin-bottom matches bottommargin"); + is(computedStyle(body[0],"margin-left"), "23px", "Ensure margin-left matches leftmargin"); + is(computedStyle(body[0],"margin-right"), "64px", "Ensure margin-right matches rightmargin"); + SimpleTest.finish(); + } + SimpleTest.waitForExplicitFinish(); + window.addEventListener("load", run); + </script> +</head> +<body topmargin="100" bottommargin="150" leftmargin="23" rightmargin="64"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=95530">Mozilla Bug 95530</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug969346.html b/dom/html/test/test_bug969346.html new file mode 100644 index 0000000000..5be76c46ec --- /dev/null +++ b/dom/html/test/test_bug969346.html @@ -0,0 +1,33 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=969346 +--> +<head> +<title>Nesting of srcdoc iframes is permitted</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=969349">Mozilla Bug 969346</a> + +<iframe id="pframe" srcdoc="<iframe id='iframe' srcdoc='I am nested'></iframe"></iframe> + +<pre id="test"> +<script> + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function () { + var pframe = $("pframe"); + var pframeDoc = pframe.contentDocument; + var iframe = pframeDoc.getElementById("iframe"); + var innerDoc = iframe.contentDocument; + + is(innerDoc.body.innerHTML, "I am nested", "Nesting not working?"); + SimpleTest.finish(); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_bug982039.html b/dom/html/test/test_bug982039.html new file mode 100644 index 0000000000..6b158413bc --- /dev/null +++ b/dom/html/test/test_bug982039.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=982039 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 982039</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 982039 **/ + SimpleTest.waitForExplicitFinish(); + function test() { + var f = document.getElementById("testform"); + f.elements[0].disabled = true; + is(f.checkValidity(), false, + "Setting a radiobutton to disabled shouldn't make form valid."); + + f.elements[1].checked = true; + ok(f.checkValidity(), "Form should be now valid."); + + f.elements[0].required = false; + f.elements[1].required = false; + f.elements[2].required = false; + SimpleTest.finish(); + } + + </script> +</head> +<body onload="test()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=982039">Mozilla Bug 982039</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +<form action="#" id="testform"> + <input type="radio" name="radio" value="1" required> + <input type="radio" name="radio" value="2" required> + <input type="radio" name="radio" value="3" required> +</form> +</body> +</html> diff --git a/dom/html/test/test_change_crossorigin.html b/dom/html/test/test_change_crossorigin.html new file mode 100644 index 0000000000..303aac7bea --- /dev/null +++ b/dom/html/test/test_change_crossorigin.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=696451 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 696451</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 696451 **/ + + SimpleTest.waitForExplicitFinish(); + + var img = new Image, + canvas = document.createElement("canvas"), + ctx = canvas.getContext("2d"), + src = "http://example.com/tests/dom/html/test/image-allow-credentials.png", + imgDone = false, + imgNotAllowedToLoadDone = false; + + img.src = src; + img.crossOrigin = "Anonymous"; + + img.addEventListener("load", function() { + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage( img, 0, 0 ); + try { + canvas.toDataURL("image/png"); + ok(true, "Image was refetched with setting crossOrigin."); + } catch (e) { + ok(false, "Image was not refetched after setting crossOrigin."); + } + + imgDone = true; + if (imgDone && imgNotAllowedToLoadDone) { + SimpleTest.finish(); + } + }); + + img.addEventListener("error", function (event) { + ok(false, "Should be able to load cross origin image with proper headers."); + + imgDone = true; + if (imgDone && imgNotAllowedToLoadDone) { + SimpleTest.finish(); + } + }); + + var imgNotAllowedToLoad = new Image; + + imgNotAllowedToLoad.src = "http://example.com/tests/dom/html/test/image.png"; + + imgNotAllowedToLoad.crossOrigin = "Anonymous"; + + imgNotAllowedToLoad.addEventListener("load", function() { + ok(false, "Image should not be allowed to load without " + + "allow-cross-origin-access headers."); + + imgNotAllowedToLoadDone = true; + if (imgDone && imgNotAllowedToLoadDone) { + SimpleTest.finish(); + } + }); + + imgNotAllowedToLoad.addEventListener("error", function() { + ok(true, "Image should not be allowed to load without " + + "allow-cross-origin-access headers."); + imgNotAllowedToLoadDone = true; + if (imgDone && imgNotAllowedToLoadDone) { + SimpleTest.finish(); + } + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=696451">Mozilla Bug 696451</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_checked.html b/dom/html/test/test_checked.html new file mode 100644 index 0000000000..d69dcf2a28 --- /dev/null +++ b/dom/html/test/test_checked.html @@ -0,0 +1,347 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=418756 +--> +<head> + <title>Test for Bug 418756</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +Mozilla bug +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418756">418756</a> +<p id="display"></p> +<div id="content"> + <form id="f1"> + </form> + <form id="f2"> + </form> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 418756 **/ +var group1; +var group2; +var group3; + +function bounce(node) { + let n = node.nextSibling; + let p = node.parentNode; + p.removeChild(node); + p.insertBefore(node, n); +} + +var createdNodes = []; + +function cleanup() { + for (let node of createdNodes) { + if (node.parentNode) { + node.remove(); + } + } + + createdNodes = []; +} + +var typeMapper = { + 'c': 'checkbox', + 'r': 'radio' +}; + +var id = 0; + +// type can be 'c' for 'checkbox' and 'r' for 'radio' +function createNode(type, name, checked) { + let node = document.createElement("input"); + node.setAttribute("type", typeMapper[type]); + if (checked) { + node.setAttribute("checked", "checked"); + } + node.setAttribute("id", type + (++id)); + node.setAttribute("name", name); + createdNodes.push(node); + return node; +} + +var types = ['c', 'r']; + +// First make sure that setting .checked makes .defaultChecked changes no +// longer affect .checked. +for (let type of types) { + let n = createNode(type, '', false); + is(n.defaultChecked, false, "Bogus defaultChecked on " + typeMapper[type]); + is(n.checked, false, "Bogus checked on " + typeMapper[type]); + n.defaultChecked = true; + is(n.defaultChecked, true, "Bogus defaultChecked on " + typeMapper[type] + + "after mutation"); + is(n.checked, true, "Bogus checked on " + typeMapper[type] + + "after mutation"); + n.checked = false; + is(n.defaultChecked, true, "Bogus defaultChecked on " + typeMapper[type] + + "after second mutation"); + is(n.checked, false, "Bogus checked on " + typeMapper[type] + + "after second mutation"); + n.defaultChecked = false; + is(n.defaultChecked, false, "Bogus defaultChecked on " + typeMapper[type] + + "after third mutation"); + is(n.checked, false, "Bogus checked on " + typeMapper[type] + + "after third mutation"); + n.defaultChecked = true; + is(n.defaultChecked, true, "Bogus defaultChecked on " + typeMapper[type] + + "after fourth mutation"); + is(n.checked, false, "Bogus checked on " + typeMapper[type] + + "after fourth mutation"); +} + +cleanup(); + +// Now check that bouncing a control that's the only one of its kind has no +// effect +for (let type of types) { + let n = createNode(type, 'test1', true); + $("f1").appendChild(n); + n.checked = false; + n.defaultChecked = false; + bounce(n); + n.defaultChecked = true; + is(n.checked, false, "We set .checked on this " + typeMapper[type]); +} + +cleanup(); + +// Now check that playing with a single radio in a group affects all +// other radios in the group (but not radios not in that group) +group1 = [ createNode('r', 'g1', false), + createNode('r', 'g1', false), + createNode('r', 'g1', false) ]; +group2 = [ createNode('r', 'g2', false), + createNode('r', 'g2', false), + createNode('r', 'g2', false) ]; +group3 = [ createNode('r', 'g1', false), + createNode('r', 'g1', false), + createNode('r', 'g1', false) ]; +for (let g of group1) { + $("f1").appendChild(g); +} +for (let g of group2) { + $("f1").appendChild(g); +} +for (let g of group3) { + $("f2").appendChild(g); +} + +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, false, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 1"); + is(g.checked, false, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checkedhecked wrong pass 1"); + } +} + +group1[1].defaultChecked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1 && group1.indexOf(g) == 1, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 2"); + is(g.checked, n == 1 && group1.indexOf(g) == 1, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 2"); + } +} + +group1[0].defaultChecked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 1 || + group1.indexOf(g) == 0), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 3"); + is(g.checked, n == 1 && group1.indexOf(g) == 0, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 3"); + } +} + +group1[2].defaultChecked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 4"); + is(g.checked, n == 1 && group1.indexOf(g) == 2, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 4"); + } +} + +var next = group1[1].nextSibling; +var p = group1[1].parentNode; +p.removeChild(group1[1]); +group1[1].defaultChecked = false; +group1[1].defaultChecked = true; +p.insertBefore(group1[1], next); +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 5"); + is(g.checked, n == 1 && group1.indexOf(g) == 1, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 5"); + } +} + +for (let g of group1) { + g.defaultChecked = false; +} +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, false, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 6"); + is(g.checked, false, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checkedhecked wrong pass 6"); + } +} + +group1[1].checked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, false, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 7"); + is(g.checked, n == 1 && group1.indexOf(g) == 1, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 7"); + } +} + +group1[0].defaultChecked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1 && group1.indexOf(g) == 0, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 8"); + is(g.checked, n == 1 && group1.indexOf(g) == 1, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 8"); + } +} + +group1[2].defaultChecked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 || + group1.indexOf(g) == 2), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 9"); + is(g.checked, n == 1 && group1.indexOf(g) == 1, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 9"); + } +} +group1[1].remove(); +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 || + group1.indexOf(g) == 2), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 10"); + is(g.checked, n == 1 && group1.indexOf(g) == 1, + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 10"); + } +} + +group1[2].checked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 || + group1.indexOf(g) == 2), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 11"); + is(g.checked, n == 1 && (group1.indexOf(g) == 1 || + group1.indexOf(g) == 2), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 11"); + } +} + +group1[0].checked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 || + group1.indexOf(g) == 2), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 12"); + is(g.checked, n == 1 && (group1.indexOf(g) == 1 || + group1.indexOf(g) == 0), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 12"); + } +} + +next = group2[1].nextSibling; +p = group2[1].parentNode; +p.removeChild(group2[1]); +p.insertBefore(group2[1], next); +group2[0].checked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 || + group1.indexOf(g) == 2), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 13"); + is(g.checked, (n == 1 && (group1.indexOf(g) == 1 || + group1.indexOf(g) == 0)) || + (n == 2 && group2.indexOf(g) == 0), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 13"); + } +} + +p.insertBefore(group2[1], next); +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, n == 1 && (group1.indexOf(g) == 0 || + group1.indexOf(g) == 2), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 14"); + is(g.checked, (n == 1 && (group1.indexOf(g) == 1 || + group1.indexOf(g) == 0)) || + (n == 2 && group2.indexOf(g) == 0), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 14"); + } +} + +group2[1].defaultChecked = true; +for (let n of [1, 2, 3]) { + for (let g of window["group"+n]) { + is(g.defaultChecked, (n == 1 && (group1.indexOf(g) == 0 || + group1.indexOf(g) == 2)) || + (n == 2 && group2.indexOf(g) == 1), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] defaultChecked wrong pass 15"); + is(g.checked, (n == 1 && (group1.indexOf(g) == 1 || + group1.indexOf(g) == 0)) || + (n == 2 && group2.indexOf(g) == 0), + "group" + n + "[" + window["group"+n].indexOf(g) + + "] checked wrong pass 15"); + } +} + +cleanup(); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_dir_attributes_reflection.html b/dom/html/test/test_dir_attributes_reflection.html new file mode 100644 index 0000000000..3aefaef9a5 --- /dev/null +++ b/dom/html/test/test_dir_attributes_reflection.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLDirectoryElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLDirectoryElement attributes reflection **/ + +// .name +reflectBoolean({ + element: document.createElement("dir"), + attribute: "compact", +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_dl_attributes_reflection.html b/dom/html/test/test_dl_attributes_reflection.html new file mode 100644 index 0000000000..100b28e9fb --- /dev/null +++ b/dom/html/test/test_dl_attributes_reflection.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLDListElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLDListElement attributes reflection **/ + +// .compact +reflectBoolean({ + element: document.createElement("dl"), + attribute: "compact" +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_document-element-inserted.html b/dom/html/test/test_document-element-inserted.html new file mode 100644 index 0000000000..6d7e8695ce --- /dev/null +++ b/dom/html/test/test_document-element-inserted.html @@ -0,0 +1,54 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Media test: document-element-inserted</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<iframe id = 'media'> +</iframe> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +if (navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 4); +} + +SimpleTest.waitForExplicitFinish(); +var loc; + +var observe = function(doc){ + if (doc == media.contentDocument) { + ok(media.contentDocument.location.toString().includes(loc), + "The loaded media should be " + loc); + next(); + } +} + +var media = document.getElementById('media'); +var tests = [ + "../../../media/test/short-video.ogv", + "../../../media/test/sound.ogg", + "../../content/test/image.png" +] + +function next() { + if (tests.length) { + var t = tests.shift(); + loc = t.substring(t.indexOf("test")); + media.setAttribute("src",t); + } + else { + SpecialPowers.removeObserver(observe, "document-element-inserted"); + SimpleTest.finish(); + } +} + +SpecialPowers.addObserver(observe, "document-element-inserted") +next(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_documentAll.html b/dom/html/test/test_documentAll.html new file mode 100644 index 0000000000..f3bb7e7df6 --- /dev/null +++ b/dom/html/test/test_documentAll.html @@ -0,0 +1,167 @@ +<html> +<!-- +Tests for document.all +--> +<head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <title>Tests for document.all</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=259332">Mozilla Bug 259332</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=393629">Mozilla Bug 393629</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=448904">Mozilla Bug 448904</a> +<p id="display"> +</p> +<div id="content" style="display: none"> + <a id="id1">A</a> + <a id="id2">B</a> + <a id="id2">C</a> + <a id="id3">D</a> + <a id="id3">E</a> + <a id="id3">F</a> +</div> +<iframe id="subframe" srcdoc="<span id='x'></span>" + style="display: none"></iframe> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +p = document.getElementById("content"); + +// Test that several elements with the same id or name behave correctly +function testNumSame() { + is(document.all.id0, undefined, "no ids"); + is(document.all.namedItem("id0"), null, "no ids"); + is(document.all.id1, p.children[0], "one id"); + is(document.all.id2[0], p.children[1], "two ids"); + is(document.all.id2[1], p.children[2], "two ids"); + is(document.all.id2.length, 2, "two length"); + is(document.all.id3[0], p.children[3], "three ids"); + is(document.all.id3[1], p.children[4], "three ids"); + is(document.all.id3[2], p.children[5], "three ids"); + is(document.all.id3.length, 3, "three length"); +} +testNumSame(); +p.innerHTML = p.innerHTML.replace(/id=/g, "name="); +testNumSame(); + + +// Test that dynamic changes behave properly + +// Add two elements and check that they are added to the correct lists +child = Array.prototype.slice.call(p.children); +child[6] = document.createElement("a"); +child[6].id = "id0"; +p.appendChild(child[6]); +child[7] = document.createElement("a"); +child[7].id = "id1"; +p.appendChild(child[7]); +is(document.all.id0, child[6], "now one id"); +is(document.all.id1[0], child[0], "now two ids"); +is(document.all.id1[1], child[7], "now two ids"); +is(document.all.id1.length, 2, "now two length"); + +// Remove and element and check that the list shrinks +rC(child[1]); +is(document.all.id2, child[2], "now just one id"); + +// Change an id and check that its removed and added to the correct lists +child[4].name = "id1"; +is(document.all.id1[0], child[0], "now three ids"); +is(document.all.id1[1], child[4], "now three ids"); +is(document.all.id1[2], child[7], "now three ids"); +is(document.all.id1.length, 3, "now three length"); +is(document.all.id3[1], child[5], "now just two ids"); +is(document.all.id3.length, 2, "now two length"); + +// Remove all elements from a list and check that it goes empty +id3list = document.all.id3; +rC(child[3]); +is(id3list.length, 1, "now one length"); +rC(child[5]); +is(document.all.id3, undefined, "now none"); +is(document.all.namedItem("id3"), null, "now none (namedItem)"); +is(id3list.length, 0, "now none length"); + +// Give an element both a name and id and check that it appears in two lists +p.insertBefore(child[1], child[2]); // restore previously removed +id1list = document.all.id1; +id2list = document.all.id2; +child[1].id = "id1"; +is(id1list[0], child[0], "now four ids"); +is(id1list[1], child[1], "now four ids"); +is(id1list[2], child[4], "now four ids"); +is(id1list[3], child[7], "now four ids"); +is(id1list.length, 4, "now four length"); +is(id2list[0], child[1], "still two ids"); +is(id2list[1], child[2], "still two ids"); +is(id2list.length, 2, "still two length"); + + +// Check that document.all behaves list a list of all elements +allElems = document.getElementsByTagName("*"); +ok(testArraysSame(document.all, allElems), "arrays same"); +length = document.all.length; +expectedLength = length + p.getElementsByTagName("*").length + 1; +p.appendChild(p.cloneNode(true)); +ok(testArraysSame(document.all, allElems), "arrays still same"); +is(document.all.length, expectedLength, "grew correctly"); + +// Check which elements the 'name' attribute works on +var elementNames = + ['abbr','acronym','address','area','a','b','base', + 'bgsound','big','blockquote','br','canvas','center','cite','code', + 'col','colgroup','dd','del','dfn','dir','div','dir','dl','dt','em','embed', + 'fieldset','font','form','frame','frameset','head','i','iframe','img', + 'input','ins','isindex','kbd','keygen','label','li','legend','link','menu', + 'multicol','noscript','noframes','object','spacer','table','td','td','th', + 'thead','tfoot','tr','textarea','select','option','spacer','param', + 'marquee','hr','title','hx','tt','u','ul','var','wbr','sub','sup','cite', + 'code','q','nobr','ol','p','pre','s','samp','small','body','html','map', + 'bdo','legend','listing','style','script','tbody','caption','meta', + 'optgroup','button','span','strike','strong','td'].sort(); +var hasName = + ['a','embed','form','iframe','img','input','object','textarea', + 'select','map','meta','button','frame','frameset'].sort(); + +elementNames.forEach(function (name) { + nameval = 'namefor' + name; + + e = document.createElement(name); + p.appendChild(e); + e.setAttribute('name', nameval); + + if (name == hasName[0]) { + is(document.all[nameval], e, "should have name"); + hasName.shift(); + } + else { + is(document.all[nameval], undefined, "shouldn't have name"); + is(document.all.namedItem(nameval), null, "shouldn't have name (namedItem)"); + } +}); +is(hasName.length, 0, "found all names"); + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + var subdoc = $("subframe").contentDocument; + is(subdoc.all.x, subdoc.body.firstChild, + "document.all should work in a subdocument"); + SimpleTest.finish(); +}); + +// Utility functions +function rC(node) { + node.remove(); +} +function testArraysSame(a1, a2) { + return Array.prototype.every.call(a1, function(e, index) { + return a2[index] === e; + }) && a1.length == a2.length; +} +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_element_prototype.html b/dom/html/test/test_element_prototype.html new file mode 100644 index 0000000000..0cb8d9745f --- /dev/null +++ b/dom/html/test/test_element_prototype.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=844127 +--> +<head> + <title>Test for Bug 844127</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=844127">Mozilla Bug 844127</a> + +<script type="text/javascript"> + +/** Test for Bug 844127 **/ + +var a1 = document.createElement('bgsound'); +var a2 = document.createElement('image'); +var a3 = document.createElement('multicol'); +var a4 = document.createElement('spacer'); +var a5 = document.createElement('isindex'); + +is(Object.getPrototypeOf(a1), HTMLUnknownElement.prototype, "Prototype for bgsound should be correct"); +is(Object.getPrototypeOf(a2), HTMLElement.prototype, "Prototype for image should be correct"); +is(Object.getPrototypeOf(a3), HTMLUnknownElement.prototype, "Prototype for multicol should be correct"); +is(Object.getPrototypeOf(a4), HTMLUnknownElement.prototype, "Prototype for spacer should be correct"); +is(Object.getPrototypeOf(a5), HTMLUnknownElement.prototype, "Prototype for isindex should be correct"); + +</script> +</body> +</html> diff --git a/dom/html/test/test_embed_attributes_reflection.html b/dom/html/test/test_embed_attributes_reflection.html new file mode 100644 index 0000000000..44338113a7 --- /dev/null +++ b/dom/html/test/test_embed_attributes_reflection.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLEmbedElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLEmbedElement attributes reflection **/ + +// .src (URL) +reflectURL({ + element: document.createElement("embed"), + attribute: "src", +}); + +// .type (String) +reflectString({ + element: document.createElement("embed"), + attribute: "type", +}); + +// .width (String) +reflectString({ + element: document.createElement("embed"), + attribute: "width", +}); + +// .height (String) +reflectString({ + element: document.createElement("embed"), + attribute: "height", +}); + +// .align (String) +reflectString({ + element: document.createElement("embed"), + attribute: "align", +}); + +// .name (String) +reflectString({ + element: document.createElement("embed"), + attribute: "name", +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_external_protocol_iframe.html b/dom/html/test/test_external_protocol_iframe.html new file mode 100644 index 0000000000..cb99a19e1a --- /dev/null +++ b/dom/html/test/test_external_protocol_iframe.html @@ -0,0 +1,80 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for external protocol URLs blocked for iframes</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/> +</head> +<body> + <div id='foo'><a href='#'>Click here to test this issue</a></div> + <script> + +function test_initialize() { + ChromeUtils.resetLastExternalProtocolIframeAllowed(); + next(); +} + +function test_noUserInteraction() { + ok(!SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, "No user interaction yet"); + is(ChromeUtils.lastExternalProtocolIframeAllowed(), 0, "No iframe loaded before this test!"); + + for (let i = 0; i < 10; ++i) { + let ifr = document.createElement('iframe'); + ifr.src = "foo+bar:all_good"; + document.body.appendChild(ifr); + + is(ChromeUtils.getPopupControlState(), "openAbused", "No user-interaction means: abuse state"); + ok(ChromeUtils.lastExternalProtocolIframeAllowed() != 0, "We have 1 iframe loaded"); + } + + next(); +} + +function test_userInteraction() { + let foo = document.getElementById('foo'); + foo.addEventListener('click', _ => { + ok(SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, "User should've interacted"); + + for (let i = 0; i < 10; ++i) { + let ifr = document.createElement('iframe'); + ifr.src = "foo+bar:all_good"; + document.body.appendChild(ifr); + + ok(!SpecialPowers.wrap(document).hasValidTransientUserGestureActivation, "User interaction should've been consumed"); + } + + next(); + + }, {once: true}); + + setTimeout(_ => { + synthesizeMouseAtCenter(foo, {}); + }, 0); +} + +let tests = [ + test_initialize, + test_noUserInteraction, + test_userInteraction, +]; + +function next() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + let test = tests.shift(); + SimpleTest.executeSoon(test); +} + +SpecialPowers.pushPrefEnv({'set': [ + ['dom.block_external_protocol_in_iframes', true], + ['dom.delay.block_external_protocol_in_iframes.enabled', true], +]}, next); + +SimpleTest.waitForExplicitFinish(); + </script> +</body> +</html> diff --git a/dom/html/test/test_fakepath.html b/dom/html/test/test_fakepath.html new file mode 100644 index 0000000000..f9819e732f --- /dev/null +++ b/dom/html/test/test_fakepath.html @@ -0,0 +1,40 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Fakepath in HTMLInputElement</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> +<input id="file" type="file"></input> +<input id="file_wd" type="file" webkitdirectory></input> +<script type="application/javascript"> + +var url = SimpleTest.getTestFileURL("script_fakepath.js"); +script = SpecialPowers.loadChromeScript(url); + +function onOpened(message) { + var e = document.getElementById("file"); + SpecialPowers.wrap(e).mozSetDndFilesAndDirectories(message.data); + ok(e.value, "C:\\fakepath\\prefs.js"); + + e = document.getElementById("file_wd"); + SpecialPowers.wrap(e).mozSetDndFilesAndDirectories(message.data); + ok(e.value, "C:\\fakepath\\prefs.js"); + + SimpleTest.finish(); +} + +function run() { + script.addMessageListener("file.opened", onOpened); + script.sendAsyncMessage("file.open"); +} + +SpecialPowers.pushPrefEnv({"set": [["dom.webkitBlink.dirPicker.enabled", true]]}, run); + +SimpleTest.waitForExplicitFinish(); + +</script> +</body> +</html> diff --git a/dom/html/test/test_filepicker_default_directory.html b/dom/html/test/test_filepicker_default_directory.html new file mode 100644 index 0000000000..2be811655a --- /dev/null +++ b/dom/html/test/test_filepicker_default_directory.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1194893 +--> +<head> + <title>Test for filepicker default directory</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1194893">Mozilla Bug 1194893</a> +<div id="content"> + <input type="file" id="f"> +</div> +<pre id="text"> +<script class="testbody" type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); +const { Cc: Cc, Ci: Ci } = SpecialPowers; + +// Platform-independent directory names are #define'd in xpcom/io/nsDirectoryServiceDefs.h + +// When we want to test an upload directory other than the default, we need to +// get a valid directory in a platform-independent way. Since NS_OS_DESKTOP_DIR +// may fallback to NS_OS_HOME_DIR, let's use NS_OS_TMP_DIR. +var customUploadDirectory = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("TmpD", Ci.nsIFile); + +// Useful for debugging +//info("customUploadDirectory" + customUploadDirectory.path); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +// need to show the MockFilePicker so .displayDirectory gets set +var f = document.getElementById("f"); +f.focus(); + +var testIndex = 0; +var tests = [ + ["", null, "Desk"], + [customUploadDirectory.path, customUploadDirectory.path, ""] +] + +MockFilePicker.showCallback = function(filepicker) { + if (tests[testIndex][1] === null) { + is(SpecialPowers.wrap(MockFilePicker).displayDirectory, null, "DisplayDirectory is null"); + } else { + is(SpecialPowers.wrap(MockFilePicker).displayDirectory.path, tests[testIndex][1], "DisplayDirectory matches the path"); + } + + is(SpecialPowers.wrap(MockFilePicker).displaySpecialDirectory, tests[testIndex][2], "DisplaySpecialDirectory matches the path"); + + if (++testIndex == tests.length) { + MockFilePicker.cleanup(); + SimpleTest.finish(); + } else { + launchNextTest(); + } +} + +function launchNextTest() { + SpecialPowers.pushPrefEnv( + { 'set': [ + ['dom.input.fallbackUploadDir', tests[testIndex][0]], + ]}, + function () { + f.click(); + }); +} + +launchNextTest(); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_focusshift_button.html b/dom/html/test/test_focusshift_button.html new file mode 100644 index 0000000000..90fadd4827 --- /dev/null +++ b/dom/html/test/test_focusshift_button.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for shifting focus while mouse clicking on button</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> + +<script class="testbody" type="application/javascript"> + +var result = ""; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.waitForFocus(function() { + synthesizeMouseAtCenter(document.getElementById("button"), { }); + is(result, "(focus button)(blur button)(focus input)", "Focus button then input"); + SimpleTest.finish(); +}); +</script> + + +<button id="button" onfocus="result += '(focus button)'; document.getElementById('input').focus()" + onblur="result += '(blur button)'">Focus</button> +<input id="input" value="Test" onfocus="result += '(focus input)'" + onblur="result += '(blur input)'"> + +</body> +</html> diff --git a/dom/html/test/test_form-parsing.html b/dom/html/test/test_form-parsing.html new file mode 100644 index 0000000000..7c4acca756 --- /dev/null +++ b/dom/html/test/test_form-parsing.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Form parsing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content" style="display: none"> + <div> + <form name="test" action="test" id="test"> + <input type="text" name="text1" id="text1" /> + <input type="text" name="text2" id="text2" /> + </div> + <input type="text" name="text3" id="text3" /> + <input type="text" name="text4" id="text4" /> + <input type="text" name="text5" id="text5" /> + <input type="text" name="text6" id="text6" /> + </form> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + + var form1 = document.getElementById("test"); + var elem1 = form1.getElementsByTagName("*"); + var elem1Length = elem1.length; + var form1ElementsLength = form1.elements.length; + + is(form1ElementsLength, 6, "form.elements must include mis-nested elements"); + is(elem1Length, 2, "form must not include mis-nested elements"); +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_formData.html b/dom/html/test/test_formData.html new file mode 100644 index 0000000000..4518f37cf5 --- /dev/null +++ b/dom/html/test/test_formData.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=690659 +--> +<head> + <title>Test for Bug 690659 and 739173</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=690659">Mozilla Bug 690659 & 739173</a> +<script type="text/javascript" src="./formData_test.js"></script> +<script type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +function runMainThreadAndWorker() { + var mt = new Promise(function(resolve) { + runTest(resolve); + }); + + var worker; + var w = new Promise(function(resolve) { + worker = new Worker("formData_worker.js"); + worker.onmessage = function(event) { + if (event.data.type == 'finish') { + resolve(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + } else if (event.data.type == 'todo') { + todo(event.data.status, event.data.msg); + } + } + + worker.onerror = function(event) { + ok(false, "Worker had an error: " + event.message + " at " + event.lineno); + resolve(); + }; + + worker.postMessage(true); + }); + + return Promise.all([mt, w]); +} + +runMainThreadAndWorker().then(SimpleTest.finish); +</script> +</body> +</html> diff --git a/dom/html/test/test_formSubmission.html b/dom/html/test/test_formSubmission.html new file mode 100644 index 0000000000..952f65e6dd --- /dev/null +++ b/dom/html/test/test_formSubmission.html @@ -0,0 +1,910 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=523771 +--> +<head> + <title>Test for Bug 523771</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8"> +</head> +<body onload="bodyLoaded()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=523771">Mozilla Bug 523771</a> +<p id="display"></p> +<iframe name="target_iframe" id="target_iframe"></iframe> +<form action="form_submit_server.sjs" target="target_iframe" id="form" + method="POST" enctype="multipart/form-data"> + <table> + <tr> + <td>Control type</td> + <td>Name and value</td> + <td>Name, empty value</td> + <td>Name, no value</td> + <td>Empty name, with value</td> + <td>No name, with value</td> + <td>No name or value</td> + <td>Strange name/value</td> + </tr> + <tr> + <td>Default input</td> + <td><input name="n1_1" value="v1_1"></td> + <td><input name="n1_2" value=""></td> + <td><input name="n1_3"></td> + <td><input name="" value="v1_4"></td> + <td><input value="v1_5"></td> + <td><input></td> + <td><input name="n1_7_ _ _ _ _"" + value="v1_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Text input</td> + <td><input type=text name="n2_1" value="v2_1"></td> + <td><input type=text name="n2_2" value=""></td> + <td><input type=text name="n2_3"></td> + <td><input type=text name="" value="v2_4"></td> + <td><input type=text value="v2_5"></td> + <td><input type=text></td> + <td><input type=text name="n2_7_ _ _ _ _"" + value="v2_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Checkbox unchecked</td> + <td><input type=checkbox name="n3_1" value="v3_1"></td> + <td><input type=checkbox name="n3_2" value=""></td> + <td><input type=checkbox name="n3_3"></td> + <td><input type=checkbox name="" value="v3_4"></td> + <td><input type=checkbox value="v3_5"></td> + <td><input type=checkbox></td> + <td><input type=checkbox name="n3_7_ _ _ _ _"" + value="v3_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Checkbox checked</td> + <td><input checked type=checkbox name="n4_1" value="v4_1"></td> + <td><input checked type=checkbox name="n4_2" value=""></td> + <td><input checked type=checkbox name="n4_3"></td> + <td><input checked type=checkbox name="" value="v4_4"></td> + <td><input checked type=checkbox value="v4_5"></td> + <td><input checked type=checkbox></td> + <td><input checked type=checkbox + name="n4_7_ _ _ _ _"" + value="v4_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Radio unchecked</td> + <td><input type=radio name="n5_1" value="v5_1"></td> + <td><input type=radio name="n5_2" value=""></td> + <td><input type=radio name="n5_3"></td> + <td><input type=radio name="" value="v5_4"></td> + <td><input type=radio value="v5_5"></td> + <td><input type=radio></td> + <td><input type=radio name="n5_7_ _ _ _ _"" + value="v5_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Radio checked</td> + <td><input checked type=radio name="n6_1" value="v6_1"></td> + <td><input checked type=radio name="n6_2" value=""></td> + <td><input checked type=radio name="n6_3"></td> + <td><input checked type=radio name="" value="v6_4"></td> + <td><input checked type=radio value="v6_5"></td> + <td><input checked type=radio></td> + <td><input checked type=radio + name="n6_7_ _ _ _ _"" + value="v6_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Hidden input</td> + <td><input type=hidden name="n7_1" value="v7_1"></td> + <td><input type=hidden name="n7_2" value=""></td> + <td><input type=hidden name="n7_3"></td> + <td><input type=hidden nane="" value="v7_4"></td> + <td><input type=hidden value="v7_5"></td> + <td><input type=hidden></td> + <td><input type=hidden name="n7_7_ _ _ _ _"" + value="v7_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Password input</td> + <td><input type=password name="n8_1" value="v8_1"></td> + <td><input type=password name="n8_2" value=""></td> + <td><input type=password name="n8_3"></td> + <td><input type=password name="" value="v8_4"></td> + <td><input type=password value="v8_5"></td> + <td><input type=password></td> + <td><input type=password name="n8_7_ _ _ _ _"" + value="v8_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Submit input</td> + <td><input type=submit name="n9_1" value="v9_1"></td> + <td><input type=submit name="n9_2" value=""></td> + <td><input type=submit name="n9_3"></td> + <td><input type=submit name="" value="v9_4"></td> + <td><input type=submit value="v9_5"></td> + <td><input type=submit></td> + <td><input type=submit name="n9_7_ _ _ _ _"" + value="v9_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Button input</td> + <td><input type=button name="n10_1" value="v10_1"></td> + <td><input type=button name="n10_2" value=""></td> + <td><input type=button name="n10_3"></td> + <td><input type=button name="" value="v10_4"></td> + <td><input type=button value="v10_5"></td> + <td><input type=button></td> + <td><input type=button name="n10_7_ _ _ _ _"" + value="v10_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Image input</td> + <td><input type=image src="file_formSubmission_img.jpg" name="n11_1" value="v11_1"></td> + <td><input type=image src="file_formSubmission_img.jpg" name="n11_2" value=""></td> + <td><input type=image src="file_formSubmission_img.jpg" name="n11_3"></td> + <td><input type=image src="file_formSubmission_img.jpg" name="" value="v11_4"></td> + <td><input type=image src="file_formSubmission_img.jpg" value="v11_5"></td> + <td><input type=image src="file_formSubmission_img.jpg"></td> + <td><input type=image src="file_formSubmission_img.jpg" + name="n11_7_ _ _ _ _"" + value="v11_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Reset input</td> + <td><input type=reset name="n12_1" value="v12_1"></td> + <td><input type=reset name="n12_2" value=""></td> + <td><input type=reset name="n12_3"></td> + <td><input type=reset name="" value="v12_4"></td> + <td><input type=reset value="v12_5"></td> + <td><input type=reset></td> + <td><input type=reset name="n12_7_ _ _ _ _"" + value="v12_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Unknown input</td> + <td><input type=foobar name="n13_1" value="v13_1"></td> + <td><input type=foobar name="n13_2" value=""></td> + <td><input type=foobar name="n13_3"></td> + <td><input type=foobar name="" value="v13_4"></td> + <td><input type=foobar value="v13_5"></td> + <td><input type=foobar></td> + <td><input type=foobar name="n13_7_ _ _ _ _"" + value="v13_7_ _ _ _ _""></td> + </tr> + <tr> + <td>Default button</td> + <td><button name="n14_1" value="v14_1"></button></td> + <td><button name="n14_2" value=""></button></td> + <td><button name="n14_3"></button></td> + <td><button name="" value="v14_4"></button></td> + <td><button value="v14_5"></button></td> + <td><button></button></td> + <td><button name="n14_7_ _ _ _ _"" + value="v14_7_ _ _ _ _""></button></td> + </tr> + <tr> + <td>Submit button</td> + <td><button type=submit name="n15_1" value="v15_1"></button></td> + <td><button type=submit name="n15_2" value=""></button></td> + <td><button type=submit name="n15_3"></button></td> + <td><button type=submit name="" value="v15_4"></button></td> + <td><button type=submit value="v15_5"></button></td> + <td><button type=submit></button></td> + <td><button type=submit name="n15_7_ _ _ _ _"" + value="v15_7_ _ _ _ _""></button></td> + </tr> + <tr> + <td>Button button</td> + <td><button type=button name="n16_1" value="v16_1"></button></td> + <td><button type=button name="n16_2" value=""></button></td> + <td><button type=button name="n16_3"></button></td> + <td><button type=button name="" value="v16_4"></button></td> + <td><button type=button value="v16_5"></button></td> + <td><button type=button></button></td> + <td><button type=button name="n16_7_ _ _ _ _"" + value="v16_7_ _ _ _ _""></button></td> + </tr> + <tr> + <td>Reset button</td> + <td><button type=reset name="n17_1" value="v17_1"></button></td> + <td><button type=reset name="n17_2" value=""></button></td> + <td><button type=reset name="n17_3"></button></td> + <td><button type=reset name="" value="v17_4"></button></td> + <td><button type=reset value="v17_5"></button></td> + <td><button type=reset></button></td> + <td><button type=reset name="n17_7_ _ _ _ _"" + value="v17_7_ _ _ _ _""></button></td> + </tr> + <tr> + <td>Unknown button</td> + <td><button type=foobar name="n18_1" value="v18_1"></button></td> + <td><button type=foobar name="n18_2" value=""></button></td> + <td><button type=foobar name="n18_3"></button></td> + <td><button type=foobar name="" value="v18_4"></button></td> + <td><button type=foobar value="v18_5"></button></td> + <td><button type=foobar ></button></td> + <td><button type=foobar name="n18_7_ _ _ _ _"" + value="v18_7_ _ _ _ _""></button></td> + </tr> + <tr> + <td><input type='url'></td> + <td><input type=url name="n19_1" value="http://v19_1.org"></td> + <td><input type=url name="n19_2" value=""></td> + <td><input type=url name="n19_3"></td> + <td><input type=url name="" value="http://v19_4.org"></td> + <td><input type=url value="http://v19_5.org"></td> + <td><input type=url ></td> + <td><input type=url name="n19_7_ _ _ __"" + value="http://v19_7_ _ _ __""> + <!-- Put UTF-8 value in the "strange" column. --> + <input type=url name="n19_8" value="http://mózillä.órg"></td> + </tr> + <tr> + <td><input type='email'></td> + <td><input type=email name="n20_1" value="v20_1@bar"></td> + <td><input type=email name="n20_2" value=""></td> + <td><input type=email name="n20_3"></td> + <td><input type=email name="" value="v20_4@bar"></td> + <td><input type=email value="v20_5@bar"></td> + <td><input type=email ></td> + <td><input type=email name="n20_7_ _ _ __"" + value="v20_7_ _ _ __"@bar"> + <!-- Put UTF-8 value is the "strange" column. --> + <input type=email name="n20_8" value="foo@mózillä.órg"></td> + </tr> + </table> + + <p> + File input: + <input type=file name="file_1" class="setfile"> + <input type=file name="file_2"> + <input type=file name="" class="setfile"> + <input type=file name=""> + <input type=file class="setfile"> + <input type=file> + </p> + <p> + Multifile input: + <input multiple type=file name="file_3" class="setfile"> + <input multiple type=file name="file_4" class="setfile multi"> + <input multiple type=file name="file_5"> + <input multiple type=file name="" class="setfile"> + <input multiple type=file name="" class="setfile multi"> + <input multiple type=file name=""> + <input multiple type=file class="setfile"> + <input multiple type=file class="setfile multi"> + <input multiple type=file> + </p> + + <p> + Textarea: + <textarea name="t1">t_1_v</textarea> + <textarea name="t2"></textarea> + <textarea name="">t_3_v</textarea> + <textarea>t_4_v</textarea> + <textarea></textarea> + <textarea name="t6"> +t_6_v</textarea> + <textarea name="t7">t_7_v +</textarea> + <textarea name="t8"> + + t_8_v  +</textarea> + <textarea name="t9_ _ _ _ _"">t_9_ _ _ _ _"_v</textarea> + <textarea name="t10" value="t_10_bogus">t_10_v</textarea> + </p> + + <p> + Select one: + + <select name="sel_1"></select> + <select name="sel_1b"><option></option></select> + <select name="sel_1c"><option selected></option></select> + + <select name="sel_2"><option value="sel_2_v"></option></select> + <select name="sel_3"><option selected value="sel_3_v"></option></select> + + <select name="sel_4"><option value="sel_4_v1"></option><option value="sel_4_v2"></option></select> + <select name="sel_5"><option selected value="sel_5_v1"></option><option value="sel_5_v2"></option></select> + <select name="sel_6"><option value="sel_6_v1"></option><option selected value="sel_6_v2"></option></select> + + <select name="sel_7"><option>sel_7_v1</option><option>sel_7_v2</option></select> + <select name="sel_8"><option selected>sel_8_v1</option><option>sel_8_v2</option></select> + <select name="sel_9"><option>sel_9_v1</option><option selected>sel_9_v2</option></select> + + <select name="sel_10"><option value="sel_10_v1">sel_10_v1_text</option><option value="sel_10_v2">sel_10_v2_text</option></select> + <select name="sel_11"><option selected value="sel_11_v1">sel_11_v1_text</option><option value="sel_11_v2">sel_11_v2_text</option></select> + <select name="sel_12"><option value="sel_12_v1">sel_12_v1_text</option><option selected value="sel_12_v2">sel_12_v2_text</option></select> + + <select name="sel_13"><option disabled>sel_13_v1</option><option>sel_13_v2</option></select> + <select name="sel_14"><option disabled selected>sel_14_v1</option><option>sel_14_v2</option></select> + <select name="sel_15"><option disabled>sel_15_v1</option><option selected>sel_15_v2</option></select> + + <select name="sel_16"><option>sel_16_v1</option><option disabled>sel_16_v2</option></select> + <select name="sel_17"><option selected>sel_17_v1</option><option disabled>sel_17_v2</option></select> + <select name="sel_18"><option>sel_18_v1</option><option disabled selected>sel_18_v2</option></select> + + <select name=""><option selected value="sel_13_v1"></option><option value="sel_13_v2"></option></select> + <select name=""><option value="sel_14_v1"></option><option selected value="sel_14_v2"></option></select> + <select name=""><option selected>sel_15_v1</option><option>sel_15_v2</option></select> + <select name=""><option>sel_16_v1</option><option selected>sel_16_v2</option></select> + + <select><option selected value="sel_17_v1"></option><option value="sel_17_v2"></option></select> + <select><option value="sel_18_v1"></option><option selected value="sel_18_v2"></option></select> + <select><option selected>sel_19_v1</option><option>sel_19_v2</option></select> + <select><option>sel_20_v1</option><option selected>sel_20_v2</option></select> + </p> + + <p> + Select multiple: + + <select multiple name="msel_1"></select> + <select multiple name="msel_1b"><option></option></select> + <select multiple name="msel_1c"><option selected></option></select> + + <select multiple name="msel_2"><option value="msel_2_v"></option></select> + <select multiple name="msel_3"><option selected value="msel_3_v"></option></select> + + <select multiple name="msel_4"><option value="msel_4_v1"></option><option value="msel_4_v2"></option></select> + <select multiple name="msel_5"><option selected value="msel_5_v1"></option><option value="msel_5_v2"></option></select> + <select multiple name="msel_6"><option value="msel_6_v1"></option><option selected value="msel_6_v2"></option></select> + <select multiple name="msel_7"><option selected value="msel_7_v1"></option><option selected value="msel_7_v2"></option></select> + + <select multiple name="msel_8"><option>msel_8_v1</option><option>msel_8_v2</option></select> + <select multiple name="msel_9"><option selected>msel_9_v1</option><option>msel_9_v2</option></select> + <select multiple name="msel_10"><option>msel_10_v1</option><option selected>msel_10_v2</option></select> + <select multiple name="msel_11"><option selected>msel_11_v1</option><option selected>msel_11_v2</option></select> + + <select multiple name="msel_12"><option value="msel_12_v1">msel_12_v1_text</option><option value="msel_12_v2">msel_12_v2_text</option></select> + <select multiple name="msel_13"><option selected value="msel_13_v1">msel_13_v1_text</option><option value="msel_13_v2">msel_13_v2_text</option></select> + <select multiple name="msel_14"><option value="msel_14_v1">msel_14_v1_text</option><option selected value="msel_14_v2">msel_14_v2_text</option></select> + <select multiple name="msel_15"><option selected value="msel_15_v1">msel_15_v1_text</option><option selected value="msel_15_v2">msel_15_v2_text</option></select> + + <select multiple name="msel_16"><option>msel_16_v1</option><option>msel_16_v2</option><option>msel_16_v3</option></select> + <select multiple name="msel_17"><option selected>msel_17_v1</option><option>msel_17_v2</option><option>msel_17_v3</option></select> + <select multiple name="msel_18"><option>msel_18_v1</option><option selected>msel_18_v2</option><option>msel_18_v3</option></select> + <select multiple name="msel_19"><option selected>msel_19_v1</option><option selected>msel_19_v2</option><option>msel_19_v3</option></select> + <select multiple name="msel_20"><option>msel_20_v1</option><option>msel_20_v2</option><option selected>msel_20_v3</option></select> + <select multiple name="msel_21"><option selected>msel_21_v1</option><option>msel_21_v2</option><option selected>msel_21_v3</option></select> + <select multiple name="msel_22"><option>msel_22_v1</option><option selected>msel_22_v2</option><option selected>msel_22_v3</option></select> + <select multiple name="msel_23"><option selected>msel_23_v1</option><option selected>msel_23_v2</option><option selected>msel_23_v3</option></select> + + <select multiple name="msel_24"><option disabled>msel_24_v1</option><option>msel_24_v2</option></select> + <select multiple name="msel_25"><option disabled selected>msel_25_v1</option><option>msel_25_v2</option></select> + <select multiple name="msel_26"><option disabled>msel_26_v1</option><option selected>msel_26_v2</option></select> + <select multiple name="msel_27"><option disabled selected>msel_27_v1</option><option selected>msel_27_v2</option></select> + + <select multiple name="msel_28"><option>msel_28_v1</option><option disabled>msel_28_v2</option></select> + <select multiple name="msel_29"><option selected>msel_29_v1</option><option disabled>msel_29_v2</option></select> + <select multiple name="msel_30"><option>msel_30_v1</option><option disabled selected>msel_30_v2</option></select> + <select multiple name="msel_31"><option selected>msel_31_v1</option><option disabled selected>msel_31_v2</option></select> + + <select multiple name="msel_32"><option disabled selected>msel_32_v1</option><option disabled selected>msel_32_v2</option></select> + + <select multiple name=""><option>msel_33_v1</option><option>msel_33_v2</option></select> + <select multiple name=""><option selected>msel_34_v1</option><option>msel_34_v2</option></select> + <select multiple name=""><option>msel_35_v1</option><option selected>msel_35_v2</option></select> + <select multiple name=""><option selected>msel_36_v1</option><option selected>msel_36_v2</option></select> + + <select multiple><option>msel_37_v1</option><option>msel_37_v2</option></select> + <select multiple><option selected>msel_38_v1</option><option>msel_38_v2</option></select> + <select multiple><option>msel_39_v1</option><option selected>msel_39_v2</option></select> + <select multiple><option selected>msel_40_v1</option><option selected>msel_40_v2</option></select> + </p> +</form> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +const placeholder_myFile1 = {}; +const placeholder_myFile2 = {}; +const placeholder_emptyFile = {}; + +var myFile1, myFile2, emptyFile; +let openerURL, opener; +var gen; + +function bodyLoaded() { + openerURL = SimpleTest.getTestFileURL("formSubmission_chrome.js"); + opener = SpecialPowers.loadChromeScript(openerURL); + + let xhr = new XMLHttpRequest; + xhr.open("GET", "/dynamic/getMyDirectory.sjs", false); + xhr.send(); + let basePath = xhr.responseText; + + opener.addMessageListener("files.opened", onFilesOpened); + opener.sendAsyncMessage("files.open", [ + basePath + "file_formSubmission_text.txt", + basePath + "file_formSubmission_img.jpg", + ]); + + /* + * The below test function uses callbacks that invoke gen.next() rather than + * creating and resolving Promises. I'm trying to minimize churn since these + * changes want to be uplifted. Some kind soul might want to clean this all up + * at some point. + */ + + $("target_iframe").onload = function() { gen.next(); }; +} + + +function onFilesOpened(files) { + let [textFile, imageFile] = files; + opener.destroy(); + + let singleFile = textFile; + let multiFile = [textFile, imageFile]; + + var addList = document.getElementsByClassName("setfile"); + let i = 0; + var input; + while ((input = addList[i++])) { + if (input.classList.contains("multi")) { + SpecialPowers.wrap(input).mozSetFileArray(multiFile); + } else { + SpecialPowers.wrap(input).mozSetFileArray([singleFile]); + } + } + + input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + SpecialPowers.wrap(input).mozSetFileArray(multiFile); + myFile1 = input.files[0]; + myFile2 = input.files[1]; + is(myFile1.size, 20, "File1 size"); + is(myFile2.size, 2711, "File2 size"); + emptyFile = { name: "", type: "application/octet-stream" }; + + // Now, actually run the tests; see below. + runAllTestVariants(); +}; + +var expectedSub = [ + // Default input + { name: "n1_1", value: "v1_1" }, + { name: "n1_2", value: "" }, + { name: "n1_3", value: "" }, + { name: "n1_7_\r\n_\r\n_\r\n_ _\"", value: "v1_7____ _\"" }, + // Text input + { name: "n2_1", value: "v2_1" }, + { name: "n2_2", value: "" }, + { name: "n2_3", value: "" }, + { name: "n2_7_\r\n_\r\n_\r\n_ _\"", value: "v2_7____ _\"" }, + // Checkbox unchecked + // Checkbox checked + { name: "n4_1", value: "v4_1" }, + { name: "n4_2", value: "" }, + { name: "n4_3", value: "on" }, + { name: "n4_7_\r\n_\r\n_\r\n_ _\"", value: "v4_7_\r\n_\r\n_\r\n_ _\"" }, + // Radio unchecked + // Radio checked + { name: "n6_1", value: "v6_1" }, + { name: "n6_2", value: "" }, + { name: "n6_3", value: "on" }, + { name: "n6_7_\r\n_\r\n_\r\n_ _\"", value: "v6_7_\r\n_\r\n_\r\n_ _\"" }, + // Hidden input + { name: "n7_1", value: "v7_1" }, + { name: "n7_2", value: "" }, + { name: "n7_3", value: "" }, + { name: "n7_7_\r\n_\r\n_\r\n_ _\"", value: "v7_7_\r\n_\r\n_\r\n_ _\"" }, + // Password input + { name: "n8_1", value: "v8_1" }, + { name: "n8_2", value: "" }, + { name: "n8_3", value: "" }, + { name: "n8_7_\r\n_\r\n_\r\n_ _\"", value: "v8_7____ _\"" }, + // Submit input + // Button input + // Image input + // Reset input + // Unknown input + { name: "n13_1", value: "v13_1" }, + { name: "n13_2", value: "" }, + { name: "n13_3", value: "" }, + { name: "n13_7_\r\n_\r\n_\r\n_ _\"", value: "v13_7____ _\"" }, + // <input type='url'> + { name: "n19_1", value: "http://v19_1.org" }, + { name: "n19_2", value: "" }, + { name: "n19_3", value: "" }, + { name: "n19_7_\r\n_\r\n_\r\n__\"", value: "http://v19_7_____\"" }, + { name: "n19_8", value: "http://m\xf3zill\xe4.\xf3rg" }, + // <input type='email'> + { name: "n20_1", value: "v20_1@bar" }, + { name: "n20_2", value: "" }, + { name: "n20_3", value: "" }, + { name: "n20_7_\r\n_\r\n_\r\n__\"", value: "v20_7_____\"@bar" }, + { name: "n20_8", value: "foo@mózillä.órg" }, + // Default button + // Submit button + // Button button + // Reset button + // Unknown button + // File + { name: "file_1", value: placeholder_myFile1 }, + { name: "file_2", value: placeholder_emptyFile }, + // Multiple file + { name: "file_3", value: placeholder_myFile1 }, + { name: "file_4", value: placeholder_myFile1 }, + { name: "file_4", value: placeholder_myFile2 }, + { name: "file_5", value: placeholder_emptyFile }, + // Textarea + { name: "t1", value: "t_1_v" }, + { name: "t2", value: "" }, + { name: "t6", value: "t_6_v" }, + { name: "t7", value: "t_7_v\r\n" }, + { name: "t8", value: "\r\n t_8_v \r\n" }, + { name: "t9_\r\n_\r\n_\r\n_ _\"", value: "t_9_\r\n_\r\n_\r\n_ _\"_v" }, + { name: "t10", value: "t_10_v" }, + + // Select one + { name: "sel_1b", value: "" }, + { name: "sel_1c", value: "" }, + { name: "sel_2", value: "sel_2_v" }, + { name: "sel_3", value: "sel_3_v" }, + { name: "sel_4", value: "sel_4_v1" }, + { name: "sel_5", value: "sel_5_v1" }, + { name: "sel_6", value: "sel_6_v2" }, + { name: "sel_7", value: "sel_7_v1" }, + { name: "sel_8", value: "sel_8_v1" }, + { name: "sel_9", value: "sel_9_v2" }, + { name: "sel_10", value: "sel_10_v1" }, + { name: "sel_11", value: "sel_11_v1" }, + { name: "sel_12", value: "sel_12_v2" }, + { name: "sel_13", value: "sel_13_v2" }, + { name: "sel_15", value: "sel_15_v2" }, + { name: "sel_16", value: "sel_16_v1" }, + { name: "sel_17", value: "sel_17_v1" }, + // Select three + { name: "msel_1c", value: "" }, + { name: "msel_3", value: "msel_3_v" }, + { name: "msel_5", value: "msel_5_v1" }, + { name: "msel_6", value: "msel_6_v2" }, + { name: "msel_7", value: "msel_7_v1" }, + { name: "msel_7", value: "msel_7_v2" }, + { name: "msel_9", value: "msel_9_v1" }, + { name: "msel_10", value: "msel_10_v2" }, + { name: "msel_11", value: "msel_11_v1" }, + { name: "msel_11", value: "msel_11_v2" }, + { name: "msel_13", value: "msel_13_v1" }, + { name: "msel_14", value: "msel_14_v2" }, + { name: "msel_15", value: "msel_15_v1" }, + { name: "msel_15", value: "msel_15_v2" }, + { name: "msel_17", value: "msel_17_v1" }, + { name: "msel_18", value: "msel_18_v2" }, + { name: "msel_19", value: "msel_19_v1" }, + { name: "msel_19", value: "msel_19_v2" }, + { name: "msel_20", value: "msel_20_v3" }, + { name: "msel_21", value: "msel_21_v1" }, + { name: "msel_21", value: "msel_21_v3" }, + { name: "msel_22", value: "msel_22_v2" }, + { name: "msel_22", value: "msel_22_v3" }, + { name: "msel_23", value: "msel_23_v1" }, + { name: "msel_23", value: "msel_23_v2" }, + { name: "msel_23", value: "msel_23_v3" }, + { name: "msel_26", value: "msel_26_v2" }, + { name: "msel_27", value: "msel_27_v2" }, + { name: "msel_29", value: "msel_29_v1" }, + { name: "msel_31", value: "msel_31_v1" }, +]; + +var expectedAugment = [ + { name: "aName", value: "aValue" }, + //{ name: "aNameBool", value: "false" }, + { name: "aNameNum", value: "9.2" }, + { name: "aNameFile1", value: placeholder_myFile1 }, + { name: "aNameFile2", value: placeholder_myFile2 }, + //{ name: "aNameObj", value: "[object XMLHttpRequest]" }, + //{ name: "aNameNull", value: "null" }, + //{ name: "aNameUndef", value: "undefined" }, +]; + +function checkMPSubmission(sub, expected, test) { + function getPropCount(o) { + var x, l = 0; + for (x in o) ++l; + return l; + } + function mpquote_name(s) { + return s.replace(/\r?\n|\r/g, "%0D%0A") + .replace(/\"/g, "%22"); + } + function mpquote_filename(s) { + return s.replace(/\r/g, "%0D") + .replace(/\n/g, "%0A") + .replace(/\"/g, "%22"); + } + + is(sub.length, expected.length, + "Correct number of multipart items in " + test); + + if (sub.length != expected.length) { + alert(JSON.stringify(sub)); + } + + var i; + for (i = 0; i < expected.length; ++i) { + if (!("fileName" in expected[i])) { + is(sub[i].headers["Content-Disposition"], + "form-data; name=\"" + mpquote_name(expected[i].name) + "\"", + "Correct name in " + test); + is (getPropCount(sub[i].headers), 1, + "Wrong number of headers in " + test); + is(sub[i].body, + expected[i].value.replace(/\r\n|\r|\n/, "\r\n"), + "Correct value in " + test); + } + else { + is(sub[i].headers["Content-Disposition"], + "form-data; name=\"" + mpquote_name(expected[i].name) + "\"; filename=\"" + + mpquote_filename(expected[i].fileName) + "\"", + "Correct name in " + test); + is(sub[i].headers["Content-Type"], + expected[i].contentType, + "Correct content type in " + test); + is (getPropCount(sub[i].headers), 2, + "Wrong number of headers in " + test); + is(sub[i].body, + expected[i].value, + "Correct value in " + test); + } + } +} + +function utf8encode(s) { + return unescape(encodeURIComponent(s)); +} + +function checkURLSubmission(sub, expected) { + function urlEscape(s) { + return escape(utf8encode(s)).replace(/%20/g, "+") + .replace(/\//g, "%2F") + .replace(/@/g, "%40"); + } + + subItems = sub.split("&"); + is(subItems.length, expected.length, + "Correct number of url items"); + var i; + for (i = 0; i < expected.length; ++i) { + let expect = urlEscape(expected[i].name) + "=" + + urlEscape(("fileName" in expected[i]) ? expected[i].fileName : expected[i].value); + is (subItems[i], expect, "expected URL part"); + } +} + +function checkPlainSubmission(sub, expected) { + + is(sub, + expected.map(function(v) { + return v.name + "=" + + (("fileName" in v) ? v.fileName : v.value) + + "\r\n"; + }).join(""), + "Correct submission"); +} + +function setDisabled(list, state) { + Array.prototype.forEach.call(list, function(e) { + e.disabled = state; + }); +} + +// Run the suite of tests for this variant, returning a Promise that will be +// resolved when the batch completes. Then and only then runTestVariant may +// be invoked to run a different variation. +function runTestVariant(variantLabel) { + info("starting test variant: " + variantLabel); + return new Promise((resolve) => { + // Instantiate the generator. + gen = runTestVariantUsingWeirdGenDriver(resolve); + // Run the generator to the first yield, at which point it is self-driving. + gen.next(); + }); +} +function* runTestVariantUsingWeirdGenDriver(finishedVariant) { + // Set up the expectedSub array + fileReader1 = new FileReader; + fileReader1.readAsBinaryString(myFile1); + fileReader2 = new FileReader; + fileReader2.readAsBinaryString(myFile2); + fileReader1.onload = fileReader2.onload = function() { gen.next(); }; + yield undefined; // Wait for both FileReaders. We don't care which order they finish. + yield undefined; + function fileFixup(o) { + if (o.value === placeholder_myFile1) { + o.value = fileReader1.result; + o.fileName = myFile1.name; + o.contentType = myFile1.type; + } + else if (o.value === placeholder_myFile2) { + o.value = fileReader2.result; + o.fileName = myFile2.name; + o.contentType = myFile2.type; + } + else if (o.value === placeholder_emptyFile) { + o.value = ""; + o.fileName = emptyFile.name; + o.contentType = emptyFile.type; + } + }; + expectedSub.forEach(fileFixup); + expectedAugment.forEach(fileFixup); + + var form = $("form"); + + // multipart/form-data + var iframe = $("target_iframe"); + + // Make normal submission + form.action = "form_submit_server.sjs"; + form.method = "POST"; + form.enctype = "multipart/form-data"; + form.submit(); + yield undefined; // Wait for iframe to load as a result of the submission + var submission = JSON.parse(iframe.contentDocument.documentElement.textContent); + checkMPSubmission(submission, expectedSub, "normal submission"); + + // Disabled controls + setDisabled(document.querySelectorAll("input, select, textarea"), true); + form.submit(); + yield undefined; + submission = JSON.parse(iframe.contentDocument.documentElement.textContent); + checkMPSubmission(submission, [], "disabled controls"); + + // Reenabled controls + setDisabled(document.querySelectorAll("input, select, textarea"), false); + form.submit(); + yield undefined; + submission = JSON.parse(iframe.contentDocument.documentElement.textContent); + checkMPSubmission(submission, expectedSub, "reenabled controls"); + + // text/plain + form.action = "form_submit_server.sjs?plain"; + form.enctype = "text/plain"; + form.submit(); + yield undefined; + submission = JSON.parse(iframe.contentDocument.documentElement.textContent); + checkPlainSubmission(submission, expectedSub); + + // application/x-www-form-urlencoded + form.action = "form_submit_server.sjs?url"; + form.enctype = "application/x-www-form-urlencoded"; + form.submit(); + yield undefined; + submission = JSON.parse(iframe.contentDocument.documentElement.textContent); + checkURLSubmission(submission, expectedSub); + + // application/x-www-form-urlencoded + form.action = "form_submit_server.sjs?xxyy"; + form.method = "GET"; + form.enctype = ""; + form.submit(); + yield undefined; + submission = JSON.parse(iframe.contentDocument.documentElement.textContent); + checkURLSubmission(submission, expectedSub); + + // application/x-www-form-urlencoded + form.action = "form_submit_server.sjs"; + form.method = ""; + form.enctype = ""; + form.submit(); + yield undefined; + submission = JSON.parse(iframe.contentDocument.documentElement.textContent); + checkURLSubmission(submission, expectedSub); + + // Send form using XHR and FormData + xhr = new XMLHttpRequest(); + xhr.onload = function() { gen.next(); }; + xhr.open("POST", "form_submit_server.sjs"); + xhr.send(new FormData(form)); + yield undefined; // Wait for XHR load + checkMPSubmission(JSON.parse(xhr.responseText), expectedSub, "send form using XHR and FormData"); + + // Send disabled form using XHR and FormData + setDisabled(document.querySelectorAll("input, select, textarea"), true); + xhr.open("POST", "form_submit_server.sjs"); + xhr.send(new FormData(form)); + yield undefined; + checkMPSubmission(JSON.parse(xhr.responseText), [], "send disabled form using XHR and FormData"); + setDisabled(document.querySelectorAll("input, select, textarea"), false); + + // Send FormData + function addToFormData(fd) { + fd.append("aName", "aValue"); + fd.append("aNameNum", 9.2); + fd.append("aNameFile1", myFile1); + fd.append("aNameFile2", myFile2); + } + var fd = new FormData(); + addToFormData(fd); + xhr.open("POST", "form_submit_server.sjs"); + xhr.send(fd); + yield undefined; + checkMPSubmission(JSON.parse(xhr.responseText), expectedAugment, "send FormData"); + + // Augment <form> using FormData + fd = new FormData(form); + addToFormData(fd); + xhr.open("POST", "form_submit_server.sjs"); + xhr.send(fd); + yield undefined; + checkMPSubmission(JSON.parse(xhr.responseText), + expectedSub.concat(expectedAugment), "send augmented FormData"); + + finishedVariant(); +} + +/** + * Install our service-worker (parameterized by appending "?MODE"), which will + * invoke skipWaiting() and clients.claim() to begin controlling us ASAP. We + * wait on the controllerchange event + */ +async function installAndBeControlledByServiceWorker(mode) { + const scriptURL = "sw_formSubmission.js?" + mode; + const controllerChanged = new Promise((resolve) => { + navigator.serviceWorker.addEventListener( + "controllerchange", () => { resolve(); }, { once: true }); + }); + + info("installing ServiceWorker: " + scriptURL); + const swr = await navigator.serviceWorker.register(scriptURL, + { scope: "./" }); + await controllerChanged; + ok(navigator.serviceWorker.controller.scriptURL.endsWith(scriptURL), + "controlled by the SW we expected"); + info("became controlled by ServiceWorker."); + + return swr; +} + +async function runAllTestVariants() { + // Run the test as it has historically been run, with no ServiceWorkers + // anywhere! + await runTestVariant("no ServiceWorker"); + + // Uncomment the below if something in the test seems broken and you're not + // sure whether it's a side-effect of the multiple passes or not. + //await runTestVariant("no ServiceWorker second paranoia time"); + + // Ensure ServiceWorkers are enabled and that testing mode (which disables + // security checks) is on too. + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}); + + // Now run the test with a ServiceWorker that covers the scope but has no + // fetch handler, so the optimization case will not actually dispatch a + // "fetch" event, but some stuff will happen that can change things enough + // to break them like in https://bugzilla.mozilla.org/show_bug.cgi?id=1383518. + await installAndBeControlledByServiceWorker("no-fetch"); + await runTestVariant("ServiceWorker that does not listen for fetch events"); + + // Now the ServiceWorker does have a "fetch" event listener, but it will reset + // interception every time. This is similar to the prior case but different + // enough that it could break things in a different exciting way. + await installAndBeControlledByServiceWorker("reset-fetch"); + await runTestVariant("ServiceWorker that resets all fetches"); + + // Now the ServiceWorker resolves the fetch event with `fetch(event.request)` + // which makes little sense but is a thing that can happen. + const swr = await installAndBeControlledByServiceWorker("proxy-fetch"); + await runTestVariant("ServiceWorker that proxies all fetches"); + + // cleanup. + info("unregistering ServiceWorker"); + await swr.unregister(); + info("ServiceWorker uninstalled"); + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_formSubmission2.html b/dom/html/test/test_formSubmission2.html new file mode 100644 index 0000000000..fc6b60cdcf --- /dev/null +++ b/dom/html/test/test_formSubmission2.html @@ -0,0 +1,220 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=523771 +--> +<head> + <title>Test for Bug 523771</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=523771">Mozilla Bug 523771</a> +<p id="display"></p> +<iframe name="target_iframe" id="target_iframe"></iframe> +<form action="form_submit_server.sjs" target="target_iframe" id="form" + method="POST" enctype="multipart/form-data"> + <table> + <tr> + <td>Control type</td> + <td>Name and value</td> + <td>Name, empty value</td> + <td>Name, no value</td> + <td>Empty name, with value</td> + <td>No name, with value</td> + <td>No name or value</td> + </tr> + <tr> + <td>Submit input</td> + <td><input type=submit name="n1_1" value="v1_1"></td> + <td><input type=submit name="n1_2" value=""></td> + <td><input type=submit name="n1_3"></td> + <td><input type=submit name="" value="v1_4"></td> + <td><input type=submit value="v1_5"></td> + <td><input type=submit></td> + </tr> + <tr> + <td>Image input</td> + <td><input type=image src="file_formSubmission_img.jpg" name="n2_1" value="v2_1"></td> + <td><input type=image src="file_formSubmission_img.jpg" name="n2_2" value=""></td> + <td><input type=image src="file_formSubmission_img.jpg" name="n2_3"></td> + <td><input type=image src="file_formSubmission_img.jpg" name="" value="v2_4"></td> + <td><input type=image src="file_formSubmission_img.jpg" value="v2_5"></td> + <td><input type=image src="file_formSubmission_img.jpg"></td> + </tr> + <tr> + <td>Submit button</td> + <td><button type=submit name="n3_1" value="v3_1"></button></td> + <td><button type=submit name="n3_2" value=""></button></td> + <td><button type=submit name="n3_3"></button></td> + <td><button type=submit name="" value="v3_4"></button></td> + <td><button type=submit value="v3_5"></button></td> + <td><button type=submit ></button></td> + </tr> + <tr> + <td>Submit button with text</td> + <td><button type=submit name="n4_1" value="v4_1">text here</button></td> + <td><button type=submit name="n4_2" value="">text here</button></td> + <td><button type=submit name="n4_3">text here</button></td> + <td><button type=submit name="" value="v4_4">text here</button></td> + <td><button type=submit value="v4_5">text here</button></td> + <td><button type=submit>text here</button></td> + </tr> + </table> +</form> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var gen = runTest(); + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(function() { + gen.next(); +}); + +var expectedSub = [ + // Submit input + [{ name: "n1_1", value: "v1_1" }], + [{ name: "n1_2", value: "" }], + [{ name: "n1_3", value: "Submit Query" }], + [], + [], + [], + // Image input + [{ name: "n2_1.x", value: "10" }, + { name: "n2_1.y", value: "7" }], + [{ name: "n2_2.x", value: "10" }, + { name: "n2_2.y", value: "7" }], + [{ name: "n2_3.x", value: "10" }, + { name: "n2_3.y", value: "7" }], + [{ name: "x", value: "10" }, + { name: "y", value: "7" }], + [{ name: "x", value: "10" }, + { name: "y", value: "7" }], + [{ name: "x", value: "10" }, + { name: "y", value: "7" }], + // Submit button + [{ name: "n3_1", value: "v3_1" }], + [{ name: "n3_2", value: "" }], + [{ name: "n3_3", value: "" }], + [], + [], + [], + // Submit button with text + [{ name: "n4_1", value: "v4_1" }], + [{ name: "n4_2", value: "" }], + [{ name: "n4_3", value: "" }], + [], + [], + [], +]; + +function checkSubmission(sub, expected) { + function getPropCount(o) { + var x, l = 0; + for (x in o) ++l; + return l; + } + + is(sub.length, expected.length, + "Correct number of items"); + var i; + for (i = 0; i < expected.length; ++i) { + if (!("fileName" in expected[i])) { + is(sub[i].headers["Content-Disposition"], + "form-data; name=\"" + expected[i].name + "\"", + "Correct name"); + is (getPropCount(sub[i].headers), 1, + "Wrong number of headers"); + } + else { + is(sub[i].headers["Content-Disposition"], + "form-data; name=\"" + expected[i].name + "\"; filename=\"" + + expected[i].fileName + "\"", + "Correct name"); + is(sub[i].headers["Content-Type"], + expected[i].contentType, + "Correct content type"); + is (getPropCount(sub[i].headers), 2, + "Wrong number of headers"); + } + is(sub[i].body, + expected[i].value, + "Correct value"); + } +} + +function clickImage(aTarget, aX, aY) +{ + aTarget.style.position = "absolute"; + aTarget.style.top = "0"; + aTarget.style.left = "0"; + aTarget.offsetTop; + + var wu = SpecialPowers.getDOMWindowUtils(aTarget.ownerDocument.defaultView); + + wu.sendMouseEvent('mousedown', aX, aY, 0, 1, 0); + wu.sendMouseEvent('mouseup', aX, aY, 0, 0, 0); + + aTarget.style.position = ""; + aTarget.style.top = ""; + aTarget.style.left = ""; +} + +function* runTest() { + // Make normal submission + var form = $("form"); + var iframe = $("target_iframe"); + iframe.onload = function() { gen.next(); }; + + var elements = form.querySelectorAll("input, button"); + + is(elements.length, expectedSub.length, + "tests vs. expected out of sync"); + + var i; + for (i = 0; i < elements.length && i < expectedSub.length; ++i) { + elem = elements[i]; + if (elem.localName != "input" || elem.type != "image") { + elem.click(); + } + else { + clickImage(elem, 10, 7); + } + yield undefined; + + var submission = JSON.parse(iframe.contentDocument.documentElement.textContent); + checkSubmission(submission, expectedSub[i]); + } + + // Disabled controls + var i; + for (i = 0; i < elements.length && i < expectedSub.length; ++i) { + elem = elements[i]; + form.onsubmit = function() { + elem.disabled = true; + } + if (elem.localName != "input" || elem.type != "image") { + elem.click(); + } + else { + clickImage(elem, 10, 7); + } + yield undefined; + + is(elem.disabled, true, "didn't disable"); + elem.disabled = false; + form.onsubmit = undefined; + + var submission = JSON.parse(iframe.contentDocument.documentElement.textContent); + checkSubmission(submission, []); + } + + SimpleTest.finish(); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_formelements.html b/dom/html/test/test_formelements.html new file mode 100644 index 0000000000..b753759bcb --- /dev/null +++ b/dom/html/test/test_formelements.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=772869 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 772869</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772869">Mozilla Bug 772869</a> +<p id="display"></p> +<div id="content" style="display: none"> + <form id="f"> + <input name="x"> + <input type="image" name="a"> + <input type="file" name="y"> + <input type="submit" name="z"> + <input id="w"> + <input name="w"> + </form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 772869 **/ +var x = $("f").elements; +x.something = "another"; +names = []; +for (var name in x) { + names.push(name); +} +is(names.length, 9, "Should have 9 enumerated names"); +is(names[0], "0", "Enum entry 1"); +is(names[1], "1", "Enum entry 2"); +is(names[2], "2", "Enum entry 3"); +is(names[3], "3", "Enum entry 4"); +is(names[4], "4", "Enum entry 5"); +is(names[5], "something", "Enum entry 6"); +is(names[6], "namedItem", "Enum entry 7"); +is(names[7], "item", "Enum entry 8"); +is(names[8], "length", "Enum entry 9"); + +names = Object.getOwnPropertyNames(x); +is(names.length, 10, "Should have 10 items"); +// Now sort entries 5 through 8, for comparison purposes. We don't sort the +// whole array, because we want to make sure the ordering between the parts +// is correct +temp = names.slice(5, 9); +temp.sort(); +names.splice.bind(names, 5, 4).apply(null, temp); +is(names.length, 10, "Should still have 10 items"); +is(names[0], "0", "Entry 1") +is(names[1], "1", "Entry 2") +is(names[2], "2", "Entry 3") +is(names[3], "3", "Entry 4") +is(names[4], "4", "Entry 5") +is(names[5], "w", "Entry 6") +is(names[6], "x", "Entry 7") +is(names[7], "y", "Entry 8") +is(names[8], "z", "Entry 9") +is(names[9], "something", "Entry 10") +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_fragment_form_pointer.html b/dom/html/test/test_fragment_form_pointer.html new file mode 100644 index 0000000000..ff40385455 --- /dev/null +++ b/dom/html/test/test_fragment_form_pointer.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=946585 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 946585</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=946585">Mozilla Bug 946585</a> +<p id="display"></p> +<div id="content" style="display: none"> +<form><div id="formdiv"></div></form> +</div> +<pre id="test"> +</pre> +<script type="application/javascript"> +/** Test for Bug 946585 **/ +var formDiv = document.getElementById("formdiv"); +formDiv.innerHTML = '<form>'; +is(formDiv.firstChild, null, "InnerHTML should not produce form element because the div has a form pointer."); +</script> +</body> +</html> diff --git a/dom/html/test/test_frame_count_with_synthetic_doc.html b/dom/html/test/test_frame_count_with_synthetic_doc.html new file mode 100644 index 0000000000..c282ed09d7 --- /dev/null +++ b/dom/html/test/test_frame_count_with_synthetic_doc.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> + <head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + </head> + <body> + <script> + SimpleTest.waitForExplicitFinish(); + function getWindowLength() { + setTimeout(function() { + if (window.length) { + ok(false, "Synthetic document shouldn't be exposed"); + } + + // Keep running this check until the test stops + getWindowLength(); + }); + } + + function addObject() { + const object = document.createElement("object"); + object.data = 'file_bug417760.png'; + document.body.appendChild(object); + + object.onload = function() { + ok(true, "Test passes"); + SimpleTest.finish(); + } + } + + getWindowLength(); + addObject(); + </script> + </body> +</html> diff --git a/dom/html/test/test_getElementsByName_after_mutation.html b/dom/html/test/test_getElementsByName_after_mutation.html new file mode 100644 index 0000000000..f88b8d579e --- /dev/null +++ b/dom/html/test/test_getElementsByName_after_mutation.html @@ -0,0 +1,51 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1376695 +--> +<head> + <title>Test for Bug 1376695</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1376695">Mozilla Bug 1376695</a> +<p id="display"></p> +<div id="originalFoo" name="foo"> +<pre id="test"> +<script type="application/javascript"> + +/** Test to ensure that the list returned by getElementsByName is updated after + * mutations. + **/ + +var fooList = document.getElementsByName("foo"); +var originalDiv = document.getElementById("originalFoo"); + +is(fooList.length, 1, "Should find one element with name 'foo' initially"); +is(fooList[0], originalDiv, "Element should be the original div"); + +var newTree = document.createElement("p"); +var child1 = document.createElement("div"); +var child2 = document.createElement("div"); +child2.setAttribute("name", "foo"); + +newTree.appendChild(child1); +newTree.appendChild(child2); +document.body.appendChild(newTree); + +is(fooList.length, 2, + "Should find two elements with name 'foo' after appending the new tree"); +is(fooList[1], child2, "Element should be the new appended div with name 'foo'"); + +document.body.removeChild(newTree); + +is(fooList.length, 1, + "Should find one element with name 'foo' after removing the new tree"); +is(fooList[0], originalDiv, + "Element should be the original div after removing the new tree"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_hidden.html b/dom/html/test/test_hidden.html new file mode 100644 index 0000000000..7b9d488c0e --- /dev/null +++ b/dom/html/test/test_hidden.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=567663 +--> +<head> + <title>Test for Bug 567663</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=567663">Mozilla Bug 567663</a> +<p id="display"></p> +<div id="content" style="display: none"> + <p></p> + <p hidden></p> +</div> +<pre id="test"> +<script> +/** Test for Bug 567663 **/ +var ps = document.getElementById("content").getElementsByTagName("p"); +is(ps[0].hidden, false, "First p's IDL attribute was wrong."); +is(ps[0].hasAttribute("hidden"), false, "First p had a content attribute."); +is(ps[1].hidden, true, "Second p's IDL attribute was wrong."); +is(ps[1].hasAttribute("hidden"), true, + "Second p didn't have a content attribute."); +is(ps[1].getAttribute("hidden"), "", + "Second p's content attribute was wrong."); + +ps[0].hidden = true; +is(ps[0].getAttribute("hidden"), "", + "Content attribute was set to an incorrect value."); +ps[1].hidden = false; +is(ps[1].hasAttribute("hidden"), false, + "Second p still had a content attribute."); + +ps[0].setAttribute("hidden", "banana"); +is(ps[0].hidden, true, "p's IDL attribute was wrong after setting."); +is(ps[0].getAttribute("hidden"), "banana", "Content attribute changed."); + +ps[0].setAttribute("hidden", "false"); +is(ps[0].hidden, true, "p's IDL attribute was wrong after setting."); +is(ps[0].getAttribute("hidden"), "false", "Content attribute changed."); + +ps[0].removeAttribute("hidden"); +is(ps[0].hidden, false, + "p's IDL attribute was wrong after removing the content attribute."); +is(ps[0].hasAttribute("hidden"), false); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_html_attributes_reflection.html b/dom/html/test/test_html_attributes_reflection.html new file mode 100644 index 0000000000..a3d6c63121 --- /dev/null +++ b/dom/html/test/test_html_attributes_reflection.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLHtmlElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLHtmlElement attributes reflection **/ + +// .version +reflectString({ + element: document.createElement("html"), + attribute: "version", +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_htmlcollection.html b/dom/html/test/test_htmlcollection.html new file mode 100644 index 0000000000..2d91189f6b --- /dev/null +++ b/dom/html/test/test_htmlcollection.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=772869 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 772869</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772869">Mozilla Bug 772869</a> +<p id="display"></p> +<div id="content" style="display: none"> + <a class="foo" name="x"></a> + <span class="foo" id="y"></span> + <span class="foo" name="x"></span> + <form class="foo" name="z" id="w"></form> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 772869 **/ +var x = document.getElementsByClassName("foo"); +x.something = "another"; +var names = []; +for (var name in x) { + names.push(name); +} +is(names.length, 8, "Should have 8 enumerated names"); +is(names[0], "0", "Enum entry 1") +is(names[1], "1", "Enum entry 2") +is(names[2], "2", "Enum entry 3") +is(names[3], "3", "Enum entry 4") +is(names[4], "something", "Enum entry 5") +is(names[5], "item", "Enum entry 6") +is(names[6], "namedItem", "Enum entry 7") +is(names[7], "length", "Enum entry 8"); + +names = Object.getOwnPropertyNames(x); +is(names.length, 9, "Should have 9 items"); +is(names[0], "0", "Entry 1") +is(names[1], "1", "Entry 2") +is(names[2], "2", "Entry 3") +is(names[3], "3", "Entry 4") +is(names[4], "x", "Entry 5") +is(names[5], "y", "Entry 6") +is(names[6], "w", "Entry 7") +is(names[7], "z", "Entry 8") +is(names[8], "something", "Entry 9") +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_iframe_sandbox_general.html b/dom/html/test/test_iframe_sandbox_general.html new file mode 100644 index 0000000000..625b7aeeb2 --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_general.html @@ -0,0 +1,283 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=341604 +Implement HTML5 sandbox attribute for IFRAMEs - general tests +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 341604 and Bug 766282</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> +/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs - general tests **/ + +SimpleTest.expectAssertions(0, 1); +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestCompleteLog(); + +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to communicate pass/fail back to this main page. +// it expects to be called with an object like {ok: true/false, desc: +// <description of the test> which it then forwards to ok() +window.addEventListener("message", receiveMessage); + +function receiveMessage(event) +{ + ok_wrapper(event.data.ok, event.data.desc); +} + +var completedTests = 0; +var passedTests = 0; + +function ok_wrapper(result, desc) { + ok(result, desc); + + completedTests++; + + if (result) { + passedTests++; + } + + if (completedTests == 32) { + is(passedTests, completedTests, "There are " + completedTests + " general tests that should pass"); + SimpleTest.finish(); + } +} + +function doTest() { + // passes twice if good + // 1) test that inline scripts (<script>) can run in an iframe sandboxed with "allow-scripts" + // (done in file_iframe_sandbox_c_if1.html which has 'allow-scripts') + + // passes twice if good + // 2) test that <script src=...> can run in an iframe sandboxed with "allow-scripts" + // (done in file_iframe_sandbox_c_if1.html which has 'allow-scripts') + + // passes twice if good + // 3) test that script in an event listener (body onload) can run in an iframe sandboxed with "allow-scripts" + // (done in file_iframe_sandbox_c_if1.html which has 'allow-scripts') + + // passes twice if good + // 4) test that script in an javascript:url can run in an iframe sandboxed with "allow-scripts" + // (done in file_iframe_sandbox_c_if1.html which has 'allow-scripts') + + // fails if bad + // 5) test that inline scripts cannot run in an iframe sandboxed without "allow-scripts" + // (done in file_iframe_sandbox_c_if2.html which has sandbox='') + + // fails if bad + // 6) test that <script src=...> cannot run in an iframe sandboxed without "allow-scripts" + // (done in file_iframe_sandbox_c_if2.html which has sandbox='') + + // fails if bad + // 7) test that script in an event listener (body onload) cannot run in an iframe sandboxed without "allow-scripts" + // (done in file_iframe_sandbox_c_if2.html which has sandbox='') + + // fails if bad + // 8) test that script in an event listener (img onerror) cannot run in an iframe sandboxed without "allow-scripts" + // (done in file_iframe_sandbox_c_if2.html which has sandbox='') + + // fails if bad + // 9) test that script in an javascript:url cannot run in an iframe sandboxed without "allow-scripts" + // (done in file_iframe_sandbox_c_if_5.html which has sandbox='allow-same-origin') + var if_w = document.getElementById('if_5').contentWindow; + sendMouseEvent({type:'click'}, 'a_link', if_w); + + // passes if good + // 10) test that a new iframe has sandbox attribute + var ifr = document.createElement("iframe"); + ok_wrapper("sandbox" in ifr, "a new iframe should have a sandbox attribute"); + + // passes if good + // 11) test that the sandbox attribute's default stringyfied value is an empty string + ok_wrapper(ifr.sandbox.length === 0 && ifr.sandbox == "", "default sandbox attribute should be an empty string"); + + // passes if good + // 12) test that a sandboxed iframe with 'allow-forms' can submit forms + // (done in file_iframe_sandbox_c_if3.html which has 'allow-forms' and 'allow-scripts') + + // fails if bad + // 13) test that a sandboxed iframe without 'allow-forms' can NOT submit forms + // (done in file_iframe_sandbox_c_if1.html which only has 'allow-scripts') + + // fails if bad + // 14) test that a sandboxed iframe can't open a new window using the target.attribute + // this is done via file_iframe_sandbox_c_if4.html which is sandboxed with "allow-scripts" and "allow-same-origin" + // the window it attempts to open calls window.opener.ok(false, ...) and file_iframe_c_if4.html has an ok() + // function that calls window.parent.ok_wrapper + + // passes if good + // 15) test that a sandboxed iframe can't open a new window using window.open + // this is done via file_iframe_sandbox_c_if4.html which is sandboxed with "allow-scripts" and "allow-same-origin" + // the window it attempts to open calls window.opener.ok(false, ...) and file_iframe_c_if4.html has an ok() + // function that calls window.parent.ok_wrapper + + // passes if good + // 16) test that a sandboxed iframe can't open a new window using window.ShowModalDialog + // this is done via file_iframe_sandbox_c_if4.html which is sandboxed with "allow-scripts" and "allow-same-origin" + // the window it attempts to open calls window.opener.ok(false, ...) and file_iframe_c_if4.html has an ok() + // function that calls window.parent.ok_wrapper + + // passes twice if good + // 17) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute + // is separated with two spaces + // done via file_iframe_sandbox_c_if6.html which is sandboxed with " allow-scripts allow-same-origin " + + // passes twice if good + // 18) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute + // is separated with tabs + // done via file_iframe_sandbox_c_if6.html which is sandboxed with "	allow-scripts	allow-same-origin	" + + // passes twice if good + // 19) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute + // is separated with line feeds + // done via file_iframe_sandbox_c_if6.html which is sandboxed with "
allow-scripts
allow-same-origin
" + + // passes twice if good + // 20) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute + // is separated with form feeds + // done via file_iframe_sandbox_c_if6.html which is sandboxed with "allow-scriptsallow-same-origin" + + // passes twice if good + // 21) test that a sandboxed iframe can access same-origin documents and run scripts when its sandbox attribute + // is separated with carriage returns + // done via file_iframe_sandbox_c_if6.html which is sandboxed with "
allow-scripts
allow-same-origin
" + + // fails if bad + // 22) test that an iframe with sandbox="" does NOT have script in a src attribute created by a javascript: + // URL executed + // done by this page, see if_7 + + // passes if good + // 23) test that an iframe with sandbox="allow-scripts" DOES have script in a src attribute created by a javascript: + // URL executed + // done by this page, see if_8 + + // fails if bad + // 24) test that an iframe with sandbox="", starting out with a document already loaded, does NOT have script in a newly + // set src attribute created by a javascript: URL executed + // done by this page, see if_9 + + // passes if good + // 25) test that an iframe with sandbox="allow-scripts", starting out with a document already loaded, DOES have script + // in a newly set src attribute created by a javascript: URL executed + // done by this page, see if_10 + + // passes if good or fails if bad + // 26) test that an sandboxed document without 'allow-same-origin' can NOT access indexedDB + // done via file_iframe_sandbox_c_if7.html, which has sandbox='allow-scripts' + + // passes if good or fails if bad + // 27) test that an sandboxed document with 'allow-same-origin' can access indexedDB + // done via file_iframe_sandbox_c_if8.html, which has sandbox='allow-scripts allow-same-origin' + + // fails if bad + // 28) Test that a sandboxed iframe can't open a new window using the target.attribute for a + // non-existing browsing context (BC341604). + // This is done via file_iframe_sandbox_c_if4.html which is sandboxed with "allow-scripts" and "allow-same-origin" + // the window it attempts to open calls window.opener.ok(false, ...) and file_iframe_c_if4.html has an ok() + // function that calls window.parent.ok_wrapper. + + // passes twice if good + // 29-32) Test that sandboxFlagsAsString returns the set flags. + // see if_14 and if_15 + + // passes once if good + // 33) Test that sandboxFlagsAsString returns null if iframe does not have sandbox flag set. + // see if_16 +} + +addLoadEvent(doTest); + +var started_if_9 = false; +var started_if_10 = false; + +function start_if_9() { + if (started_if_9) + return; + + started_if_9 = true; + sendMouseEvent({type:'click'}, 'a_button'); +} + +function start_if_10() { + if (started_if_10) + return; + + started_if_10 = true; + sendMouseEvent({type:'click'}, 'a_button2'); +} + +function do_if_9() { + var if_9 = document.getElementById('if_9'); + if_9.src = 'javascript:"<html><script>window.parent.ok_wrapper(false, \'an iframe sandboxed without allow-scripts should not execute script in a javascript URL in a newly set src attribute\');<\/script><\/html>"'; +} + +function do_if_10() { + var if_10 = document.getElementById('if_10'); + if_10.src = 'javascript:"<html><script>window.parent.ok_wrapper(true, \'an iframe sandboxed with allow-scripts should execute script in a javascript URL in a newly set src attribute\');<\/script><\/html>"'; +} + +function eqFlags(a, b) { + // both a and b should be either null or have the array same flags + if (a === null && b === null) { return true; } + if (a === null || b === null) { return false; } + if (a.length !== b.length) { return false; } + var a_sorted = a.sort(); + var b_sorted = b.sort(); + for (var i in a_sorted) { + if (a_sorted[i] !== b_sorted[i]) { return false; } + } + return true; +} + +function getSandboxFlags(doc) { + var flags = doc.sandboxFlagsAsString; + if (flags === null) { return null; } + return flags? flags.split(" "):[]; +} + +function test_sandboxFlagsAsString(name, expected) { + var ifr = document.getElementById(name); + try { + var flags = getSandboxFlags(SpecialPowers.wrap(ifr).contentDocument); + ok_wrapper(eqFlags(flags, expected), name + ' expected: "' + expected + '", got: "' + flags + '"'); + } catch (e) { + ok_wrapper(false, name + ' expected "' + expected + ', but failed with ' + e); + } +} + +</script> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs +<p id="display"></p> +<div id="content"> +<iframe sandbox="allow-same-origin allow-scripts" id="if_1" src="file_iframe_sandbox_c_if1.html" height="10" width="10"></iframe> +<iframe sandbox="aLlOw-SAME-oRiGin ALLOW-sCrIpTs" id="if_1_case_insensitive" src="file_iframe_sandbox_c_if1.html" height="10" width="10"></iframe> +<iframe sandbox="" id="if_2" src="file_iframe_sandbox_c_if2.html" height="10" width="10"></iframe> +<iframe sandbox="allow-forms allow-scripts" id="if_3" src="file_iframe_sandbox_c_if3.html" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin allow-scripts" id="if_4" src="file_iframe_sandbox_c_if4.html" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin" id="if_5" src="file_iframe_sandbox_c_if5.html" height="10" width="10"></iframe> +<iframe sandbox=" allow-same-origin allow-scripts " id="if_6_a" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe> +<iframe sandbox="	allow-same-origin	allow-scripts	" id="if_6_b" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe> +<iframe sandbox="
allow-same-origin
allow-scripts
" id="if_6_c" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe> +<iframe sandbox="allow-same-originallow-scripts" id="if_6_d" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe> +<iframe sandbox="
allow-same-origin
allow-scripts
" id="if_6_e" src="file_iframe_sandbox_c_if6.html" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin" id='if_7' src="javascript:'<html><script>window.parent.ok_wrapper(false, \'an iframe sandboxed without allow-scripts should not execute script in a javascript URL in its src attribute\');<\/script><\/html>';" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin allow-scripts" id='if_8' src="javascript:'<html><script>window.parent.ok_wrapper(true, \'an iframe sandboxed without allow-scripts should execute script in a javascript URL in its src attribute\');<\/script><\/html>';" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin" onload='start_if_9()' id='if_9' src="about:blank" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin allow-scripts" onload='start_if_10()' id='if_10' src="about:blank" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id='if_11' src="file_iframe_sandbox_c_if7.html" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin allow-scripts" id='if_12' src="file_iframe_sandbox_c_if8.html" height="10" width="10"></iframe> +<iframe sandbox="allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation " id='if_13' src="file_iframe_sandbox_c_if9.html" height="10" width="10" onload='test_sandboxFlagsAsString("if_13",["allow-forms", "allow-pointer-lock", "allow-popups", "allow-same-origin", "allow-scripts", "allow-top-navigation"])'></iframe> +<iframe sandbox="	allow-same-origin	allow-scripts	" id="if_14" src="file_iframe_sandbox_c_if6.html" height="10" width="10" onload='test_sandboxFlagsAsString("if_14",["allow-same-origin","allow-scripts"])'></iframe> +<iframe sandbox="" id="if_15" src="file_iframe_sandbox_c_if9.html" height="10" width="10" onload='test_sandboxFlagsAsString("if_15",[])'></iframe> +<iframe id="if_16" src="file_iframe_sandbox_c_if9.html" height="10" width="10" onload='test_sandboxFlagsAsString("if_16",null)'></iframe> +<input type='button' id="a_button" onclick='do_if_9()'> +<input type='button' id="a_button2" onclick='do_if_10()'> +</div> +</body> +</html> diff --git a/dom/html/test/test_iframe_sandbox_inheritance.html b/dom/html/test/test_iframe_sandbox_inheritance.html new file mode 100644 index 0000000000..991e7ef78f --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_inheritance.html @@ -0,0 +1,202 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=341604 +Implement HTML5 sandbox attribute for IFRAMEs - inheritance tests +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> +/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/ +/** Inheritance Tests **/ + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +// A postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to communicate pass/fail back to this main page. +// It expects to be called with an object like {ok: true/false, desc: +// <description of the test> which it then forwards to ok(). +window.addEventListener("message", receiveMessage); + +function receiveMessage(event) { + switch (event.data.type) { + case "attempted": + testAttempted(); + break; + case "ok": + ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted); + break; + default: + // allow for old style message + if (event.data.ok != undefined) { + ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted); + } + } +} + +var attemptedTests = 0; +var passedTests = 0; +var totalTestsToPass = 15; +var totalTestsToAttempt = 19; + +function ok_wrapper(result, desc, addToAttempted = true) { + ok(result, desc); + + if (result) { + passedTests++; + } + + if (addToAttempted) { + testAttempted(); + } +} + +// Added so that tests that don't register unless they fail, +// can at least notify that they've attempted to run. +function testAttempted() { + attemptedTests++; + if (attemptedTests == totalTestsToAttempt) { + // Make sure all tests have had a chance to complete. + setTimeout(function() {finish();}, 1000); + } +} + +var finishCalled = false; + +function finish() { + if (!finishCalled) { + finishCalled = true; + is(passedTests, totalTestsToPass, "There are " + totalTestsToPass + " inheritance tests that should pass"); + + SimpleTest.finish(); + } +} + +function doTest() { + // fails if bad + // 1) an iframe with no sandbox attribute inside an iframe that has sandbox = "" + // should not be able to execute scripts (cannot ever loosen permissions) + // (done by file_iframe_sandbox_a_if2.html contained within file_iframe_sandbox_a_if1.html) + testAttempted(); + + // fails if bad + // 2) an iframe with sandbox = "allow-scripts" inside an iframe that has sandbox = "" + // should not be able to execute scripts (cannot ever loosen permissions) + // (done by file_iframe_sandbox_a_if2.html contained within file_iframe_sandbox_a_if1.html) + testAttempted(); + + // passes if good and fails if bad + // 3) an iframe with no sandbox attribute inside an iframe that has sandbox = "allow-scripts" + // should not be same origin with the top window + // (done by file_iframe_sandbox_a_if4.html contained within file_iframe_sandbox_a_if3.html) + + // passes if good and fails if bad + // 4) an iframe with no sandbox attribute inside an iframe that has sandbox = "allow-scripts" + // should not be same origin with its parent + // (done by file_iframe_sandbox_a_if4.html contained within file_iframe_sandbox_a_if3.html) + + // passes if good + // 5) an iframe with 'allow-same-origin' and 'allow-scripts' inside an iframe with 'allow-same-origin' + // and 'allow-scripts' should be same origin with the top window + // (done by file_iframe_sandbox_a_if6.html contained within file_iframe_sandbox_a_if5.html) + + // passes if good + // 6) an iframe with 'allow-same-origin' and 'allow-scripts' inside an iframe with 'allow-same-origin' + // and 'allow-scripts' should be same origin with its parent + // (done by file_iframe_sandbox_a_if6.html contained within file_iframe_sandbox_a_if5.html) + + // passes if good + // 7) an iframe with no sandbox attribute inside an iframe that has sandbox = "allow-scripts" + // should be able to execute scripts + // (done by file_iframe_sandbox_a_if7.html contained within file_iframe_sandbox_a_if3.html) + + // fails if bad + // 8) an iframe with sandbox="" inside an iframe that has allow-scripts should not be able + // to execute scripts + // (done by file_iframe_sandbox_a_if2.html contained within file_iframe_sandbox_a_if3.html) + testAttempted(); + + // passes if good + // 9) make sure that changing the sandbox flags on an iframe (if_8) doesn't affect + // the sandboxing of subloads of content within that iframe + var if_8 = document.getElementById('if_8'); + if_8.sandbox = 'allow-scripts'; + if_8.contentWindow.doSubload(); + + // passes if good + // 10) a <frame> inside an <iframe> sandboxed with 'allow-scripts' should not be same + // origin with this document + // done by file_iframe_sandbox_a_if11.html which is contained with file_iframe_sandbox_a_if10.html + + // passes if good + // 11) a <frame> inside a <frame> inside an <iframe> sandboxed with 'allow-scripts' should not be same + // origin with its parent frame or this document + // done by file_iframe_sandbox_a_if12.html which is contained with file_iframe_sandbox_a_if11.html + + // passes if good, fails if bad + // 12) An <object> inside an <iframe> sandboxed with 'allow-scripts' should not be same + // origin with this document + // Done by file_iframe_sandbox_a_if14.html which is contained within file_iframe_sandbox_a_if13.html + + // passes if good, fails if bad + // 13) An <object> inside an <object> inside an <iframe> sandboxed with 'allow-scripts' should not be same + // origin with its parent frame or this document + // Done by file_iframe_sandbox_a_if15.html which is contained within file_iframe_sandbox_a_if14.html + + // passes if good, fails if bad + // 14) An <object> inside a <frame> inside an <iframe> sandboxed with 'allow-scripts' should not be same + // origin with its parent frame or this document + // Done by file_iframe_sandbox_a_if15.html which is contained within file_iframe_sandbox_a_if16.html + // which is contained within file_iframe_sandbox_a_if10.html + + // passes if good + // 15) An <object> inside an <object> inside an <iframe> sandboxed with 'allow-scripts allow-forms' + // should be able to submit forms. + // Done by file_iframe_sandbox_a_if15.html which is contained within file_iframe_sandbox_a_if14.html + + // passes if good + // 16) An <object> inside a <frame> inside an <iframe> sandboxed with 'allow-scripts allow-forms' + // should be able to submit forms. + // Done by file_iframe_sandbox_a_if15.html which is contained within file_iframe_sandbox_a_if16.html + // which is contained within file_iframe_sandbox_a_if10.html + + // fails if bad + // 17) An <object> inside an <iframe> sandboxed with 'allow-same-origin' + // should not be able to run scripts. + // Done by iframe "if_no_scripts", which loads file_iframe_sandbox_srcdoc_no_allow_scripts.html. + testAttempted(); + + // passes if good + // 18) An <object> inside an <iframe> sandboxed with 'allow-scripts allow-same-origin' + // should be able to run scripts and be same origin with this document. + // Done by iframe "if_scripts", which loads file_iframe_sandbox_srcdoc_allow_scripts.html. + + // passes if good, fails if bad + // 19) Make sure that the parent's document's sandboxing flags are copied when + // changing the sandbox flags on an iframe inside an iframe. + // Done in file_iframe_sandbox_a_if17.html and file_iframe_sandbox_a_if18.html +} + +addLoadEvent(doTest); +</script> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs +<p id="display"></p> +<div id="content"> +<iframe sandbox="" id="if_1" src="file_iframe_sandbox_a_if1.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id="if_3" src="file_iframe_sandbox_a_if3.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts allow-same-origin" id="if_5" src="file_iframe_sandbox_a_if5.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts allow-same-origin" id="if_8" src="file_iframe_sandbox_a_if8.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts allow-forms" id="if_10" src="file_iframe_sandbox_a_if10.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts allow-forms" id="if_13" src="file_iframe_sandbox_a_if13.html" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin" id="if_no_scripts" srcdoc="<object data='file_iframe_sandbox_srcdoc_no_allow_scripts.html'></object>" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts allow-same-origin" id="if_scripts" srcdoc="<object data='file_iframe_sandbox_srcdoc_allow_scripts.html'></object>" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id="if_17" src="file_iframe_sandbox_a_if17.html" height="10" width="10"></iframe> +</div> +</body> +</html> diff --git a/dom/html/test/test_iframe_sandbox_navigation.html b/dom/html/test/test_iframe_sandbox_navigation.html new file mode 100644 index 0000000000..caaf4439b8 --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_navigation.html @@ -0,0 +1,285 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=341604 +Implement HTML5 sandbox attribute for IFRAMEs +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604 - navigation</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> +/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/ +/** Navigation tests Part 1**/ + +SimpleTest.requestLongerTimeout(2); // slow on Android +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin'/other windows to communicate pass/fail back to this main page. +// it expects to be called with an object like {ok: true/false, desc: +// <description of the test> which it then forwards to ok() +var bc = new BroadcastChannel("test_iframe_sandbox_navigation"); +bc.addEventListener("message", receiveMessage); +window.addEventListener("message", receiveMessage); + +var testPassesReceived = 0; + +function receiveMessage(event) { + switch (event.data.type) { + case "attempted": + testAttempted(); + break; + case "ok": + ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted); + break; + case "if_10": + doIf10TestPart2(); + break; + default: + // allow for old style message + if (event.data.ok != undefined) { + ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted); + } + } +} + +// Open windows for tests to attempt to navigate later. +var windowsToClose = new Array(); + +var attemptedTests = 0; +var passedTests = 0; +var totalTestsToPass = 7; +var totalTestsToAttempt = 13; + +function ok_wrapper(result, desc, addToAttempted = true) { + ok(result, desc); + + if (result) { + passedTests++; + } + + if (addToAttempted) { + testAttempted(); + } +} + +// Added so that tests that don't register unless they fail, +// can at least notify that they've attempted to run. +function testAttempted() { + attemptedTests++; + if (attemptedTests == totalTestsToAttempt) { + // Make sure all tests have had a chance to complete. + setTimeout(function() {finish();}, 1000); + } +} + +var finishCalled = false; + +function finish() { + if (!finishCalled) { + finishCalled = true; + is(passedTests, totalTestsToPass, "There are " + totalTestsToPass + " navigation tests that should pass"); + + closeWindows(); + + bc.close(); + + SimpleTest.finish(); + } +} + +function checkTestsFinished() { + // If our own finish() has not been called, probably failed due to a timeout, so close remaining windows. + if (!finishCalled) { + closeWindows(); + } +} + +function closeWindows() { + for (var i = 0; i < windowsToClose.length; i++) { + windowsToClose[i].close(); + } +} + +function doTest() { + // passes if good + // 1) A sandboxed iframe is allowed to navigate itself + // (done by file_iframe_sandbox_d_if1.html which has 'allow-scripts' and navigates to + // file_iframe_sandbox_navigation_pass.html). + + // passes if good + // 2) A sandboxed iframe is allowed to navigate its children, even if they are sandboxed + // (done by file_iframe_sandbox_d_if2.html which has 'allow-scripts', it navigates a child + // iframe containing file_iframe_sandbox_navigation_start.html to file_iframe_sandbox_navigation_pass.html). + + // fails if bad + // 3) A sandboxed iframe is not allowed to navigate its ancestor + // (done by file_iframe_sandbox_d_if4.html contained within file_iframe_sandbox_d_if3.html, + // it attempts to navigate file_iframe_sandbox_d_if3.html to file_iframe_sandbox_navigation_fail.html). + + // fails if bad + // 4) A sandboxed iframe is not allowed to navigate its sibling + // (done by file_iframe_sandbox_d_if5.html which has 'allow scripts allow-same-origin' + // and attempts to navigate file_iframe_navigation_start.html contained in if_sibling on this + // page to file_iframe_sandbox_navigation_fail.html). + + // passes if good, fails if bad + // 5) When a link is clicked in a sandboxed iframe, the document navigated to is sandboxed + // the same as the original document and is not same origin with parent document + // (done by file_iframe_sandbox_d_if6.html which simulates a link click and navigates + // to file_iframe_sandbox_d_if7.html which attempts to call back into its parent). + + // fails if bad + // 6) An iframe (if_8) has sandbox="allow-same-origin allow-scripts", the sandboxed document + // (file_iframe_sandbox_d_if_8.html) that it contains accesses its parent (this file) and removes + // 'allow-same-origin' and then triggers a reload. + // The document should not be able to access its parent (this file). + + // fails if bad + // 7) An iframe (if_9) has sandbox="allow-same-origin allow-scripts", the sandboxed document + // (file_iframe_sandbox_d_if_9.html) that it contains accesses its parent (this file) and removes + // 'allow-scripts' and then triggers a reload. + // The document should not be able to run a script and access its parent (this file). + + // passes if good + // 8) a document in an iframe with sandbox='allow-scripts' should have a different null + // principal in its original document than a document to which it navigates itself + // file_iframe_sandbox_d_if_10.html does this, co-ordinating with this page via postMessage + + // passes if good + // 9) a document (file_iframe_sandbox_d_if11.html in an iframe (if_11) with sandbox='allow-scripts' + // is navigated to file_iframe_sandbox_d_if12.html - when that document loads + // a message is sent back to this document, which adds 'allow-same-origin' to if_11 and then + // calls .back on it - file_iframe_sandbox_if12.html should be able to call back into this + // document - this is all contained in file_iframe_sandbox_d_if13.html which is opened in another + // tab so it has its own isolated session history + window.open("file_iframe_sandbox_d_if13.html"); + + // open up the top navigation tests + + // fails if bad + // 10) iframe with sandbox='allow-scripts' can NOT navigate top + // file_iframe_sandbox_e_if1.html contains file_iframe_sandbox_e_if6.html which + // attempts to navigate top + windowsToClose.push(window.open("file_iframe_sandbox_e_if1.html")); + + // fails if bad + // 11) iframe with sandbox='allow-scripts' nested inside iframe with + // 'allow-top-navigation allow-scripts' can NOT navigate top + // file_iframe_sandbox_e_if2.html contains file_iframe_sandbox_e_if1.html which + // contains file_iframe_sandbox_e_if6.html which attempts to navigate top + windowsToClose.push(window.open("file_iframe_sandbox_e_if2.html")); + + // passes if good + // 12) iframe with sandbox='allow-top-navigation allow-scripts' can navigate top + // file_iframe_sandbox_e_if3.html contains file_iframe_sandbox_e_if5.html which navigates top + window.open("file_iframe_sandbox_e_if3.html"); + + // passes if good + // 13) iframe with sandbox='allow-top-navigation allow-scripts' nested inside an iframe with + // 'allow-top-navigation allow-scripts' can navigate top + // file_iframe_sandbox_e_if4.html contains file_iframe_sandbox_e_if3.html which contains + // file_iframe_sandbox_e_if5.html which navigates top + window.open("file_iframe_sandbox_e_if4.html"); +} + +addLoadEvent(doTest); + +window.modified_if_8 = false; + +function reload_if_8() { + var if_8 = document.getElementById('if_8'); + if_8.src = 'file_iframe_sandbox_d_if8.html'; +} + +function modify_if_8() { + // If this is the second time this has been called + // that's a failed test (allow-same-origin was removed + // the first time). + if (window.modified_if_8) { + ok_wrapper(false, "a sandboxed iframe from which 'allow-same-origin' was removed should not be able to access its parent"); + + // need to return here since we end up in an infinite loop otherwise + return; + } + + var if_8 = document.getElementById('if_8'); + window.modified_if_8 = true; + + if_8.sandbox = 'allow-scripts'; + testAttempted(); + sendMouseEvent({type:'click'}, 'a_button'); +} + +window.modified_if_9 = false; + +function reload_if_9() { + var if_9 = document.getElementById('if_9'); + if_9.src = 'file_iframe_sandbox_d_if9.html'; +} + +function modify_if_9() { + // If this is the second time this has been called + // that's a failed test (allow-scripts was removed + // the first time). + if (window.modified_if_9) { + ok_wrapper(false, "an sandboxed iframe from which 'allow-scripts' should be removed should not be able to access its parent via a script", false); + + // need to return here since we end up in an infinite loop otherwise + return; + } + + var if_9 = document.getElementById('if_9'); + window.modified_if_9 = true; + + if_9.sandbox = 'allow-same-origin'; + + testAttempted(); + sendMouseEvent({type:'click'}, 'a_button2'); +} + +var firstPrincipal = ""; +var secondPrincipal; + +function doIf10TestPart1() { + if (firstPrincipal != "") + return; + + // use SpecialPowers to get the principal of if_10. + // NB: We stringify here and below because special-powers wrapping doesn't + // preserve identity. + var if_10 = document.getElementById('if_10'); + firstPrincipal = SpecialPowers.wrap(if_10).contentDocument.nodePrincipal.origin; + if_10.src = 'file_iframe_sandbox_d_if10.html'; +} + +function doIf10TestPart2() { + var if_10 = document.getElementById('if_10'); + // use SpecialPowers to get the principal of if_10 + secondPrincipal = SpecialPowers.wrap(if_10).contentDocument.nodePrincipal.origin; + ok_wrapper(firstPrincipal != secondPrincipal, "documents should NOT have the same principal if they are sandboxed without" + + " allow-same-origin and the first document is navigated to the second"); +} +</script> +<body onunload="checkTestsFinished()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs +<p id="display"></p> +<div id="content"> +<iframe sandbox="allow-scripts" id="if_1" src="file_iframe_sandbox_d_if1.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id="if_2" src="file_iframe_sandbox_d_if2.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id="if_3" src="file_iframe_sandbox_d_if3.html" height="10" width="10"></iframe> +<iframe id="if_sibling" name="if_sibling" src="about:blank" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts allow-same-origin" id="if_5" src="file_iframe_sandbox_d_if5.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id="if_6" src="file_iframe_sandbox_d_if6.html" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin allow-scripts" id="if_8" src="file_iframe_sandbox_d_if8.html" height="10" width="10"></iframe> +<iframe sandbox="allow-same-origin allow-scripts" id="if_9" src="file_iframe_sandbox_d_if9.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id="if_10" src="file_iframe_sandbox_navigation_start.html" onload='doIf10TestPart1()' height="10" width="10"></iframe> +</div> +<input type='button' id="a_button" onclick='reload_if_8()'> +<input type='button' id="a_button2" onclick='reload_if_9()'> +</body> +</html> diff --git a/dom/html/test/test_iframe_sandbox_navigation2.html b/dom/html/test/test_iframe_sandbox_navigation2.html new file mode 100644 index 0000000000..f17c23a458 --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_navigation2.html @@ -0,0 +1,216 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=341604 +Implement HTML5 sandbox attribute for IFRAMEs +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604 - navigation</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> +/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/ +/** Navigation tests Part 2**/ + +SimpleTest.expectAssertions(0); +SimpleTest.requestLongerTimeout(2); // slow on Android +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin'/other windows to communicate pass/fail back to this main page. +// it expects to be called with an object like {ok: true/false, desc: +// <description of the test> which it then forwards to ok() +var bc = new BroadcastChannel("test_iframe_sandbox_navigation"); +bc.addEventListener("message", receiveMessage); +window.addEventListener("message", receiveMessage); + +var testPassesReceived = 0; + +function receiveMessage(event) { + switch (event.data.type) { + case "attempted": + testAttempted(); + break; + case "ok": + ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted); + break; + default: + // allow for old style message + if (event.data.ok != undefined) { + ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted); + } + } +} + +// Open windows for tests to attempt to navigate later. +var windowsToClose = new Array(); +windowsToClose.push(window.open("about:blank", "window_to_navigate")); +windowsToClose.push(window.open("about:blank", "window_to_navigate2")); +var iframesWithWindowsToClose = new Array(); + +var attemptedTests = 0; +var passedTests = 0; +var totalTestsToPass = 12; +var totalTestsToAttempt = 15; + +function ok_wrapper(result, desc, addToAttempted = true) { + ok(result, desc); + + if (result) { + passedTests++; + } + + if (addToAttempted) { + testAttempted(); + } +} + +// Added so that tests that don't register unless they fail, +// can at least notify that they've attempted to run. +function testAttempted() { + attemptedTests++; + if (attemptedTests == totalTestsToAttempt) { + // Make sure all tests have had a chance to complete. + setTimeout(function() {finish();}, 1000); + } +} + +var finishCalled = false; + +function finish() { + if (!finishCalled) { + finishCalled = true; + is(passedTests, totalTestsToPass, "There are " + totalTestsToPass + " navigation tests that should pass"); + + for (var i = 0; i < windowsToClose.length; i++) { + windowsToClose[i].close(); + } + + bc.close(); + + SimpleTest.finish(); + } +} + +function checkTestsFinished() { + // If our own finish() has not been called, probably failed due to a timeout, so close remaining windows. + if (!finishCalled) { + for (var i = 0; i < windowsToClose.length; i++) { + windowsToClose[i].close(); + } + } +} + +function doTest() { + // fails if bad + // 14) iframe with sandbox='allow-same-origin allow-scripts allow-top-navigation' should not + // be able to navigate another window (opened by another browsing context) using its name. + // file_iframe_sandbox_d_if14.html in if_14 attempts to navigate "window_to_navigate", + // which has been opened in preparation. + + // fails if bad + // 15) iframe with sandbox='allow-scripts' should not be able to navigate top using its + // real name (instead of _top) as allow-top-navigation is not specified. + // file_iframe_sandbox_e_if7.html contains file_iframe_sandbox_e_if8.html, which + // attempts to navigate top by name. + windowsToClose.push(window.open("file_iframe_sandbox_e_if7.html")); + + // fails if bad + // 16) iframe with sandbox='allow-same-origin allow-scripts allow-top-navigation' should not + // be able to use its parent's name (instead of _parent) to navigate it, when it is not top. + // (Note: this would apply to other ancestors that are not top as well.) + // file_iframe_sandbox_d_if15.html in if_15 contains file_iframe_sandbox_d_if16.html, which + // tries to navigate if_15 by its name (if_parent). + + // passes if good, fails if bad + // 17) A sandboxed iframe is allowed to navigate itself using window.open(). + // (Done by file_iframe_sandbox_d_if17.html which has 'allow-scripts' and navigates to + // file_iframe_sandbox_navigation_pass.html). + + // passes if good, fails if bad + // 18) A sandboxed iframe is allowed to navigate its children with window.open(), even if + // they are sandboxed. (Done by file_iframe_sandbox_d_if18.html which has 'allow-scripts', + // it navigates a child iframe to file_iframe_sandbox_navigation_pass.html). + + // passes if good, fails if bad + // 19) A sandboxed iframe is not allowed to navigate its ancestor with window.open(). + // (Done by file_iframe_sandbox_d_if20.html contained within file_iframe_sandbox_d_if19.html, + // it attempts to navigate file_iframe_sandbox_d_if19.html to file_iframe_sandbox_navigation_fail.html). + + // passes if good, fails if bad + // 20) iframe with sandbox='allow-same-origin allow-scripts allow-top-navigation' should not + // be able to navigate another window (opened by another browsing context) using window.open(..., "<name>"). + // file_iframe_sandbox_d_if14.html in if_14 attempts to navigate "window_to_navigate2", + // which has been opened in preparation, using window.open(..., "window_to_navigate2"). + + // passes if good, fails if bad + // 21) iframe with sandbox='allow-same-origin allow-scripts allow-top-navigation' should not + // be able to use its parent's name (not _parent) to navigate it using window.open(), when it is not top. + // (Note: this would apply to other ancestors that are not top as well.) + // file_iframe_sandbox_d_if21.html in if_21 contains file_iframe_sandbox_d_if22.html, which + // tries to navigate if_21 by its name (if_parent2). + + // passes if good, fails if bad + // 22) iframe with sandbox='allow-top-navigation allow-scripts' can navigate top with window.open(). + // file_iframe_sandbox_e_if9.html contains file_iframe_sandbox_e_if11.html which navigates top. + window.open("file_iframe_sandbox_e_if9.html"); + + // passes if good, fails if bad + // 23) iframe with sandbox='allow-top-navigation allow-scripts' nested inside an iframe with + // 'allow-top-navigation allow-scripts' can navigate top, with window.open(). + // file_iframe_sandbox_e_if10.html contains file_iframe_sandbox_e_if9.html which contains + // file_iframe_sandbox_e_if11.html which navigates top. + window.open("file_iframe_sandbox_e_if10.html"); + + // passes if good, fails if bad + // 24) iframe with sandbox='allow-scripts' can NOT navigate top with window.open(). + // file_iframe_sandbox_e_if12.html contains file_iframe_sandbox_e_if14.html which navigates top. + window.open("file_iframe_sandbox_e_if12.html"); + + // passes if good, fails if bad + // 25) iframe with sandbox='allow-scripts' nested inside an iframe with + // 'allow-top-navigation allow-scripts' can NOT navigate top, with window.open(..., "_top"). + // file_iframe_sandbox_e_if13.html contains file_iframe_sandbox_e_if12.html which contains + // file_iframe_sandbox_e_if14.html which navigates top. + window.open("file_iframe_sandbox_e_if13.html"); + + // passes if good, fails if bad + // 26) iframe with sandbox='allow-scripts' should not be able to navigate top using its real name + // (not with _top e.g. window.open(..., "topname")) as allow-top-navigation is not specified. + // file_iframe_sandbox_e_if15.html contains file_iframe_sandbox_e_if16.html, which + // attempts to navigate top by name using window.open(). + window.open("file_iframe_sandbox_e_if15.html"); + + // passes if good + // 27) iframe with sandbox='allow-scripts allow-popups' should be able to + // navigate a window, that it has opened, using it's name. + // file_iframe_sandbox_d_if23.html in if_23 opens a window and then attempts + // to navigate it using it's name in the target of an anchor. + iframesWithWindowsToClose.push("if_23"); + + // passes if good, fails if bad + // 28) iframe with sandbox='allow-scripts allow-popups' should be able to + // navigate a window, that it has opened, using window.open(..., "<name>"). + // file_iframe_sandbox_d_if23.html in if_23 opens a window and then attempts + // to navigate it using it's name in the target of window.open(). +} + +addLoadEvent(doTest); +</script> +<body onunload="checkTestsFinished()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs +<p id="display"></p> +<div id="content"> +<iframe sandbox="allow-same-origin allow-scripts allow-top-navigation" id="if_14" src="file_iframe_sandbox_d_if14.html" height="10" width="10"></iframe> +<iframe id="if_15" name="if_parent" src="file_iframe_sandbox_d_if15.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id="if_17" src="file_iframe_sandbox_d_if17.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id="if_18" src="file_iframe_sandbox_d_if18.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts" id="if_19" src="file_iframe_sandbox_d_if19.html" height="10" width="10"></iframe> +<iframe id="if_21" name="if_parent2" src="file_iframe_sandbox_d_if21.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts allow-popups" id="if_23" src="file_iframe_sandbox_d_if23.html" height="10" width="10"></iframe> +</div> +</body> +</html> diff --git a/dom/html/test/test_iframe_sandbox_popups.html b/dom/html/test/test_iframe_sandbox_popups.html new file mode 100644 index 0000000000..c05b1fc67f --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_popups.html @@ -0,0 +1,78 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=766282 +implement allow-popups directive for iframe sandbox +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 766282</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to communicate pass/fail back to this main page. +// it expects to be called with an object like {ok: true/false, desc: +// <description of the test> which it then forwards to ok() +window.addEventListener("message", receiveMessage); + +function receiveMessage(event) +{ + ok_wrapper(event.data.ok, event.data.desc); +} + +var completedTests = 0; +var passedTests = 0; + +function ok_wrapper(result, desc) { + ok(result, desc); + + completedTests++; + + if (result) { + passedTests++; + } + + if (completedTests == 3) { + is(passedTests, completedTests, "There are " + completedTests + " popups tests that should pass"); + SimpleTest.finish(); + } +} + +function doTest() { + // passes if good + // 1) Test that a sandboxed iframe with "allow-popups" can open a new window using the target.attribute. + // This is done via file_iframe_sandbox_h_if1.html which is sandboxed with "allow-popups allow-scripts allow-same-origin". + // The window it attempts to open calls window.opener.ok(true, ...) and file_iframe_h_if1.html has an ok() + // function that calls window.parent.ok_wrapper. + + // passes if good + // 2) Test that a sandboxed iframe with "allow-popups" can open a new window using window.open. + // This is done via file_iframe_sandbox_h_if1.html which is sandboxed with "allow-popups allow-scripts allow-same-origin". + // The window it attempts to open calls window.opener.ok(true, ...) and file_iframe_h_if1.html has an ok() + // function that calls window.parent.ok_wrapper. + + // passes if good, fails if bad + // 3) Test that a sandboxed iframe with "allow-popups" can open a new window using the target.attribute + // for a non-existing browsing context (BC766282). + // This is done via file_iframe_sandbox_h_if1.html which is sandboxed with "allow-popups allow-scripts allow-same-origin". + // The window it attempts to open calls window.opener.ok(true, ...) and file_iframe_h_if1.html has an ok() + // function that calls window.parent.ok_wrapper. +} + +addLoadEvent(doTest); + +</script> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=766282">Mozilla Bug 766282</a> - implement allow-popups directive for iframe sandbox +<p id="display"></p> +<div id="content"> +<iframe sandbox="allow-popups allow-same-origin allow-scripts" id="if1" src="file_iframe_sandbox_h_if1.html" height="10" width="10"></iframe> +</div> +</body> +</html> diff --git a/dom/html/test/test_iframe_sandbox_popups_inheritance.html b/dom/html/test/test_iframe_sandbox_popups_inheritance.html new file mode 100644 index 0000000000..af4a03932e --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_popups_inheritance.html @@ -0,0 +1,157 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=766282 +Implement HTML5 sandbox allow-popuos directive for IFRAMEs - inheritance tests +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 766282</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> + +<script type="application/javascript"> + +SimpleTest.expectAssertions(0, 5); +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +// A postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to communicate pass/fail back to this main page. +window.addEventListener("message", receiveMessage); + +function receiveMessage(event) { + switch (event.data.type) { + case "attempted": + testAttempted(); + break; + case "ok": + ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted); + break; + default: + // allow for old style message + if (event.data.ok != undefined) { + ok_wrapper(event.data.ok, event.data.desc, event.data.addToAttempted); + } + } +} + +var iframesWithWindowsToClose = new Array(); + +var attemptedTests = 0; +var passedTests = 0; +var totalTestsToPass = 15; +var totalTestsToAttempt = 21; + +function ok_wrapper(result, desc, addToAttempted = true) { + ok(result, desc); + + if (result) { + passedTests++; + } + + if (addToAttempted) { + testAttempted(); + } +} + +// Added so that tests that don't register unless they fail, +// can at least notify that they've attempted to run. +function testAttempted() { + attemptedTests++; + if (attemptedTests == totalTestsToAttempt) { + // Make sure all tests have had a chance to complete. + setTimeout(function() {finish();}, 1000); + } +} + +var finishCalled = false; + +function finish() { + if (!finishCalled) { + finishCalled = true; + is(passedTests, totalTestsToPass, "There are " + totalTestsToPass + " inheritance tests that should pass"); + + closeWindows(); + + SimpleTest.finish(); + } +} + +function checkTestsFinished() { + // If our own finish() has not been called, probably failed due to a timeout, so close remaining windows. + if (!finishCalled) { + closeWindows(); + } +} + +function closeWindows() { + for (var i = 0; i < iframesWithWindowsToClose.length; i++) { + document.getElementById(iframesWithWindowsToClose[i]).contentWindow.postMessage({type: "closeWindows"}, "*"); + } +} + +function doTest() { + // passes if good and fails if bad + // 1,2,3) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups + // allow-same-origin" should not have its origin sandbox flag set and be able to access document.cookie. + // (Done by file_iframe_sandbox_k_if5.html opened from file_iframe_sandbox_k_if4.html) + // This is repeated for 3 different ways of opening the window, + // see file_iframe_sandbox_k_if4.html for details. + + // passes if good + // 4,5,6) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups + // allow-top-navigation" should not have its top-level navigation sandbox flag set and be able to + // navigate top. (Done by file_iframe_sandbox_k_if5.html (and if6) opened from + // file_iframe_sandbox_k_if4.html). This is repeated for 3 different ways of opening the window, + // see file_iframe_sandbox_k_if4.html for details. + + // passes if good + // 7,8,9) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups + // all-forms" should not have its forms sandbox flag set and be able to submit forms. + // (Done by file_iframe_sandbox_k_if7.html opened from file_iframe_sandbox_k_if4.html) + // This is repeated for 3 different ways of opening the window, + // see file_iframe_sandbox_k_if4.html for details. + + // passes if good + // 10,11,12) Make sure that the sandbox flags copied to a new browsing context are taken from the + // current active document not the browsing context (iframe / docShell). + // This is done by removing allow-same-origin and calling doSubOpens from file_iframe_sandbox_k_if8.html, + // which opens file_iframe_sandbox_k_if9.html in 3 different ways. + // It then navigates to file_iframe_sandbox_k_if1.html to run tests 13 - 21 below. + var if_8_1 = document.getElementById('if_8_1'); + if_8_1.sandbox = 'allow-scripts allow-popups'; + if_8_1.contentWindow.doSubOpens(); + + // passes if good and fails if bad + // 13,14,15) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups" + // should have its origin sandbox flag set and not be able to access document.cookie. + // This is done by file_iframe_sandbox_k_if8.html navigating to file_iframe_sandbox_k_if1.html + // after allow-same-origin has been removed from iframe if_8_1. file_iframe_sandbox_k_if1.html + // opens file_iframe_sandbox_k_if2.html in 3 different ways to perform the tests. + iframesWithWindowsToClose.push("if_8_1"); + + // fails if bad + // 16,17,18) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups" + // should have its forms sandbox flag set and not be able to submit forms. + // This is done by file_iframe_sandbox_k_if2.html, see test 10 for details of how this is opened. + + // fails if bad + // 19,20,21) A window opened from inside an iframe that has sandbox = "allow-scripts allow-popups" + // should have its top-level navigation sandbox flag set and not be able to navigate top. + // This is done by file_iframe_sandbox_k_if2.html, see test 10 for details of how this is opened. +} + +addLoadEvent(doTest); +</script> + +<body onunload="checkTestsFinished()"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=766282">Mozilla Bug 766282</a> - Implement HTML5 sandbox allow-popups directive for IFRAMEs +<p id="display"></p> +<div id="content"> +<iframe sandbox="allow-scripts allow-popups allow-same-origin allow-forms allow-top-navigation" id="if_4" src="file_iframe_sandbox_k_if4.html" height="10" width="10"></iframe> +<iframe sandbox="allow-scripts allow-popups allow-same-origin" id="if_8_1" src="file_iframe_sandbox_k_if8.html" height="10" width="10"></iframe> +</div> +</body> +</html> diff --git a/dom/html/test/test_iframe_sandbox_redirect.html b/dom/html/test/test_iframe_sandbox_redirect.html new file mode 100644 index 0000000000..ff13e52487 --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_redirect.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=985135 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 985135</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 985135 **/ + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + try { + var doc = frames[0].document; + ok(false, "Should not be able to get the document"); + isnot(doc.body.textContent.slice(0, -1), "I have been redirected", + "Should not happen"); + SimpleTest.finish(); + } catch (e) { + // Check that we got the right document + window.onmessage = function(event) { + is(event.data, "who are you? redirect target", + "Should get the message we expect"); + SimpleTest.finish(); + } + + frames[0].postMessage("who are you?", "*"); + } + }); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=985135">Mozilla Bug 985135</a> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe src="file_iframe_sandbox_redirect.html" sandbox="allow-scripts"></iframe> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_iframe_sandbox_refresh.html b/dom/html/test/test_iframe_sandbox_refresh.html new file mode 100644 index 0000000000..81107fe3dc --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_refresh.html @@ -0,0 +1,101 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1156059 +--> +<head> + <meta charset="utf-8"> + <title>Tests for Bug 1156059</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + // Tests for Bug 1156059 + // See ok messages in iframes for test cases. + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("We cannot detect when the sandbox blocks the META REFRESH, so we need to allow a reasonable amount of time for them to fail."); + + var testCases = [ + { + desc: "Meta refresh without allow-scripts should be ignored.", + numberOfLoads: 0, + numberOfLoadsExpected: 1 + }, + { + desc: "Meta refresh check should be case insensitive.", + numberOfLoads: 0, + numberOfLoadsExpected: 1 + }, + { + desc: "Meta refresh with allow-scripts should work.", + numberOfLoads: 0, + numberOfLoadsExpected: 2 + }, + { + desc: "Refresh HTTP headers should not be affected by sandbox.", + numberOfLoads: 0, + numberOfLoadsExpected: 2 + } + ]; + + var totalLoads = 0; + var totalLoadsExpected = testCases.reduce(function(partialSum, testCase) { + return partialSum + testCase.numberOfLoadsExpected; + }, 0); + + function processLoad(testCaseIndex) { + testCases[testCaseIndex].numberOfLoads++; + + if (++totalLoads == totalLoadsExpected) { + // Give the tests that should block the refresh a bit of extra time to + // fail. The worst that could happen here is that we get a false pass. + window.setTimeout(processResults, 500); + } + } + + function processResults() { + testCases.forEach(function(testCase, index) { + var msg = "Test Case " + index + ": " + testCase.desc; + is(testCase.numberOfLoads, testCase.numberOfLoadsExpected, msg); + }); + + SimpleTest.finish(); + } + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1156059">Mozilla Bug 1156059</a> +<p id="display"></p> +<div id="content" style="display: none"> + +<iframe + onload="processLoad(0)" + srcdoc="<meta http-equiv='refresh' content='0; url=data:text/html,Refreshed'>" + sandbox="allow-forms allow-pointer-lock allow-popups allow-same-origin allow-top-navigation" +></iframe> + +<iframe + onload="processLoad(1)" + srcdoc="<meta http-equiv='rEfReSh' content='0; url=data:text/html,Refreshed'>" + sandbox="allow-forms allow-pointer-lock allow-popups allow-same-origin allow-top-navigation" +></iframe> + +<iframe + onload="processLoad(2)" + srcdoc="<meta http-equiv='refresh' content='0; url=data:text/html,Refreshed'>" + sandbox="allow-scripts" +></iframe> + +<iframe + onload="processLoad(3)" + src="file_iframe_sandbox_refresh.html" + sandbox +></iframe> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_iframe_sandbox_same_origin.html b/dom/html/test/test_iframe_sandbox_same_origin.html new file mode 100644 index 0000000000..b936453bbd --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_same_origin.html @@ -0,0 +1,108 @@ +\<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=341604
+Implement HTML5 sandbox attribute for IFRAMEs - same origin tests
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 341604</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<script type="application/javascript">
+/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs **/
+/** Same Origin Tests **/
+
+SimpleTest.waitForExplicitFinish();
+
+var completedTests = 0;
+var passedTests = 0;
+
+function ok_wrapper(result, desc) {
+ ok(result, desc);
+
+ completedTests++;
+
+ if (result) {
+ passedTests++;
+ }
+
+ if (completedTests == 14) {
+ is(passedTests, completedTests, "There are " + completedTests + " same-origin tests that should pass");
+
+ SimpleTest.finish();
+ }
+}
+
+function receiveMessage(event)
+{
+ ok_wrapper(event.data.ok, event.data.desc);
+}
+
+// a postMessage handler that is used by sandboxed iframes without
+// 'allow-same-origin' to communicate pass/fail back to this main page.
+// it expects to be called with an object like {ok: true/false, desc:
+// <description of the test> which it then forwards to ok()
+window.addEventListener("message", receiveMessage);
+
+function doTest() {
+ // 1) test that we can't access an iframe sandboxed without "allow-same-origin"
+ var if_1 = document.getElementById("if_1");
+ try {
+ var b = if_1.contentDocument.body;
+ ok_wrapper(false, "accessing body of a sandboxed document should not be allowed");
+ } catch (err){
+ ok_wrapper(true, "accessing body of a sandboxed document should not be allowed");
+ }
+
+ // 2) test that we can access an iframe sandboxed with "allow-same-origin"
+ var if_2 = document.getElementById("if_2");
+
+ try {
+ var b = if_2.contentDocument.body;
+ ok_wrapper(true, "accessing body of a sandboxed document with allow-same-origin should be allowed");
+ } catch (err) {
+ ok_wrapper(false, "accessing body of a sandboxed document with allow-same-origin should be allowed");
+ }
+
+ // 3) test that a sandboxed iframe without 'allow-same-origin' cannot access its parent
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+
+ // 4) test that a sandboxed iframe with 'allow-same-origin' can access its parent
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 5) check that a sandboxed iframe with "allow-same-origin" can access document.cookie
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 6) check that a sandboxed iframe with "allow-same-origin" can access window.localStorage
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 7) check that a sandboxed iframe with "allow-same-origin" can access window.sessionStorage
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 8) check that a sandboxed iframe WITHOUT "allow-same-origin" can NOT access document.cookie
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+
+ // 9) check that a sandboxed iframe WITHOUT "allow-same-origin" can NOT access window.localStorage
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+
+ // 10) check that a sandboxed iframe WITHOUT "allow-same-origin" can NOT access window.sessionStorage
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+
+ // 11) check that XHR works normally in a sandboxed iframe with "allow-same-origin" and "allow-scripts"
+ // this is done by file_iframe_b_if2.html which has 'allow-same-origin' and 'allow-scripts'
+
+ // 12) check that XHR is blocked in a sandboxed iframe with "allow-scripts" but WITHOUT "allow-same-origin"
+ // this is done by file_iframe_b_if3.html which has 'allow-scripts' but not 'allow-same-origin'
+}
+addLoadEvent(doTest);
+</script>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs
+<p id="display"></p>
+<div id="content">
+<iframe sandbox="" id="if_1" src="file_iframe_sandbox_b_if1.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-same-origin allow-scripts" id="if_2" src="file_iframe_sandbox_b_if2.html" height="10" width="10"></iframe>
+<iframe sandbox="allow-scripts" id="if_3" src="file_iframe_sandbox_b_if3.html" height="10" width="10"></iframe>
+</div>
diff --git a/dom/html/test/test_iframe_sandbox_workers.html b/dom/html/test/test_iframe_sandbox_workers.html new file mode 100644 index 0000000000..c86f2ab528 --- /dev/null +++ b/dom/html/test/test_iframe_sandbox_workers.html @@ -0,0 +1,74 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=341604 +Implement HTML5 sandbox attribute for IFRAMEs - tests for workers +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 341604</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> +/** Test for Bug 341604 - Implement HTML5 sandbox attribute for IFRAMEs - test for workers **/ + +SimpleTest.waitForExplicitFinish(); + +// a postMessage handler that is used by sandboxed iframes without +// 'allow-same-origin' to communicate pass/fail back to this main page. +// it expects to be called with an object like {ok: true/false, desc: +// <description of the test> which it then forwards to ok() +window.addEventListener("message", receiveMessage); + +function receiveMessage(event) +{ + ok_wrapper(event.data.ok, event.data.desc); +} + +var completedTests = 0; +var passedTests = 0; + +function ok_wrapper(result, desc) { + ok(result, desc); + + completedTests++; + + if (result) { + passedTests++; + } + + if (completedTests == 3) { + is(passedTests, 3, "There are 3 worker tests that should pass"); + SimpleTest.finish(); + } +} + +function doTest() { + // passes if good + // 1) test that a worker in a sandboxed iframe with 'allow-scripts' can be loaded + // from a data: URI + // (done by file_iframe_sandbox_g_if1.html) + + // passes if good + // 2) test that a worker in a sandboxed iframe with 'allow-scripts' can be loaded + // from a blob URI created by the sandboxed document itself + // (done by file_iframe_sandbox_g_if1.html) + + // passes if good + // 3) test that a worker in a sandboxed iframe with 'allow-scripts' without + // 'allow-same-origin' cannot load a script via a relative URI + // (done by file_iframe_sandbox_g_if1.html) +} + +addLoadEvent(doTest); +</script> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=341604">Mozilla Bug 341604</a> - Implement HTML5 sandbox attribute for IFRAMEs +<p id="display"></p> +<div id="content"> +<iframe sandbox="allow-scripts" id="if_1" src="file_iframe_sandbox_g_if1.html" height="10" width="10"></iframe> +</div> +</body> +</html> diff --git a/dom/html/test/test_imageSrcSet.html b/dom/html/test/test_imageSrcSet.html new file mode 100644 index 0000000000..695d1c2643 --- /dev/null +++ b/dom/html/test/test_imageSrcSet.html @@ -0,0 +1,38 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=980243 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 980243</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 980243 **/ + SimpleTest.waitForExplicitFinish(); + + addLoadEvent(function() { + var img = document.querySelector("img"); + img.onload = function() { + ok(true, "Reached here"); + SimpleTest.finish(); + } + // If ths spec ever changes to treat .src sets differently from + // setAttribute("src"), we'll need some sort of canonicalization step + // earlier to make the attr value an absolute URI. + img.setAttribute("src", img.getAttribute("src")); + }); + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=980243">Mozilla Bug 980243</a> +<p id="display"></p> +<div id="content" style="display: none"> + <img src="file_formSubmission_img.jpg"> +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_image_clone_load.html b/dom/html/test/test_image_clone_load.html new file mode 100644 index 0000000000..e808c80a53 --- /dev/null +++ b/dom/html/test/test_image_clone_load.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test for image clones doing their load</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +var t = async_test("The clone of an image should do the load of the same image, and do it synchronously"); +t.step(function() { + var img = new Image(); + img.onload = t.step_func(function() { + var clone = img.cloneNode(); + assert_not_equals(img.naturalWidth, 0, "Should have a width"); + assert_equals(clone.naturalWidth, img.naturalWidth, + "Clone should have a width too"); + // And make sure the clone fires onload too, which happens async. + clone.onload = function() { t.done() } + }); + img.src = "image.png"; +}); +</script> diff --git a/dom/html/test/test_img_attributes_reflection.html b/dom/html/test/test_img_attributes_reflection.html new file mode 100644 index 0000000000..b89b4cec05 --- /dev/null +++ b/dom/html/test/test_img_attributes_reflection.html @@ -0,0 +1,103 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLImageElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> +/** Test for HTMLImageElement attributes reflection **/ + +reflectString({ + element: document.createElement("img"), + attribute: "alt", +}) + +reflectURL({ + element: document.createElement("img"), + attribute: "src", +}) + +reflectString({ + element: document.createElement("img"), + attribute: "srcset", +}) + +reflectLimitedEnumerated({ + element: document.createElement("img"), + attribute: "crossOrigin", + // "" is a valid value per spec, but gets mapped to the "anonymous" state, + // just like invalid values, so just list it under invalidValues + validValues: [ "anonymous", "use-credentials" ], + invalidValues: [ + "", " aNOnYmous ", " UsE-CreDEntIALS ", "foobar", "FOOBAR", " fOoBaR " + ], + defaultValue: { invalid: "anonymous", missing: null }, + nullable: true, +}) + +reflectString({ + element: document.createElement("img"), + attribute: "useMap", +}) + +reflectBoolean({ + element: document.createElement("img"), + attribute: "isMap", +}) + +ok("width" in document.createElement("img"), "img.width is present") +ok("height" in document.createElement("img"), "img.height is present") +ok("naturalWidth" in document.createElement("img"), "img.naturalWidth is present") +ok("naturalHeight" in document.createElement("img"), "img.naturalHeight is present") +ok("complete" in document.createElement("img"), "img.complete is present") + +reflectString({ + element: document.createElement("img"), + attribute: "name", +}) + +reflectString({ + element: document.createElement("img"), + attribute: "align", +}) + +reflectUnsignedInt({ + element: document.createElement("img"), + attribute: "hspace", +}) + +reflectUnsignedInt({ + element: document.createElement("img"), + attribute: "vspace", +}) + +reflectURL({ + element: document.createElement("img"), + attribute: "longDesc", +}) + +reflectString({ + element: document.createElement("img"), + attribute: "border", + extendedAttributes: { TreatNullAs: "EmptyString" }, +}) + +reflectURL({ + element: document.createElement("img"), + attribute: "lowsrc", +}) + +ok("x" in document.createElement("img"), "img.x is present") +ok("y" in document.createElement("img"), "img.y is present") + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_input_file_cancel_event.html b/dom/html/test/test_input_file_cancel_event.html new file mode 100644 index 0000000000..f0fd81c433 --- /dev/null +++ b/dom/html/test/test_input_file_cancel_event.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for the input type=file cancel event</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<input type=file></input> + +<script> +SimpleTest.waitForExplicitFinish(); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); +MockFilePicker.useBlobFile(); +MockFilePicker.returnValue = MockFilePicker.returnCancel; + +let input = document.querySelector('input[type=file]'); +input.addEventListener('cancel', event => { + ok(true, "cancel event correctly sent"); + + is(event.target, input, "Has correct event target"); + is(event.isTrusted, true, "Event is trusted"); + is(event.bubbles, true, "Event bubbles"); + is(event.cancelable, false, "Event is not cancelable"); + is(event.composed, false, "Event is not composed"); + + SimpleTest.executeSoon(function() { + MockFilePicker.cleanup(); + SimpleTest.finish(); + }); +}); +input.addEventListener('change' , () => { + ok(false, "unexpected change event"); +}) +input.click(); +</script> +</body> +</html> + diff --git a/dom/html/test/test_input_files_not_nsIFile.html b/dom/html/test/test_input_files_not_nsIFile.html new file mode 100644 index 0000000000..e70bc093ee --- /dev/null +++ b/dom/html/test/test_input_files_not_nsIFile.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for <input type='file'> handling when its "files" do not implement nsIFile</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + +<div id="content"> + <input id='a' type='file'> +</div> +<button id='b' onclick="document.getElementById('a').click();">Show Filepicker</button> + +<input type="file" id="file" /> +<pre id="test"> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +SimpleTest.waitForFocus(function() { + MockFilePicker.useBlobFile(); + MockFilePicker.returnValue = MockFilePicker.returnOK; + + var b = document.getElementById('b'); + b.focus(); // Be sure the element is visible. + + document.getElementById('a').addEventListener("change", function(aEvent) { + ok(true, "change event correctly sent"); + + SimpleTest.executeSoon(function() { + MockFilePicker.cleanup(); + SimpleTest.finish(); + }); + }); + + b.click(); +}); + +</script> +</pre> +</body> +</html> + diff --git a/dom/html/test/test_input_lastInteractiveValue.html b/dom/html/test/test_input_lastInteractiveValue.html new file mode 100644 index 0000000000..6ac29edaef --- /dev/null +++ b/dom/html/test/test_input_lastInteractiveValue.html @@ -0,0 +1,134 @@ +<!doctype html> +<title>Test for HTMLInputElement.lastInteractiveValue</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<script src="/tests/SimpleTest/NativeKeyCodes.js"></script> +<link href="/tests/SimpleTest/test.css"/> +<body> +<script> +const kIsMac = navigator.platform.indexOf("Mac") > -1; +const kIsWin = navigator.platform.indexOf("Win") > -1; + +function getFreshInput() { + let input = document.body.appendChild(document.createElement("input")); + input.focus(); + return input; +} + +// XXX This should be add_setup, but bug 1776589 +add_task(async function ensure_focus() { + await SimpleTest.promiseFocus(window); +}); + +add_task(async function simple() { + let input = getFreshInput(); + + is(SpecialPowers.wrap(input).lastInteractiveValue, "", "Initial state"); + + sendString("abc"); + + is(input.value, "abc", ".value after interactive edit"); + is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after interactive edit"); + + input.value = "muahahaha"; + is(input.value, "muahahaha", ".value after script edit"); + + is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after script edit"); +}); + +add_task(async function test_default_value() { + let input = getFreshInput(); + input.defaultValue = "default value"; + + is(input.value, "default value", ".defaultValue affects .value"); + is(SpecialPowers.wrap(input).lastInteractiveValue, "", "Default value is not interactive"); + + sendString("abc"); + + is(SpecialPowers.wrap(input).lastInteractiveValue, "default valueabc", "After interaction with default value"); +}); + +// This happens in imdb.com login form. +add_task(async function clone_after_interactive_edit() { + let input = getFreshInput(); + + sendString("abc"); + + is(input.value, "abc", ".value after interactive edit"); + is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after interactive edit"); + + let clone = input.cloneNode(true); + is(clone.value, "abc", ".value after clone"); + is(SpecialPowers.wrap(clone).lastInteractiveValue, "abc", ".lastInteractiveValue after clone"); + + clone.type = "hidden"; + + clone.value = "something random"; + is(SpecialPowers.wrap(clone).lastInteractiveValue, "", ".lastInteractiveValue after clone in non-text-input"); +}); + +add_task(async function set_user_input() { + let input = getFreshInput(); + + input.value = ""; + + SpecialPowers.wrap(input).setUserInput("abc"); + + is(input.value, "abc", ".value after setUserInput edit"); + is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after setUserInput"); + + input.value = "foobar"; + is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after script edit after setUserInput"); +}); + + +// TODO(emilio): Maybe execCommand shouldn't be considered interactive, but it +// matches pre-existing behavior effectively. +add_task(async function exec_command() { + let input = getFreshInput(); + + document.execCommand("insertText", false, "a"); + + is(input.value, "a", ".value after execCommand edit"); + + is(SpecialPowers.wrap(input).lastInteractiveValue, "a", ".lastInteractiveValue after execCommand"); + + input.value = "foobar"; + is(SpecialPowers.wrap(input).lastInteractiveValue, "a", ".lastInteractiveValue after script edit after execCommand"); +}); + +add_task(async function cut_paste() { + if (true) { + // TODO: the above condition should be if (!kIsMac && !kIsWin), but this + // fails (intermittently?) in those platforms, see bug 1776838. Disable for + // now. + todo(false, "synthesizeNativeKey doesn't work elsewhere (yet)"); + return; + } + + function doSynthesizeNativeKey(keyCode, modifiers, chars) { + return new Promise((resolve, reject) => { + if (!synthesizeNativeKey(KEYBOARD_LAYOUT_EN_US, keyCode, modifiers, chars, chars, resolve)) { + reject(new Error("Couldn't synthesize native key")); + } + }); + } + + let input = getFreshInput(); + + sendString("abc"); + + input.select(); + + is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue before cut"); + + await doSynthesizeNativeKey(kIsMac ? MAC_VK_ANSI_X : WIN_VK_X, { accelKey: true }, "x"); + + is(SpecialPowers.wrap(input).lastInteractiveValue, "", ".lastInteractiveValue after cut"); + + await doSynthesizeNativeKey(kIsMac ? MAC_VK_ANSI_V : WIN_VK_V, { accelKey: true }, "v"); + + is(SpecialPowers.wrap(input).lastInteractiveValue, "abc", ".lastInteractiveValue after paste"); +}); + +</script> diff --git a/dom/html/test/test_inputmode.html b/dom/html/test/test_inputmode.html new file mode 100644 index 0000000000..56bb101e8a --- /dev/null +++ b/dom/html/test/test_inputmode.html @@ -0,0 +1,132 @@ +<!DOCTYPE html> +<html> +<head> +<title>Tests for inputmode attribute</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/SpecialPowers.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<div> +<input id="a1" inputmode="none"> +<input id="a2" inputmode="text"> +<input id="a3" inputmode="tel"> +<input id="a4" inputmode="url"> +<input id="a5" inputmode="email"> +<input id="a6" inputmode="numeric"> +<input id="a7" inputmode="decimal"> +<input id="a8" inputmode="search"> +<input id="a9"> +<input id="a10" type="number" inputmode="numeric"> +<input id="a11" type="date" inputmode="numeric"> +<input id="a12" type="time" inputmode="numeric"> +<textarea id="b1" inputmode="none"></textarea> +<textarea id="b2" inputmode="text"></textarea> +<textarea id="b3" inputmode="tel"></textarea> +<textarea id="b4" inputmode="url"></textarea> +<textarea id="b5" inputmode="email"></textarea> +<textarea id="b6" inputmode="numeric"></textarea> +<textarea id="b7" inputmode="decimal"></textarea> +<textarea id="b8" inputmode="search"></textarea> +<textarea id="b9"></textarea> +<div contenteditable id="c1" inputmode="none"><span>c1</span></div> +<div contenteditable id="c2" inputmode="text"><span>c2</span></div> +<div contenteditable id="c3" inputmode="tel"><span>c3</span></div> +<div contenteditable id="c4" inputmode="url"><span>c4</span></div> +<div contenteditable id="c5" inputmode="email"><span>c5</span></div> +<div contenteditable id="c6" inputmode="numeric"><span>c6</span></div> +<div contenteditable id="c7" inputmode="decimal"><span>c7</span></div> +<div contenteditable id="c8" inputmode="search"><span>c8</span></div> +<div contenteditable id="c9"><span>c9</span></div> +<input id="d1" inputmode="URL"> <!-- no lowercase --> +</div> +<pre id="test"> +<script class=testbody" type="application/javascript"> +// eslint-disable-next-line mozilla/no-addtask-setup +add_task(async function setup() { + await new Promise(r => SimpleTest.waitForFocus(r)); +}); + +add_task(async function basic() { + const tests = [ + { id: "a1", inputmode: "none", desc: "inputmode of input element is none" }, + { id: "a2", inputmode: "text", desc: "inputmode of input element is text" }, + { id: "a3", inputmode: "tel", desc: "inputmode of input element is tel" }, + { id: "a4", inputmode: "url", desc: "inputmode of input element is url" }, + { id: "a5", inputmode: "email", desc: "inputmode of input element is email" }, + { id: "a6", inputmode: "numeric", desc: "inputmode of input element is numeric" }, + { id: "a7", inputmode: "decimal", desc: "inputmode of input element is decimal" }, + { id: "a8", inputmode: "search", desc: "inputmode of input element is search" }, + { id: "a9", inputmode: "", desc: "no inputmode of input element" }, + { id: "a10", inputmode: "numeric", desc: "inputmode of input type=number is numeric" }, + { id: "a11", inputmode: "", desc: "no inputmode due to type=date" }, + { id: "a12", inputmode: "", desc: "no inputmode due to type=time" }, + { id: "b1", inputmode: "none", desc: "inputmode of textarea element is none" }, + { id: "b2", inputmode: "text", desc: "inputmode of textarea element is text" }, + { id: "b3", inputmode: "tel", desc: "inputmode of textarea element is tel" }, + { id: "b4", inputmode: "url", desc: "inputmode of textarea element is url" }, + { id: "b5", inputmode: "email", desc: "inputmode of textarea element is email" }, + { id: "b6", inputmode: "numeric", desc: "inputmode of textarea element is numeric" }, + { id: "b7", inputmode: "decimal", desc: "inputmode of textarea element is decimal" }, + { id: "b8", inputmode: "search", desc: "inputmode of textarea element is search" }, + { id: "b9", inputmode: "", desc: "no inputmode of textarea element" }, + { id: "c1", inputmode: "none", desc: "inputmode of contenteditable is none" }, + { id: "c2", inputmode: "text", desc: "inputmode of contenteditable is text" }, + { id: "c3", inputmode: "tel", desc: "inputmode of contentedtiable is tel" }, + { id: "c4", inputmode: "url", desc: "inputmode of contentedtiable is url" }, + { id: "c5", inputmode: "email", desc: "inputmode of contentedtable is email" }, + { id: "c6", inputmode: "numeric", desc: "inputmode of contenteditable is numeric" }, + { id: "c7", inputmode: "decimal", desc: "inputmode of contenteditable is decimal" }, + { id: "c8", inputmode: "search", desc: "inputmode of contenteditable is search" }, + { id: "c9", inputmode: "", desc: "no inputmode of contentedtiable" }, + { id: "d1", inputmode: "url", desc: "inputmode of input element is URL" }, + ]; + + for (let test of tests) { + let element = document.getElementById(test.id); + if (element.tagName == "DIV") { + // Set caret to text node in contenteditable + window.getSelection().removeAllRanges(); + let range = document.createRange(); + range.setStart(element.firstChild.firstChild, 1); + range.setEnd(element.firstChild.firstChild, 1); + window.getSelection().addRange(range); + } else { + // input and textarea element + element.focus(); + } + is(SpecialPowers.DOMWindowUtils.focusedInputMode, test.inputmode, test.desc); + } +}); + +add_task(async function dynamicChange() { + const tests = ["a3", "b3", "c3"]; + for (let test of tests) { + let element = document.getElementById(test); + element.focus(); + is(SpecialPowers.DOMWindowUtils.focusedInputMode, "tel", "Initial inputmode"); + element.inputMode = "url"; + is(SpecialPowers.DOMWindowUtils.focusedInputMode, "url", + "inputmode in InputContext has to sync with current inputMode property"); + element.setAttribute("inputmode", "decimal"); + is(SpecialPowers.DOMWindowUtils.focusedInputMode, "decimal", + "inputmode in InputContext has to sync with current inputmode attribute"); + // Storing the original value may be safer. + element.inputMode = "tel"; + } + + let element = document.getElementById("a3"); + element.focus(); + is(SpecialPowers.DOMWindowUtils.focusedInputMode, "tel", "Initial inputmode"); + document.getElementById("a4").inputMode = "email"; + is(SpecialPowers.DOMWindowUtils.focusedInputMode, "tel", + "inputmode in InputContext keeps focused inputmode value"); + // Storing the original value may be safer. + document.getElementById("a4").inputMode = "url"; +}); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_li_attributes_reflection.html b/dom/html/test/test_li_attributes_reflection.html new file mode 100644 index 0000000000..fd6795226b --- /dev/null +++ b/dom/html/test/test_li_attributes_reflection.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLLIElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLLIElement attributes reflection **/ + +// .value +reflectInt({ + element: document.createElement("li"), + attribute: "value", + nonNegative: false, +}); + +// .type +reflectString({ + element: document.createElement("li"), + attribute: "type" +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_link_attributes_reflection.html b/dom/html/test/test_link_attributes_reflection.html new file mode 100644 index 0000000000..c75c9e2572 --- /dev/null +++ b/dom/html/test/test_link_attributes_reflection.html @@ -0,0 +1,96 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLLinkElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLLinkElement attributes reflection **/ + +// .href (URL) +reflectURL({ + element: document.createElement("link"), + attribute: "href", +}); + +// .crossOrigin (String or null) +reflectLimitedEnumerated({ + element: document.createElement("link"), + attribute: "crossOrigin", + // "" is a valid value per spec, but gets mapped to the "anonymous" state, + // just like invalid values, so just list it under invalidValues + validValues: [ "anonymous", "use-credentials" ], + invalidValues: [ + "", " aNOnYmous ", " UsE-CreDEntIALS ", "foobar", "FOOBAR", " fOoBaR " + ], + defaultValue: { invalid: "anonymous", missing: null }, + nullable: true, +}) + +// .rel (String) +reflectString({ + element: document.createElement("link"), + attribute: "rel", +}); + +// .media (String) +reflectString({ + element: document.createElement("link"), + attribute: "media", +}); + +// .hreflang (String) +reflectString({ + element: document.createElement("link"), + attribute: "hreflang", +}); + +// .type (String) +reflectString({ + element: document.createElement("link"), + attribute: "type", +}); + + +// .charset (String) +reflectString({ + element: document.createElement("link"), + attribute: "charset", +}); + +// .rev (String) +reflectString({ + element: document.createElement("link"), + attribute: "rev", +}); + +// .target (String) +reflectString({ + element: document.createElement("link"), + attribute: "target", +}); + +// .as (String) +reflectLimitedEnumerated({ + element: document.createElement("link"), + attribute: "as", + validValues: [ "fetch", "audio", "font", "image", "script", "style", "track", "video" ], + invalidValues: [ + "", "audi", "doc", "Emb", "foobar", "FOOBAR", " fOoBaR ", "OBJ", "document", "embed", "manifest", "object", "report", "serviceworker", "sharedworker", "worker", "xslt" + ], + defaultValue: { invalid: "", missing: "" }, + nullable: false, +}) + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_link_sizes.html b/dom/html/test/test_link_sizes.html new file mode 100644 index 0000000000..b242748886 --- /dev/null +++ b/dom/html/test/test_link_sizes.html @@ -0,0 +1,35 @@ +<!doctype html> +<html> +<head> +<title>Test link.sizes attribute</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" sizes="16x16 24x24 32x32 48x48"> +</head> +<body> + +<pre id="test"> +<script> + + var links = document.getElementsByTagName('link'); + for (var i = 0; i < links.length; ++i) { + var link = links[i]; + ok("sizes" in link, "link.sizes exists"); + + if (link.rel == 'shortcut icon') { + is(link.sizes.value, "16x16 24x24 32x32 48x48", 'link.sizes.value correct value'); + is(link.sizes.length, 4, 'link.sizes.length correct value'); + ok(link.sizes.contains('32x32'), 'link.sizes.contains() works'); + link.sizes.add('64x64'); + is(link.sizes.length, 5, 'link.sizes.length correct value'); + link.sizes.remove('64x64'); + is(link.sizes.length, 4, 'link.sizes.length correct value'); + is(link.sizes + "", "16x16 24x24 32x32 48x48", 'link.sizes stringify correct value'); + } else { + is(link.sizes.value, "", 'link.sizes correct value'); + } + } +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_map_attributes_reflection.html b/dom/html/test/test_map_attributes_reflection.html new file mode 100644 index 0000000000..8835fb29d2 --- /dev/null +++ b/dom/html/test/test_map_attributes_reflection.html @@ -0,0 +1,27 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLMapElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLMapElement attributes reflection **/ + +// .name (String) +reflectString({ + element: document.createElement("map"), + attribute: "name", +}) + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_meta_attributes_reflection.html b/dom/html/test/test_meta_attributes_reflection.html new file mode 100644 index 0000000000..e0cf0c347d --- /dev/null +++ b/dom/html/test/test_meta_attributes_reflection.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLMetaElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLMetaElement attributes reflection **/ + +// .name (String) +reflectString({ + element: document.createElement("meta"), + attribute: "name", +}) + +// .httpEquiv (String) +reflectString({ + element: document.createElement("meta"), + attribute: { content: "http-equiv", idl: "httpEquiv" }, +}) + +// .content (String) +reflectString({ + element: document.createElement("meta"), + attribute: "content", +}) + +// .scheme (String) +reflectString({ + element: document.createElement("meta"), + attribute: "scheme", +}) + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_mod_attributes_reflection.html b/dom/html/test/test_mod_attributes_reflection.html new file mode 100644 index 0000000000..0efa7c52bf --- /dev/null +++ b/dom/html/test/test_mod_attributes_reflection.html @@ -0,0 +1,41 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLModElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLModElement attributes reflection **/ + +// .cite (URL) +reflectURL({ + element: document.createElement("ins"), + attribute: "cite", +}) +reflectURL({ + element: document.createElement("del"), + attribute: "cite", +}) + +// .dateTime (String) +reflectString({ + element: document.createElement("ins"), + attribute: "dateTime", +}) +reflectString({ + element: document.createElement("del"), + attribute: "dateTime", +}) + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_multipleFilePicker.html b/dom/html/test/test_multipleFilePicker.html new file mode 100644 index 0000000000..c4a71151aa --- /dev/null +++ b/dom/html/test/test_multipleFilePicker.html @@ -0,0 +1,79 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for single filepicker per event</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + <div id='foo'><a href='#'>Click here to test this issue</a></div> + <script> + +SimpleTest.requestFlakyTimeout("Timeouts are needed to simulate user-interaction"); +SimpleTest.waitForExplicitFinish(); + +let clickCount = 0; +let foo = document.getElementById('foo'); +foo.addEventListener('click', _ => { + if (++clickCount < 10) { + let input = document.createElement('input'); + input.type = 'file'; + foo.appendChild(input); + input.click(); + } +}); + +let MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.init(window); + +let pickerCount = 0; + +SpecialPowers.pushPrefEnv({ + set: [["dom.disable_open_during_load", true]], +}) +// Let's do the first click. +.then(() => { + return new Promise(resolve => { + MockFilePicker.showCallback = function(filepicker) { + ++pickerCount; + resolve(); + } + setTimeout(_ => { + is(pickerCount, 0, "No file picker initially"); + synthesizeMouseAtCenter(foo, {}); + }, 0); + }) +}) + +// Let's wait a bit more, then let's do a click. +.then(() => { + return new Promise(resolve => { + MockFilePicker.showCallback = function(filepicker) { + ++pickerCount; + resolve(); + } + + setTimeout(() => { + is(pickerCount, 1, "Only 1 file picker"); + is(clickCount, 10, "10 clicks triggered"); + clickCount = 0; + pickerCount = 0; + synthesizeMouseAtCenter(foo, {}); + }, 1000); + }); +}) + +// Another click... +.then(_ => { + setTimeout(() => { + is(pickerCount, 1, "Only 1 file picker"); + is(clickCount, 10, "10 clicks triggered"); + MockFilePicker.cleanup(); + SimpleTest.finish(); + }, 1000); +}); + +</script> +</body> +</html> diff --git a/dom/html/test/test_named_options.html b/dom/html/test/test_named_options.html new file mode 100644 index 0000000000..8c38425240 --- /dev/null +++ b/dom/html/test/test_named_options.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=772869 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 772869</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772869">Mozilla Bug 772869</a> +<p id="display"></p> +<div id="content" style="display: none"> + <select id="s"> + <option name="x"></option> + <option name="y" id="z"></option> + <option name="z" id="x"></option> + <option id="w"></option> + </select> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 772869 **/ +var opt = $("s").options; +opt.loopy = "something" +var names = Object.getOwnPropertyNames(opt); +is(names.length, 9, "Should have nine entries"); +is(names[0], "0", "Entry 1") +is(names[1], "1", "Entry 2") +is(names[2], "2", "Entry 3") +is(names[3], "3", "Entry 4") +is(names[4], "x", "Entry 5") +is(names[5], "y", "Entry 6") +is(names[6], "z", "Entry 7") +is(names[7], "w", "Entry 8") +is(names[8], "loopy", "Entry 9") + +var names2 = []; +for (var name in opt) { + names2.push(name); +} +is(names2.length, 11, "Should have eleven enumerated names"); +is(names2[0], "0", "Enum entry 1") +is(names2[1], "1", "Enum entry 2") +is(names2[2], "2", "Enum entry 3") +is(names2[3], "3", "Enum entry 4") +is(names2[4], "loopy", "Enum entry 5") +is(names2[5], "add", "Enum entrry 6") +is(names2[6], "remove", "Enum entry 7") +is(names2[7], "length", "Enum entry 8") +is(names2[8], "selectedIndex", "Enum entry 9") +is(names2[9], "item", "Enum entry 10") +is(names2[10], "namedItem", "Enum entry 11") + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_nested_invalid_fieldsets.html b/dom/html/test/test_nested_invalid_fieldsets.html new file mode 100644 index 0000000000..7c00693697 --- /dev/null +++ b/dom/html/test/test_nested_invalid_fieldsets.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=914029 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 914029</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script type="application/javascript"> + + /** Test for Bug 914029 **/ + + var innerFieldset = document.createElement("fieldset"); + var outerFieldset = document.createElement("fieldset"); + var textarea = document.createElement("textarea"); + textarea.setAttribute("required", ""); + innerFieldset.appendChild(textarea); + outerFieldset.appendChild(innerFieldset); + SpecialPowers.forceGC(); + ok(true, "This page did not crash - dynamically added nested invalid fieldsets" + + " work correctly."); + var innerFieldset = document.createElement("fieldset"); + var outerFieldset = document.createElement("fieldset"); + var textarea = document.createElement("textarea"); + var textarea2 = document.createElement("textarea"); + textarea.setAttribute("required", ""); + innerFieldset.appendChild(textarea); + innerFieldset.appendChild(textarea2); + outerFieldset.appendChild(innerFieldset); + SpecialPowers.forceGC(); + ok(true, "This page did not crash - dynamically added nested invalid fieldsets" + + " work correctly."); + + </script> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=914029">Mozilla Bug 914029</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> +</body> +</html> diff --git a/dom/html/test/test_nestediframe.html b/dom/html/test/test_nestediframe.html new file mode 100644 index 0000000000..ddbf0ca9dc --- /dev/null +++ b/dom/html/test/test_nestediframe.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for same URLs nested iframes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> + test_nestediframe body +<script> + +SimpleTest.waitForExplicitFinish(); + +function reportState(msg) { + if (location.href.includes("#")) { + parent.postMessage(msg, "*"); + return; + } + + if (msg == "OK 1") { + ok(true, "First frame loaded"); + } else if (msg == "KO 2") { + ok(true, "Second frame load failed"); + SimpleTest.finish(); + } else { + ok(false, "Unknown message: " + msg); + } +} + +addEventListener("message", event => { + reportState(event.data); +}); + +var recursion; +if (!location.href.includes("#")) { + recursion = 1; +} else { + recursion = parseInt(location.href.split("#")[1]) + 1; +} + +var ifr = document.createElement('iframe'); +ifr.src = location.href.split("#")[0] + "#" + recursion; + +ifr.onload = function() { + reportState("OK " + recursion); +} +ifr.onerror = function() { + reportState("KO " + recursion); +} + +document.body.appendChild(ifr); + +</script> +</body> +</html> diff --git a/dom/html/test/test_non-ascii-cookie.html b/dom/html/test/test_non-ascii-cookie.html new file mode 100644 index 0000000000..a15923f39d --- /dev/null +++ b/dom/html/test/test_non-ascii-cookie.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=784367 +--> +<head> + <meta charset="utf-8"> + <title>Test for non-ASCII cookie values</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=784367">Mozilla Bug 784367</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for non-ASCII cookie values **/ + +SimpleTest.waitForExplicitFinish(); + +var gScript = SpecialPowers.loadChromeScript(SimpleTest.getTestFileURL("file_cookiemanager.js")); + +function getCookieFromManager() { + return new Promise(resolve => { + gScript.addMessageListener("getCookieFromManager:return", function gcfm({ cookie }) { + gScript.removeMessageListener("getCookieFromManager:return", gcfm); + resolve(cookie); + }); + gScript.sendAsyncMessage("getCookieFromManager", { host: location.hostname, path: location.pathname }); + }); +} + +SpecialPowers.pushPrefEnv({ + "set": [ + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ] +}, () => { + var c = document.cookie; + is(document.cookie, 'abc=012©ABC\ufffdDEF', "document.cookie should be decoded as UTF-8"); + + var newCookie; + + getCookieFromManager().then((cookie) => { + is(cookie, document.cookie, "nsICookieManager should be consistent with document.cookie"); + newCookie = 'def=∼≩≭≧∯≳≲≣∽≸≸∺≸∠≯≮≥≲≲≯≲∽≡≬≥≲≴∨∱∩∾'; + document.cookie = newCookie; + is(document.cookie, c + '; ' + newCookie, "document.cookie should be encoded as UTF-8"); + + return getCookieFromManager(); + }).then((cookie) => { + is(cookie, document.cookie, "nsICookieManager should be consistent with document.cookie"); + var date1 = new Date(); + date1.setTime(0); + document.cookie = newCookie + 'def=;expires=' + date1.toGMTString(); + gScript.destroy(); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); + }); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_non-ascii-cookie.html^headers^ b/dom/html/test/test_non-ascii-cookie.html^headers^ new file mode 100644 index 0000000000..54aa6c3e72 --- /dev/null +++ b/dom/html/test/test_non-ascii-cookie.html^headers^ @@ -0,0 +1 @@ +Set-Cookie: abc=012©ABC©DEF diff --git a/dom/html/test/test_object_attributes_reflection.html b/dom/html/test/test_object_attributes_reflection.html new file mode 100644 index 0000000000..d55183db07 --- /dev/null +++ b/dom/html/test/test_object_attributes_reflection.html @@ -0,0 +1,117 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLObjectElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLObjectElement attributes reflection **/ + +// .data (URL) +reflectURL({ + element: document.createElement("object"), + attribute: "data", +}); + +// .type (String) +reflectString({ + element: document.createElement("object"), + attribute: "type", +}); + +// .name (String) +reflectString({ + element: document.createElement("object"), + attribute: "name", +}); + +// .useMap (String) +reflectString({ + element: document.createElement("object"), + attribute: "useMap", +}); + +// .width (String) +reflectString({ + element: document.createElement("object"), + attribute: "width", +}); + +// .height (String) +reflectString({ + element: document.createElement("object"), + attribute: "height", +}); + +// .align (String) +reflectString({ + element: document.createElement("object"), + attribute: "align", +}); + +// .archive (String) +reflectString({ + element: document.createElement("object"), + attribute: "archive", +}); + +// .code (String) +reflectString({ + element: document.createElement("object"), + attribute: "code", +}); + +// .declare (String) +reflectBoolean({ + element: document.createElement("object"), + attribute: "declare", +}); + +// .hspace (unsigned int) +reflectUnsignedInt({ + element: document.createElement("object"), + attribute: "hspace", +}); + +// .standby (String) +reflectString({ + element: document.createElement("object"), + attribute: "standby", +}); + +// .vspace (unsigned int) +reflectUnsignedInt({ + element: document.createElement("object"), + attribute: "vspace", +}); + +// .codeBase (URL) +reflectURL({ + element: document.createElement("object"), + attribute: "codeBase", +}); + +// .codeType (String) +reflectString({ + element: document.createElement("object"), + attribute: "codeType", +}); + +// .border (String) +reflectString({ + element: document.createElement("object"), + attribute: "border", + extendedAttributes: { TreatNullAs: "EmptyString" }, +}); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_ol_attributes_reflection.html b/dom/html/test/test_ol_attributes_reflection.html new file mode 100644 index 0000000000..a941914077 --- /dev/null +++ b/dom/html/test/test_ol_attributes_reflection.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLOLElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLOLElement attributes reflection **/ + +// .reversed (boolean) +reflectBoolean({ + element: document.createElement("ol"), + attribute: "reversed", +}) + +// .start +reflectInt({ + element: document.createElement("ol"), + attribute: "start", + nonNegative: false, + defaultValue: 1, +}); + +// .type +reflectString({ + element: document.createElement("ol"), + attribute: "type" +}); + +// .compact +reflectBoolean({ + element: document.createElement("ol"), + attribute: "compact", +}) + +// Additional tests for ol.start behavior when li elements are added +var ol = document.createElement("ol"); +var li = document.createElement("li"); +li.value = 42; +ol.appendChild(li); +is(ol.start, 1, "ol.start with one li child, li.value = 42:"); +li.value = -42; +is(ol.start, 1, "ol.start with one li child, li.value = 42:"); +ol.removeAttribute("start"); +li.removeAttribute("value"); +ol.appendChild(document.createElement("li")); +ol.reversed = true; +todo_is(ol.start, 2, "ol.start with two li children, ol.reversed == true:"); +li.value = 42; +todo_is(ol.start, 2, "ol.start with two li childern, ol.reversed == true:"); +ol.start = 42; +is(ol.start, 42, "ol.start = 42:"); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_option_defaultSelected.html b/dom/html/test/test_option_defaultSelected.html new file mode 100644 index 0000000000..6d999c0b29 --- /dev/null +++ b/dom/html/test/test_option_defaultSelected.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=927796 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 927796</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=927796">Mozilla Bug 927796</a> +<p id="display"> +<select id="s1"> + <option selected>one</option> + <option>two</option> +</select> +<select id="s2" size="5"> + <option selected>one</option> + <option>two</option> +</select> +</p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + <script type="application/javascript"> + + /** Test for Bug 927796 **/ + var s1 = $("s1"); + s1.options[0].defaultSelected = false; + is(s1.options[0].selected, true, + "First option in combobox should still be selected"); + is(s1.options[1].selected, false, + "Second option in combobox should not be selected"); + + var s2 = $("s2"); + s2.options[0].defaultSelected = false; + is(s2.options[0].selected, false, + "First option in listbox should not be selected"); + is(s2.options[1].selected, false, + "Second option in listbox should not be selected"); + </script> +</body> +</html> diff --git a/dom/html/test/test_option_selected_state.html b/dom/html/test/test_option_selected_state.html new file mode 100644 index 0000000000..30a634de58 --- /dev/null +++ b/dom/html/test/test_option_selected_state.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=942648 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 942648</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=942648">Mozilla Bug 942648</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + <select> + <option value="1">1</option> + <option id="e1" value="2">2</option> + </select> + <select> + <option value="1">1</option> + <option id="e2" selected value="2">2</option> + </select> + <select> + <option value="1">1</option> + <option id="e3" selected="" value="2">2</option> + </select> + <select> + <option value="1">1</option> + <option id="e4" selected="selected" value="2">2</option> + </select> +</pre> + <script type="application/javascript"> + + /** Test for Bug 942648 **/ +SimpleTest.waitForExplicitFinish(); + window.onload = function() { + var e1 = document.getElementById('e1'); + var e2 = document.getElementById('e2'); + var e3 = document.getElementById('e3'); + var e4 = document.getElementById('e4'); + ok(!e1.selected, "e1 should not be selected"); + ok(e2.selected, "e2 should be selected"); + ok(e3.selected, "e3 should be selected"); + ok(e4.selected, "e4 should be selected"); + e1.setAttribute('selected', 'selected'); + e2.setAttribute('selected', 'selected'); + e3.setAttribute('selected', 'selected'); + e4.setAttribute('selected', 'selected'); + ok(e1.selected, "e1 should now be selected"); + ok(e2.selected, "e2 should still be selected"); + ok(e3.selected, "e3 should still be selected"); + ok(e4.selected, "e4 should still be selected"); + SimpleTest.finish(); + }; + </script> +</body> +</html> diff --git a/dom/html/test/test_param_attributes_reflection.html b/dom/html/test/test_param_attributes_reflection.html new file mode 100644 index 0000000000..977fb61935 --- /dev/null +++ b/dom/html/test/test_param_attributes_reflection.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLParamElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLParamElement attributes reflection **/ + +// .name +reflectString({ + element: document.createElement("param"), + attribute: "name", +}); + +// .value +reflectString({ + element: document.createElement("param"), + attribute: "value" +}); + +// .type +reflectString({ + element: document.createElement("param"), + attribute: "type" +}); + +// .valueType +reflectString({ + element: document.createElement("param"), + attribute: "valueType" +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_plugin.tst b/dom/html/test/test_plugin.tst new file mode 100644 index 0000000000..323fae03f4 --- /dev/null +++ b/dom/html/test/test_plugin.tst @@ -0,0 +1 @@ +foobar diff --git a/dom/html/test/test_q_attributes_reflection.html b/dom/html/test/test_q_attributes_reflection.html new file mode 100644 index 0000000000..a840e6f0e5 --- /dev/null +++ b/dom/html/test/test_q_attributes_reflection.html @@ -0,0 +1,32 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLQuoteElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLQuoteElement attributes reflection **/ + +// .cite +reflectURL({ + element: document.createElement("q"), + attribute: "cite", +}); + +reflectURL({ + element: document.createElement("blockquote"), + attribute: "cite", +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_restore_from_parser_fragment.html b/dom/html/test/test_restore_from_parser_fragment.html new file mode 100644 index 0000000000..7fb3b75e46 --- /dev/null +++ b/dom/html/test/test_restore_from_parser_fragment.html @@ -0,0 +1,59 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=644959 +--> +<head> + <title>Test for Bug 644959</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=644959">Mozilla Bug 644959</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 644959 **/ + +var content = document.getElementById('content'); + +function appendHTML(aParent, aElementString) +{ + aParent.innerHTML = "<form>" + aElementString + "</form>"; +} + +function clearHTML(aParent) +{ + aParent.innerHTML = ""; +} + +var tests = [ + [ "button", "<button></button>" ], + [ "input", "<input>" ], + [ "textarea", "<textarea></textarea>" ], + [ "select", "<select></select>" ], +]; + +var element = null; + +for (var test of tests) { + appendHTML(content, test[1]); + element = content.getElementsByTagName(test[0])[0]; + is(element.disabled, false, "element shouldn't be disabled"); + element.disabled = true; + is(element.disabled, true, "element should be disabled"); + + clearHTML(content); + + appendHTML(content, test[1]); + element = content.getElementsByTagName(test[0])[0]; + is(element.disabled, false, "element shouldn't be disabled"); +} + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_rowscollection.html b/dom/html/test/test_rowscollection.html new file mode 100644 index 0000000000..0e5152e1d5 --- /dev/null +++ b/dom/html/test/test_rowscollection.html @@ -0,0 +1,69 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=772869 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 772869</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=772869">Mozilla Bug 772869</a> +<p id="display"></p> +<div id="content" style="display: none"> + <table id="f"> + <thead> + <tr id="x"></tr> + </thead> + <tfoot> + <tr id="z"></tr> + <tr id="w"></tr> + </tfoot> + <tr id="x"></tr> + <tr id="y"></tr> + <tbody> + <tr id="z"></tr> + </tbody> + </table> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Bug 772869 **/ +var x = $("f").rows; +x.something = "another"; +var names = []; +for (var name in x) { + names.push(name); +} +is(names.length, 10, "Should have 10 enumerated names"); +is(names[0], "0", "Enum entry 1") +is(names[1], "1", "Enum entry 2") +is(names[2], "2", "Enum entry 3") +is(names[3], "3", "Enum entry 4") +is(names[4], "4", "Enum entry 5") +is(names[5], "5", "Enum entry 6") +is(names[6], "something", "Enum entry 7") +is(names[7], "item", "Enum entry 8") +is(names[8], "namedItem", "Enum entry 9") +is(names[9], "length", "Enum entry 10"); + +names = Object.getOwnPropertyNames(x); +is(names.length, 11, "Should have 11 items"); +is(names[0], "0", "Entry 1") +is(names[1], "1", "Entry 2") +is(names[2], "2", "Entry 3") +is(names[3], "3", "Entry 4") +is(names[4], "4", "Entry 5") +is(names[5], "5", "Entry 6") +is(names[6], "x", "Entry 7") +is(names[7], "y", "Entry 8") +is(names[8], "z", "Entry 9") +is(names[9], "w", "Entry 10") +is(names[10], "something", "Entry 11") +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_script_module.html b/dom/html/test/test_script_module.html new file mode 100644 index 0000000000..26b9cd6f65 --- /dev/null +++ b/dom/html/test/test_script_module.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLScriptElement with nomodule attribute</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> + +<body> + <script> +onmessage = (e) => { + if ("done" in e.data) { + SimpleTest.finish(); + } else if ("check" in e.data) { + ok(e.data.check, e.data.msg); + } else { + ok(false, "Unknown message"); + } +} + + +var ifr = document.createElement('iframe'); +ifr.src = "file_script_module.html"; +document.body.appendChild(ifr); + + +SimpleTest.waitForExplicitFinish(); + </script> + +</body> +</html> diff --git a/dom/html/test/test_set_input_files.html b/dom/html/test/test_set_input_files.html new file mode 100644 index 0000000000..3b7bf20909 --- /dev/null +++ b/dom/html/test/test_set_input_files.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1384030 +--> +<head> + <title>Test for Setting <input type=file>.files </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1384030">Mozilla Bug 1384030</a> +<pre id="test"> +<script type="application/javascript"> + +/** Test for Setting <input type=file>.files **/ + +function runTest() +{ + const form = document.createElement("form"); + const formInput = document.createElement("input"); + formInput.type = "file"; + formInput.name = "inputFile"; + form.appendChild(formInput); + + const input = document.createElement("input"); + input.type = "file"; + SpecialPowers.wrap(input).mozSetFileArray([ + new File(["foo"], "foo"), + new File(["bar"], "bar") + ]); + + formInput.files = input.files; + + const inputFiles = (new FormData(form)).getAll("inputFile"); + is(inputFiles.length, 2, "FormData should contain two input files"); + + is(inputFiles[0].name, "foo", "Input file name should be 'foo'"); + is(inputFiles[1].name, "bar", "Input file name should be 'bar'"); + + is(inputFiles[0], input.files[0], + "Expect the same File object as input file 'foo'"); + is(inputFiles[1], input.files[1], + "Expect the same File object as input file 'bar'"); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +window.addEventListener('load', runTest); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_srcdoc-2.html b/dom/html/test/test_srcdoc-2.html new file mode 100644 index 0000000000..5db7d69529 --- /dev/null +++ b/dom/html/test/test_srcdoc-2.html @@ -0,0 +1,57 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=802895 +--> + <head> +<title>Test session history for srcdoc iframes introduced in bug 802895</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> + +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=802895">Mozilla Bug 802895</a> + +<iframe id="pframe" name="pframe" src="file_srcdoc-2.html"></iframe> +<pre id="test"> +<script> + + SimpleTest.waitForExplicitFinish(); + var pframe = $("pframe"); + + //disable bfcache + pframe.contentWindow.addEventListener("unload", function () { }); + + var loadState = 0; + pframe.onload = function () { + SimpleTest.executeSoon(function () { + + var pDoc = pframe.contentDocument; + + if (loadState == 0) { + var div = pDoc.createElement("div"); + div.id = "modifyCheck"; + div.innerHTML = "hello again"; + pDoc.body.appendChild(div); + ok(pDoc.getElementById("modifyCheck"), "Child element not created"); + pframe.src = "about:blank"; + loadState = 1; + } + else if (loadState == 1) { + loadState = 2; + window.history.back(); + } + else if (loadState == 2) { + ok(!pDoc.getElementById("modifyCheck"), "modifyCheck element shouldn't be present"); + is(pDoc.getElementById("iframe").contentDocument.body.innerHTML, + "Hello World", "srcdoc iframe not present"); + SimpleTest.finish(); + } + + }) + }; + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_srcdoc.html b/dom/html/test/test_srcdoc.html new file mode 100644 index 0000000000..b3137f4e0a --- /dev/null +++ b/dom/html/test/test_srcdoc.html @@ -0,0 +1,118 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=802895 +--> + <head> +<title>Tests for srcdoc iframes introduced in bug 802895</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=802895">Mozilla Bug 802895</a> + +<iframe id="pframe" src="file_srcdoc.html"></iframe> + +<pre id="test"> +<script> + + SimpleTest.waitForExplicitFinish(); + SimpleTest.requestFlakyTimeout("untriaged"); + var pframe = $("pframe"); + + var loadState = 0; + pframe.contentWindow.addEventListener("load", function () { + + var pframeDoc = pframe.contentDocument; + + var iframe = pframeDoc.getElementById("iframe"); + var innerDoc = iframe.contentDocument; + var iframe1 = pframeDoc.getElementById("iframe1"); + var innerDoc1 = iframe1.contentDocument; + + var finish = false; + var finish1 = false; + var finish3 = false; + + + + is(iframe.srcdoc, "Hello World", "Bad srcdoc attribute contents") + + is(innerDoc.domain, document.domain, "Wrong domain"); + is(innerDoc.referrer, pframeDoc.referrer, "Wrong referrer"); + is(innerDoc.body.innerHTML, "Hello World", "Wrong body"); + is(innerDoc.compatMode, "CSS1Compat", "Not standards compliant"); + + is(innerDoc1.domain, document.domain, "Wrong domain with src attribute"); + is(innerDoc1.referrer, pframeDoc.referrer, "Wrong referrer with src attribute"); + is(innerDoc1.body.innerHTML, "Goodbye World", "Wrong body with src attribute") + is(innerDoc1.compatMode, "CSS1Compat", "Not standards compliant with src attribute"); + + var iframe2 = pframeDoc.getElementById("iframe2"); + var innerDoc2 = iframe2.contentDocument; + try { + innerDoc2.domain; + foundError = false; + } + catch (error) { + foundError = true; + } + ok(foundError, "srcdoc iframe not sandboxed"); + + //Test changed srcdoc attribute + iframe.onload = function () { + + iframe = pframeDoc.getElementById("iframe"); + innerDoc = iframe.contentDocument; + + is(iframe.srcdoc, "Hello again", "Bad srcdoc attribute contents with srcdoc attribute changed"); + is(innerDoc.domain, document.domain, "Wrong domain with srcdoc attribute changed"); + is(innerDoc.referrer, pframeDoc.referrer, "Wrong referrer with srcdoc attribute changed"); + is(innerDoc.body.innerHTML, "Hello again", "Wrong body with srcdoc attribute changed"); + is(innerDoc.compatMode, "CSS1Compat", "Not standards compliant with srcdoc attribute changed"); + + finish = true; + if (finish && finish1 && finish3) { + SimpleTest.finish(); + } + }; + + iframe.srcdoc = "Hello again"; + + var iframe3 = pframeDoc.getElementById("iframe3"); + + // Test srcdoc attribute removal + iframe3.onload = function () { + var innerDoc3 = iframe3.contentDocument; + is(innerDoc3.body.innerText, "Gone", "Bad srcdoc attribute removal"); + finish3 = true; + if (finish && finish1 && finish3) { + SimpleTest.finish(); + } + } + + iframe3.removeAttribute("srcdoc"); + + + var iframe1load = false; + iframe1.onload = function () { + iframe1load = true; + } + + iframe1.src = "data:text/plain;charset=US-ASCII,Goodbyeeee"; + + // Need to test that changing the src doesn't change the iframe. + setTimeout(function () { + ok(!iframe1load, "Changing src attribute shouldn't cause a load when srcdoc is set"); + finish1 = true; + if (finish && finish1 && finish3) { + SimpleTest.finish(); + } + }, 2000); + + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_style_attributes_reflection.html b/dom/html/test/test_style_attributes_reflection.html new file mode 100644 index 0000000000..745ed7435f --- /dev/null +++ b/dom/html/test/test_style_attributes_reflection.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test for HTMLStyleElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLStyleElement attributes reflection **/ + +var e = document.createElement("style"); + +// .media +reflectString({ + element: e, + attribute: "media" +}); + +// .type +reflectString({ + element: e, + attribute: "type" +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_track.html b/dom/html/test/test_track.html new file mode 100644 index 0000000000..50051bf2d6 --- /dev/null +++ b/dom/html/test/test_track.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=833386 +--> +<head> + <meta charset='utf-8'> + <title>Test for Bug 833386 - HTMLTrackElement</title> + <script type="text/javascript" src="/MochiKit/MochiKit.js"></script> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/html/test/reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +reflectLimitedEnumerated({ + element: document.createElement("track"), + attribute: "kind", + validValues: ["subtitles", "captions", "descriptions", "chapters", + "metadata"], + invalidValues: ["foo", "bar", "\u0000", "null", "", "subtitle", "caption", + "description", "chapter", "meta"], + defaultValue: { missing: "subtitles", invalid: "metadata" }, +}); + +// Default attribute +reflectBoolean({ + element: document.createElement("track"), + attribute: "default" +}); + +// Label attribute +reflectString({ + element: document.createElement("track"), + attribute: "label", + otherValues: [ "foo", "BAR", "_FoO", "\u0000", "null", "white space" ] +}); + +// Source attribute +reflectURL({ + element: document.createElement("track"), + attribute: "src", + otherValues: ["foo", "bar", "\u0000", "null", ""] +}); + +// Source Language attribute +reflectString({ + element: document.createElement("track"), + attribute: "srclang", + otherValues: ["foo", "bar", "\u0000", "null", ""] +}); + +var track = document.createElement("track"); +is(track.readyState, 0, "Default ready state should be 0 (NONE)."); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_ul_attributes_reflection.html b/dom/html/test/test_ul_attributes_reflection.html new file mode 100644 index 0000000000..cd5f6b1cc2 --- /dev/null +++ b/dom/html/test/test_ul_attributes_reflection.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for HTMLUListElement attributes reflection</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" src="reflect.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script type="application/javascript"> + +/** Test for HTMLUListElement attributes reflection **/ + +// .compact +reflectBoolean({ + element: document.createElement("ul"), + attribute: "compact" +}); + +// .type +reflectString({ + element: document.createElement("ul"), + attribute: "type" +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_viewport_resize.html b/dom/html/test/test_viewport_resize.html new file mode 100644 index 0000000000..e800aa592a --- /dev/null +++ b/dom/html/test/test_viewport_resize.html @@ -0,0 +1,44 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1135812 +--> +<head> + <title>Test for Bug 1135812</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1135812">Mozilla Bug 1135812</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> + +<iframe style="width: 50px;" + srcdoc='<picture><source srcset="data:,a" media="(min-width: 150px)" /><source srcset="data:,b" media="(min-width: 100px)" /><img src="data:,c" /></picture>'></iframe> +<script> + SimpleTest.waitForExplicitFinish(); + addEventListener('load', function() { + var iframe = document.querySelector('iframe'); + var img = iframe.contentDocument.querySelector('img'); + is(img.currentSrc, 'data:,c'); + + img.onload = function() { + is(img.currentSrc, 'data:,a'); + img.onload = function() { + is(img.currentSrc, 'data:,b'); + SimpleTest.finish(); + } + img.onerror = img.onload; + iframe.style.width = '120px'; + }; + img.onerror = img.onload; + + iframe.style.width = '200px'; + }, true); +</script> +</pre> +</body> +</html> diff --git a/dom/html/test/test_window_open_close.html b/dom/html/test/test_window_open_close.html new file mode 100644 index 0000000000..0869100b4c --- /dev/null +++ b/dom/html/test/test_window_open_close.html @@ -0,0 +1,53 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/EventUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<script type="application/javascript"> + +SimpleTest.waitForExplicitFinish(); + +// Opens a popup. Link should load in main browser window. Popup should be closed when link clicked. +function openWindow1() { + return window.open('file_window_open_close_outer.html','','width=300,height=200'); +} + +// Opens a new tab T1. Link opens in another new tab T2. T1 should close when link clicked. +function openWindow2() { + return window.open('file_window_open_close_outer.html'); +} + +// Opens a new window. Link should open in a new tab of that window, but then both windows should close. +function openWindow3() { + return window.open('file_window_open_close_outer.html', '', 'toolbar=1'); +} + +var TESTS = [openWindow1, openWindow2, openWindow3]; + +function popupLoad(win) +{ + info("Sending click"); + sendMouseEvent({type: "click"}, "link", win); + ok(true, "Didn't crash"); + + next(); +} + +function next() +{ + if (!TESTS.length) { + SimpleTest.finish(); + } else { + var test = TESTS.shift(); + var w = test(); + w.addEventListener("load", (e) => popupLoad(w)); + } +} +</script> + +<body onload="next()"> +</body> +</html> diff --git a/dom/html/test/test_window_open_from_closing.html b/dom/html/test/test_window_open_from_closing.html new file mode 100644 index 0000000000..0d38c88d84 --- /dev/null +++ b/dom/html/test/test_window_open_from_closing.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> +<head> + <title>window.open from a window being closed</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> + <h1>window.open from a window being closed</h1> +<script> +add_task(async function() { + const RELS = ["", "#noopener", "#opener"]; + const FEATURES = [ + "", + "noopener", + "width=300", + "width=300,noopener", + ]; + + let resolver; + let channel = new BroadcastChannel("test"); + channel.onmessage = function(e) { + info("message from broadcastchannel: " + e.data); + if (e.data == "load") { + resolver(); + } + }; + + for (let rel of RELS) { + for (let feature of FEATURES) { + info(`running test: rel=${rel}, feature=${feature}`); + + let loadPromise = new Promise(r => { resolver = r; }); + window.open("file_window_close_and_open.html" + rel, "_blank", feature); + await loadPromise; + ok(true, "popup opened successfully - closing..."); + channel.postMessage("close"); + } + } +}); +</script> +</body> +</html> |