diff options
Diffstat (limited to '')
-rw-r--r-- | dom/html/input/DateTimeInputTypes.cpp | 504 |
1 files changed, 504 insertions, 0 deletions
diff --git a/dom/html/input/DateTimeInputTypes.cpp b/dom/html/input/DateTimeInputTypes.cpp new file mode 100644 index 0000000000..c113d07718 --- /dev/null +++ b/dom/html/input/DateTimeInputTypes.cpp @@ -0,0 +1,504 @@ +/* -*- 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->IsDisabled() && + !mInputElement->HasAttr(nsGkAtoms::readonly); +} + +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); +} + +bool DateInputType::ConvertStringToNumber(nsAString& aValue, + Decimal& aResultValue) const { + uint32_t year, month, day; + if (!ParseDate(aValue, &year, &month, &day)) { + return false; + } + + JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day)); + if (!time.isValid()) { + return false; + } + + aResultValue = Decimal::fromDouble(time.toDouble()); + return true; +} + +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); +} + +bool TimeInputType::ConvertStringToNumber(nsAString& aValue, + Decimal& aResultValue) const { + uint32_t milliseconds; + if (!ParseTime(aValue, &milliseconds)) { + return false; + } + + aResultValue = Decimal(int32_t(milliseconds)); + return true; +} + +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); +} + +bool WeekInputType::ConvertStringToNumber(nsAString& aValue, + Decimal& aResultValue) const { + uint32_t year, week; + if (!ParseWeek(aValue, &year, &week)) { + return false; + } + + if (year < kMinimumYear || year > kMaximumYear) { + return false; + } + + // Maximum week is 275760-W37, the week of 275760-09-13. + if (year == kMaximumYear && week > kMaximumWeekInMaximumYear) { + return false; + } + + double days = DaysSinceEpochFromWeek(year, week); + aResultValue = Decimal::fromDouble(days * kMsPerDay); + return true; +} + +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; + + // 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); +} + +bool MonthInputType::ConvertStringToNumber(nsAString& aValue, + Decimal& aResultValue) const { + uint32_t year, month; + if (!ParseMonth(aValue, &year, &month)) { + return false; + } + + if (year < kMinimumYear || year > kMaximumYear) { + return false; + } + + // Maximum valid month is 275760-09. + if (year == kMaximumYear && month > kMaximumMonthInMaximumYear) { + return false; + } + + int32_t months = MonthsSinceJan1970(year, month); + aResultValue = Decimal(int32_t(months)); + return true; +} + +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); +} + +bool DateTimeLocalInputType::ConvertStringToNumber( + nsAString& aValue, Decimal& aResultValue) const { + uint32_t year, month, day, timeInMs; + if (!ParseDateTimeLocal(aValue, &year, &month, &day, &timeInMs)) { + return false; + } + + JS::ClippedTime time = + JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs)); + if (!time.isValid()) { + return false; + } + + aResultValue = Decimal::fromDouble(time.toDouble()); + return true; +} + +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 |