Line data Source code
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 "DateTimeInputTypes.h"
8 :
9 : #include "js/Date.h"
10 : #include "mozilla/dom/HTMLInputElement.h"
11 : #include "nsDateTimeControlFrame.h"
12 :
13 : const double DateTimeInputTypeBase::kMinimumYear = 1;
14 : const double DateTimeInputTypeBase::kMaximumYear = 275760;
15 : const double DateTimeInputTypeBase::kMaximumMonthInMaximumYear = 9;
16 : const double DateTimeInputTypeBase::kMaximumWeekInMaximumYear = 37;
17 : const double DateTimeInputTypeBase::kMsPerDay = 24 * 60 * 60 * 1000;
18 :
19 : /* static */ bool
20 0 : DateTimeInputTypeBase::IsInputDateTimeEnabled()
21 : {
22 : static bool sDateTimeEnabled = false;
23 : static bool sDateTimePrefCached = false;
24 0 : if (!sDateTimePrefCached) {
25 0 : sDateTimePrefCached = true;
26 : mozilla::Preferences::AddBoolVarCache(&sDateTimeEnabled,
27 : "dom.forms.datetime",
28 0 : false);
29 : }
30 :
31 0 : return sDateTimeEnabled;
32 : }
33 :
34 : bool
35 0 : DateTimeInputTypeBase::IsMutable() const
36 : {
37 0 : return !mInputElement->IsDisabled() &&
38 0 : !mInputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::readonly);
39 : }
40 :
41 : bool
42 0 : DateTimeInputTypeBase::IsValueMissing() const
43 : {
44 0 : if (!mInputElement->HasAttr(kNameSpaceID_None, nsGkAtoms::required)) {
45 0 : return false;
46 : }
47 :
48 0 : if (!IsMutable()) {
49 0 : return false;
50 : }
51 :
52 0 : return IsValueEmpty();
53 : }
54 :
55 : bool
56 0 : DateTimeInputTypeBase::IsRangeOverflow() const
57 : {
58 0 : mozilla::Decimal maximum = mInputElement->GetMaximum();
59 0 : if (maximum.isNaN()) {
60 0 : return false;
61 : }
62 :
63 0 : mozilla::Decimal value = mInputElement->GetValueAsDecimal();
64 0 : if (value.isNaN()) {
65 0 : return false;
66 : }
67 :
68 0 : return value > maximum;
69 : }
70 :
71 : bool
72 0 : DateTimeInputTypeBase::IsRangeUnderflow() const
73 : {
74 0 : mozilla::Decimal minimum = mInputElement->GetMinimum();
75 0 : if (minimum.isNaN()) {
76 0 : return false;
77 : }
78 :
79 0 : mozilla::Decimal value = mInputElement->GetValueAsDecimal();
80 0 : if (value.isNaN()) {
81 0 : return false;
82 : }
83 :
84 0 : return value < minimum;
85 : }
86 :
87 : bool
88 0 : DateTimeInputTypeBase::HasStepMismatch(bool aUseZeroIfValueNaN) const
89 : {
90 0 : mozilla::Decimal value = mInputElement->GetValueAsDecimal();
91 0 : if (value.isNaN()) {
92 0 : if (aUseZeroIfValueNaN) {
93 0 : value = mozilla::Decimal(0);
94 : } else {
95 : // The element can't suffer from step mismatch if it's value isn't a number.
96 0 : return false;
97 : }
98 : }
99 :
100 0 : mozilla::Decimal step = mInputElement->GetStep();
101 0 : if (step == kStepAny) {
102 0 : return false;
103 : }
104 :
105 : // Value has to be an integral multiple of step.
106 0 : return NS_floorModulo(value - GetStepBase(), step) != mozilla::Decimal(0);
107 : }
108 :
109 : bool
110 0 : DateTimeInputTypeBase::HasBadInput() const
111 : {
112 0 : nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
113 0 : if (!frame) {
114 0 : return false;
115 : }
116 :
117 0 : return frame->HasBadInput();;
118 : }
119 :
120 : nsresult
121 0 : DateTimeInputTypeBase::GetRangeOverflowMessage(nsXPIDLString& aMessage)
122 : {
123 0 : nsAutoString maxStr;
124 0 : mInputElement->GetAttr(kNameSpaceID_None, nsGkAtoms::max, maxStr);
125 :
126 0 : const char16_t* params[] = { maxStr.get() };
127 : return nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
128 0 : "FormValidationDateTimeRangeOverflow", params, aMessage);
129 : }
130 :
131 : nsresult
132 0 : DateTimeInputTypeBase::GetRangeUnderflowMessage(nsXPIDLString& aMessage)
133 : {
134 0 : nsAutoString minStr;
135 0 : mInputElement->GetAttr(kNameSpaceID_None, nsGkAtoms::min, minStr);
136 :
137 0 : const char16_t* params[] = { minStr.get() };
138 : return nsContentUtils::FormatLocalizedString(nsContentUtils::eDOM_PROPERTIES,
139 0 : "FormValidationDateTimeRangeUnderflow", params, aMessage);
140 : }
141 :
142 : nsresult
143 0 : DateTimeInputTypeBase::MinMaxStepAttrChanged()
144 : {
145 0 : nsDateTimeControlFrame* frame = do_QueryFrame(GetPrimaryFrame());
146 0 : if (frame) {
147 0 : frame->OnMinMaxStepAttrChanged();
148 : }
149 :
150 0 : return NS_OK;
151 : }
152 :
153 : bool
154 0 : DateTimeInputTypeBase::GetTimeFromMs(double aValue, uint16_t* aHours,
155 : uint16_t* aMinutes, uint16_t* aSeconds,
156 : uint16_t* aMilliseconds) const {
157 0 : MOZ_ASSERT(aValue >= 0 && aValue < kMsPerDay,
158 : "aValue must be milliseconds within a day!");
159 :
160 0 : uint32_t value = floor(aValue);
161 :
162 0 : *aMilliseconds = value % 1000;
163 0 : value /= 1000;
164 :
165 0 : *aSeconds = value % 60;
166 0 : value /= 60;
167 :
168 0 : *aMinutes = value % 60;
169 0 : value /= 60;
170 :
171 0 : *aHours = value;
172 :
173 0 : return true;
174 : }
175 :
176 : // input type=date
177 :
178 : nsresult
179 0 : DateInputType::GetBadInputMessage(nsXPIDLString& aMessage)
180 : {
181 0 : if (!IsInputDateTimeEnabled()) {
182 0 : return NS_ERROR_UNEXPECTED;
183 : }
184 :
185 : return nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
186 0 : "FormValidationInvalidDate", aMessage);
187 : }
188 :
189 : bool
190 0 : DateInputType::ConvertStringToNumber(nsAString& aValue,
191 : mozilla::Decimal& aResultValue) const
192 : {
193 : uint32_t year, month, day;
194 0 : if (!ParseDate(aValue, &year, &month, &day)) {
195 0 : return false;
196 : }
197 :
198 0 : JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day));
199 0 : if (!time.isValid()) {
200 0 : return false;
201 : }
202 :
203 0 : aResultValue = mozilla::Decimal::fromDouble(time.toDouble());
204 0 : return true;
205 : }
206 :
207 : bool
208 0 : DateInputType::ConvertNumberToString(mozilla::Decimal aValue,
209 : nsAString& aResultString) const
210 : {
211 0 : MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
212 :
213 0 : aResultString.Truncate();
214 :
215 : // The specs (and our JS APIs) require |aValue| to be truncated.
216 0 : aValue = aValue.floor();
217 :
218 0 : double year = JS::YearFromTime(aValue.toDouble());
219 0 : double month = JS::MonthFromTime(aValue.toDouble());
220 0 : double day = JS::DayFromTime(aValue.toDouble());
221 :
222 0 : if (mozilla::IsNaN(year) || mozilla::IsNaN(month) || mozilla::IsNaN(day)) {
223 0 : return false;
224 : }
225 :
226 0 : aResultString.AppendPrintf("%04.0f-%02.0f-%02.0f", year, month + 1, day);
227 0 : return true;
228 : }
229 :
230 : // input type=time
231 :
232 : bool
233 0 : TimeInputType::ConvertStringToNumber(nsAString& aValue,
234 : mozilla::Decimal& aResultValue) const
235 : {
236 : uint32_t milliseconds;
237 0 : if (!ParseTime(aValue, &milliseconds)) {
238 0 : return false;
239 : }
240 :
241 0 : aResultValue = mozilla::Decimal(int32_t(milliseconds));
242 0 : return true;
243 : }
244 :
245 : bool
246 0 : TimeInputType::ConvertNumberToString(mozilla::Decimal aValue,
247 : nsAString& aResultString) const
248 : {
249 0 : MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
250 :
251 0 : aResultString.Truncate();
252 :
253 0 : aValue = aValue.floor();
254 : // Per spec, we need to truncate |aValue| and we should only represent
255 : // times inside a day [00:00, 24:00[, which means that we should do a
256 : // modulo on |aValue| using the number of milliseconds in a day (86400000).
257 : uint32_t value =
258 0 : NS_floorModulo(aValue, mozilla::Decimal::fromDouble(kMsPerDay)).toDouble();
259 :
260 : uint16_t milliseconds, seconds, minutes, hours;
261 0 : if (!GetTimeFromMs(value, &hours, &minutes, &seconds, &milliseconds)) {
262 0 : return false;
263 : }
264 :
265 0 : if (milliseconds != 0) {
266 0 : aResultString.AppendPrintf("%02d:%02d:%02d.%03d",
267 0 : hours, minutes, seconds, milliseconds);
268 0 : } else if (seconds != 0) {
269 0 : aResultString.AppendPrintf("%02d:%02d:%02d",
270 0 : hours, minutes, seconds);
271 : } else {
272 0 : aResultString.AppendPrintf("%02d:%02d", hours, minutes);
273 : }
274 :
275 0 : return true;
276 : }
277 :
278 : // input type=week
279 :
280 : bool
281 0 : WeekInputType::ConvertStringToNumber(nsAString& aValue,
282 : mozilla::Decimal& aResultValue) const
283 : {
284 : uint32_t year, week;
285 0 : if (!ParseWeek(aValue, &year, &week)) {
286 0 : return false;
287 : }
288 :
289 0 : if (year < kMinimumYear || year > kMaximumYear) {
290 0 : return false;
291 : }
292 :
293 : // Maximum week is 275760-W37, the week of 275760-09-13.
294 0 : if (year == kMaximumYear && week > kMaximumWeekInMaximumYear) {
295 0 : return false;
296 : }
297 :
298 0 : double days = DaysSinceEpochFromWeek(year, week);
299 0 : aResultValue = mozilla::Decimal::fromDouble(days * kMsPerDay);
300 0 : return true;
301 : }
302 :
303 : bool
304 0 : WeekInputType::ConvertNumberToString(mozilla::Decimal aValue,
305 : nsAString& aResultString) const
306 : {
307 0 : MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
308 :
309 0 : aResultString.Truncate();
310 :
311 0 : aValue = aValue.floor();
312 :
313 : // Based on ISO 8601 date.
314 0 : double year = JS::YearFromTime(aValue.toDouble());
315 0 : double month = JS::MonthFromTime(aValue.toDouble());
316 0 : double day = JS::DayFromTime(aValue.toDouble());
317 : // Adding 1 since day starts from 0.
318 0 : double dayInYear = JS::DayWithinYear(aValue.toDouble(), year) + 1;
319 :
320 : // Adding 1 since month starts from 0.
321 0 : uint32_t isoWeekday = DayOfWeek(year, month + 1, day, true);
322 : // Target on Wednesday since ISO 8601 states that week 1 is the week
323 : // with the first Thursday of that year.
324 0 : uint32_t week = (dayInYear - isoWeekday + 10) / 7;
325 :
326 0 : if (week < 1) {
327 0 : year--;
328 0 : if (year < 1) {
329 0 : return false;
330 : }
331 0 : week = MaximumWeekInYear(year);
332 0 : } else if (week > MaximumWeekInYear(year)) {
333 0 : year++;
334 0 : if (year > kMaximumYear ||
335 0 : (year == kMaximumYear && week > kMaximumWeekInMaximumYear)) {
336 0 : return false;
337 : }
338 0 : week = 1;
339 : }
340 :
341 0 : aResultString.AppendPrintf("%04.0f-W%02d", year, week);
342 0 : return true;
343 : }
344 :
345 : // input type=month
346 :
347 : bool
348 0 : MonthInputType::ConvertStringToNumber(nsAString& aValue,
349 : mozilla::Decimal& aResultValue) const
350 : {
351 : uint32_t year, month;
352 0 : if (!ParseMonth(aValue, &year, &month)) {
353 0 : return false;
354 : }
355 :
356 0 : if (year < kMinimumYear || year > kMaximumYear) {
357 0 : return false;
358 : }
359 :
360 : // Maximum valid month is 275760-09.
361 0 : if (year == kMaximumYear && month > kMaximumMonthInMaximumYear) {
362 0 : return false;
363 : }
364 :
365 0 : int32_t months = MonthsSinceJan1970(year, month);
366 0 : aResultValue = mozilla::Decimal(int32_t(months));
367 0 : return true;
368 : }
369 :
370 : bool
371 0 : MonthInputType::ConvertNumberToString(mozilla::Decimal aValue,
372 : nsAString& aResultString) const
373 : {
374 0 : MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
375 :
376 0 : aResultString.Truncate();
377 :
378 0 : aValue = aValue.floor();
379 :
380 0 : double month = NS_floorModulo(aValue, mozilla::Decimal(12)).toDouble();
381 0 : month = (month < 0 ? month + 12 : month);
382 :
383 0 : double year = 1970 + (aValue.toDouble() - month) / 12;
384 :
385 : // Maximum valid month is 275760-09.
386 0 : if (year < kMinimumYear || year > kMaximumYear) {
387 0 : return false;
388 : }
389 :
390 0 : if (year == kMaximumYear && month > 8) {
391 0 : return false;
392 : }
393 :
394 0 : aResultString.AppendPrintf("%04.0f-%02.0f", year, month + 1);
395 0 : return true;
396 :
397 : }
398 :
399 : // input type=datetime-local
400 :
401 : bool
402 0 : DateTimeLocalInputType::ConvertStringToNumber(
403 : nsAString& aValue, mozilla::Decimal& aResultValue) const
404 : {
405 : uint32_t year, month, day, timeInMs;
406 0 : if (!ParseDateTimeLocal(aValue, &year, &month, &day, &timeInMs)) {
407 0 : return false;
408 : }
409 :
410 : JS::ClippedTime time = JS::TimeClip(JS::MakeDate(year, month - 1, day,
411 0 : timeInMs));
412 0 : if (!time.isValid()) {
413 0 : return false;
414 : }
415 :
416 0 : aResultValue = mozilla::Decimal::fromDouble(time.toDouble());
417 0 : return true;
418 : }
419 :
420 : bool
421 0 : DateTimeLocalInputType::ConvertNumberToString(mozilla::Decimal aValue,
422 : nsAString& aResultString) const
423 : {
424 0 : MOZ_ASSERT(aValue.isFinite(), "aValue must be a valid non-Infinite number.");
425 :
426 0 : aResultString.Truncate();
427 :
428 0 : aValue = aValue.floor();
429 :
430 : uint32_t timeValue =
431 0 : NS_floorModulo(aValue, mozilla::Decimal::fromDouble(kMsPerDay)).toDouble();
432 :
433 : uint16_t milliseconds, seconds, minutes, hours;
434 0 : if (!GetTimeFromMs(timeValue, &hours, &minutes, &seconds, &milliseconds)) {
435 0 : return false;
436 : }
437 :
438 0 : double year = JS::YearFromTime(aValue.toDouble());
439 0 : double month = JS::MonthFromTime(aValue.toDouble());
440 0 : double day = JS::DayFromTime(aValue.toDouble());
441 :
442 0 : if (mozilla::IsNaN(year) || mozilla::IsNaN(month) || mozilla::IsNaN(day)) {
443 0 : return false;
444 : }
445 :
446 0 : if (milliseconds != 0) {
447 0 : aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d.%03d",
448 : year, month + 1, day, hours, minutes,
449 0 : seconds, milliseconds);
450 0 : } else if (seconds != 0) {
451 0 : aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d:%02d",
452 : year, month + 1, day, hours, minutes,
453 0 : seconds);
454 : } else {
455 0 : aResultString.AppendPrintf("%04.0f-%02.0f-%02.0fT%02d:%02d",
456 0 : year, month + 1, day, hours, minutes);
457 : }
458 :
459 0 : return true;
460 : }
|