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   332 use Mojo::Base -base;
  54         96  
  54         358  
3              
4 54     54   329 use Mojo::Asset::File;
  54         130  
  54         430  
5 54     54   226 use Mojo::Asset::Memory;
  54         113  
  54         299  
6 54     54   214 use Mojo::Date;
  54         115  
  54         329  
7 54     54   237 use Mojo::File qw(curfile path);
  54         82  
  54         2985  
8 54     54   240 use Mojo::Loader qw(data_section file_is_binary);
  54         94  
  54         2380  
9 54     54   207 use Mojo::Util qw(encode md5_sum trim);
  54         117  
  54         112275  
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 37 my ($self, $asset) = @_;
23 20 100       65 $asset = "/$asset" unless $asset =~ /^\//;
24 20   50     54 my $assets = $self->{assets} //= {};
25 20   66     52 return $self->file_path('/' . $self->asset_dir . ($assets->{$asset} // $asset));
26             }
27              
28             sub dispatch {
29 1093     1093 1 1954 my ($self, $c) = @_;
30              
31             # Method (GET or HEAD)
32 1093         2996 my $req = $c->req;
33 1093         2746 my $method = $req->method;
34 1093 100 100     3695 return undef unless $method eq 'GET' || $method eq 'HEAD';
35              
36             # Canonical path
37 938         1863 my $stash = $c->stash;
38 938         2050 my $path = $req->url->path;
39 938 100       3566 $path = $stash->{path} ? $path->new($stash->{path}) : $path->clone;
40 938 100       1388 return undef unless my @parts = @{$path->canonicalize->parts};
  938         2171  
41              
42             # Serve static file and prevent path traversal
43 855         2075 my $canon_path = join '/', @parts;
44 855 100 100     6343 return undef if $canon_path =~ /^\.\.\/|\\/ || !$self->serve($c, $canon_path);
45 71         176 $stash->{'mojo.static'} = 1;
46              
47             # Development assets will be rebuilt a lot, do not let browsers cache them
48 71 100 100     180 $c->res->headers->cache_control('no-cache')
49             if $c->app->mode eq 'development' && index($canon_path, $self->asset_dir) == 0;
50              
51 71         229 return !!$c->rendered;
52             }
53              
54             sub file {
55 852     852 1 1604 my ($self, $rel) = @_;
56              
57 852 100       2153 $self->warmup unless $self->{index};
58              
59             # Search all paths
60 852         2797 my @parts = split /\//, $rel;
61 852         1161 for my $path (@{$self->paths}) {
  852         2217  
62 857 100       3632 next unless my $asset = _get_file(path($path, @parts)->to_string);
63 44         186 return $asset;
64             }
65              
66             # Search DATA
67 808 100       2929 if (my $asset = $self->_get_data_file($rel)) { return $asset }
  18         187  
68              
69             # Search extra files
70 790         2308 my $extra = $self->extra;
71 790 100       8406 return exists $extra->{$rel} ? _get_file($extra->{$rel}) : undef;
72             }
73              
74             sub file_path {
75 402     402 1 614 my ($self, $file) = @_;
76 402 100       1009 $file = "/$file" unless $file =~ /^\//;
77 402 100       969 return $file unless my $prefix = $self->prefix;
78 54         171 return "$prefix$file";
79             }
80              
81             sub is_fresh {
82 101     101 1 207 my ($self, $c, $options) = @_;
83              
84 101         305 my $res_headers = $c->res->headers;
85 101         315 my ($last, $etag) = @$options{qw(last_modified etag)};
86 101 100       562 $res_headers->last_modified(Mojo::Date->new($last)->to_string) if $last;
87 101 100       635 $res_headers->etag($etag = ($etag =~ m!^W/"! ? $etag : qq{"$etag"})) if $etag;
    100          
88              
89             # Unconditional
90 101         246 my $req_headers = $c->req->headers;
91 101         284 my $match = $req_headers->if_none_match;
92 101 100 100     233 return undef unless (my $since = $req_headers->if_modified_since) || $match;
93              
94             # If-None-Match
95 21   100     50 $etag //= $res_headers->etag // '';
      66        
96 21 100 100     101 return undef if $match && !grep { $_ eq $etag || "W/$_" eq $etag } map { trim($_) } split /,/, $match;
  23 100       141  
  23         56  
97              
98             # If-Modified-Since
99 17 100 100     79 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 1936 my ($self, $c, $rel) = @_;
105              
106             # Prefix
107 854 100       2791 if (my $prefix = $self->prefix) {
108 11         20 $rel = "/$rel";
109 11 100       129 return undef unless $rel =~ s/^\Q$prefix\E\///;
110             }
111              
112 849 100       2312 return undef unless my $asset = $self->file($rel);
113 77         281 $c->app->types->content_type($c, {file => $rel});
114 77         307 return !!$self->serve_asset($c, $asset);
115             }
116              
117             sub serve_asset {
118 81     81 1 163 my ($self, $c, $asset) = @_;
119              
120             # Content-Type
121 81 100       300 $c->app->types->content_type($c, {file => $asset->path}) if $asset->is_file;
122              
123             # Last-Modified and ETag
124 81         231 my $res = $c->res;
125 81         241 $res->code(200)->headers->accept_ranges('bytes');
126 81         233 my $mtime = $asset->mtime;
127 81         525 my $options = {etag => md5_sum($mtime), last_modified => $mtime};
128 81 100       315 return $res->code(304) if $self->is_fresh($c, $options);
129              
130             # Range
131 78 100       197 return $res->content->asset($asset) unless my $range = $c->req->headers->range;
132              
133             # Not satisfiable
134 17 100       56 return $res->code(416) unless my $size = $asset->size;
135 16 50       108 return $res->code(416) unless $range =~ /^bytes=(\d+)?-(\d+)?/;
136 16 100 50     137 my ($start, $end) = ($1 // 0, defined $2 && $2 < $size ? $2 : $size - 1);
      100        
137 16 100       47 return $res->code(416) if $start > $end;
138              
139             # Satisfiable
140 13         36 $res->code(206)->headers->content_length($end - $start + 1)->content_range("bytes $start-$end/$size");
141 13         29 return $res->content->asset($asset->start_range($start)->end_range($end));
142             }
143              
144             sub warmup {
145 72     72 1 129 my $self = shift;
146              
147             # DATA sections
148 72         262 my $index = $self->{index} = {};
149 72         131 for my $class (reverse @{$self->classes}) { $index->{$_} = $class for keys %{data_section $class} }
  72         263  
  97         130  
  97         340  
150              
151             # Static assets
152 72         229 my $assets = $self->{assets} = {};
153 72         276 my $asset_dir = $self->asset_dir;
154 72         142 for my $path (@{$self->paths}) {
  72         223  
155 84         406 my $asset_path = path($path, $asset_dir);
156 84 100       853 next unless -d $asset_path;
157              
158 44         305 for my $asset_file ($asset_path->list_tree({recursive => 1})->each) {
159 308         639 my $parts = $asset_file->to_rel($asset_path)->to_array;
160 308         1329 my $filename = pop @$parts;
161 308         481 my $prefix = join '/', @$parts;
162              
163 308 100       1216 next unless $filename =~ /^([^.]+)\.([^.]+)\.(.+)$/;
164 264         528 my $checksum = $2;
165 264 100       843 my $short = $prefix eq '' ? "/$1.$3" : "/$prefix/$1.$3";
166 264         452 my $long = '/' . join('/', @$parts, $filename);
167              
168 264 50 66     1348 $assets->{$short} = $long if !exists($assets->{$short}) || $checksum eq 'development';
169             }
170             }
171             }
172              
173 12     12   23 sub _epoch { Mojo::Date->new(shift)->epoch }
174              
175             sub _get_data_file {
176 808     808   1543 my ($self, $rel) = @_;
177              
178             # Protect files without extensions and templates with two extensions
179 808 100 100     4729 return undef if $rel !~ /\.\w+$/ || $rel =~ /\.\w+\.\w+$/;
180              
181             # Find file
182 96         353 my @args = ($self->{index}{$rel}, $rel);
183 96 100       401 return undef unless defined(my $data = data_section(@args));
184 18 100       69 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   1384 my $path = shift;
189 54     54   403 no warnings 'newline';
  54         88  
  54         6591  
190 873 100 66     36188 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