File Coverage

blib/lib/Mojolicious/Static.pm
Criterion Covered Total %
statement 121 121 100.0
branch 72 74 97.3
condition 42 47 89.3
subroutine 19 19 100.0
pod 8 8 100.0
total 262 269 97.4


line stmt bran cond sub pod time code
1             package Mojolicious::Static;
2 51     51   471 use Mojo::Base -base;
  51         154  
  51         407  
3              
4 51     51   450 use Mojo::Asset::File;
  51         164  
  51         563  
5 51     51   430 use Mojo::Asset::Memory;
  51         208  
  51         479  
6 51     51   449 use Mojo::Date;
  51         177  
  51         491  
7 51     51   408 use Mojo::File qw(curfile path);
  51         218  
  51         3252  
8 51     51   367 use Mojo::Loader qw(data_section file_is_binary);
  51         187  
  51         3134  
9 51     51   440 use Mojo::Util qw(encode md5_sum trim);
  51         229  
  51         127328  
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 52 my ($self, $asset) = @_;
23 20 100       186 $asset = "/$asset" unless $asset =~ /^\//;
24 20   50     72 my $assets = $self->{assets} //= {};
25 20   66     65 return $self->file_path('/' . $self->asset_dir . ($assets->{$asset} // $asset));
26             }
27              
28             sub dispatch {
29 1027     1027 1 2277 my ($self, $c) = @_;
30              
31             # Method (GET or HEAD)
32 1027         3148 my $req = $c->req;
33 1027         3158 my $method = $req->method;
34 1027 100 100     4431 return undef unless $method eq 'GET' || $method eq 'HEAD';
35              
36             # Canonical path
37 874         2127 my $stash = $c->stash;
38 874         2574 my $path = $req->url->path;
39 874 100       4199 $path = $stash->{path} ? $path->new($stash->{path}) : $path->clone;
40 874 100       1670 return undef unless my @parts = @{$path->canonicalize->parts};
  874         2628  
41              
42             # Serve static file and prevent path traversal
43 792         2496 my $canon_path = join '/', @parts;
44 792 100 100     6113 return undef if $canon_path =~ /^\.\.\/|\\/ || !$self->serve($c, $canon_path);
45 72         286 $stash->{'mojo.static'} = 1;
46              
47             # Development assets will be rebuilt a lot, do not let browsers cache them
48 72 100 100     270 $c->res->headers->cache_control('no-cache')
49             if $c->app->mode eq 'development' && index($canon_path, $self->asset_dir) == 0;
50              
51 72         260 return !!$c->rendered;
52             }
53              
54             sub file {
55 790     790 1 1807 my ($self, $rel) = @_;
56              
57 790 100       2457 $self->warmup unless $self->{index};
58              
59             # Search all paths
60 790         3286 my @parts = split /\//, $rel;
61 790         1528 for my $path (@{$self->paths}) {
  790         2304  
62 795 100       3158 next unless my $asset = _get_file(path($path, @parts)->to_string);
63 44         301 return $asset;
64             }
65              
66             # Search DATA
67 746 100       3744 if (my $asset = $self->_get_data_file($rel)) { return $asset }
  18         89  
68              
69             # Search extra files
70 728         2809 my $extra = $self->extra;
71 728 100       10466 return exists $extra->{$rel} ? _get_file($extra->{$rel}) : undef;
72             }
73              
74             sub file_path {
75 394     394 1 913 my ($self, $file) = @_;
76 394 100       1723 $file = "/$file" unless $file =~ /^\//;
77 394 100       1119 return $file unless my $prefix = $self->prefix;
78 54         219 return "$prefix$file";
79             }
80              
81             sub is_fresh {
82 102     102 1 304 my ($self, $c, $options) = @_;
83              
84 102         413 my $res_headers = $c->res->headers;
85 102         433 my ($last, $etag) = @$options{qw(last_modified etag)};
86 102 100       733 $res_headers->last_modified(Mojo::Date->new($last)->to_string) if $last;
87 102 100       885 $res_headers->etag($etag = ($etag =~ m!^W/"! ? $etag : qq{"$etag"})) if $etag;
    100          
88              
89             # Unconditional
90 102         380 my $req_headers = $c->req->headers;
91 102         439 my $match = $req_headers->if_none_match;
92 102 100 100     417 return undef unless (my $since = $req_headers->if_modified_since) || $match;
93              
94             # If-None-Match
95 21   100     74 $etag //= $res_headers->etag // '';
      100        
96 21 100 100     94 return undef if $match && !grep { $_ eq $etag || "W/$_" eq $etag } map { trim($_) } split /,/, $match;
  23 100       141  
  23         67  
97              
98             # If-Modified-Since
99 17 100 100     82 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 791     791 1 2047 my ($self, $c, $rel) = @_;
105              
106             # Prefix
107 791 100       2645 if (my $prefix = $self->prefix) {
108 11         48 $rel = "/$rel";
109 11 100       160 return undef unless $rel =~ s/^\Q$prefix\E\///;
110             }
111              
112 786 100       2422 return undef unless my $asset = $self->file($rel);
113 78         367 $c->app->types->content_type($c, {file => $rel});
114 78         549 return !!$self->serve_asset($c, $asset);
115             }
116              
117             sub serve_asset {
118 82     82 1 274 my ($self, $c, $asset) = @_;
119              
120             # Content-Type
121 82 100       481 $c->app->types->content_type($c, {file => $asset->path}) if $asset->is_file;
122              
123             # Last-Modified and ETag
124 82         410 my $res = $c->res;
125 82         358 $res->code(200)->headers->accept_ranges('bytes');
126 82         381 my $mtime = $asset->mtime;
127 82         857 my $options = {etag => md5_sum($mtime), last_modified => $mtime};
128 82 100       387 return $res->code(304) if $self->is_fresh($c, $options);
129              
130             # Range
131 79 100       325 return $res->content->asset($asset) unless my $range = $c->req->headers->range;
132              
133             # Not satisfiable
134 17 100       84 return $res->code(416) unless my $size = $asset->size;
135 16 50       166 return $res->code(416) unless $range =~ /^bytes=(\d+)?-(\d+)?/;
136 16 100 100     187 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         54 $res->code(206)->headers->content_length($end - $start + 1)->content_range("bytes $start-$end/$size");
141 13         43 return $res->content->asset($asset->start_range($start)->end_range($end));
142             }
143              
144             sub warmup {
145 67     67 1 219 my $self = shift;
146              
147             # DATA sections
148 67         273 my $index = $self->{index} = {};
149 67         144 for my $class (reverse @{$self->classes}) { $index->{$_} = $class for keys %{data_section $class} }
  67         338  
  92         207  
  92         951  
150              
151             # Static assets
152 67         384 my $assets = $self->{assets} = {};
153 67         367 my $asset_dir = $self->asset_dir;
154 67         178 for my $path (@{$self->paths}) {
  67         321  
155 79         417 my $asset_path = path($path, $asset_dir);
156 79 100       1525 next unless -d $asset_path;
157              
158 42         717 for my $asset_file ($asset_path->list_tree({recursive => 1})->each) {
159 294         913 my $parts = $asset_file->to_rel($asset_path)->to_array;
160 294         1909 my $filename = pop @$parts;
161 294         689 my $prefix = join '/', @$parts;
162              
163 294 100       2064 next unless $filename =~ /^([^.]+)\.([^.]+)\.(.+)$/;
164 252         657 my $checksum = $2;
165 252 100       1166 my $short = $prefix eq '' ? "/$1.$3" : "/$prefix/$1.$3";
166 252         750 my $long = '/' . join('/', @$parts, $filename);
167              
168 252 50 66     1425 $assets->{$short} = $long if !exists($assets->{$short}) || $checksum eq 'development';
169             }
170             }
171             }
172              
173 12     12   35 sub _epoch { Mojo::Date->new(shift)->epoch }
174              
175             sub _get_data_file {
176 746     746   1969 my ($self, $rel) = @_;
177              
178             # Protect files without extensions and templates with two extensions
179 746 100 100     5295 return undef if $rel !~ /\.\w+$/ || $rel =~ /\.\w+\.\w+$/;
180              
181             # Find file
182 98         567 my @args = ($self->{index}{$rel}, $rel);
183 98 100       428 return undef unless defined(my $data = data_section(@args));
184 18 100       106 return Mojo::Asset::Memory->new->add_chunk(file_is_binary(@args) ? $data : encode 'UTF-8', $data);
185             }
186              
187             sub _get_file {
188 813     813   1616 my $path = shift;
189 51     51   521 no warnings 'newline';
  51         188  
  51         7221  
190 813 100 66     27128 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