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

(-)a/Koha/DateTime/Format/RFC3339.pm (+94 lines)
Line 0 Link Here
1
package Koha::DateTime::Format::RFC3339;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
=head1 NAME
19
20
Koha::DateTime::Format::RFC3339 - Parse and format RFC3339 dates
21
22
=head1 SYNOPSIS
23
24
    $datetime = Koha::DateTime::Format::RFC3339->parse_datetime($rfc3339_datetime_string);
25
    $rfc3339_datetime_string = Koha::DateTime::Format::RFC3339->format_datetime($datetime);
26
27
=head1 API
28
29
=head2 Class methods
30
31
=head3 parse_datetime
32
33
Parse an RFC3339 datetime string and returns a corresponding L<DateTime> object
34
35
    $datetime = Koha::DateTime::Format::RFC3339->parse_datetime($rfc3339_datetime_string);
36
37
=cut
38
39
use Modern::Perl;
40
41
use DateTime::Format::Builder (
42
    parsers => {
43
        parse_datetime => [
44
            {
45
                params => [qw( year month day hour minute second time_zone )],
46
                regex  =>
47
                    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]))$/,
48
                postprocess => \&_postprocess_datetime,
49
            },
50
        ],
51
    }
52
);
53
54
=head3 format_datetime
55
56
Format a L<DateTime> object into an RFC3339 datetime string
57
58
    $rfc3339_datetime_string = Koha::DateTime::Format::RFC3339->format_datetime($datetime);
59
60
=cut
61
62
sub format_datetime {
63
    my ( $class, $dt ) = @_;
64
65
    my $date = $dt->strftime('%FT%T%z');
66
    substr( $date, -2, 0, ':' );    # timezone "HHmm" => "HH:mm"
67
68
    return $date;
69
}
70
71
=head2 Internal methods
72
73
=head3 _postprocess_datetime
74
75
Called by C<parse_datetime> after parsing the datetime string.
76
77
It allows to change C<DateTime::new> parameters just before C<parse_datetime>
78
calls it.
79
80
=cut
81
82
sub _postprocess_datetime {
83
    my %args   = @_;
84
    my $parsed = $args{parsed};
85
86
    # system allows the 0th of the month
87
    $parsed->{day} = '01' if $parsed->{day} eq '00';
88
89
    $parsed->{time_zone} = 'UTC' if $parsed->{time_zone} =~ /^[Zz]$/;
90
91
    return 1;
92
}
93
94
1;
(-)a/Koha/DateTime/Format/SQL.pm (+66 lines)
Line 0 Link Here
1
package Koha::DateTime::Format::SQL;
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
=head1 NAME
19
20
Koha::DateTime::Format::SQL - Parse SQL dates
21
22
=head1 SYNOPSIS
23
24
    $datetime = Koha::DateTime::Format::SQL->parse_datetime($sql_datetime_string);
25
26
=cut
27
28
use Modern::Perl;
29
30
use DateTime::Format::MySQL;
31
32
use Koha::Config;
33
34
our $timezone;
35
36
=head1 API
37
38
=head2 Class methods
39
40
=head3 parse_datetime
41
42
Parse an SQL datetime string and returns a corresponding L<DateTime> object
43
44
    $datetime = Koha::DateTime::Format::SQL->parse_datetime($rfc3339_datetime_string);
45
46
DateTime's time zone is automatically set to the configured timezone (or
47
'local' if none is configured), unless the year is 9999 in which case the
48
timezone is 'floating'.
49
50
=cut
51
52
sub parse_datetime {
53
    my ( $class, $date ) = @_;
54
55
    my $dt = DateTime::Format::MySQL->parse_datetime($date);
56
57
    # No TZ for dates 'infinite' => see bug 13242
58
    if ( $dt->year < 9999 ) {
59
        $timezone //= Koha::Config->get_instance->timezone;
60
        $dt->set_time_zone($timezone);
61
    }
62
63
    return $dt;
64
}
65
66
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 (+61 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
    }
58
    qr/Invalid date format/;
59
};
60
61
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