1# OpenXPKI::DateTime.pm
2# Written by Martin Bartosch for the OpenXPKI project
3# Copyright (C) 2005-2006 by The OpenXPKI Project
4
5use strict;
6use warnings;
7
8package OpenXPKI::DateTime;
9
10use OpenXPKI::Debug;
11use OpenXPKI::Exception;
12use English;
13
14use DateTime;
15use Date::Parse;
16
17# static function
18sub convert_date {
19    my $params = shift;
20
21    my $outformat =
22      exists $params->{OUTFORMAT}
23      ? $params->{OUTFORMAT}
24      : 'iso8601';
25
26    my $date = $params->{DATE};
27
28    if ( !defined $date ) {
29        OpenXPKI::Exception->throw(
30            message => "I18N_OPENXPKI_DATETIME_CONVERT_DATE_INVALID_DATE", );
31    }
32
33    # convert to UTC
34    eval { $date->set_time_zone('UTC'); };
35    if ($EVAL_ERROR) {
36        OpenXPKI::Exception->throw(
37            message => "I18N_OPENXPKI_DATETIME_CONVERT_DATE_INVALID_DATE",
38            params  => { ERROR => $EVAL_ERROR, },
39        );
40    }
41
42    return $date->epoch()                   if ( $outformat eq 'epoch' );
43    return $date->iso8601()                 if ( $outformat eq 'iso8601' );
44    return $date->strftime("%y%m%d%H%M%SZ") if ( $outformat eq 'openssltime' );
45    return $date->strftime("%Y%m%d%H%M%SZ") if ( $outformat eq 'generalizedtime' );
46    return $date->strftime("%Y%m%d%H%M%S")  if ( $outformat eq 'terse' );
47    return $date->strftime("%F %T")         if ( $outformat eq 'printable' );
48
49    OpenXPKI::Exception->throw(
50        message => "I18N_OPENXPKI_DATETIME_CONVERT_DATE_INVALID_FORMAT",
51        params  => { OUTFORMAT => $outformat, }
52    );
53}
54
55sub get_validity {
56    my $params = shift;
57
58    my $validity =
59      defined $params->{VALIDITY}
60      ? $params->{VALIDITY}
61      : "";
62
63    my $validityformat =
64      defined $params->{VALIDITYFORMAT}
65      ? $params->{VALIDITYFORMAT}
66      : 'relativedate';
67
68    # referencedate is used for relative date computations
69    my $refdate;
70    if ( defined $params->{REFERENCEDATE} && ref $params->{REFERENCEDATE} ) {
71        $refdate = $params->{REFERENCEDATE}->clone();
72    }
73    elsif ( $params->{REFERENCEDATE} ) {
74
75        #parse from string
76        $refdate = parse_date_utc( $params->{REFERENCEDATE} );
77
78    }
79    else {
80        $refdate = DateTime->now( time_zone => 'UTC' );
81    }
82
83    if ( $validityformat eq 'detect' ) {
84        if ( $validity =~ m{\A [+\-]}xms ) {
85            $validityformat = 'relativedate';
86        }
87        # we hopefully wont have validities that far in the past
88        # and I guess this software wont run if we reach the next epoch
89        # so it should be safe to distinguish between terse date and epoch
90        elsif ($validity =~ m{\A \d{9,10} \z }xms ) {
91            $validityformat = 'epoch';
92
93        # strip non-digits from iso date
94        # also accept dates without time and missing "T" character
95        } elsif ($validity =~ m{\A\d{4}-\d{2}-\d{2}([T\s]\d{2}:\d{2}:\d{2})?\z}) {
96            $validity =~ s/[^0-9]//g;
97            $validityformat = 'absolutedate';
98
99        } elsif ($validity =~ m{\A\d{8}(\d{4}(\d{2})?)?\z}) {
100            $validityformat = 'absolutedate';
101
102        } else {
103            OpenXPKI::Exception->throw(
104                message => "Invalid format given to detect",
105                params => {
106                    VALIDITY  => $validity,
107                },
108            );
109        }
110    }
111
112    if ( $validityformat eq 'epoch' ) {
113         return DateTime->from_epoch( epoch => $validity );
114    }
115
116    if ( $validityformat eq 'days' ) {
117        if ( $validity !~ m{ \A [+\-]?\d+ \z }xms ) {
118            OpenXPKI::Exception->throw(
119                message =>
120                  "I18N_OPENXPKI_DATETIME_GET_VALIDITY_INVALID_VALIDITY",
121                params => {
122                    VALIDITYFORMAT => $validityformat,
123                    VALIDITY       => $validity,
124                },
125            );
126        }
127        $refdate->add( days => $validity );
128
129        return $refdate;
130    }
131
132    ##! 16: "$validityformat / $validity"
133    if (   ( $validityformat eq 'absolutedate' )
134        || ( $validityformat eq 'relativedate' ) )
135    {
136
137        my $relative = "";
138        if ( $validityformat eq 'relativedate' ) {
139            ( $relative, $validity ) =
140              ( $validity =~ m{ \A ([+\-]?)(\d+) \z }xms );
141        }
142
143        if ( ( !defined $validity ) || ( $validity eq "" ) ) {
144            OpenXPKI::Exception->throw(
145                message =>
146                  "I18N_OPENXPKI_DATETIME_GET_VALIDITY_INVALID_VALIDITY",
147                params => {
148                    VALIDITYFORMAT => $validityformat,
149                    VALIDITY       => $params->{VALIDITY},
150                },
151            );
152        }
153
154        my %date;
155
156        # get year
157        my $datelength = ( $relative eq "" ) ? 4 : 2;
158        ( $date{year}, $validity ) =
159          ( $validity =~ m{ \A (\d{$datelength}) (\d*) \z }xms );
160
161        # month, day, hour, minute, second
162        foreach my $item (qw ( month day hour minute second )) {
163            if ( defined $validity ) {
164                my $value;
165                ( $value, $validity ) =
166                  ( $validity =~ m{ \A (\d{2}) (\d*) \z }xms );
167                if ( defined $value ) {
168                    $date{$item} = $value;
169                }
170            }
171        }
172        ##! 32: \%date
173
174        # e.g. if '+0' was given
175        if (not defined $date{year}) {
176            OpenXPKI::Exception->throw(
177                message =>
178                  "I18N_OPENXPKI_DATETIME_GET_VALIDITY_INVALID_VALIDITY",
179                params => {
180                    VALIDITYFORMAT => $validityformat,
181                    VALIDITY       => $params->{VALIDITY},
182                },
183            );
184        }
185
186        # absolute validity
187        if ( $relative eq "" ) {
188            return DateTime->new( %date, time_zone => 'UTC', );
189        }
190        else {
191
192            # append an 's' character to the has keys (year -> years)
193            %date = map { $_ . 's' => $date{$_} } keys %date;
194
195            if ( $relative eq "+" ) {
196                $refdate->add(%date);
197                return $refdate;
198            }
199
200            if ( $relative eq "-" ) {
201                $refdate->subtract(%date);
202                return $refdate;
203            }
204        }
205    }
206
207    OpenXPKI::Exception->throw(
208        message =>
209          "I18N_OPENXPKI_DATETIME_GET_VALIDITY_INVALID_VALIDITY_FORMAT",
210        params => {
211            VALIDITYFORMAT => $validityformat,
212            VALIDITY       => $params->{VALIDITY},
213        },
214    );
215}
216
217sub parse_date_utc {
218
219    my $date_string = shift;
220
221    my ( $ss, $mm, $hh, $day, $month, $year, $zone ) = strptime($date_string);
222    $month++;
223    $year += 1900;
224    return DateTime->new(
225        (
226            year      => $year,
227            month     => $month,
228            day       => $day,
229            hour      => $hh,
230            minute    => $mm,
231            second    => $ss,
232            time_zone => $zone,
233        ),
234        time_zone => 'UTC',
235    );
236}
237
238sub is_relative {
239    my $datestring = shift;
240    return $datestring =~ m{\A [+\-]}xms;
241}
242
2431;
244__END__
245
246=head1 Name
247
248OpenXPKI::DateTime - tools to handle various date and timestamp formats.
249
250=head1 Description
251
252Tools for date/time manipulation.
253
254=head1 Functions
255
256
257=head2 convert_date
258
259Converts a DateTime object to various date formats used throughout
260OpenXPKI and returns the corresponding representation. Before converting
261the object the Time Zone is adjusted to UTC.
262
263If OUTFORMAT is not specified the output format defaults to iso8601.
264
265Possible output formats:
266  iso8601:     ISO 8601 formatted date (YYYY-MM-DDTHH:MM:SS), default
267  epoch:       seconds since the epoch
268  openssltime: time format used in OpenSSL index files (YYMMDDHHMMSSZ)
269  generalizedtime: time format used in OpenSSL index files (YYYYMMDDHHMMSSZ)
270  terse:       terse time format (YYYYMMDDHHMMSS)
271  printable:   human readable ISO-like time format (YYYY-MM-DD HH:MM:SS)
272
273=head3 Example
274
275    my $dt = DateTime->now();
276
277    print OpenXPKI::DateTime::convert_date({
278        DATE      => $dt,
279        OUTFORMAT => 'iso8601',
280    });
281
282
283=head2 get_validity
284
285Returns a DateTime object that reflects the requested validity in UTC.
286
287Possible validity formats (specified via VALIDITYFORMAT):
288
289=over 4
290
291=item *
292
293'relativedate': the specified validity is interpreted as a relative
294terse date string. This is the default.
295
296=item *
297
298'absolutedate': the specified validity is interpreted as an absolute
299terse date string.
300
301=item *
302
303'days': the specified validity is interpreted as an integer number of days
304(positive or negative) as an offset to the reference date.
305
306=item *
307
308'epoch': the specified validity is a unix epoch, used as absolute date.
309
310=item *
311
312'detect': tries to guess what it got, relativedate if it has a sign (+/-),
313epoch if it has between 9 and 10 digits and absolutedate otherwise. Also
314consumes iso8601 formated strings. Days can not be autodetected as they
315look like relativedate.
316
317'absolutedate' is only valid with eight (day only), 12 (minutes) or
31814 (seconds) digits.
319
320=back
321
322=head3 Reference date
323
324If a relative validity is specified the duration is added to a reference
325date that defaults to the current time (UTC).
326
327If the named parameter REFERENCEDATE is specified, this date is taken as the basis for calculating the relative
328date. The parameter could either contain a DateTime object or a parsable date string
329(i.e. '2012-05-24T08:33:47' see Date::Parse for a list of valid strings) which will be converted to an UTC DateTime object.
330
331=head3 Terse date strings
332
333The validity specification is passed in as the named parameter VALIDITY.
334
335Absolute validities are specified in the format
336
337  YYYYMMDD[HH[MM[SS]]]
338
339Missing optional time specifications are replaced with '00'.
340Example:
341
342  2006031618   is interpreted as 2006-03-16 18:00:00 UTC
343
344
345Relative validities are specified as a partial terse date string in
346the format
347
348  +YY[MM[DD[HH[MM[SS]]]]]   or
349  -YY[MM[DD[HH[MM[SS]]]]]
350
351Positive relative validities are interpreted as date offsets in the future
352as seen from reference date, negative relativie validities are interpreted
353as date offsets in the past.
354
355Examples:
356
357  -000001    (yesterday)
358  +0003      (three months from now)
359
360=head3 Usage example
361
362  my $offset = DateTime->now( timezone => 'UTC' );
363  $offset->add( months => 2 );
364
365  my $somedate = OpenXPKI::DateTime::get_validity(
366        {
367        REFERENCEDATE => $offset,
368        VALIDITY => '+0205',
369        VALIDITYFORMAT => 'relativedate',
370        },
371    );
372  print $somedate->datetime()
373
374After this has been executed a date should be printed that is 2 years
375and 7 months in the future: the relative validity 2 years, 5 months
376is added to the offset which is 2 months in the future from now.
377
378=head2 parse_date_utc
379
380Helpermethod. Passes the given parameter $date_string  to Date::Parse::strptime and constructs from the return an UTC DateTime object
381
382=head2 is_relative
383
384Static helper, check if a datestring looks like a relative format.
385(Check if the first character is a +/-).
386