File Coverage

blib/lib/Plack/Middleware/Memento.pm
Criterion Covered Total %
statement 24 119 20.1
branch 0 30 0.0
condition 0 17 0.0
subroutine 8 22 36.3
pod 1 3 33.3
total 33 191 17.2


line stmt bran cond sub pod time code
1             package Plack::Middleware::Memento;
2              
3 1     1   49385 use strict;
  1         2  
  1         23  
4 1     1   4 use warnings;
  1         2  
  1         31  
5              
6             our $VERSION = '0.0101';
7              
8 1     1   426 use Plack::Request;
  1         60387  
  1         28  
9 1     1   393 use Plack::Util;
  1         2029  
  1         28  
10 1     1   716 use DateTime;
  1         413693  
  1         39  
11 1     1   436 use DateTime::Format::HTTP;
  1         3881  
  1         27  
12 1     1   6 use parent 'Plack::Middleware';
  1         2  
  1         12  
13 1     1   1962 use namespace::clean;
  1         2  
  1         9  
14              
15             sub timegate_path {
16 0   0 0 0   $_[0]->{timegate_path} ||= '/timegate';
17             }
18              
19             sub timemap_path {
20 0   0 0 0   $_[0]->{timemap_path} ||= '/timemap';
21             }
22              
23             sub _handler_options {
24 0     0     my ($self) = @_;
25 0   0       $self->{_handler_options} ||= do {
26 0           my $options = {};
27 0           for my $key (keys %$self) {
28             next
29 0 0         if $key
30             =~ /(?:^_)|(?:^(?:handler|timegate_path|timemap_path)$)/;
31 0           $options->{$key} = $self->{$key};
32             }
33 0           $options;
34             };
35             }
36              
37             sub _handler {
38 0     0     my ($self) = @_;
39 0   0       $self->{_handler} ||= do {
40             my $class = Plack::Util::load_class($self->{handler},
41 0           'Plack::Middleware::Memento::Handler');
42 0           $class->new($self->_handler_options);
43             };
44             }
45              
46             sub call {
47 0     0 1   my ($self, $env) = @_;
48 0 0 0       $self->_handle_timegate_request($env)
49             || $self->_handle_timemap_request($env)
50             || $self->_wrap_request($env);
51             }
52              
53             sub _wrap_request {
54 0     0     my ($self, $env) = @_;
55 0           my $res = $self->app->($env);
56 0           my $req = Plack::Request->new($env);
57 0 0         if (my ($uri_r, $dt) = $self->_handler->wrap_memento_request($req)) {
58 0           my @links = (
59             $self->_original_link($uri_r),
60             $self->_timegate_link($req->base, $uri_r),
61             $self->_timemap_link($req->base, $uri_r, 'timemap'),
62             );
63 0           Plack::Util::header_set($res->[1], 'Memento-Datetime',
64             DateTime::Format::HTTP->format_datetime($dt));
65 0           Plack::Util::header_push($res->[1], 'Link', join(",", @links));
66             }
67 0 0         if ($self->_handler->wrap_original_resource_request($req)) {
68 0           Plack::Util::header_push($res->[1], 'Link',
69             $self->_timegate_link($req->base, $req->uri->as_string));
70             }
71 0           $res;
72             }
73              
74             sub _handle_timegate_request {
75 0     0     my ($self, $env) = @_;
76              
77 0           my $prefix = $self->timegate_path;
78 0           my $uri_r = $env->{PATH_INFO};
79 0 0         $uri_r =~ s|^${prefix}/|| or return;
80              
81 0           my $req = Plack::Request->new($env);
82              
83 0   0       my $mementos = $self->_handler->get_all_mementos($uri_r, $req)
84             || return $self->_not_found;
85              
86 0           $mementos = [sort {DateTime->compare($a->[1], $b->[1])} @$mementos];
  0            
87              
88 0           my $closest_mem;
89              
90 0 0         if (defined(my $date = $req->header('Accept-Datetime'))) {
91 0 0         my $dt = eval {DateTime::Format::HTTP->parse_datetime($date)}
  0            
92             or return $self->_bad_request;
93              
94 0           my ($closest) = sort {$a->[1] <=> $b->[1]} map {
95 0           my $diff = abs($_->[1]->epoch - $dt->epoch);
  0            
96 0           [$_, $diff];
97             } @$mementos;
98              
99 0           $closest_mem = $closest->[0];
100             }
101             else {
102 0           $closest_mem = $mementos->[-1];
103             }
104              
105 0           my @links = (
106             $self->_original_link($uri_r),
107             $self->_timemap_link($req->base, $uri_r, 'timemap', $mementos),
108             );
109              
110 0 0         if (@$mementos == 1) {
    0          
    0          
111 0           push @links, $self->_memento_link($closest_mem, 'first last memento');
112             }
113             elsif ($closest_mem->[0] eq $mementos->[0]->[0]) {
114 0           push @links, $self->_memento_link($closest_mem, 'first memento');
115 0           push @links, $self->_memento_link($mementos->[-1], 'last memento');
116             }
117             elsif ($closest_mem->[0] eq $mementos->[-1]->[0]) {
118 0           push @links, $self->_memento_link($mementos->[0], 'first memento');
119 0           push @links, $self->_memento_link($closest_mem, 'last memento');
120             }
121             else {
122 0           push @links, $self->_memento_link($mementos->[0], 'first memento');
123 0           push @links, $self->_memento_link($closest_mem, 'memento');
124 0           push @links, $self->_memento_link($mementos->[-1], 'last memento');
125             }
126              
127             [
128 0           302,
129             [
130             'Vary' => 'accept-datetime',
131             'Location' => $closest_mem->[0],
132             'Content-Type' => 'text/plain; charset=UTF-8',
133             'Link' => join(",", @links),
134             ],
135             [],
136             ];
137             }
138              
139             sub _handle_timemap_request {
140 0     0     my ($self, $env) = @_;
141              
142 0           my $prefix = $self->timemap_path;
143 0           my $uri_r = $env->{PATH_INFO};
144 0 0         $uri_r =~ s|^${prefix}/|| or return;
145              
146 0           my $req = Plack::Request->new($env);
147              
148 0   0       my $mementos = $self->_handler->get_all_mementos($uri_r, $req)
149             || return $self->_not_found;
150              
151 0           $mementos = [sort {DateTime->compare($a->[1], $b->[1])} @$mementos];
  0            
152              
153 0           my @links = (
154             $self->_original_link($uri_r),
155             $self->_timemap_link($req->base, $uri_r, 'self', $mementos),
156             $self->_timegate_link($req->base, $uri_r),
157             );
158              
159 0 0         if (@$mementos == 1) {
160 0           push @links,
161             $self->_memento_link($mementos->[0], 'first last memento');
162             }
163             else {
164 0 0         if (my $first_mem = shift @$mementos) {
165 0           push @links, $self->_memento_link($first_mem, 'first memento');
166             }
167 0 0         if (my $last_mem = pop @$mementos) {
168 0           push @links, $self->_memento_link($last_mem, 'last memento');
169             }
170 0           push @links, map {$self->_memento_link($_, 'memento')} @$mementos;
  0            
171             }
172              
173             [
174 0           200,
175             ['Content-Type' => 'application/link-format',],
176             [join(",\n", @links),],
177             ];
178             }
179              
180             sub _not_found {
181 0     0     my ($self) = @_;
182 0           [404, ['Content-Type' => 'text/plain; charset=UTF-8'], []];
183             }
184              
185             sub _bad_request {
186 0     0     my ($self) = @_;
187 0           [400, ['Content-Type' => 'text/plain; charset=UTF-8'], []];
188             }
189              
190             sub _original_link {
191 0     0     my ($self, $uri_r) = @_;
192 0           qq|<$uri_r>; rel="original"|;
193             }
194              
195             sub _timemap_link {
196 0     0     my ($self, $base_url, $uri_r, $rel, $mementos) = @_;
197 0           $base_url->path(join('/', $self->timemap_path, $uri_r));
198 0           my $uri_t = $base_url->canonical->as_string;
199 0           my $link = qq|<$uri_t>; rel="$rel"; type="application/link-format"|;
200 0 0         if ($mementos) {
201 0           my $from
202             = DateTime::Format::HTTP->format_datetime($mementos->[0]->[1]);
203 0           my $until
204             = DateTime::Format::HTTP->format_datetime($mementos->[-1]->[1]);
205 0           $link .= qq|; from="$from"; until="$until"|;
206             }
207 0           $link;
208             }
209              
210             sub _timegate_link {
211 0     0     my ($self, $base_url, $uri_r) = @_;
212 0           $base_url->path(join('/', $self->timegate_path, $uri_r));
213 0           my $uri_g = $base_url->canonical->as_string;
214 0           qq|<$uri_g>; rel="timegate"|;
215             }
216              
217             sub _memento_link {
218 0     0     my ($self, $mem, $rel) = @_;
219 0           my $uri_m = $mem->[0];
220 0           my $datetime = DateTime::Format::HTTP->format_datetime($mem->[1]);
221 0           qq|<$uri_m>; rel="$rel"; datetime="$datetime"|;
222             }
223              
224             1;
225              
226             __END__
227              
228             =encoding utf-8
229              
230             =head1 NAME
231              
232             Plack::Middleware::Memento - Enable the Memento protocol
233              
234             =head1 SYNOPSIS
235              
236             use Plack::Builder;
237             use Plack::App::Catmandu::Bag;
238              
239             builder {
240             enable 'Memento', handler => 'Catmandu::Bag', store => 'authority', bag => 'person';
241             Plack::App::Catmandu::Bag->new(
242             store => 'authority',
243             bag => 'person',
244             )->to_app;
245             };
246              
247             =head1 DESCRIPTION
248              
249             This is an early minimal release, documentation and tests are lacking.
250              
251             =head1 AUTHOR
252              
253             Nicolas Steenlant E<lt>nicolas.steenlant@ugent.beE<gt>
254              
255             =head1 COPYRIGHT
256              
257             Copyright 2017- Nicolas Steenlant
258              
259             =head1 LICENSE
260              
261             This library is free software; you can redistribute it and/or modify
262             it under the same terms as Perl itself.
263              
264             =head1 SEE ALSO
265              
266             =cut