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