File Coverage

blib/lib/Mojolicious/Static.pm
Criterion Covered Total %
statement 121 121 100.0
branch 72 74 97.3
condition 40 47 85.1
subroutine 19 19 100.0
pod 8 8 100.0
total 260 269 96.6


line stmt bran cond sub pod time code
1             package Mojolicious::Static;
2 54     54   316 use Mojo::Base -base;
  54         92  
  54         370  
3              
4 54     54   281 use Mojo::Asset::File;
  54         109  
  54         409  
5 54     54   217 use Mojo::Asset::Memory;
  54         100  
  54         319  
6 54     54   200 use Mojo::Date;
  54         102  
  54         314  
7 54     54   201 use Mojo::File qw(curfile path);
  54         81  
  54         2980  
8 54     54   270 use Mojo::Loader qw(data_section file_is_binary);
  54         82  
  54         2540  
9 54     54   216 use Mojo::Util qw(encode md5_sum trim);
  54         111  
  54         111148  
10              
11             # Bundled files
12             my $PUBLIC = curfile->sibling('resources', 'public');
13             my %EXTRA = $PUBLIC->list_tree->map(sub { join('/', @{$_->to_rel($PUBLIC)}), $_->realpath->to_string })->each;
14              
15             has asset_dir => 'assets';
16             has classes => sub { ['main'] };
17             has extra => sub { +{%EXTRA} };
18             has paths => sub { [] };
19             has 'prefix';
20              
21             sub asset_path {
22 20     20 1 32 my ($self, $asset) = @_;
23 20 100       49 $asset = "/$asset" unless $asset =~ /^\//;
24 20   50     44 my $assets = $self->{assets} //= {};
25 20   66     43 return $self->file_path('/' . $self->asset_dir . ($assets->{$asset} // $asset));
26             }
27              
28             sub dispatch {
29 1097     1097 1 1898 my ($self, $c) = @_;
30              
31             # Method (GET or HEAD)
32 1097         2995 my $req = $c->req;
33 1097         2731 my $method = $req->method;
34 1097 100 100     3780 return undef unless $method eq 'GET' || $method eq 'HEAD';
35              
36             # Canonical path
37 938         1814 my $stash = $c->stash;
38 938         2200 my $path = $req->url->path;
39 938 100       3390 $path = $stash->{path} ? $path->new($stash->{path}) : $path->clone;
40 938 100       1325 return undef unless my @parts = @{$path->canonicalize->parts};
  938         2298  
41              
42             # Serve static file and prevent path traversal
43 855         2226 my $canon_path = join '/', @parts;
44 855 100 100     6450 return undef if $canon_path =~ /^\.\.\/|\\/ || !$self->serve($c, $canon_path);
45 71         171 $stash->{'mojo.static'} = 1;
46              
47             # Development assets will be rebuilt a lot, do not let browsers cache them
48 71 100 100     162 $c->res->headers->cache_control('no-cache')
49             if $c->app->mode eq 'development' && index($canon_path, $self->asset_dir) == 0;
50              
51 71         218 return !!$c->rendered;
52             }
53              
54             sub file {
55 852     852 1 1617 my ($self, $rel) = @_;
56              
57 852 100       2215 $self->warmup unless $self->{index};
58              
59             # Search all paths
60 852         2729 my @parts = split /\//, $rel;
61 852         1210 for my $path (@{$self->paths}) {
  852         2308  
62 857 100       3715 next unless my $asset = _get_file(path($path, @parts)->to_string);
63 44         209 return $asset;
64             }
65              
66             # Search DATA
67 808 100       2915 if (my $asset = $self->_get_data_file($rel)) { return $asset }
  18         62  
68              
69             # Search extra files
70 790         2371 my $extra = $self->extra;
71 790 100       8453 return exists $extra->{$rel} ? _get_file($extra->{$rel}) : undef;
72             }
73              
74             sub file_path {
75 402     402 1 575 my ($self, $file) = @_;
76 402 100       905 $file = "/$file" unless $file =~ /^\//;
77 402 100       866 return $file unless my $prefix = $self->prefix;
78 54         222 return "$prefix$file";
79             }
80              
81             sub is_fresh {
82 101     101 1 190 my ($self, $c, $options) = @_;
83              
84 101         316 my $res_headers = $c->res->headers;
85 101         300 my ($last, $etag) = @$options{qw(last_modified etag)};
86 101 100       582 $res_headers->last_modified(Mojo::Date->new($last)->to_string) if $last;
87 101 100       675 $res_headers->etag($etag = ($etag =~ m!^W/"! ? $etag : qq{"$etag"})) if $etag;
    100          
88              
89             # Unconditional
90 101         276 my $req_headers = $c->req->headers;
91 101         619 my $match = $req_headers->if_none_match;
92 101 100 100     270 return undef unless (my $since = $req_headers->if_modified_since) || $match;
93              
94             # If-None-Match
95 21   100     51 $etag //= $res_headers->etag // '';
      66        
96 21 100 100     74 return undef if $match && !grep { $_ eq $etag || "W/$_" eq $etag } map { trim($_) } split /,/, $match;
  23 100       93  
  23         53  
97              
98             # If-Modified-Since
99 17 100 100     60 return !!$match unless ($last //= $res_headers->last_modified) && $since;
      100        
100 6   50     17 return _epoch($last) <= (_epoch($since) // 0);
101             }
102              
103             sub serve {
104 854     854 1 1874 my ($self, $c, $rel) = @_;
105              
106             # Prefix
107 854 100       2810 if (my $prefix = $self->prefix) {
108 11         21 $rel = "/$rel";
109 11 100       157 return undef unless $rel =~ s/^\Q$prefix\E\///;
110             }
111              
112 849 100       2315 return undef unless my $asset = $self->file($rel);
113 77         252 $c->app->types->content_type($c, {file => $rel});
114 77         326 return !!$self->serve_asset($c, $asset);
115             }
116              
117             sub serve_asset {
118 81     81 1 175 my ($self, $c, $asset) = @_;
119              
120             # Content-Type
121 81 100       337 $c->app->types->content_type($c, {file => $asset->path}) if $asset->is_file;
122              
123             # Last-Modified and ETag
124 81         217 my $res = $c->res;
125 81         252 $res->code(200)->headers->accept_ranges('bytes');
126 81         246 my $mtime = $asset->mtime;
127 81         513 my $options = {etag => md5_sum($mtime), last_modified => $mtime};
128 81 100       263 return $res->code(304) if $self->is_fresh($c, $options);
129              
130             # Range
131 78 100       191 return $res->content->asset($asset) unless my $range = $c->req->headers->range;
132              
133             # Not satisfiable
134 17 100       78 return $res->code(416) unless my $size = $asset->size;
135 16 50       95 return $res->code(416) unless $range =~ /^bytes=(\d+)?-(\d+)?/;
136 16 100 50     140 my ($start, $end) = ($1 // 0, defined $2 && $2 < $size ? $2 : $size - 1);
      100        
137 16 100       43 return $res->code(416) if $start > $end;
138              
139             # Satisfiable
140 13         60 $res->code(206)->headers->content_length($end - $start + 1)->content_range("bytes $start-$end/$size");
141 13         30 return $res->content->asset($asset->start_range($start)->end_range($end));
142             }
143              
144             sub warmup {
145 72     72 1 117 my $self = shift;
146              
147             # DATA sections
148 72         250 my $index = $self->{index} = {};
149 72         126 for my $class (reverse @{$self->classes}) { $index->{$_} = $class for keys %{data_section $class} }
  72         264  
  97         174  
  97         353  
150              
151             # Static assets
152 72         241 my $assets = $self->{assets} = {};
153 72         276 my $asset_dir = $self->asset_dir;
154 72         144 for my $path (@{$self->paths}) {
  72         215  
155 84         361 my $asset_path = path($path, $asset_dir);
156 84 100       870 next unless -d $asset_path;
157              
158 44         309 for my $asset_file ($asset_path->list_tree({recursive => 1})->each) {
159 308         594 my $parts = $asset_file->to_rel($asset_path)->to_array;
160 308         1248 my $filename = pop @$parts;
161 308         493 my $prefix = join '/', @$parts;
162              
163 308 100       1195 next unless $filename =~ /^([^.]+)\.([^.]+)\.(.+)$/;
164 264         434 my $checksum = $2;
165 264 100       813 my $short = $prefix eq '' ? "/$1.$3" : "/$prefix/$1.$3";
166 264         468 my $long = '/' . join('/', @$parts, $filename);
167              
168 264 50 66     1402 $assets->{$short} = $long if !exists($assets->{$short}) || $checksum eq 'development';
169             }
170             }
171             }
172              
173 12     12   26 sub _epoch { Mojo::Date->new(shift)->epoch }
174              
175             sub _get_data_file {
176 808     808   1518 my ($self, $rel) = @_;
177              
178             # Protect files without extensions and templates with two extensions
179 808 100 100     4673 return undef if $rel !~ /\.\w+$/ || $rel =~ /\.\w+\.\w+$/;
180              
181             # Find file
182 96         397 my @args = ($self->{index}{$rel}, $rel);
183 96 100       395 return undef unless defined(my $data = data_section(@args));
184 18 100       77 return Mojo::Asset::Memory->new->add_chunk(file_is_binary(@args) ? $data : encode 'UTF-8', $data);
185             }
186              
187             sub _get_file {
188 873     873   1495 my $path = shift;
189 54     54   414 no warnings 'newline';
  54         81  
  54         6788  
190 873 100 66     37326 return -f $path && -r _ ? Mojo::Asset::File->new(path => $path) : undef;
191             }
192              
193             1;
194              
195             =encoding utf8
196              
197             =head1 NAME
198              
199             Mojolicious::Static - Serve static files
200              
201             =head1 SYNOPSIS
202              
203             use Mojolicious::Static;
204              
205             my $static = Mojolicious::Static->new;
206             push @{$static->classes}, 'MyApp::Controller::Foo';
207             push @{$static->paths}, '/home/sri/public';
208              
209             =head1 DESCRIPTION
210              
211             L is a static file server with C, C and C support, based
212             on L and L.
213              
214             =head1 ATTRIBUTES
215              
216             L implements the following attributes.
217              
218             =head2 asset_dir
219              
220             my $dir = $static->asset_dir;
221             $static = $static->asset_dir('assets');
222              
223             Subdirectory used for all static assets, defaults to C.
224              
225             =head2 classes
226              
227             my $classes = $static->classes;
228             $static = $static->classes(['main']);
229              
230             Classes to use for finding files in C sections with L, first one has the highest precedence,
231             defaults to C
. Only files with exactly one extension will be used, like C. Note that for files to be
232             detected, these classes need to have already been loaded and added before L is called, which usually happens
233             automatically during application startup.
234              
235             # Add another class with static files in DATA section
236             push @{$static->classes}, 'Mojolicious::Plugin::Fun';
237              
238             # Add another class with static files in DATA section and higher precedence
239             unshift @{$static->classes}, 'Mojolicious::Plugin::MoreFun';
240              
241             =head2 extra
242              
243             my $extra = $static->extra;
244             $static = $static->extra({'foo/bar.txt' => '/home/sri/myapp/bar.txt'});
245              
246             Paths for extra files to be served from locations other than L, such as the images used by the built-in
247             exception and not found pages. Note that extra files are only served if no better alternative could be found in
248             L and L.
249              
250             # Remove built-in favicon
251             delete $static->extra->{'favicon.ico'};
252              
253             =head2 paths
254              
255             my $paths = $static->paths;
256             $static = $static->paths(['/home/sri/public']);
257              
258             Directories to serve static files from, first one has the highest precedence.
259              
260             # Add another "public" directory
261             push @{$static->paths}, '/home/sri/public';
262              
263             # Add another "public" directory with higher precedence
264             unshift @{$static->paths}, '/home/sri/themes/blue/public';
265              
266             =head2 prefix
267              
268             my $prefix = $static->prefix;
269             $static = $static->prefix('/static');
270              
271             Prefix to use for all static files, defaults to C. This can be very useful for production deployments where the
272             reverse proxy server should take over serving static files.
273              
274             =head1 METHODS
275              
276             L inherits all methods from L and implements the following new ones.
277              
278             =head2 asset_path
279              
280             my $path = $static->asset_path('/app.js');
281              
282             Get static asset path.
283              
284             =head2 dispatch
285              
286             my $bool = $static->dispatch(Mojolicious::Controller->new);
287              
288             Serve static file for L object.
289              
290             =head2 file
291              
292             my $asset = $static->file('images/logo.png');
293             my $asset = $static->file('../lib/MyApp.pm');
294              
295             Build L or L object for a file, relative to L or from L,
296             or return C if it doesn't exist. Note that this method uses a relative path, but does not protect from
297             traversing to parent directories.
298              
299             my $content = $static->file('foo/bar.html')->slurp;
300              
301             =head2 file_path
302              
303             my $path = $static->file_path('/index.html');
304              
305             Get static file path with L if it has been configured.
306              
307             =head2 is_fresh
308              
309             my $bool = $static->is_fresh(Mojolicious::Controller->new, {etag => 'abc'});
310             my $bool = $static->is_fresh(
311             Mojolicious::Controller->new, {etag => 'W/"def"'});
312              
313             Check freshness of request by comparing the C and C request headers to the C
314             and C response headers.
315              
316             These options are currently available:
317              
318             =over 2
319              
320             =item etag
321              
322             etag => 'abc'
323             etag => 'W/"abc"'
324              
325             Add C header before comparing.
326              
327             =item last_modified
328              
329             last_modified => $epoch
330              
331             Add C header before comparing.
332              
333             =back
334              
335             =head2 serve
336              
337             my $bool = $static->serve(Mojolicious::Controller->new, 'images/logo.png');
338             my $bool = $static->serve(Mojolicious::Controller->new, '../lib/MyApp.pm');
339              
340             Serve a specific file, relative to L or from L. Note that this method uses a relative path, but
341             does not protect from traversing to parent directories.
342              
343             =head2 serve_asset
344              
345             $static->serve_asset(Mojolicious::Controller->new, Mojo::Asset::File->new);
346              
347             Serve a L or L object with C, C and C
348             support.
349              
350             =head2 warmup
351              
352             $static->warmup();
353              
354             Prepare static files from L and static assets for future use.
355              
356             =head1 SEE ALSO
357              
358             L, L, L.
359              
360             =cut