rt 4.0.23
[freeside.git] / rt / lib / RT / Date.pm
index e51a775..4405b07 100644 (file)
@@ -2,7 +2,7 @@
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
@@ -68,13 +68,16 @@ The fact that it assumes that a time of 0 means "never" is probably a bug.
 
 package RT::Date;
 
-use Time::Local;
-use POSIX qw(tzset);
 
 use strict;
 use warnings;
+
 use base qw/RT::Base/;
 
+use DateTime;
+
+use Time::Local;
+use POSIX qw(tzset);
 use vars qw($MINUTE $HOUR $DAY $WEEK $MONTH $YEAR);
 
 $MINUTE = 60;
@@ -110,17 +113,14 @@ our @DAYS_OF_WEEK = (
 );
 
 our @FORMATTERS = (
-    'DefaultFormat', # loc
-    'ISO',           # loc
-    'W3CDTF',        # loc
-    'RFC2822',       # loc
-    'RFC2616',       # loc
-    'iCal',          # loc
+    'DefaultFormat',     # loc
+    'ISO',               # loc
+    'W3CDTF',            # loc
+    'RFC2822',           # loc
+    'RFC2616',           # loc
+    'iCal',              # loc
+    'LocalizedDateTime', # loc
 );
-if ( eval 'use DateTime qw(); 1;' && eval 'use DateTime::Locale qw(); 1;' && 
-     DateTime->can('format_cldr') && DateTime::Locale::root->can('date_format_full') ) {
-    push @FORMATTERS, 'LocalizedDateTime'; # loc
-}
 
 =head2 new
 
@@ -164,7 +164,7 @@ sub Set {
         @_
     );
 
-    return $self->Unix(0) unless $args{'Value'};
+    return $self->Unix(0) unless $args{'Value'} && $args{'Value'} =~ /\S/;
 
     if ( $args{'Format'} =~ /^unix$/i ) {
         return $self->Unix( $args{'Value'} );
@@ -208,7 +208,7 @@ sub Set {
         # should be applied to absolute times, so compensate shift in NOW
         my $now = time;
         $now += ($self->Localtime( $args{Timezone}, $now ))[9];
-        my $date = Time::ParseDate::parsedate(
+        my ($date, $error) = Time::ParseDate::parsedate(
             $args{'Value'},
             GMT           => 1,
             NOW           => $now,
@@ -216,6 +216,13 @@ sub Set {
             PREFER_PAST   => RT->Config->Get('AmbiguousDayInPast'),
             PREFER_FUTURE => RT->Config->Get('AmbiguousDayInFuture'),
         );
+        unless ( defined $date ) {
+            $RT::Logger->warning(
+                "Couldn't parse date '$args{'Value'}' by Time::ParseDate"
+            );
+            return $self->Unix(0);
+        }
+
         # apply timezone offset
         $date -= ($self->Localtime( $args{Timezone}, $date ))[9];
 
@@ -273,6 +280,41 @@ sub SetToMidnight {
     return $self->Unix( $new );
 }
 
+=head2 SetToStart PERIOD[, Timezone => 'utc' ]
+
+Set to the beginning of the current PERIOD, which can be 
+"year", "month", "day", "hour", or "minute".
+
+=cut
+
+sub SetToStart {
+    my $self = shift;
+    my $p = uc(shift);
+    my %args = @_;
+    my $tz = $args{'Timezone'} || '';
+    my @localtime = $self->Localtime($tz);
+    #remove 'offset' so that DST is figured based on the resulting time.
+    pop @localtime;
+
+    # This is the cleanest way to implement it, I swear.
+    {
+        $localtime[0]=0;
+        last if ($p eq 'MINUTE');
+        $localtime[1]=0;
+        last if ($p eq 'HOUR');
+        $localtime[2]=0;
+        last if ($p eq 'DAY');
+        $localtime[3]=1;
+        last if ($p eq 'MONTH');
+        $localtime[4]=0;
+        last if ($p eq 'YEAR');
+        $RT::Logger->warning("Couldn't find start date of '$p'.");
+        return;
+    }
+    my $new = $self->Timelocal($tz, @localtime);
+    return $self->Unix($new);
+}
+
 =head2 Diff
 
 Takes either an C<RT::Date> object or the date in unixtime format as a string,
@@ -467,7 +509,8 @@ Returns new unix time.
 
 sub AddDays {
     my $self = shift;
-    my $days = shift || 1;
+    my $days = shift;
+    $days = 1 unless defined $days;
     return $self->AddSeconds( $days * $DAY );
 }
 
@@ -479,6 +522,30 @@ Adds 24 hours to the current time. Returns new unix time.
 
 sub AddDay { return $_[0]->AddSeconds($DAY) }
 
+=head2 AddMonth
+
+Adds one month to the current time. Returns new 
+unix time.
+
+=cut
+
+sub AddMonth {    
+    my $self = shift;
+    my %args = @_;
+    my @localtime = $self->Localtime($args{'Timezone'});
+    # remove offset, as with SetToStart
+    pop @localtime;
+    
+    $localtime[4]++; #month
+    if ( $localtime[4] == 12 ) {
+      $localtime[4] = 0;
+      $localtime[5]++; #year
+    }
+
+    my $new = $self->Timelocal($args{'Timezone'}, @localtime);
+    return $self->Unix($new);
+}
+
 =head2 Unix [unixtime]
 
 Optionally takes a date in unix seconds since the epoch format.
@@ -545,6 +612,10 @@ sub Get
     my $self = shift;
     my %args = (Format => 'ISO', @_);
     my $formatter = $args{'Format'};
+    unless ( $self->ValidFormatter($formatter) ) {
+        RT->Logger->warning("Invalid date formatter '$formatter', falling back to ISO");
+        $formatter = 'ISO';
+    }
     $formatter = 'ISO' unless $self->can($formatter);
     return $self->$formatter( %args );
 }
@@ -583,6 +654,20 @@ sub Formatters
     return @FORMATTERS;
 }
 
+=head3 ValidFormatter FORMAT
+
+Returns a true value if C<FORMAT> is a known formatter.  Otherwise returns
+false.
+
+=cut
+
+sub ValidFormatter {
+    my $self   = shift;
+    my $format = shift;
+    return (grep { $_ eq $format } $self->Formatters and $self->can($format))
+                ? 1 : 0;
+}
+
 =head3 DefaultFormat
 
 =cut
@@ -626,17 +711,33 @@ sub DefaultFormat
     }
 }
 
+=head2 LocaleObj
+
+Returns the L<DateTime::Locale> object representing the current user's locale.
+
+=cut
+
+sub LocaleObj {
+    my $self = shift;
+
+    my $lang = $self->CurrentUser->UserObj->Lang;
+    unless ($lang) {
+        require I18N::LangTags::Detect;
+        $lang = ( I18N::LangTags::Detect::detect(), 'en' )[0];
+    }
+
+    return DateTime::Locale->load($lang);
+}
+
 =head3 LocalizedDateTime
 
 Returns date and time as string, with user localization.
 
 Supports arguments: C<DateFormat> and C<TimeFormat> which may contains date and
-time format as specified in DateTime::Locale (default to full_date_format and
+time format as specified in L<DateTime::Locale> (default to full_date_format and
 medium_time_format), C<AbbrDay> and C<AbbrMonth> which may be set to 0 if
 you want full Day/Month names instead of abbreviated ones.
 
-Require optionnal DateTime::Locale module.
-
 =cut
 
 sub LocalizedDateTime
@@ -645,32 +746,21 @@ sub LocalizedDateTime
     my %args = ( Date => 1,
                  Time => 1,
                  Timezone => '',
-                 DateFormat => 'date_format_full',
-                 TimeFormat => 'time_format_medium',
+                 DateFormat => '',
+                 TimeFormat => '',
                  AbbrDay => 1,
                  AbbrMonth => 1,
                  @_,
                );
 
-    return $self->loc("DateTime module missing") unless ( eval 'use DateTime qw(); 1;' );
-    return $self->loc("DateTime::Locale module missing") unless ( eval 'use DateTime::Locale qw(); 1;' );
-    return $self->loc("DateTime doesn't support format_cldr, you must upgrade to use this feature") 
-        unless can DateTime::('format_cldr');
-
-
-    my $date_format = $args{'DateFormat'};
-    my $time_format = $args{'TimeFormat'};
+    # Require valid names for the format methods
+    my $date_format = $args{DateFormat} =~ /^\w+$/
+                    ? $args{DateFormat} : 'date_format_full';
 
-    my $lang = $self->CurrentUser->UserObj->Lang;
-    unless ($lang) {
-        require I18N::LangTags::Detect;
-        $lang = ( I18N::LangTags::Detect::detect(), 'en' )[0];
-    }
-    
+    my $time_format = $args{TimeFormat} =~ /^\w+$/
+                    ? $args{TimeFormat} : 'time_format_medium';
 
-    my $formatter = DateTime::Locale->load($lang);
-    return $self->loc("DateTime::Locale doesn't support date_format_full, you must upgrade to use this feature") 
-        unless $formatter->can('date_format_full');
+    my $formatter = $self->LocaleObj;
     $date_format = $formatter->$date_format;
     $time_format = $formatter->$time_format;
     $date_format =~ s/EEEE/EEE/g if ( $args{'AbbrDay'} );
@@ -683,7 +773,7 @@ sub LocalizedDateTime
 
     # FIXME : another way to call this module without conflict with local
     # DateTime method?
-    my $dt = new DateTime::( locale => $lang,
+    my $dt = DateTime::->new( locale => $formatter,
                             time_zone => $tz,
                             year => $year,
                             month => $mon,
@@ -813,7 +903,7 @@ sub RFC2822 {
 
     my ($date, $time) = ('','');
     $date .= "$DAYS_OF_WEEK[$wday], " if $args{'DayOfWeek'} && $args{'Date'};
-    $date .= "$mday $MONTHS[$mon] $year" if $args{'Date'};
+    $date .= sprintf("%02d %s %04d", $mday, $MONTHS[$mon], $year) if $args{'Date'};
 
     if ( $args{'Time'} ) {
         $time .= sprintf("%02d:%02d", $hour, $min);
@@ -878,21 +968,20 @@ sub iCal {
         Date => 1, Time => 1,
         @_,
     );
-    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$ydaym,$isdst,$offset) =
-        $self->Localtime( 'utc' );
-
-    #the month needs incrementing, as gmtime returns 0-11
-    $mon++;
 
     my $res;
     if ( $args{'Date'} && !$args{'Time'} ) {
-        $res = sprintf( '%04d%02d%02d', $year, $mon, $mday );
-    }
-    elsif ( !$args{'Date'} && $args{'Time'} ) {
+        my (undef, undef, undef, $mday, $mon, $year) =
+            $self->Localtime( 'user' );
+        $res = sprintf( '%04d%02d%02d', $year, $mon+1, $mday );
+    } elsif ( !$args{'Date'} && $args{'Time'} ) {
+        my ($sec, $min, $hour) =
+            $self->Localtime( 'utc' );
         $res = sprintf( 'T%02d%02d%02dZ', $hour, $min, $sec );
-    }
-    else {
-        $res = sprintf( '%04d%02d%02dT%02d%02d%02dZ', $year, $mon, $mday, $hour, $min, $sec );
+    } else {
+        my ($sec, $min, $hour, $mday, $mon, $year) =
+            $self->Localtime( 'utc' );
+        $res = sprintf( '%04d%02d%02dT%02d%02d%02dZ', $year, $mon+1, $mday, $hour, $min, $sec );
     }
     return $res;
 }
@@ -1022,6 +1111,12 @@ If both server's and user's timezone names are undefined returns 'UTC'.
 
 sub Timezone {
     my $self = shift;
+
+    if (@_ == 0) {
+        Carp::carp "RT::Date->Timezone is a setter only";
+        return undef;
+    }
+
     my $context = lc(shift);
 
     $context = 'utc' unless $context =~ /^(?:utc|server|user)$/i;
@@ -1040,9 +1135,6 @@ sub Timezone {
 }
 
 
-eval "require RT::Date_Vendor";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Vendor.pm});
-eval "require RT::Date_Local";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/Date_Local.pm});
+RT::Base->_ImportOverlays();
 
 1;