File Coverage

blib/lib/Mojolicious/Static.pm
Criterion Covered Total %
statement 93 93 100.0
branch 49 50 98.0
condition 34 36 94.4
subroutine 17 17 100.0
pod 6 6 100.0
total 199 202 98.5


line stmt bran cond sub pod time code
1             package Mojolicious::Static;
2 46     46   334 use Mojo::Base -base;
  46         115  
  46         331  
3              
4 46     46   332 use Mojo::Asset::File;
  46         118  
  46         473  
5 46     46   301 use Mojo::Asset::Memory;
  46         112  
  46         365  
6 46     46   341 use Mojo::Date;
  46         132  
  46         431  
7 46     46   290 use Mojo::File 'path';
  46         126  
  46         2425  
8 46     46   316 use Mojo::Loader qw(data_section file_is_binary);
  46         124  
  46         2412  
9 46     46   318 use Mojo::Util qw(encode md5_sum trim);
  46         99  
  46         70310  
10              
11             # Bundled files
12             my $PUBLIC = path(__FILE__)->sibling('resources', 'public');
13             my %EXTRA = $PUBLIC->list_tree->map(
14             sub { join('/', @{$_->to_rel($PUBLIC)}), $_->realpath->to_string })->each;
15              
16             has classes => sub { ['main'] };
17             has extra => sub { +{%EXTRA} };
18             has paths => sub { [] };
19              
20             sub dispatch {
21 893     893 1 1942 my ($self, $c) = @_;
22              
23             # Method (GET or HEAD)
24 893         3155 my $req = $c->req;
25 893         2704 my $method = $req->method;
26 893 100 100     3665 return undef unless $method eq 'GET' || $method eq 'HEAD';
27              
28             # Canonical path
29 746         2191 my $stash = $c->stash;
30 746         2171 my $path = $req->url->path;
31 746 100       3323 $path = $stash->{path} ? $path->new($stash->{path}) : $path->clone;
32 746 100       1347 return undef unless my @parts = @{$path->canonicalize->parts};
  746         2247  
33              
34             # Serve static file and prevent path traversal
35 673         2022 my $canon_path = join '/', @parts;
36 673 100 100     5065 return undef if $canon_path =~ /^\.\.\/|\\/ || !$self->serve($c, $canon_path);
37 57         183 $stash->{'mojo.static'} = 1;
38 57         200 return !!$c->rendered;
39             }
40              
41             sub file {
42 675     675 1 1462 my ($self, $rel) = @_;
43              
44             # Search all paths
45 675         2462 my @parts = split '/', $rel;
46 675         1170 for my $path (@{$self->paths}) {
  675         2241  
47 680 100       2843 next unless my $asset = _get_file(path($path, @parts)->to_string);
48 31         167 return $asset;
49             }
50              
51             # Search DATA
52 644 100       3065 if (my $asset = $self->_get_data_file($rel)) { return $asset }
  15         67  
53              
54             # Search extra files
55 629         2755 my $extra = $self->extra;
56 629 100       7785 return exists $extra->{$rel} ? _get_file($extra->{$rel}) : undef;
57             }
58              
59             sub is_fresh {
60 78     78 1 218 my ($self, $c, $options) = @_;
61              
62 78         309 my $res_headers = $c->res->headers;
63 78         279 my ($last, $etag) = @$options{qw(last_modified etag)};
64 78 100       540 $res_headers->last_modified(Mojo::Date->new($last)->to_string) if $last;
65 78 100       572 $res_headers->etag($etag = qq{"$etag"}) if $etag;
66              
67             # Unconditional
68 78         275 my $req_headers = $c->req->headers;
69 78         269 my $match = $req_headers->if_none_match;
70 78 100 100     258 return undef unless (my $since = $req_headers->if_modified_since) || $match;
71              
72             # If-None-Match
73 13   100     43 $etag //= $res_headers->etag // '';
      100        
74 13 100 100     54 return undef if $match && !grep { trim($_) eq $etag } split ',', $match;
  13         42  
75              
76             # If-Modified-Since
77 12 100 100     66 return !!$match unless ($last //= $res_headers->last_modified) && $since;
      100        
78 6   50     20 return _epoch($last) <= (_epoch($since) // 0);
79             }
80              
81             sub serve {
82 675     675 1 2113 my ($self, $c, $rel) = @_;
83 675 100       1814 return undef unless my $asset = $self->file($rel);
84 63         252 $c->app->types->content_type($c, {file => $rel});
85 63         303 return !!$self->serve_asset($c, $asset);
86             }
87              
88             sub serve_asset {
89 67     67 1 186 my ($self, $c, $asset) = @_;
90              
91             # Content-Type
92 67 100       304 $c->app->types->content_type($c, {file => $asset->path}) if $asset->is_file;
93              
94             # Last-Modified and ETag
95 67         269 my $res = $c->res;
96 67         288 $res->code(200)->headers->accept_ranges('bytes');
97 67         264 my $mtime = $asset->mtime;
98 67         607 my $options = {etag => md5_sum($mtime), last_modified => $mtime};
99 67 100       324 return $res->code(304) if $self->is_fresh($c, $options);
100              
101             # Range
102 64 100       214 return $res->content->asset($asset)
103             unless my $range = $c->req->headers->range;
104              
105             # Not satisfiable
106 16 100       76 return $res->code(416) unless my $size = $asset->size;
107 15 50       127 return $res->code(416) unless $range =~ /^bytes=(\d+)?-(\d+)?/;
108 15 100 100     181 my ($start, $end) = ($1 // 0, defined $2 && $2 < $size ? $2 : $size - 1);
      100        
109 15 100       59 return $res->code(416) if $start > $end;
110              
111             # Satisfiable
112 12         61 $res->code(206)->headers->content_length($end - $start + 1)
113             ->content_range("bytes $start-$end/$size");
114 12         42 return $res->content->asset($asset->start_range($start)->end_range($end));
115             }
116              
117             sub warmup {
118 32     32 1 95 my $self = shift;
119 32         136 my $index = $self->{index} = {};
120 32         117 for my $class (reverse @{$self->classes}) {
  32         140  
121 44         98 $index->{$_} = $class for keys %{data_section $class};
  44         179  
122             }
123             }
124              
125 12     12   32 sub _epoch { Mojo::Date->new(shift)->epoch }
126              
127             sub _get_data_file {
128 644     644   1661 my ($self, $rel) = @_;
129              
130             # Protect files without extensions and templates with two extensions
131 644 100 100     4271 return undef if $rel !~ /\.\w+$/ || $rel =~ /\.\w+\.\w+$/;
132              
133 93 100       405 $self->warmup unless $self->{index};
134              
135             # Find file
136 93         406 my @args = ($self->{index}{$rel}, $rel);
137 93 100       393 return undef unless defined(my $data = data_section(@args));
138 15 100       72 return Mojo::Asset::Memory->new->add_chunk(
139             file_is_binary(@args) ? $data : encode 'UTF-8', $data);
140             }
141              
142             sub _get_file {
143 697     697   1430 my $path = shift;
144 46     46   441 no warnings 'newline';
  46         125  
  46         5817  
145 697 100 66     23700 return -f $path && -r _ ? Mojo::Asset::File->new(path => $path) : undef;
146             }
147              
148             1;
149              
150             =encoding utf8
151              
152             =head1 NAME
153              
154             Mojolicious::Static - Serve static files
155              
156             =head1 SYNOPSIS
157              
158             use Mojolicious::Static;
159              
160             my $static = Mojolicious::Static->new;
161             push @{$static->classes}, 'MyApp::Controller::Foo';
162             push @{$static->paths}, '/home/sri/public';
163              
164             =head1 DESCRIPTION
165              
166             L is a static file server with C,
167             C and C support, based on
168             L and
169             L.
170              
171             =head1 ATTRIBUTES
172              
173             L implements the following attributes.
174              
175             =head2 classes
176              
177             my $classes = $static->classes;
178             $static = $static->classes(['main']);
179              
180             Classes to use for finding files in C sections with L,
181             first one has the highest precedence, defaults to C
. Only files with
182             exactly one extension will be used, like C. Note that for files to
183             be detected, these classes need to have already been loaded and added before
184             L is called, which usually happens automatically during application
185             startup.
186              
187             # Add another class with static files in DATA section
188             push @{$static->classes}, 'Mojolicious::Plugin::Fun';
189              
190             # Add another class with static files in DATA section and higher precedence
191             unshift @{$static->classes}, 'Mojolicious::Plugin::MoreFun';
192              
193             =head2 extra
194              
195             my $extra = $static->extra;
196             $static = $static->extra({'foo/bar.txt' => '/home/sri/myapp/bar.txt'});
197              
198             Paths for extra files to be served from locations other than L, such
199             as the images used by the built-in exception and not found pages. Note that
200             extra files are only served if no better alternative could be found in
201             L and L.
202              
203             # Remove built-in favicon
204             delete $static->extra->{'favicon.ico'};
205              
206             =head2 paths
207              
208             my $paths = $static->paths;
209             $static = $static->paths(['/home/sri/public']);
210              
211             Directories to serve static files from, first one has the highest precedence.
212              
213             # Add another "public" directory
214             push @{$static->paths}, '/home/sri/public';
215              
216             # Add another "public" directory with higher precedence
217             unshift @{$static->paths}, '/home/sri/themes/blue/public';
218              
219             =head1 METHODS
220              
221             L inherits all methods from L and implements
222             the following new ones.
223              
224             =head2 dispatch
225              
226             my $bool = $static->dispatch(Mojolicious::Controller->new);
227              
228             Serve static file for L object.
229              
230             =head2 file
231              
232             my $asset = $static->file('images/logo.png');
233             my $asset = $static->file('../lib/MyApp.pm');
234              
235             Build L or L object for a file,
236             relative to L or from L, or return C if it doesn't
237             exist. Note that this method uses a relative path, but does not protect from
238             traversing to parent directories.
239              
240             my $content = $static->file('foo/bar.html')->slurp;
241              
242             =head2 is_fresh
243              
244             my $bool = $static->is_fresh(Mojolicious::Controller->new, {etag => 'abc'});
245              
246             Check freshness of request by comparing the C and
247             C request headers to the C and C
248             response headers.
249              
250             These options are currently available:
251              
252             =over 2
253              
254             =item etag
255              
256             etag => 'abc'
257              
258             Add C header before comparing.
259              
260             =item last_modified
261              
262             last_modified => $epoch
263              
264             Add C header before comparing.
265              
266             =back
267              
268             =head2 serve
269              
270             my $bool = $static->serve(Mojolicious::Controller->new, 'images/logo.png');
271             my $bool = $static->serve(Mojolicious::Controller->new, '../lib/MyApp.pm');
272              
273             Serve a specific file, relative to L or from L. Note that
274             this method uses a relative path, but does not protect from traversing to parent
275             directories.
276              
277             =head2 serve_asset
278              
279             $static->serve_asset(Mojolicious::Controller->new, Mojo::Asset::File->new);
280              
281             Serve a L or L object with C,
282             C and C support.
283              
284             =head2 warmup
285              
286             $static->warmup;
287              
288             Prepare static files from L for future use.
289              
290             =head1 SEE ALSO
291              
292             L, L, L.
293              
294             =cut