File Coverage

blib/lib/Mojolicious/Plugin/AppCacheManifest.pm
Criterion Covered Total %
statement 71 74 95.9
branch 25 36 69.4
condition 14 22 63.6
subroutine 7 7 100.0
pod 1 1 100.0
total 118 140 84.2


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::AppCacheManifest;
2              
3 3     3   373906 use Mojo::Base qw( Mojolicious::Plugin );
  3         9271  
  3         22  
4              
5             our $VERSION = "0.05";
6             our %HEADERS = map +( $_, undef ), qw( CACHE FALLBACK NETWORK SETTINGS );
7             our %CACHE;
8              
9             has "timeout";
10              
11             sub register
12             {
13 3     3 1 1558 my ( $self, $app, $conf ) = @_;
14 3   50     24 my $extension = $conf->{extension} // "appcache";
15 3 50       21 my $re = join( "|", map quotemeta,
16             ref( $extension )
17             ? @$extension
18             : ( $extension )
19             );
20 3         67 my $redir = qr!\A (.*?) /+ [^/]+ \. (?: $re ) \z!x;
21 3         84 my $static = $app->static();
22              
23 3   50     93 $self->timeout( $conf->{timeout} // 0 );
24              
25             $app->hook( before_dispatch => sub {
26 12     12   299432 my $c = shift;
27 12         57 my $stash = $c->stash();
28 12         550 my $tx = $c->tx();
29 12         382 my $req = $tx->req();
30 12         347 my $res = $tx->res();
31 12         408 my $url = $req->url();
32 12 50       159 my $path = $stash->{path}
33             ? Mojo::Path->new( $stash->{path} )
34             : $url->path->clone();
35 12         509 my $parts = $path->canonicalize->parts();
36              
37             # catch nonsense
38 12 50 33     3993 return if @$parts == 0 || $parts->[0] eq "..";
39              
40             # check for extensions
41 12 50       89 unless ( $path =~ $redir ) {
42             # otherwise do what the static dispatch code does
43 0 0       0 return unless $static->serve( $c, join( "/", @$parts ) );
44              
45 0         0 $stash->{"mojo.static"}++;
46 0         0 return !!$c->rendered();
47             }
48              
49             # setup and prepare
50 12         3307 my $code = 200;
51 12         184 my $asset = $static->file( join( "/", @$parts ) );
52 12         2372 my ( $output, $last_modified ) = $self->_process(
53             $asset->slurp(),
54             $asset->path(),
55             $static->paths(),
56             );
57              
58             # save bandwith if possible
59 12 100       94 if ( my $date = $req->headers->if_modified_since() ) {
60 6 100       472 $code = 304
61             if $last_modified eq Mojo::Date->new( $date );
62             }
63              
64             # send response with conditional body
65 12         2424 $res->code( $code );
66 12         125 $res->headers->content_type( "text/cache-manifest" );
67 12         5154 $res->headers->last_modified( $last_modified );
68 12 100       2001 $res->body( $output )
69             if $code == 200;
70              
71 12         1361 $stash->{"mojo.static"}++;
72 12         80 return !!$c->rendered();
73 3         58 } );
74              
75 3         135 return $self;
76             }
77              
78             sub _process
79             {
80 12     12   17113 my ( $self, $body, $path, $dirs ) = @_;
81 12         755 my $timeout = $self->timeout();
82 12         442 my $mtime = ( stat( $path ) )[9];
83 12         168 my $time = time();
84              
85             # use cache when file modification and timeout are fine
86 12 100 100     184 return @{ $CACHE{ $path } }[2,3] if
  2   100     13  
87             exists( $CACHE{ $path } ) &&
88             $CACHE{ $path }[0] >= $mtime &&
89             $CACHE{ $path }[1] + $timeout > $time;
90              
91             # extract structure, find highest last modification and generate new output
92 10         260 my $manifest = $self->_parse( $body );
93 10         49 my $date = $self->_find_last_modified( $manifest, $mtime, $dirs );
94 10         1150 my $output = $self->_generate( $manifest, $date );
95              
96             # put into cache when a timeout is given
97 10 100       143 $CACHE{ $path } = [ $date->epoch(), $time, $output, $date ]
98             if $timeout > 0;
99              
100 10         223 return ( $output, $date );
101             }
102              
103             sub _parse
104             {
105 10     10   26 my ( $self, $body ) = @_;
106              
107             return unless # found and removed the header
108 10 50       102 $body =~ s!\A CACHE [ ] MANIFEST [ \t\r\n] \s* !!sx;
109              
110             # split sections by header; prepend header for initial section
111 10         473 my @body = ( "CACHE", split( m/
112             ^ \s* (\S+) \s* : \s* \r?\n \s*
113             /mx, $body ) );
114              
115 10         19 my %seen;
116             my %manifest;
117              
118 10   66     94 while ( @body and my $header = uc( shift( @body ) ) ) {
119 40         68 my $part = shift( @body );
120              
121             # skip unknown
122 40 50       214 next unless exists( $HEADERS{ $header } );
123              
124             # separate lines without comments
125 40         469 my @lines = grep( +(
126             ! m!\A [#] !x
127             ), split( m! \s* \r?\n \s* !x, $part ) );
128              
129             # unique elements in order; fallback section has pairs
130 40 50 33     58 push( @{ $manifest{ $header } }, map +( $header eq "FALLBACK"
  40 50       704  
    100          
131             ? m!\A (\S+) \s+ (\S+) !x && ! $seen{ $header, $1, $2 }++
132             ? [ $1, $2 ] : ( )
133             : ! $seen{ $header, $_ }++
134             ? ( $_ ) : ( )
135             ), @lines );
136             }
137              
138 10         261 return \%manifest;
139             }
140              
141             sub _find_last_modified
142             {
143 10     10   23 my ( $self, $manifest, $maxmtime, $dirs ) = @_;
144 10         92 my @parts = map Mojo::URL->new( $_ )->path->canonicalize->parts(),
145 10         16 @{ $manifest->{CACHE} };
146              
147             # check all paths but prevent path traversal attempts
148 10         30687 for my $path ( map join( "/", @$_ ), grep $_->[0] ne "..", @parts ) {
149 61         90 my $mtime = 0;
150              
151             # try the path in each directory
152             stat( "$_/$path" ) and
153             # keep the modification time
154             $mtime = ( stat( _ ) )[9],
155             # stop on the first hit
156             last
157 61   66     1541 for @$dirs;
158              
159 61 100       492 $maxmtime = $mtime if $mtime > $maxmtime;
160             }
161              
162 10         136 return Mojo::Date->new( $maxmtime );
163             }
164              
165             sub _generate
166             {
167 10     10   55 my ( $self, $manifest, $date ) = @_;
168 10         137 my @output = (
169             "CACHE MANIFEST",
170             "# $date",
171             );
172              
173             # put cache section explicitely first
174 10 50       615 push( @output, @{ $manifest->{CACHE} } )
  10         182  
175             if $manifest->{CACHE};
176              
177             # followed by fallback in pairs
178 10 100       39 push( @output, "FALLBACK:", map "@$_", @{ $manifest->{FALLBACK} } )
  7         47  
179             if $manifest->{FALLBACK};
180              
181             # finally settings and network in that order
182 15         54 push( @output, "$_:", @{ $manifest->{ $_ } } )
183 10         53 for grep $manifest->{ $_ }, qw( SETTINGS NETWORK );
184              
185 10         63 return join( "\n", @output, "" ); # trailing newline
186             }
187              
188             1;
189              
190             __END__