DateTimeInputTypes.cpp (15303B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "mozilla/dom/DateTimeInputTypes.h" 8 9 #include "js/Date.h" 10 #include "mozilla/AsyncEventDispatcher.h" 11 #include "mozilla/StaticPrefs_dom.h" 12 #include "mozilla/dom/HTMLInputElement.h" 13 #include "mozilla/dom/ShadowRoot.h" 14 #include "nsDOMTokenList.h" 15 16 namespace mozilla::dom { 17 18 const double DateTimeInputTypeBase::kMinimumYear = 1; 19 const double DateTimeInputTypeBase::kMaximumYear = 275760; 20 const double DateTimeInputTypeBase::kMaximumMonthInMaximumYear = 9; 21 const double DateTimeInputTypeBase::kMaximumWeekInMaximumYear = 37; 22 const double DateTimeInputTypeBase::kMsPerDay = 24 * 60 * 60 * 1000; 23 24 bool DateTimeInputTypeBase::IsMutable() const { 25 return !mInputElement->IsDisabledOrReadOnly(); 26 } 27 28 bool DateTimeInputTypeBase::IsValueMissing() const { 29 if (!mInputElement->IsRequired()) { 30 return false; 31 } 32 33 if (!IsMutable()) { 34 return false; 35 } 36 37 return IsValueEmpty(); 38 } 39 40 bool DateTimeInputTypeBase::IsRangeOverflow() const { 41 Decimal maximum = mInputElement->GetMaximum(); 42 if (maximum.isNaN()) { 43 return false; 44 } 45 46 Decimal value = mInputElement->GetValueAsDecimal(); 47 if (value.isNaN()) { 48 return false; 49 } 50 51 return value > maximum; 52 } 53 54 bool DateTimeInputTypeBase::IsRangeUnderflow() const { 55 Decimal minimum = mInputElement->GetMinimum(); 56 if (minimum.isNaN()) { 57 return false; 58 } 59 60 Decimal value = mInputElement->GetValueAsDecimal(); 61 if (value.isNaN()) { 62 return false; 63 } 64 65 return value < minimum; 66 } 67 68 bool DateTimeInputTypeBase::HasStepMismatch() const { 69 Decimal value = mInputElement->GetValueAsDecimal(); 70 return mInputElement->ValueIsStepMismatch(value); 71 } 72 73 bool DateTimeInputTypeBase::HasBadInput() const { 74 ShadowRoot* shadow = mInputElement->GetShadowRoot(); 75 if (!shadow) { 76 return false; 77 } 78 79 Element* editWrapperElement = shadow->GetElementById(u"edit-wrapper"_ns); 80 if (!editWrapperElement) { 81 return false; 82 } 83 84 bool allEmpty = true; 85 // Empty field does not imply bad input, but incomplete field does. 86 for (Element* child = editWrapperElement->GetFirstElementChild(); child; 87 child = child->GetNextElementSibling()) { 88 if (!child->ClassList()->Contains(u"datetime-edit-field"_ns)) { 89 continue; 90 } 91 nsAutoString value; 92 child->GetAttr(nsGkAtoms::value, value); 93 if (!value.IsEmpty()) { 94 allEmpty = false; 95 break; 96 } 97 } 98 99 // If some fields are available but input element's value is empty implies it 100 // has been sanitized. 101 return !allEmpty && IsValueEmpty(); 102 } 103 104 nsresult DateTimeInputTypeBase::GetRangeOverflowMessage(nsAString& aMessage) { 105 nsAutoString maxStr; 106 mInputElement->GetAttr(nsGkAtoms::max, maxStr); 107 108 return nsContentUtils::FormatMaybeLocalizedString( 109 aMessage, nsContentUtils::eDOM_PROPERTIES, 110 "FormValidationDateTimeRangeOverflow", mInputElement->OwnerDoc(), maxStr); 111 } 112 113 nsresult DateTimeInputTypeBase::GetRangeUnderflowMessage(nsAString& aMessage) { 114 nsAutoString minStr; 115 mInputElement->GetAttr(nsGkAtoms::min, minStr); 116 117 return nsContentUtils::FormatMaybeLocalizedString( 118 aMessage, nsContentUtils::eDOM_PROPERTIES, 119 "FormValidationDateTimeRangeUnderflow", mInputElement->OwnerDoc(), 120 minStr); 121 } 122 123 void DateTimeInputTypeBase::MinMaxStepAttrChanged() { 124 if (Element* dateTimeBoxElement = mInputElement->GetDateTimeBoxElement()) { 125 AsyncEventDispatcher::RunDOMEventWhenSafe( 126 *dateTimeBoxElement, u"MozNotifyMinMaxStepAttrChanged"_ns, 127 CanBubble::eNo, ChromeOnlyDispatch::eNo); 128 } 129 } 130 131 bool DateTimeInputTypeBase::GetTimeFromMs(double aValue, uint16_t* aHours, 132 uint16_t* aMinutes, 133 uint16_t* aSeconds, 134 uint16_t* aMilliseconds) const { 135 MOZ_ASSERT(aValue >= 0 && aValue < kMsPerDay, 136 "aValue must be milliseconds within a day!"); 137 138 uint32_t value = floor(aValue); 139 140 *aMilliseconds = value % 1000; 141 value /= 1000; 142 143 *aSeconds = value % 60; 144 value /= 60; 145 146 *aMinutes = value % 60; 147 value /= 60; 148 149 *aHours = value; 150 151 return true; 152 } 153 154 // input type=date 155 156 nsresult DateInputType::GetBadInputMessage(nsAString& aMessage) { 157 return nsContentUtils::GetMaybeLocalizedString( 158 nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidDate", 159 mInputElement->OwnerDoc(), aMessage); 160 } 161 162 auto DateInputType::ConvertStringToNumber(const nsAString& aValue) const 163 -> StringToNumberResult { 164 uint32_t year, month, day; 165 if (!ParseDate(aValue, &year, &month, &day)) { 166 return {}; 167 } 168 JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day)); 169 if (!time.isValid()) { 170 return {}; 171 } 172 return {Decimal::fromDouble(time.toDouble())}; 173 } 174 175 bool DateInputType::ConvertNumberToString(Decimal aValue, Localized, 176 nsAString& aResultString) const { 177 MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); 178 179 aResultString.Truncate(); 180 181 // The specs (and our JS APIs) require |aValue| to be truncated. 182 aValue = aValue.floor(); 183 184 double year = JS::YearFromTime(aValue.toDouble()); 185 double month = JS::MonthFromTime(aValue.toDouble()); 186 double day = JS::DayFromTime(aValue.toDouble()); 187 188 if (std::isnan(year) || std::isnan(month) || std::isnan(day)) { 189 return false; 190 } 191 192 aResultString.AppendPrintf("%04.0f-%02.0f-%02.0f", year, month + 1, day); 193 return true; 194 } 195 196 // input type=time 197 198 nsresult TimeInputType::GetBadInputMessage(nsAString& aMessage) { 199 return nsContentUtils::GetMaybeLocalizedString( 200 nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidTime", 201 mInputElement->OwnerDoc(), aMessage); 202 } 203 204 auto TimeInputType::ConvertStringToNumber(const nsAString& aValue) const 205 -> StringToNumberResult { 206 uint32_t milliseconds; 207 if (!ParseTime(aValue, &milliseconds)) { 208 return {}; 209 } 210 return {Decimal(int32_t(milliseconds))}; 211 } 212 213 bool TimeInputType::ConvertNumberToString(Decimal aValue, Localized, 214 nsAString& aResultString) const { 215 MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); 216 217 aResultString.Truncate(); 218 219 aValue = aValue.floor(); 220 // Per spec, we need to truncate |aValue| and we should only represent 221 // times inside a day [00:00, 24:00[, which means that we should do a 222 // modulo on |aValue| using the number of milliseconds in a day (86400000). 223 uint32_t value = 224 NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); 225 226 uint16_t milliseconds, seconds, minutes, hours; 227 if (!GetTimeFromMs(value, &hours, &minutes, &seconds, &milliseconds)) { 228 return false; 229 } 230 231 if (milliseconds != 0) { 232 aResultString.AppendPrintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, 233 milliseconds); 234 } else if (seconds != 0) { 235 aResultString.AppendPrintf("%02d:%02d:%02d", hours, minutes, seconds); 236 } else { 237 aResultString.AppendPrintf("%02d:%02d", hours, minutes); 238 } 239 240 return true; 241 } 242 243 bool TimeInputType::HasReversedRange() const { 244 mozilla::Decimal maximum = mInputElement->GetMaximum(); 245 if (maximum.isNaN()) { 246 return false; 247 } 248 249 mozilla::Decimal minimum = mInputElement->GetMinimum(); 250 if (minimum.isNaN()) { 251 return false; 252 } 253 254 return maximum < minimum; 255 } 256 257 bool TimeInputType::IsReversedRangeUnderflowAndOverflow() const { 258 mozilla::Decimal maximum = mInputElement->GetMaximum(); 259 mozilla::Decimal minimum = mInputElement->GetMinimum(); 260 mozilla::Decimal value = mInputElement->GetValueAsDecimal(); 261 262 MOZ_ASSERT(HasReversedRange(), "Must have reserved range."); 263 264 if (value.isNaN()) { 265 return false; 266 } 267 268 // When an element has a reversed range, and the value is more than the 269 // maximum and less than the minimum the element is simultaneously suffering 270 // from an underflow and suffering from an overflow. 271 return value > maximum && value < minimum; 272 } 273 274 bool TimeInputType::IsRangeOverflow() const { 275 return HasReversedRange() ? IsReversedRangeUnderflowAndOverflow() 276 : DateTimeInputTypeBase::IsRangeOverflow(); 277 } 278 279 bool TimeInputType::IsRangeUnderflow() const { 280 return HasReversedRange() ? IsReversedRangeUnderflowAndOverflow() 281 : DateTimeInputTypeBase::IsRangeUnderflow(); 282 } 283 284 nsresult TimeInputType::GetReversedRangeUnderflowAndOverflowMessage( 285 nsAString& aMessage) { 286 nsAutoString maxStr; 287 mInputElement->GetAttr(nsGkAtoms::max, maxStr); 288 289 nsAutoString minStr; 290 mInputElement->GetAttr(nsGkAtoms::min, minStr); 291 292 return nsContentUtils::FormatMaybeLocalizedString( 293 aMessage, nsContentUtils::eDOM_PROPERTIES, 294 "FormValidationTimeReversedRangeUnderflowAndOverflow", 295 mInputElement->OwnerDoc(), minStr, maxStr); 296 } 297 298 nsresult TimeInputType::GetRangeOverflowMessage(nsAString& aMessage) { 299 return HasReversedRange() 300 ? GetReversedRangeUnderflowAndOverflowMessage(aMessage) 301 : DateTimeInputTypeBase::GetRangeOverflowMessage(aMessage); 302 } 303 304 nsresult TimeInputType::GetRangeUnderflowMessage(nsAString& aMessage) { 305 return HasReversedRange() 306 ? GetReversedRangeUnderflowAndOverflowMessage(aMessage) 307 : DateTimeInputTypeBase::GetRangeUnderflowMessage(aMessage); 308 } 309 310 // input type=week 311 312 nsresult WeekInputType::GetBadInputMessage(nsAString& aMessage) { 313 return nsContentUtils::GetMaybeLocalizedString( 314 nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidWeek", 315 mInputElement->OwnerDoc(), aMessage); 316 } 317 318 auto WeekInputType::ConvertStringToNumber(const nsAString& aValue) const 319 -> StringToNumberResult { 320 uint32_t year, week; 321 if (!ParseWeek(aValue, &year, &week)) { 322 return {}; 323 } 324 if (year < kMinimumYear || year > kMaximumYear) { 325 return {}; 326 } 327 // Maximum week is 275760-W37, the week of 275760-09-13. 328 if (year == kMaximumYear && week > kMaximumWeekInMaximumYear) { 329 return {}; 330 } 331 double days = DaysSinceEpochFromWeek(year, week); 332 return {Decimal::fromDouble(days * kMsPerDay)}; 333 } 334 335 bool WeekInputType::ConvertNumberToString(Decimal aValue, Localized, 336 nsAString& aResultString) const { 337 MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); 338 339 aResultString.Truncate(); 340 341 aValue = aValue.floor(); 342 343 // Based on ISO 8601 date. 344 double year = JS::YearFromTime(aValue.toDouble()); 345 double month = JS::MonthFromTime(aValue.toDouble()); 346 double day = JS::DayFromTime(aValue.toDouble()); 347 // Adding 1 since day starts from 0. 348 double dayInYear = JS::DayWithinYear(aValue.toDouble(), year) + 1; 349 350 // Return if aValue is outside the valid JS date-time range. 351 if (std::isnan(year) || std::isnan(month) || std::isnan(day) || 352 std::isnan(dayInYear)) { 353 return false; 354 } 355 356 // DayOfWeek requires the year to be non-negative. 357 if (year < 0) { 358 return false; 359 } 360 361 // Adding 1 since month starts from 0. 362 uint32_t isoWeekday = DayOfWeek(year, month + 1, day, true); 363 // Target on Wednesday since ISO 8601 states that week 1 is the week 364 // with the first Thursday of that year. 365 uint32_t week = (dayInYear - isoWeekday + 10) / 7; 366 367 if (week < 1) { 368 year--; 369 if (year < 1) { 370 return false; 371 } 372 week = MaximumWeekInYear(year); 373 } else if (week > MaximumWeekInYear(year)) { 374 year++; 375 if (year > kMaximumYear || 376 (year == kMaximumYear && week > kMaximumWeekInMaximumYear)) { 377 return false; 378 } 379 week = 1; 380 } 381 382 aResultString.AppendPrintf("%04.0f-W%02d", year, week); 383 return true; 384 } 385 386 // input type=month 387 388 nsresult MonthInputType::GetBadInputMessage(nsAString& aMessage) { 389 return nsContentUtils::GetMaybeLocalizedString( 390 nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidMonth", 391 mInputElement->OwnerDoc(), aMessage); 392 } 393 394 auto MonthInputType::ConvertStringToNumber(const nsAString& aValue) const 395 -> StringToNumberResult { 396 uint32_t year, month; 397 if (!ParseMonth(aValue, &year, &month)) { 398 return {}; 399 } 400 401 if (year < kMinimumYear || year > kMaximumYear) { 402 return {}; 403 } 404 405 // Maximum valid month is 275760-09. 406 if (year == kMaximumYear && month > kMaximumMonthInMaximumYear) { 407 return {}; 408 } 409 410 int32_t months = MonthsSinceJan1970(year, month); 411 return {Decimal(int32_t(months))}; 412 } 413 414 bool MonthInputType::ConvertNumberToString(Decimal aValue, Localized, 415 nsAString& aResultString) const { 416 MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); 417 418 aResultString.Truncate(); 419 420 aValue = aValue.floor(); 421 422 double month = NS_floorModulo(aValue, Decimal(12)).toDouble(); 423 month = (month < 0 ? month + 12 : month); 424 425 double year = 1970 + (aValue.toDouble() - month) / 12; 426 427 // Maximum valid month is 275760-09. 428 if (year < kMinimumYear || year > kMaximumYear) { 429 return false; 430 } 431 432 if (year == kMaximumYear && month > 8) { 433 return false; 434 } 435 436 aResultString.AppendPrintf("%04.0f-%02.0f", year, month + 1); 437 return true; 438 } 439 440 // input type=datetime-local 441 442 nsresult DateTimeLocalInputType::GetBadInputMessage(nsAString& aMessage) { 443 return nsContentUtils::GetMaybeLocalizedString( 444 nsContentUtils::eDOM_PROPERTIES, "FormValidationInvalidDateTime", 445 mInputElement->OwnerDoc(), aMessage); 446 } 447 448 auto DateTimeLocalInputType::ConvertStringToNumber( 449 const nsAString& aValue) const -> StringToNumberResult { 450 uint32_t year, month, day, timeInMs; 451 if (!ParseDateTimeLocal(aValue, &year, &month, &day, &timeInMs)) { 452 return {}; 453 } 454 JS::ClippedTime time = 455 JS::TimeClip(JS::MakeDate(year, month - 1, day, timeInMs)); 456 if (!time.isValid()) { 457 return {}; 458 } 459 return {Decimal::fromDouble(time.toDouble())}; 460 } 461 462 bool DateTimeLocalInputType::ConvertNumberToString( 463 Decimal aValue, Localized, nsAString& aResultString) const { 464 MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number."); 465 466 aResultString.Truncate(); 467 468 aValue = aValue.floor(); 469 470 uint32_t timeValue = 471 NS_floorModulo(aValue, Decimal::fromDouble(kMsPerDay)).toDouble(); 472 473 uint16_t milliseconds, seconds, minutes, hours; 474 if (!GetTimeFromMs(timeValue, &hours, &minutes, &seconds, &milliseconds)) { 475 return false; 476 } 477 478 double year = JS::YearFromTime(aValue.toDouble()); 479 double month = JS::MonthFromTime(aValue.toDouble()); 480 double day = JS::DayFromTime(aValue.toDouble()); 481 482 if (std::isnan(year) || std::isnan(month) || std::isnan(day)) { 483 return false; 484 } 485 486 if (milliseconds != 0) { 487 aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d.%03d", year, 488 month + 1, day, hours, minutes, seconds, 489 milliseconds); 490 } else if (seconds != 0) { 491 aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d", year, 492 month + 1, day, hours, minutes, seconds); 493 } else { 494 aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d", year, 495 month + 1, day, hours, minutes); 496 } 497 498 return true; 499 } 500 501 } // namespace mozilla::dom