View | Details | Raw Unified | Return to bug 36432
Collapse All | Expand All

(-)a/Koha/DateTime/Format/RFC3339.pm (+38 lines)
Line 0 Link Here
1
package Koha::DateTime::Format::RFC3339;
2
3
use Modern::Perl;
4
5
use DateTime::Format::Builder (
6
    parsers => {
7
        parse_datetime => [
8
            {
9
                params => [ qw( year month day hour minute second time_zone ) ],
10
                regex => qr/^(\d{4})-(\d{2})-(\d{2})[Tt\s](\d{2}):(\d{2}):(\d{2})(?:\.\d{1,3})?([Zz]|(?:[\+|\-](?:[01][0-9]|2[0-3]):[0-5][0-9]))$/,
11
                postprocess => \&_postprocess_datetime,
12
            },
13
        ],
14
    }
15
);
16
17
sub format_datetime {
18
    my ($class, $dt) = @_;
19
20
    my $date = $dt->strftime('%FT%T%z');
21
    substr($date, -2, 0, ':'); # timezone "HHmm" => "HH:mm"
22
23
    return $date;
24
}
25
26
sub _postprocess_datetime {
27
    my %args = @_;
28
    my $parsed = $args{parsed};
29
30
    # system allows the 0th of the month
31
    $parsed->{day} = '01' if $parsed->{day} eq '00';
32
33
    $parsed->{time_zone} = 'UTC' if $parsed->{time_zone} =~ /^[Zz]$/;
34
35
    return 1;
36
}
37
38
1;
(-)a/Koha/DateTime/Format/SQL.pm (+25 lines)
Line 0 Link Here
1
package Koha::DateTime::Format::SQL;
2
3
use Modern::Perl;
4
5
use DateTime::Format::MySQL;
6
7
use Koha::Config;
8
9
our $timezone;
10
11
sub parse_datetime {
12
    my ($class, $date) = @_;
13
14
    my $dt = DateTime::Format::MySQL->parse_datetime($date);
15
16
    # No TZ for dates 'infinite' => see bug 13242
17
    if ($dt->year < 9999) {
18
        $timezone //= Koha::Config->get_instance->timezone;
19
        $dt->set_time_zone($timezone);
20
    }
21
22
    return $dt;
23
}
24
25
1;
(-)a/Koha/DateUtils.pm (-25 / +7 lines)
Lines 20-25 use Modern::Perl; Link Here
20
use DateTime;
20
use DateTime;
21
use C4::Context;
21
use C4::Context;
22
use Koha::Exceptions;
22
use Koha::Exceptions;
23
use Koha::DateTime::Format::RFC3339;
23
24
24
use vars qw(@ISA @EXPORT_OK);
25
use vars qw(@ISA @EXPORT_OK);
25
BEGIN {
26
BEGIN {
Lines 72-77 sub dt_from_string { Link Here
72
        return $date_string->clone();
73
        return $date_string->clone();
73
    }
74
    }
74
75
76
    if ($date_format eq 'rfc3339') {
77
        return Koha::DateTime::Format::RFC3339->parse_datetime($date_string);
78
    }
79
75
    my $regex;
80
    my $regex;
76
81
77
    # The fallback format is sql/iso
82
    # The fallback format is sql/iso
Lines 113-140 sub dt_from_string { Link Here
113
            (?<year>\d{4})
118
            (?<year>\d{4})
114
        |xms;
119
        |xms;
115
    }
120
    }
116
    elsif ( $date_format eq 'rfc3339' ) {
117
        $regex = qr/
118
            (?<year>\d{4})
119
            -
120
            (?<month>\d{2})
121
            -
122
            (?<day>\d{2})
123
            ([Tt\s])
124
            (?<hour>\d{2})
125
            :
126
            (?<minute>\d{2})
127
            :
128
            (?<second>\d{2})
129
            (\.\d{1,3})?(([Zz]$)|((?<offset>[\+|\-])(?<hours>[01][0-9]|2[0-3]):(?<minutes>[0-5][0-9])))
130
        /xms;
131
132
        # Default to UTC (when 'Z' is passed) for inbound timezone.
133
        # The regex above succeeds for both 'z', 'Z' and '+/-' offset.
134
        # We set tz as though Z was passed by default and then correct it later if an offset is detected
135
        # by the presence fo the <offset> variable.
136
        $tz = DateTime::TimeZone->new( name => 'UTC' );
137
    }
138
    elsif ( $date_format eq 'iso' or $date_format eq 'sql' ) {
121
    elsif ( $date_format eq 'iso' or $date_format eq 'sql' ) {
139
        # iso or sql format are yyyy-dd-mm[ hh:mm:ss]"
122
        # iso or sql format are yyyy-dd-mm[ hh:mm:ss]"
140
        $regex = $fallback_re;
123
        $regex = $fallback_re;
Lines 164-170 sub dt_from_string { Link Here
164
                )?
147
                )?
165
            )?
148
            )?
166
    }xms;
149
    }xms;
167
    $regex .= $time_re unless ( $date_format eq 'rfc3339' );
150
    $regex .= $time_re;
168
    $fallback_re .= $time_re;
151
    $fallback_re .= $time_re;
169
152
170
    # Ensure we only accept date strings and not other characters.
153
    # Ensure we only accept date strings and not other characters.
Lines 310-317 sub output_pref { Link Here
310
    }
293
    }
311
    elsif ( $pref =~ m/^rfc3339/ ) {
294
    elsif ( $pref =~ m/^rfc3339/ ) {
312
        if (!$dateonly) {
295
        if (!$dateonly) {
313
            $date = $dt->strftime('%FT%T%z');
296
            $date = Koha::DateTime::Format::RFC3339->format_datetime($dt);
314
            substr($date, -2, 0, ':'); # timezone "HHmm" => "HH:mm"
315
        }
297
        }
316
        else {
298
        else {
317
            $date = $dt->strftime("%Y-%m-%d");
299
            $date = $dt->strftime("%Y-%m-%d");
(-)a/Koha/Object.pm (-16 / +18 lines)
Lines 25-34 use Mojo::JSON; Link Here
25
use Scalar::Util qw( blessed looks_like_number );
25
use Scalar::Util qw( blessed looks_like_number );
26
use Try::Tiny qw( catch try );
26
use Try::Tiny qw( catch try );
27
use List::MoreUtils qw( any );
27
use List::MoreUtils qw( any );
28
use DateTime::Format::MySQL;
28
29
29
use Koha::Database;
30
use Koha::Database;
31
use Koha::DateTime::Format::RFC3339;
32
use Koha::DateTime::Format::SQL;
30
use Koha::Exceptions::Object;
33
use Koha::Exceptions::Object;
31
use Koha::DateUtils qw( dt_from_string output_pref );
32
use Koha::Object::Message;
34
use Koha::Object::Message;
33
35
34
=head1 NAME
36
=head1 NAME
Lines 423-432 sub TO_JSON { Link Here
423
        elsif ( _datetime_column_type( $columns_info->{$col}->{data_type} ) ) {
425
        elsif ( _datetime_column_type( $columns_info->{$col}->{data_type} ) ) {
424
            eval {
426
            eval {
425
                return unless $unblessed->{$col};
427
                return unless $unblessed->{$col};
426
                $unblessed->{$col} = output_pref({
428
                my $dt = Koha::DateTime::Format::SQL->parse_datetime( $unblessed->{$col} );
427
                    dateformat => 'rfc3339',
429
                $unblessed->{$col} = Koha::DateTime::Format::RFC3339->format_datetime($dt);
428
                    dt         => dt_from_string($unblessed->{$col}, 'sql'),
429
                });
430
            };
430
            };
431
        }
431
        }
432
    }
432
    }
Lines 832-850 sub attributes_from_api { Link Here
832
            $value = ( $value ) ? 1 : 0;
832
            $value = ( $value ) ? 1 : 0;
833
        }
833
        }
834
        elsif ( _date_or_datetime_column_type( $columns_info->{$koha_field_name}->{data_type} ) ) {
834
        elsif ( _date_or_datetime_column_type( $columns_info->{$koha_field_name}->{data_type} ) ) {
835
            try {
835
            if (defined $value) {
836
                if ( $columns_info->{$koha_field_name}->{data_type} eq 'date' ) {
836
                try {
837
                    $value = $dtf->format_date(dt_from_string($value, 'iso'))
837
                    if ( $columns_info->{$koha_field_name}->{data_type} eq 'date' ) {
838
                        if defined $value;
838
                        my $dt = DateTime::Format::MySQL->parse_date($value);
839
                }
839
                        $value = $dtf->format_date($dt);
840
                else {
840
                    }
841
                    $value = $dtf->format_datetime(dt_from_string($value, 'rfc3339'))
841
                    else {
842
                        if defined $value;
842
                        my $dt = Koha::DateTime::Format::RFC3339->parse_datetime($value);
843
                        $value = $dtf->format_datetime($dt);
844
                    }
843
                }
845
                }
846
                catch {
847
                    Koha::Exceptions::BadParameter->throw( parameter => $key );
848
                };
844
            }
849
            }
845
            catch {
846
                Koha::Exceptions::BadParameter->throw( parameter => $key );
847
            };
848
        }
850
        }
849
851
850
        $params->{$koha_field_name} = $value;
852
        $params->{$koha_field_name} = $value;
(-)a/t/DateUtils.t (-1 / +1 lines)
Lines 140-146 cmp_ok( $dt0->epoch(), 'eq', '1325455199', 'dt_from_string handles seconds with Link Here
140
eval {
140
eval {
141
    $dt0 = dt_from_string( '2012-01-01T23:59:59.999Z+02:00', 'rfc3339' ); # Do not combine Z with +02 !
141
    $dt0 = dt_from_string( '2012-01-01T23:59:59.999Z+02:00', 'rfc3339' ); # Do not combine Z with +02 !
142
};
142
};
143
like( $@, qr/.*does not match the date format \(rfc3339\).*/, 'dt_from_string should die when passed a bad rfc3339 date string' );
143
like( $@, qr/Invalid date format/, 'dt_from_string should die when passed a bad rfc3339 date string' );
144
144
145
eval {
145
eval {
146
    $dt0 = dt_from_string('2021-11-03T10:16:59Z+00:00', 'iso'); # Z and +00 are the same, but should not be together
146
    $dt0 = dt_from_string('2021-11-03T10:16:59Z+00:00', 'iso'); # Z and +00 are the same, but should not be together
(-)a/t/Koha/DateTime/Format/RFC3339.t (+60 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
use Test::More;
5
use Test::Exception;
6
7
BEGIN { use_ok('Koha::DateTime::Format::RFC3339'); }
8
9
subtest 'UTC datetime' => sub {
10
    plan tests => 7;
11
12
    my $dt = Koha::DateTime::Format::RFC3339->parse_datetime('2024-01-02T10:11:12Z');
13
14
    is($dt->year, 2024);
15
    is($dt->month, 1);
16
    is($dt->day, 2);
17
    is($dt->hour, 10);
18
    is($dt->minute, 11);
19
    is($dt->second, 12);
20
    ok($dt->time_zone->is_utc);
21
};
22
23
subtest 'with timezone' => sub {
24
    plan tests => 7;
25
26
    my $dt = Koha::DateTime::Format::RFC3339->parse_datetime('2024-01-02T10:11:12+01:30');
27
28
    is($dt->year, 2024);
29
    is($dt->month, 1);
30
    is($dt->day, 2);
31
    is($dt->hour, 10);
32
    is($dt->minute, 11);
33
    is($dt->second, 12);
34
    is($dt->time_zone->name, '+0130');
35
};
36
37
subtest 'fractions of seconds are ignored' => sub {
38
    plan tests => 8;
39
40
    my $dt = Koha::DateTime::Format::RFC3339->parse_datetime('2024-01-02T10:11:12.34+01:30');
41
42
    is($dt->year, 2024);
43
    is($dt->month, 1);
44
    is($dt->day, 2);
45
    is($dt->hour, 10);
46
    is($dt->minute, 11);
47
    is($dt->second, 12);
48
    is($dt->nanosecond, 0);
49
    is($dt->time_zone->name, '+0130');
50
};
51
52
subtest 'invalid date throws an exception' => sub {
53
    plan tests => 1;
54
55
    throws_ok {
56
        my $dt = Koha::DateTime::Format::RFC3339->parse_datetime('2024-01-02T10:11:12');
57
    } qr/Invalid date format/;
58
};
59
60
done_testing;
(-)a/t/db_dependent/Koha/Object.t (-2 / +1 lines)
Lines 213-219 subtest 'TO_JSON tests' => sub { Link Here
213
            (([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))
213
            (([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))
214
        /xms;
214
        /xms;
215
    like( $updated_on, $rfc3999_regex, "Date-time $updated_on formatted correctly");
215
    like( $updated_on, $rfc3999_regex, "Date-time $updated_on formatted correctly");
216
    like( $lastseen, $rfc3999_regex, "Date-time $updated_on formatted correctly");
216
    like( $lastseen, $rfc3999_regex, "Date-time $lastseen formatted correctly");
217
217
218
    # Test JSON doesn't receive strings
218
    # Test JSON doesn't receive strings
219
    my $order = $builder->build_object({ class => 'Koha::Acquisition::Orders' });
219
    my $order = $builder->build_object({ class => 'Koha::Acquisition::Orders' });
220
- 

Return to bug 36432