Line data Source code
1 : // Copyright (c) 2011-2022 Made to Order Software Corp. All Rights Reserved
2 : //
3 : // https://snapwebsites.org/project/edhttp
4 : // contact@m2osw.com
5 : //
6 : // This program is free software: you can redistribute it and/or modify
7 : // it under the terms of the GNU General Public License as published by
8 : // the Free Software Foundation, either version 3 of the License, or
9 : // (at your option) any later version.
10 : //
11 : // This program is distributed in the hope that it will be useful,
12 : // but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 : // GNU General Public License for more details.
15 : //
16 : // You should have received a copy of the GNU General Public License
17 : // along with this program. If not, see <https://www.gnu.org/licenses/>.
18 :
19 :
20 : // self
21 : //
22 : #include "edhttp/http_date.h"
23 :
24 : #include "edhttp/exception.h"
25 : #include "edhttp/mkgmtime.h"
26 :
27 :
28 : // snapdev
29 : //
30 : #include <snapdev/to_lower.h>
31 : #include <snapdev/trim_string.h>
32 :
33 :
34 : // C++
35 : //
36 : #include <iomanip>
37 : #include <sstream>
38 :
39 :
40 : // C
41 : //
42 : #include <string.h>
43 :
44 :
45 : // last include
46 : //
47 : #include <snapdev/poison.h>
48 :
49 :
50 :
51 : namespace edhttp
52 : {
53 :
54 :
55 : namespace
56 : {
57 :
58 :
59 : char const * g_week_day_name[] =
60 : {
61 : "Sunday", "Monday", "Tuesday", "Wedneday", "Thursday", "Friday", "Saturday"
62 : };
63 :
64 : int const g_week_day_length[] = { 6, 6, 7, 8, 8, 6, 8 }; // strlen() of g_week_day_name's
65 :
66 : char const * g_month_name[] =
67 : {
68 : "January", "February", "Marsh", "April", "May", "June",
69 : "July", "August", "September", "October", "November", "December"
70 : };
71 :
72 : int const g_month_length[] = { 7, 8, 5, 5, 3, 4, 4, 6, 9, 7, 8, 8 }; // strlen() of g_month_name
73 :
74 : int const g_month_days[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
75 :
76 : signed char const g_timezone_adjust[26] =
77 : {
78 : /* A */ -1,
79 : /* B */ -2,
80 : /* C */ -3,
81 : /* D */ -4,
82 : /* E */ -5,
83 : /* F */ -6,
84 : /* G */ -7,
85 : /* H */ -8,
86 : /* I */ -9,
87 : /* J */ 0, // not used
88 : /* K */ -10,
89 : /* L */ -11,
90 : /* M */ -12,
91 : /* N */ 1,
92 : /* O */ 2,
93 : /* P */ 3,
94 : /* Q */ 4,
95 : /* R */ 5,
96 : /* S */ 6,
97 : /* T */ 7,
98 : /* U */ 8,
99 : /* V */ 9,
100 : /* W */ 10,
101 : /* X */ 11,
102 : /* Y */ 12,
103 : /* Z */ 0, // Zulu time is zero
104 : };
105 :
106 :
107 : }
108 : // noname namespace
109 :
110 : /** \brief Convert a time/date value to a string.
111 : *
112 : * This function transform a date such as the content::modified field
113 : * to a format that is useful to the XSL parser. It supports various
114 : * formats:
115 : *
116 : * \li DATE_FORMAT_SHORT -- YYYY-MM-DD
117 : * \li DATE_FORMAT_LONG -- YYYY-MM-DDTHH:MM:SSZ
118 : * \li DATE_FORMAT_TIME -- HH:MM:SS
119 : * \li DATE_FORMAT_EMAIL -- dd MMM yyyy hh:mm:ss +0000
120 : * \li DATE_FORMAT_HTTP -- ddd, dd MMM yyyy hh:mm:ss +0000
121 : *
122 : * The long format includes the time.
123 : *
124 : * The HTTP format uses the day and month names in English only since
125 : * the HTTP protocol only expects English.
126 : *
127 : * The date is always output as UTC (opposed to local time.)
128 : *
129 : * \note
130 : * In order to display a date to the end user (in the HTML for the users)
131 : * you may want to setup the timezone information first and then use
132 : * the various functions supplied by the locale plugin to generate the
133 : * date. The locale plugin also supports formatting numbers, time,
134 : * messages, etc. going both ways (from the formatted data to internal
135 : * data and vice versa.) There is also JavaScript support for editor
136 : * widgets.
137 : *
138 : * \warning
139 : * The input value is now seconds instead of microseconds.
140 : *
141 : * \param[in] seconds A time & date value in seconds.
142 : * \param[in] date_format Which format should be used.
143 : *
144 : * \return The formatted date and time.
145 : */
146 0 : std::string date_to_string(time_t seconds, date_format_t date_format)
147 : {
148 0 : struct tm time_info;
149 0 : gmtime_r(&seconds, &time_info);
150 :
151 0 : char buf[256];
152 0 : buf[0] = '\0';
153 :
154 0 : switch(date_format)
155 : {
156 0 : case date_format_t::DATE_FORMAT_SHORT:
157 0 : strftime(buf, sizeof(buf), "%Y-%m-%d", &time_info);
158 0 : break;
159 :
160 0 : case date_format_t::DATE_FORMAT_SHORT_US:
161 0 : strftime(buf, sizeof(buf), "%m-%d-%Y", &time_info);
162 0 : break;
163 :
164 0 : case date_format_t::DATE_FORMAT_LONG:
165 : // TBD do we want the Z when generating time for HTML headers?
166 : // (it is useful for the sitemap.xml at this point)
167 0 : strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &time_info);
168 0 : break;
169 :
170 0 : case date_format_t::DATE_FORMAT_TIME:
171 0 : strftime(buf, sizeof(buf), "%H:%M:%S", &time_info);
172 0 : break;
173 :
174 0 : case date_format_t::DATE_FORMAT_EMAIL:
175 : { // dd MMM yyyy hh:mm:ss +0000
176 : // do it manually so the date is ALWAYS in English
177 0 : std::stringstream ss;
178 : ss << std::setfill('0')
179 0 : << std::setw(2) << time_info.tm_mday
180 : << ' '
181 0 : << g_month_name[time_info.tm_mon][0]
182 0 : << g_month_name[time_info.tm_mon][1]
183 0 : << g_month_name[time_info.tm_mon][2]
184 : << ' '
185 0 : << std::setw(2) << time_info.tm_year
186 : << ' '
187 0 : << std::setw(2) << time_info.tm_hour
188 : << ':'
189 0 : << std::setw(2) << time_info.tm_min
190 : << ':'
191 0 : << std::setw(2) << time_info.tm_sec
192 0 : << " +0000";
193 0 : return ss.str();
194 :
195 : //return QString("%1 %2 %3 %4:%5:%6 +0000")
196 : // .arg(time_info.tm_mday, 2, 10, QChar('0'))
197 : // .arg(QString::fromLatin1(g_month_name[time_info.tm_mon], 3))
198 : // .arg(time_info.tm_year + 1900, 4, 10, QChar('0'))
199 : // .arg(time_info.tm_hour, 2, 10, QChar('0'))
200 : // .arg(time_info.tm_min, 2, 10, QChar('0'))
201 : // .arg(time_info.tm_sec, 2, 10, QChar('0'));
202 : }
203 : break;
204 :
205 0 : case date_format_t::DATE_FORMAT_HTTP:
206 : { // ddd, dd MMM yyyy hh:mm:ss GMT
207 : // do it manually so the date is ALWAYS in English
208 0 : std::stringstream ss;
209 : ss << std::setfill('0')
210 0 : << g_week_day_name[time_info.tm_wday][0]
211 0 : << g_week_day_name[time_info.tm_wday][1]
212 0 : << g_week_day_name[time_info.tm_wday][2]
213 : << ", "
214 0 : << std::setw(2) << time_info.tm_mday
215 : << ' '
216 0 : << g_month_name[time_info.tm_mon][0]
217 0 : << g_month_name[time_info.tm_mon][1]
218 0 : << g_month_name[time_info.tm_mon][2]
219 : << ' '
220 0 : << std::setw(4) << time_info.tm_year + 1900
221 : << ' '
222 0 : << std::setw(2) << time_info.tm_hour
223 : << ':'
224 0 : << std::setw(2) << time_info.tm_min
225 : << ':'
226 0 : << std::setw(2) << time_info.tm_sec
227 0 : << " +0000";
228 0 : return ss.str();
229 :
230 : //return QString("%1, %2 %3 %4 %5:%6:%7 +0000")
231 : // .arg(QString::fromLatin1(g_week_day_name[time_info.tm_wday], 3))
232 : // .arg(time_info.tm_mday, 2, 10, QChar('0'))
233 : // .arg(QString::fromLatin1(g_month_name[time_info.tm_mon], 3))
234 : // .arg(time_info.tm_year + 1900, 4, 10, QChar('0'))
235 : // .arg(time_info.tm_hour, 2, 10, QChar('0'))
236 : // .arg(time_info.tm_min, 2, 10, QChar('0'))
237 : // .arg(time_info.tm_sec, 2, 10, QChar('0'));
238 : }
239 : break;
240 :
241 : }
242 :
243 0 : return buf;
244 : }
245 :
246 :
247 : /** \brief Convert a date from a string to a time_t.
248 : *
249 : * This function transforms a date received by the client to a Unix
250 : * time_t value. We programmed our own because several fields are
251 : * optional and the strptime() function does not support such. Also
252 : * the strptime() uses the locale() for the day and month check
253 : * which is not expected for HTTP. The QDateTime object has similar
254 : * flaws.
255 : *
256 : * The function supports the RFC822, RFC850, and ANSI formats. On top
257 : * of these formats, the function understands the month name in full,
258 : * and the week day name and timezone parameters are viewed as optional.
259 : *
260 : * See the document we used to make sure we'd support pretty much all the
261 : * dates that a client might send to us:
262 : * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.3.1
263 : *
264 : * Formats that we support:
265 : *
266 : * \code
267 : * YYYY-MM-DD
268 : * DD-MMM-YYYY HH:MM:SS TZ
269 : * DD-MMM-YYYY HH:MM:SS TZ
270 : * WWW, DD-MMM-YYYY HH:MM:SS TZ
271 : * MMM-DD-YYYY HH:MM:SS TZ
272 : * WWW MMM-DD HH:MM:SS YYYY
273 : * \endcode
274 : *
275 : * The month and weekday may be a 3 letter abbreviation or the full English
276 : * name. The month must use letters. We support the ANSI format which must
277 : * start with the month or week name in letters. We distinguish the ANSI
278 : * format from the other RFC-2616 date if it starts with a week day and is
279 : * followed by a space, or it directly starts with the month name.
280 : *
281 : * The year may be 2 or 4 digits.
282 : *
283 : * The timezone is optional. It may use a space or a + or a - as a separator.
284 : * The timezone may be an abbreviation or a 4 digit number.
285 : *
286 : * \param[in] date The date to convert to a time_t.
287 : *
288 : * \return The date and time as a Unix time_t number, -1 if the convertion fails.
289 : */
290 0 : time_t string_to_date(std::string const & date)
291 : {
292 : #pragma GCC diagnostic push
293 : #pragma GCC diagnostic ignored "-Weffc++"
294 0 : struct parser_t
295 : {
296 0 : parser_t(std::string const & date)
297 0 : : f_date(snapdev::to_lower(snapdev::trim_string(date, true, true, true)))
298 0 : , f_s(f_date.c_str())
299 : {
300 0 : }
301 :
302 : parser_t(parser_t const & rhs) = delete;
303 : parser_t & operator = (parser_t const & rhs) = delete;
304 :
305 0 : void skip_spaces()
306 : {
307 0 : while(isspace(*f_s))
308 : {
309 0 : ++f_s;
310 : }
311 0 : }
312 :
313 0 : bool parse_week_day()
314 : {
315 : // day = "Mon" / "Tue" / "Wed" / "Thu"
316 : // / "Fri" / "Sat" / "Sun"
317 0 : if(f_s[0] == 'm' && f_s[1] == 'o' && f_s[2] == 'n')
318 : {
319 0 : f_time_info.tm_wday = 1;
320 : }
321 0 : else if(f_s[0] == 't' && f_s[1] == 'u' && f_s[2] == 'e')
322 : {
323 0 : f_time_info.tm_wday = 2;
324 : }
325 0 : else if(f_s[0] == 'w' && f_s[1] == 'e' && f_s[2] == 'd')
326 : {
327 0 : f_time_info.tm_wday = 3;
328 : }
329 0 : else if(f_s[0] == 't' && f_s[1] == 'h' && f_s[2] == 'u')
330 : {
331 0 : f_time_info.tm_wday = 4;
332 : }
333 0 : else if(f_s[0] == 'f' && f_s[1] == 'r' && f_s[2] == 'i')
334 : {
335 0 : f_time_info.tm_wday = 5;
336 : }
337 0 : else if(f_s[0] == 's' && f_s[1] == 'a' && f_s[2] == 't')
338 : {
339 0 : f_time_info.tm_wday = 6;
340 : }
341 0 : else if(f_s[0] == 's' && f_s[1] == 'u' && f_s[2] == 'n')
342 : {
343 0 : f_time_info.tm_wday = 0;
344 : }
345 : else
346 : {
347 0 : return false;
348 : }
349 :
350 : // check whether the other characters follow
351 0 : if(strncmp(f_s + 3, g_week_day_name[f_time_info.tm_wday] + 3, g_week_day_length[f_time_info.tm_wday] - 3) == 0)
352 : {
353 : // full day (RFC850)
354 0 : f_s += g_week_day_length[f_time_info.tm_wday];
355 : }
356 : else
357 : {
358 0 : f_s += 3;
359 : }
360 :
361 0 : return true;
362 : }
363 :
364 0 : bool parse_month()
365 : {
366 : // month = "Jan" / "Feb" / "Mar" / "Apr"
367 : // / "May" / "Jun" / "Jul" / "Aug"
368 : // / "Sep" / "Oct" / "Nov" / "Dec"
369 0 : if(f_s[0] == 'j' && f_s[1] == 'a' && f_s[2] == 'n')
370 : {
371 0 : f_time_info.tm_mon = 0;
372 : }
373 0 : else if(f_s[0] == 'f' && f_s[1] == 'e' && f_s[2] == 'b')
374 : {
375 0 : f_time_info.tm_mon = 1;
376 : }
377 0 : else if(f_s[0] == 'm' && f_s[1] == 'a' && f_s[2] == 'r')
378 : {
379 0 : f_time_info.tm_mon = 2;
380 : }
381 0 : else if(f_s[0] == 'a' && f_s[1] == 'p' && f_s[2] == 'r')
382 : {
383 0 : f_time_info.tm_mon = 3;
384 : }
385 0 : else if(f_s[0] == 'm' && f_s[1] == 'a' && f_s[2] == 'y')
386 : {
387 0 : f_time_info.tm_mon = 4;
388 : }
389 0 : else if(f_s[0] == 'j' && f_s[1] == 'u' && f_s[2] == 'n')
390 : {
391 0 : f_time_info.tm_mon = 5;
392 : }
393 0 : else if(f_s[0] == 'j' && f_s[1] == 'u' && f_s[2] == 'l')
394 : {
395 0 : f_time_info.tm_mon = 6;
396 : }
397 0 : else if(f_s[0] == 'a' && f_s[1] == 'u' && f_s[2] == 'g')
398 : {
399 0 : f_time_info.tm_mon = 7;
400 : }
401 0 : else if(f_s[0] == 's' && f_s[1] == 'e' && f_s[2] == 'p')
402 : {
403 0 : f_time_info.tm_mon = 8;
404 : }
405 0 : else if(f_s[0] == 'o' && f_s[1] == 'c' && f_s[2] == 't')
406 : {
407 0 : f_time_info.tm_mon = 9;
408 : }
409 0 : else if(f_s[0] == 'n' && f_s[1] == 'o' && f_s[2] == 'v')
410 : {
411 0 : f_time_info.tm_mon = 10;
412 : }
413 0 : else if(f_s[0] == 'd' && f_s[1] == 'e' && f_s[2] == 'c')
414 : {
415 0 : f_time_info.tm_mon = 11;
416 : }
417 : else
418 : {
419 0 : return false;
420 : }
421 :
422 : // check whether the other characters follow
423 0 : if(strncmp(f_s + 3, g_month_name[f_time_info.tm_mon] + 3, g_month_length[f_time_info.tm_mon] - 3) == 0)
424 : {
425 : // full month (not in the specs)
426 0 : f_s += g_month_length[f_time_info.tm_mon];
427 : }
428 : else
429 : {
430 0 : f_s += 3;
431 : }
432 :
433 0 : skip_spaces();
434 0 : return true;
435 : }
436 :
437 0 : bool integer(
438 : unsigned int min_len
439 : , unsigned int max_len
440 : , unsigned int min_value
441 : , unsigned int max_value
442 : , int & result)
443 : {
444 0 : unsigned int u_result = 0;
445 0 : unsigned int count(0);
446 0 : for(; *f_s >= '0' && *f_s <= '9'; ++f_s, ++count)
447 : {
448 0 : u_result = u_result * 10 + *f_s - '0';
449 : }
450 0 : if(count < min_len || count > max_len
451 0 : || u_result < min_value || u_result > max_value)
452 : {
453 0 : result = static_cast<int>(u_result);
454 0 : return false;
455 : }
456 0 : result = static_cast<int>(u_result);
457 0 : return true;
458 : }
459 :
460 0 : bool parse_time()
461 : {
462 0 : if(!integer(2, 2, 0, 23, f_time_info.tm_hour))
463 : {
464 0 : return false;
465 : }
466 0 : if(*f_s != ':')
467 : {
468 0 : return false;
469 : }
470 0 : ++f_s;
471 0 : if(!integer(2, 2, 0, 59, f_time_info.tm_min))
472 : {
473 0 : return false;
474 : }
475 0 : if(*f_s != ':')
476 : {
477 0 : return false;
478 : }
479 0 : ++f_s;
480 0 : if(!integer(2, 2, 0, 60, f_time_info.tm_sec))
481 : {
482 0 : return false;
483 : }
484 0 : skip_spaces();
485 0 : return true;
486 : }
487 :
488 0 : bool parse_timezone()
489 : {
490 : // any timezone?
491 0 : if(*f_s == '\0')
492 : {
493 0 : return true;
494 : }
495 :
496 : // XXX not too sure that the zone is properly handled at this point
497 : // (i.e. should I do += or -=, it may be wrong in many places...)
498 : //
499 : // The newest HTTP format is to only support "+/-####"
500 : //
501 : // zone = "UT" / "GMT"
502 : // / "EST" / "EDT"
503 : // / "CST" / "CDT"
504 : // / "MST" / "MDT"
505 : // / "PST" / "PDT"
506 : // / 1ALPHA
507 : // / ( ("+" / "-") 4DIGIT )
508 0 : if((f_s[0] == 'u' && f_s[1] == 't' && f_s[2] == '\0') // UT
509 0 : || (f_s[0] == 'u' && f_s[1] == 't' && f_s[2] == 'c' && f_s[3] == '\0') // UTC (not in the spec...)
510 0 : || (f_s[0] == 'g' && f_s[1] == 'm' && f_s[2] == 't' && f_s[3] == '\0')) // GMT
511 : {
512 : // no adjustment for UTC (GMT)
513 : }
514 0 : else if(f_s[0] == 'e' && f_s[1] == 's' && f_s[2] == 't' && f_s[3] == '\0') // EST
515 : {
516 0 : f_time_info.tm_hour -= 5;
517 : }
518 0 : else if(f_s[0] == 'e' && f_s[1] == 'd' && f_s[2] == 't' && f_s[3] == '\0') // EDT
519 : {
520 0 : f_time_info.tm_hour -= 4;
521 : }
522 0 : else if(f_s[0] == 'c' && f_s[1] == 's' && f_s[2] == 't' && f_s[3] == '\0') // CST
523 : {
524 0 : f_time_info.tm_hour -= 6;
525 : }
526 0 : else if(f_s[0] == 'c' && f_s[1] == 'd' && f_s[2] == 't' && f_s[3] == '\0') // CDT
527 : {
528 0 : f_time_info.tm_hour -= 5;
529 : }
530 0 : else if(f_s[0] == 'm' && f_s[1] == 's' && f_s[2] == 't' && f_s[3] == '\0') // MST
531 : {
532 0 : f_time_info.tm_hour -= 7;
533 : }
534 0 : else if(f_s[0] == 'm' && f_s[1] == 'd' && f_s[2] == 't' && f_s[3] == '\0') // MDT
535 : {
536 0 : f_time_info.tm_hour -= 6;
537 : }
538 0 : else if(f_s[0] == 'p' && f_s[1] == 's' && f_s[2] == 't' && f_s[3] == '\0') // PST
539 : {
540 0 : f_time_info.tm_hour -= 8;
541 : }
542 0 : else if(f_s[0] == 'p' && f_s[1] == 'd' && f_s[2] == 't' && f_s[3] == '\0') // PDT
543 : {
544 0 : f_time_info.tm_hour -= 7;
545 : }
546 0 : else if(f_s[0] >= 'a' && f_s[0] <= 'z' && f_s[0] != 'j' && f_s[1] == '\0')
547 : {
548 0 : f_time_info.tm_hour += g_timezone_adjust[f_s[0] - 'a'];
549 : }
550 0 : else if((f_s[0] == '+' || f_s[0] == '-')
551 0 : && f_s[1] >= '0' && f_s[1] <= '9'
552 0 : && f_s[2] >= '0' && f_s[2] <= '9'
553 0 : && f_s[3] >= '0' && f_s[3] <= '9'
554 0 : && f_s[4] >= '0' && f_s[4] <= '9'
555 0 : && f_s[5] == '\0')
556 : {
557 0 : f_time_info.tm_hour += ((f_s[1] - '0') * 10 + f_s[2] - '0') * (f_s[0] == '+' ? 1 : -1);
558 0 : f_time_info.tm_min += ((f_s[3] - '0') * 10 + f_s[4] - '0') * (f_s[0] == '+' ? 1 : -1);
559 : }
560 : else
561 : {
562 : // invalid time zone
563 0 : return false;
564 : }
565 :
566 : // WARNING: the time zone doesn't get skipped!
567 0 : return true;
568 : }
569 :
570 0 : bool parse_ansi()
571 : {
572 0 : skip_spaces();
573 0 : if(!parse_month())
574 : {
575 0 : return false;
576 : }
577 0 : if(!integer(1, 2, 1, 31, f_time_info.tm_mday))
578 : {
579 0 : return false;
580 : }
581 0 : skip_spaces();
582 0 : if(!parse_time())
583 : {
584 0 : return false;
585 : }
586 0 : if(!integer(2, 4, 0, 3000, f_time_info.tm_year))
587 : {
588 0 : return false;
589 : }
590 0 : skip_spaces();
591 0 : return parse_timezone();
592 : }
593 :
594 0 : bool parse_us()
595 : {
596 0 : skip_spaces();
597 0 : if(!parse_month())
598 : {
599 0 : return false;
600 : }
601 0 : skip_spaces();
602 0 : if(!integer(1, 2, 1, 31, f_time_info.tm_mday))
603 : {
604 0 : return false;
605 : }
606 0 : skip_spaces();
607 0 : if(!integer(2, 4, 0, 3000, f_time_info.tm_year))
608 : {
609 0 : return false;
610 : }
611 0 : skip_spaces();
612 0 : return parse_time();
613 : }
614 :
615 0 : bool parse()
616 : {
617 : // support for YYYY-MM-DD
618 0 : if(f_date.size() == 10
619 0 : && f_s[4] == '-'
620 0 : && f_s[7] == '-')
621 : {
622 0 : if(!integer(4, 4, 0, 3000, f_time_info.tm_year))
623 : {
624 0 : return false;
625 : }
626 0 : if(*f_s != '-')
627 : {
628 0 : return false;
629 : }
630 0 : ++f_s;
631 0 : if(!integer(2, 2, 1, 12, f_time_info.tm_mon))
632 : {
633 0 : return false;
634 : }
635 0 : --f_time_info.tm_mon; // expect 0 to 11 in final structure
636 0 : if(*f_s != '-')
637 : {
638 0 : return false;
639 : }
640 0 : ++f_s;
641 0 : if(!integer(2, 2, 1, 31, f_time_info.tm_mday))
642 : {
643 0 : return false;
644 : }
645 0 : return true;
646 : }
647 :
648 : // week day (optional in RFC822)
649 0 : if(*f_s >= 'a' && *f_s <= 'z')
650 : {
651 0 : if(!parse_week_day())
652 : {
653 : // maybe that was the month, not the day
654 : // if the time is last, we have a preprocessor date/time
655 : // the second test is needed because the string gets
656 : // simplified and thus numbers 1 to 9 generate a string
657 : // one shorter
658 0 : if((strlen(f_s) == 11 + 1 + 8
659 0 : && f_s[11 + 1 + 8 - 6] == ':'
660 0 : && f_s[11 + 1 + 8 - 3] == ':')
661 0 : ||
662 0 : (strlen(f_s) == 10 + 1 + 8
663 0 : && f_s[10 + 1 + 8 - 6] == ':'
664 0 : && f_s[10 + 1 + 8 - 3] == ':'))
665 : {
666 0 : return parse_us();
667 : }
668 0 : return parse_ansi();
669 : }
670 :
671 0 : if(f_s[0] == ' ')
672 : {
673 : // the ANSI format is completely random!
674 0 : return parse_ansi();
675 : }
676 :
677 0 : if(f_s[0] != ',')
678 : {
679 0 : return false;
680 : }
681 0 : ++f_s; // skip the comma
682 0 : skip_spaces();
683 : }
684 :
685 0 : if(!integer(1, 2, 1, 31, f_time_info.tm_mday))
686 : {
687 0 : return false;
688 : }
689 :
690 0 : if(*f_s == '-')
691 : {
692 0 : ++f_s;
693 : }
694 0 : skip_spaces();
695 :
696 0 : if(!parse_month())
697 : {
698 0 : return false;
699 : }
700 0 : if(*f_s == '-')
701 : {
702 0 : ++f_s;
703 0 : skip_spaces();
704 : }
705 0 : if(!integer(2, 4, 0, 3000, f_time_info.tm_year))
706 : {
707 0 : return false;
708 : }
709 0 : skip_spaces();
710 0 : if(!parse_time())
711 : {
712 0 : return false;
713 : }
714 :
715 0 : return parse_timezone();
716 : }
717 :
718 : struct tm f_time_info = tm();
719 : std::string f_date = std::string();
720 : char const * f_s = nullptr;
721 0 : } parser(date);
722 : #pragma GCC diagnostic pop
723 :
724 0 : if(!parser.parse())
725 : {
726 0 : return -1;
727 : }
728 :
729 : // 2 digit year?
730 : // How to handle this one? At this time I do not expect our software
731 : // to work beyond 2070 which is probably short sighted (ha! ha!)
732 : // However, that way we avoid calling time() and transform that in
733 : // a tm structure and check that date
734 0 : if(parser.f_time_info.tm_year < 100)
735 : {
736 0 : parser.f_time_info.tm_year += 1900;
737 0 : if(parser.f_time_info.tm_year < 1970)
738 : {
739 0 : parser.f_time_info.tm_year += 100;
740 : }
741 : }
742 :
743 : // make sure the day is valid for that month/year
744 0 : if(parser.f_time_info.tm_mday > last_day_of_month(parser.f_time_info.tm_mon + 1, parser.f_time_info.tm_year))
745 : {
746 0 : return -1;
747 : }
748 :
749 : // now we have a time_info which is fully adjusted except for DST...
750 : // let's make time
751 0 : parser.f_time_info.tm_year -= 1900;
752 0 : return mkgmtime(&parser.f_time_info);
753 : }
754 :
755 :
756 : /** \brief From a month and year, get the last day of the month.
757 : *
758 : * This function gives you the number of the last day of the month.
759 : * In all cases, except February, it returns 30 or 31.
760 : *
761 : * For the month of February, we first compute the leap year flag.
762 : * If the year is a leap year, then it returns 29, otherwise it
763 : * returns 28.
764 : *
765 : * The leap year formula is:
766 : *
767 : * \code
768 : * leap = !(year % 4) && (year % 100 || !(year % 400));
769 : * \endcode
770 : *
771 : * \warning
772 : * This function throws if called with September 1752 because the
773 : * month has missing days within the month (days 3 to 13).
774 : *
775 : * \exception edhttp_client_server_logic_error
776 : * This exception is raised if the month is not between 1 and 12 inclusive
777 : * or if the month/year is September 1752 (because that month never existed).
778 : *
779 : * \param[in] month A number from 1 to 12 representing a month.
780 : * \param[in] year A year, including the century.
781 : *
782 : * \return Last day of month, 30, 31, or in February, 28 or 29.
783 : */
784 0 : int last_day_of_month(int month, int year)
785 : {
786 0 : if(month < 1 || month > 12)
787 : {
788 : throw edhttp_client_server_logic_error(
789 : "last_day_of_month called with "
790 0 : + std::to_string(month)
791 0 : + " as the month number");
792 : }
793 :
794 0 : if(month == 2)
795 : {
796 : // special case for February
797 : //
798 : // The time when people switch from Julian to Gregorian is country
799 : // dependent, Great Britain changed on September 2, 1752, but some
800 : // countries changed as late as 1952...
801 : //
802 : // For now, we use the GB date. Once we have a valid way to handle
803 : // this with the locale, we can look into updating the code. That
804 : // being said, it should not matter too much because most dates on
805 : // the Internet are past 2000.
806 : //
807 0 : if(year <= 1752)
808 : {
809 0 : return year % 4 == 0 ? 29 : 28;
810 : }
811 0 : return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) ? 29 : 28;
812 : }
813 :
814 0 : if(month == 9 && year == 1752)
815 : {
816 : // we cannot handle this nice one here, days 3 to 13 are missing on
817 : // this month... (to adjust the calendar all at once!)
818 : throw edhttp_client_server_logic_error(
819 : "last_day_of_month called with "
820 0 : + std::to_string(year)
821 0 : + " as the year number");
822 : }
823 :
824 0 : return g_month_days[month - 1];
825 : }
826 :
827 :
828 :
829 : } // namespace edhttp
830 : // vim: ts=4 sw=4 et
|