File Coverage

blib/lib/Mojolicious/Static.pm
Criterion Covered Total %
statement 114 114 100.0
branch 64 66 96.9
condition 42 47 89.3
subroutine 18 18 100.0
pod 7 7 100.0
total 245 252 97.2


line stmt bran cond sub pod time code
1             package Mojolicious::Static;
2 50     50   426 use Mojo::Base -base;
  50         168  
  50         441  
3              
4 50     50   410 use Mojo::Asset::File;
  50         162  
  50         544  
5 50     50   316 use Mojo::Asset::Memory;
  50         137  
  50         492  
6 50     50   352 use Mojo::Date;
  50         172  
  50         447  
7 50     50   395 use Mojo::File qw(curfile path);
  50         199  
  50         3238  
8 50     50   401 use Mojo::Loader qw(data_section file_is_binary);
  50         150  
  50         3130  
9 50     50   378 use Mojo::Util qw(encode md5_sum trim);
  50         166  
  50         127673  
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              
20             sub asset_path {
21 14     14 1 34 my ($self, $asset) = @_;
22 14 100       68 $asset = "/$asset" unless $asset =~ /^\//;
23 14   50     42 my $assets = $self->{assets} //= {};
24 14   66     40 return '/' . $self->asset_dir . ($assets->{$asset} // $asset);
25             }
26              
27             sub dispatch {
28 1015     1015 1 2227 my ($self, $c) = @_;
29              
30             # Method (GET or HEAD)
31 1015         3025 my $req = $c->req;
32 1015         3004 my $method = $req->method;
33 1015 100 100     4073 return undef unless $method eq 'GET' || $method eq 'HEAD';
34              
35             # Canonical path
36 862         2141 my $stash = $c->stash;
37 862         2506 my $path = $req->url->path;
38 862 100       4279 $path = $stash->{path} ? $path->new($stash->{path}) : $path->clone;
39 862 100       1508 return undef unless my @parts = @{$path->canonicalize->parts};
  862         2591  
40              
41             # Serve static file and prevent path traversal
42 780         2510 my $canon_path = join '/', @parts;
43 780 100 100     6077 return undef if $canon_path =~ /^\.\.\/|\\/ || !$self->serve($c, $canon_path);
44 67         223 $stash->{'mojo.static'} = 1;
45              
46             # Development assets will be rebuilt a lot, do not let browsers cache them
47 67 100 100     230 $c->res->headers->cache_control('no-cache')
48             if $c->app->mode eq 'development' && index($canon_path, $self->asset_dir) == 0;
49              
50 67         270 return !!$c->rendered;
51             }
52              
53             sub file {
54 783     783 1 1705 my ($self, $rel) = @_;
55              
56 783 100       2792 $self->warmup unless $self->{index};
57              
58             # Search all paths
59 783         3114 my @parts = split /\//, $rel;
60 783         1521 for my $path (@{$self->paths}) {
  783         2875  
61 788 100       3277 next unless my $asset = _get_file(path($path, @parts)->to_string);
62 42         268 return $asset;
63             }
64              
65             # Search DATA
66 741 100       3601 if (my $asset = $self->_get_data_file($rel)) { return $asset }
  16         70  
67              
68             # Search extra files
69 725         2864 my $extra = $self->extra;
70 725 100       10584 return exists $extra->{$rel} ? _get_file($extra->{$rel}) : undef;
71             }
72              
73             sub is_fresh {
74 97     97 1 297 my ($self, $c, $options) = @_;
75              
76 97         357 my $res_headers = $c->res->headers;
77 97         419 my ($last, $etag) = @$options{qw(last_modified etag)};
78 97 100       676 $res_headers->last_modified(Mojo::Date->new($last)->to_string) if $last;
79 97 100       881 $res_headers->etag($etag = ($etag =~ m!^W/"! ? $etag : qq{"$etag"})) if $etag;
    100          
80              
81             # Unconditional
82 97         340 my $req_headers = $c->req->headers;
83 97         433 my $match = $req_headers->if_none_match;
84 97 100 100     334 return undef unless (my $since = $req_headers->if_modified_since) || $match;
85              
86             # If-None-Match
87 21   100     70 $etag //= $res_headers->etag // '';
      100        
88 21 100 100     93 return undef if $match && !grep { $_ eq $etag || "W/$_" eq $etag } map { trim($_) } split /,/, $match;
  23 100       144  
  23         76  
89              
90             # If-Modified-Since
91 17 100 100     91 return !!$match unless ($last //= $res_headers->last_modified) && $since;
      100        
92 6   50     18 return _epoch($last) <= (_epoch($since) // 0);
93             }
94              
95             sub serve {
96 779     779 1 1986 my ($self, $c, $rel) = @_;
97 779 100       2071 return undef unless my $asset = $self->file($rel);
98 73         315 $c->app->types->content_type($c, {file => $rel});
99 73         437 return !!$self->serve_asset($c, $asset);
100             }
101              
102             sub serve_asset {
103 77     77 1 205 my ($self, $c, $asset) = @_;
104              
105             # Content-Type
106 77 100       339 $c->app->types->content_type($c, {file => $asset->path}) if $asset->is_file;
107              
108             # Last-Modified and ETag
109 77         346 my $res = $c->res;
110 77         341 $res->code(200)->headers->accept_ranges('bytes');
111 77         411 my $mtime = $asset->mtime;
112 77         758 my $options = {etag => md5_sum($mtime), last_modified => $mtime};
113 77 100       347 return $res->code(304) if $self->is_fresh($c, $options);
114              
115             # Range
116 74 100       244 return $res->content->asset($asset) unless my $range = $c->req->headers->range;
117              
118             # Not satisfiable
119 16 100       95 return $res->code(416) unless my $size = $asset->size;
120 15 50       126 return $res->code(416) unless $range =~ /^bytes=(\d+)?-(\d+)?/;
121 15 100 100     175 my ($start, $end) = ($1 // 0, defined $2 && $2 < $size ? $2 : $size - 1);
      100        
122 15 100       73 return $res->code(416) if $start > $end;
123              
124             # Satisfiable
125 12         59 $res->code(206)->headers->content_length($end - $start + 1)->content_range("bytes $start-$end/$size");
126 12         54 return $res->content->asset($asset->start_range($start)->end_range($end));
127             }
128              
129             sub warmup {
130 66     66 1 210 my $self = shift;
131              
132             # DATA sections
133 66         273 my $index = $self->{index} = {};
134 66         204 for my $class (reverse @{$self->classes}) { $index->{$_} = $class for keys %{data_section $class} }
  66         309  
  91         196  
  91         389  
135              
136             # Static assets
137 66         310 my $assets = $self->{assets} = {};
138 66         306 my $asset_dir = $self->asset_dir;
139 66         154 for my $path (@{$self->paths}) {
  66         286  
140 78         411 my $asset_path = path($path, $asset_dir);
141 78 100       1036 next unless -d $asset_path;
142              
143 41         491 for my $asset_file ($asset_path->list_tree({recursive => 1})->each) {
144 287         862 my $parts = $asset_file->to_rel($asset_path)->to_array;
145 287         1874 my $filename = pop @$parts;
146 287         682 my $prefix = join '/', @$parts;
147              
148 287 100       1891 next unless $filename =~ /^([^.]+)\.([^.]+)\.(.+)$/;
149 246         1438 my $checksum = $2;
150 246 100       967 my $short = $prefix eq '' ? "/$1.$3" : "/$prefix/$1.$3";
151 246         715 my $long = '/' . join('/', @$parts, $filename);
152              
153 246 50 66     1395 $assets->{$short} = $long if !exists($assets->{$short}) || $checksum eq 'development';
154             }
155             }
156             }
157              
158 12     12   34 sub _epoch { Mojo::Date->new(shift)->epoch }
159              
160             sub _get_data_file {
161 741     741   2167 my ($self, $rel) = @_;
162              
163             # Protect files without extensions and templates with two extensions
164 741 100 100     5150 return undef if $rel !~ /\.\w+$/ || $rel =~ /\.\w+\.\w+$/;
165              
166             # Find file
167 95         518 my @args = ($self->{index}{$rel}, $rel);
168 95 100       428 return undef unless defined(my $data = data_section(@args));
169 16 100       92 return Mojo::Asset::Memory->new->add_chunk(file_is_binary(@args) ? $data : encode 'UTF-8', $data);
170             }
171              
172             sub _get_file {
173 805     805   1646 my $path = shift;
174 50     50   533 no warnings 'newline';
  50         157  
  50         6797  
175 805 100 66     27202 return -f $path && -r _ ? Mojo::Asset::File->new(path => $path) : undef;
176             }
177              
178             1;
179              
180             =encoding utf8
181              
182             =head1 NAME
183              
184             Mojolicious::Static - Serve static files
185              
186             =head1 SYNOPSIS
187              
188             use Mojolicious::Static;
189              
190             my $static = Mojolicious::Static->new;
191             push @{$static->classes}, 'MyApp::Controller::Foo';
192             push @{$static->paths}, '/home/sri/public';
193              
194             =head1 DESCRIPTION
195              
196             L is a static file server with C, C and C support, based
197             on L and L.
198              
199             =head1 ATTRIBUTES
200              
201             L implements the following attributes.
202              
203             =head2 asset_dir
204              
205             my $dir = $static->asset_dir;
206             $static = $static->asset_dir('assets');
207              
208             Subdirectory used for all static assets, defaults to C.
209              
210             =head2 classes
211              
212             my $classes = $static->classes;
213             $static = $static->classes(['main']);
214              
215             Classes to use for finding files in C sections with L, first one has the highest precedence,
216             defaults to C
. Only files with exactly one extension will be used, like C. Note that for files to be
217             detected, these classes need to have already been loaded and added before L is called, which usually happens
218             automatically during application startup.
219              
220             # Add another class with static files in DATA section
221             push @{$static->classes}, 'Mojolicious::Plugin::Fun';
222              
223             # Add another class with static files in DATA section and higher precedence
224             unshift @{$static->classes}, 'Mojolicious::Plugin::MoreFun';
225              
226             =head2 extra
227              
228             my $extra = $static->extra;
229             $static = $static->extra({'foo/bar.txt' => '/home/sri/myapp/bar.txt'});
230              
231             Paths for extra files to be served from locations other than L, such as the images used by the built-in
232             exception and not found pages. Note that extra files are only served if no better alternative could be found in
233             L and L.
234              
235             # Remove built-in favicon
236             delete $static->extra->{'favicon.ico'};
237              
238             =head2 paths
239              
240             my $paths = $static->paths;
241             $static = $static->paths(['/home/sri/public']);
242              
243             Directories to serve static files from, first one has the highest precedence.
244              
245             # Add another "public" directory
246             push @{$static->paths}, '/home/sri/public';
247              
248             # Add another "public" directory with higher precedence
249             unshift @{$static->paths}, '/home/sri/themes/blue/public';
250              
251             =head1 METHODS
252              
253             L inherits all methods from L and implements the following new ones.
254              
255             =head2 asset_path
256              
257             my $path = $static->asset_path('/app.js');
258              
259             Get static asset path.
260              
261             =head2 dispatch
262              
263             my $bool = $static->dispatch(Mojolicious::Controller->new);
264              
265             Serve static file for L object.
266              
267             =head2 file
268              
269             my $asset = $static->file('images/logo.png');
270             my $asset = $static->file('../lib/MyApp.pm');
271              
272             Build L or L object for a file, relative to L or from L,
273             or return C if it doesn't exist. Note that this method uses a relative path, but does not protect from
274             traversing to parent directories.
275              
276             my $content = $static->file('foo/bar.html')->slurp;
277              
278             =head2 is_fresh
279              
280             my $bool = $static->is_fresh(Mojolicious::Controller->new, {etag => 'abc'});
281             my $bool = $static->is_fresh(
282             Mojolicious::Controller->new, {etag => 'W/"def"'});
283              
284             Check freshness of request by comparing the C and C request headers to the C
285             and C response headers.
286              
287             These options are currently available:
288              
289             =over 2
290              
291             =item etag
292              
293             etag => 'abc'
294             etag => 'W/"abc"'
295              
296             Add C header before comparing.
297              
298             =item last_modified
299              
300             last_modified => $epoch
301              
302             Add C header before comparing.
303              
304             =back
305              
306             =head2 serve
307              
308             my $bool = $static->serve(Mojolicious::Controller->new, 'images/logo.png');
309             my $bool = $static->serve(Mojolicious::Controller->new, '../lib/MyApp.pm');
310              
311             Serve a specific file, relative to L or from L. Note that this method uses a relative path, but
312             does not protect from traversing to parent directories.
313              
314             =head2 serve_asset
315              
316             $static->serve_asset(Mojolicious::Controller->new, Mojo::Asset::File->new);
317              
318             Serve a L or L object with C, C and C
319             support.
320              
321             =head2 warmup
322              
323             $static->warmup();
324              
325             Prepare static files from L and static assets for future use.
326              
327             =head1 SEE ALSO
328              
329             L, L, L.
330              
331             =cut