summaryrefslogtreecommitdiffstats
path: root/dom/html/input/DateTimeInputTypes.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /dom/html/input/DateTimeInputTypes.cpp
parentInitial commit. (diff)
downloadfirefox-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/input/DateTimeInputTypes.cpp')
-rw-r--r--dom/html/input/DateTimeInputTypes.cpp501
1 files changed, 501 insertions, 0 deletions
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