File Coverage

blib/lib/Plack/Middleware/Memento.pm
Criterion Covered Total %
statement 24 120 20.0
branch 0 32 0.0
condition 0 17 0.0
subroutine 8 22 36.3
pod 1 3 33.3
total 33 194 17.0


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