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   566 use Mojo::Base -base;
  54         134  
  54         494  
3              
4 54     54   480 use Mojo::Asset::File;
  54         143  
  54         574  
5 54     54   333 use Mojo::Asset::Memory;
  54         125  
  54         466  
6 54     54   424 use Mojo::Date;
  54         117  
  54         444  
7 54     54   375 use Mojo::File qw(curfile path);
  54         140  
  54         4592  
8 54     54   392 use Mojo::Loader qw(data_section file_is_binary);
  54         166  
  54         4037  
9 54     54   373 use Mojo::Util qw(encode md5_sum trim);
  54         179  
  54         180719  
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 50 my ($self, $asset) = @_;
23 20 100       76 $asset = "/$asset" unless $asset =~ /^\//;
24 20   50     68 my $assets = $self->{assets} //= {};
25 20   66     71 return $self->file_path('/' . $self->asset_dir . ($assets->{$asset} // $asset));
26             }
27              
28             sub dispatch {
29 1093     1093 1 2829 my ($self, $c) = @_;
30              
31             # Method (GET or HEAD)
32 1093         5051 my $req = $c->req;
33 1093         3919 my $method = $req->method;
34 1093 100 100     5840 return undef unless $method eq 'GET' || $method eq 'HEAD';
35              
36             # Canonical path
37 938         3067 my $stash = $c->stash;
38 938         3339 my $path = $req->url->path;
39 938 100       5642 $path = $stash->{path} ? $path->new($stash->{path}) : $path->clone;
40 938 100       1898 return undef unless my @parts = @{$path->canonicalize->parts};
  938         3476  
41              
42             # Serve static file and prevent path traversal
43 855         3125 my $canon_path = join '/', @parts;
44 855 100 100     9769 return undef if $canon_path =~ /^\.\.\/|\\/ || !$self->serve($c, $canon_path);
45 71         303 $stash->{'mojo.static'} = 1;
46              
47             # Development assets will be rebuilt a lot, do not let browsers cache them
48 71 100 100     333 $c->res->headers->cache_control('no-cache')
49             if $c->app->mode eq 'development' && index($canon_path, $self->asset_dir) == 0;
50              
51 71         445 return !!$c->rendered;
52             }
53              
54             sub file {
55 852     852 1 2347 my ($self, $rel) = @_;
56              
57 852 100       3593 $self->warmup unless $self->{index};
58              
59             # Search all paths
60 852         4314 my @parts = split /\//, $rel;
61 852         1712 for my $path (@{$self->paths}) {
  852         3659  
62 857 100       5723 next unless my $asset = _get_file(path($path, @parts)->to_string);
63 44         306 return $asset;
64             }
65              
66             # Search DATA
67 808 100       4165 if (my $asset = $self->_get_data_file($rel)) { return $asset }
  18         90  
68              
69             # Search extra files
70 790         3528 my $extra = $self->extra;
71 790 100       12062 return exists $extra->{$rel} ? _get_file($extra->{$rel}) : undef;
72             }
73              
74             sub file_path {
75 402     402 1 934 my ($self, $file) = @_;
76 402 100       1492 $file = "/$file" unless $file =~ /^\//;
77 402 100       1305 return $file unless my $prefix = $self->prefix;
78 54         321 return "$prefix$file";
79             }
80              
81             sub is_fresh {
82 101     101 1 338 my ($self, $c, $options) = @_;
83              
84 101         451 my $res_headers = $c->res->headers;
85 101         503 my ($last, $etag) = @$options{qw(last_modified etag)};
86 101 100       992 $res_headers->last_modified(Mojo::Date->new($last)->to_string) if $last;
87 101 100       1174 $res_headers->etag($etag = ($etag =~ m!^W/"! ? $etag : qq{"$etag"})) if $etag;
    100          
88              
89             # Unconditional
90 101         432 my $req_headers = $c->req->headers;
91 101         474 my $match = $req_headers->if_none_match;
92 101 100 100     441 return undef unless (my $since = $req_headers->if_modified_since) || $match;
93              
94             # If-None-Match
95 21   100     85 $etag //= $res_headers->etag // '';
      66        
96 21 100 100     119 return undef if $match && !grep { $_ eq $etag || "W/$_" eq $etag } map { trim($_) } split /,/, $match;
  23 100       201  
  23         102  
97              
98             # If-Modified-Since
99 17 100 100     106 return !!$match unless ($last //= $res_headers->last_modified) && $since;
      100        
100 6   50     31 return _epoch($last) <= (_epoch($since) // 0);
101             }
102              
103             sub serve {
104 854     854 1 2624 my ($self, $c, $rel) = @_;
105              
106             # Prefix
107 854 100       4432 if (my $prefix = $self->prefix) {
108 11         59 $rel = "/$rel";
109 11 100       190 return undef unless $rel =~ s/^\Q$prefix\E\///;
110             }
111              
112 849 100       3986 return undef unless my $asset = $self->file($rel);
113 77         422 $c->app->types->content_type($c, {file => $rel});
114 77         496 return !!$self->serve_asset($c, $asset);
115             }
116              
117             sub serve_asset {
118 81     81 1 267 my ($self, $c, $asset) = @_;
119              
120             # Content-Type
121 81 100       509 $c->app->types->content_type($c, {file => $asset->path}) if $asset->is_file;
122              
123             # Last-Modified and ETag
124 81         426 my $res = $c->res;
125 81         345 $res->code(200)->headers->accept_ranges('bytes');
126 81         441 my $mtime = $asset->mtime;
127 81         883 my $options = {etag => md5_sum($mtime), last_modified => $mtime};
128 81 100       443 return $res->code(304) if $self->is_fresh($c, $options);
129              
130             # Range
131 78 100       821 return $res->content->asset($asset) unless my $range = $c->req->headers->range;
132              
133             # Not satisfiable
134 17 100       145 return $res->code(416) unless my $size = $asset->size;
135 16 50       178 return $res->code(416) unless $range =~ /^bytes=(\d+)?-(\d+)?/;
136 16 100 50     241 my ($start, $end) = ($1 // 0, defined $2 && $2 < $size ? $2 : $size - 1);
      100        
137 16 100       71 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         57 return $res->content->asset($asset->start_range($start)->end_range($end));
142             }
143              
144             sub warmup {
145 72     72 1 198 my $self = shift;
146              
147             # DATA sections
148 72         348 my $index = $self->{index} = {};
149 72         190 for my $class (reverse @{$self->classes}) { $index->{$_} = $class for keys %{data_section $class} }
  72         340  
  97         191  
  97         502  
150              
151             # Static assets
152 72         294 my $assets = $self->{assets} = {};
153 72         412 my $asset_dir = $self->asset_dir;
154 72         176 for my $path (@{$self->paths}) {
  72         397  
155 84         485 my $asset_path = path($path, $asset_dir);
156 84 100       1164 next unless -d $asset_path;
157              
158 44         452 for my $asset_file ($asset_path->list_tree({recursive => 1})->each) {
159 308         932 my $parts = $asset_file->to_rel($asset_path)->to_array;
160 308         2062 my $filename = pop @$parts;
161 308         701 my $prefix = join '/', @$parts;
162              
163 308 100       2372 next unless $filename =~ /^([^.]+)\.([^.]+)\.(.+)$/;
164 264         712 my $checksum = $2;
165 264 100       1300 my $short = $prefix eq '' ? "/$1.$3" : "/$prefix/$1.$3";
166 264         678 my $long = '/' . join('/', @$parts, $filename);
167              
168 264 50 66     1700 $assets->{$short} = $long if !exists($assets->{$short}) || $checksum eq 'development';
169             }
170             }
171             }
172              
173 12     12   65 sub _epoch { Mojo::Date->new(shift)->epoch }
174              
175             sub _get_data_file {
176 808     808   2458 my ($self, $rel) = @_;
177              
178             # Protect files without extensions and templates with two extensions
179 808 100 100     7173 return undef if $rel !~ /\.\w+$/ || $rel =~ /\.\w+\.\w+$/;
180              
181             # Find file
182 96         517 my @args = ($self->{index}{$rel}, $rel);
183 96 100       604 return undef unless defined(my $data = data_section(@args));
184 18 100       115 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   2145 my $path = shift;
189 54     54   620 no warnings 'newline';
  54         149  
  54         11675  
190 873 100 66     55883 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