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   410 use Mojo::Base -base;
  50         140  
  50         397  
3              
4 50     50   390 use Mojo::Asset::File;
  50         146  
  50         454  
5 50     50   335 use Mojo::Asset::Memory;
  50         146  
  50         362  
6 50     50   349 use Mojo::Date;
  50         143  
  50         357  
7 50     50   392 use Mojo::File qw(curfile path);
  50         196  
  50         2988  
8 50     50   372 use Mojo::Loader qw(data_section file_is_binary);
  50         134  
  50         2828  
9 50     50   358 use Mojo::Util qw(encode md5_sum trim);
  50         145  
  50         110139  
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 28 my ($self, $asset) = @_;
22 14 100       98 $asset = "/$asset" unless $asset =~ /^\//;
23 14   50     47 my $assets = $self->{assets} //= {};
24 14   66     36 return '/' . $self->asset_dir . ($assets->{$asset} // $asset);
25             }
26              
27             sub dispatch {
28 1015     1015 1 2332 my ($self, $c) = @_;
29              
30             # Method (GET or HEAD)
31 1015         3108 my $req = $c->req;
32 1015         3141 my $method = $req->method;
33 1015 100 100     4134 return undef unless $method eq 'GET' || $method eq 'HEAD';
34              
35             # Canonical path
36 862         2437 my $stash = $c->stash;
37 862         2520 my $path = $req->url->path;
38 862 100       3896 $path = $stash->{path} ? $path->new($stash->{path}) : $path->clone;
39 862 100       1527 return undef unless my @parts = @{$path->canonicalize->parts};
  862         2407  
40              
41             # Serve static file and prevent path traversal
42 780         2423 my $canon_path = join '/', @parts;
43 780 100 100     5875 return undef if $canon_path =~ /^\.\.\/|\\/ || !$self->serve($c, $canon_path);
44 67         253 $stash->{'mojo.static'} = 1;
45              
46             # Development assets will be rebuilt a lot, do not let browsers cache them
47 67 100 100     239 $c->res->headers->cache_control('no-cache')
48             if $c->app->mode eq 'development' && index($canon_path, $self->asset_dir) == 0;
49              
50 67         228 return !!$c->rendered;
51             }
52              
53             sub file {
54 783     783 1 1630 my ($self, $rel) = @_;
55              
56 783 100       2542 $self->warmup unless $self->{index};
57              
58             # Search all paths
59 783         3080 my @parts = split /\//, $rel;
60 783         1504 for my $path (@{$self->paths}) {
  783         2695  
61 788 100       2972 next unless my $asset = _get_file(path($path, @parts)->to_string);
62 42         232 return $asset;
63             }
64              
65             # Search DATA
66 741 100       3347 if (my $asset = $self->_get_data_file($rel)) { return $asset }
  16         76  
67              
68             # Search extra files
69 725         2686 my $extra = $self->extra;
70 725 100       9831 return exists $extra->{$rel} ? _get_file($extra->{$rel}) : undef;
71             }
72              
73             sub is_fresh {
74 97     97 1 263 my ($self, $c, $options) = @_;
75              
76 97         340 my $res_headers = $c->res->headers;
77 97         424 my ($last, $etag) = @$options{qw(last_modified etag)};
78 97 100       622 $res_headers->last_modified(Mojo::Date->new($last)->to_string) if $last;
79 97 100       832 $res_headers->etag($etag = ($etag =~ m!^W/"! ? $etag : qq{"$etag"})) if $etag;
    100          
80              
81             # Unconditional
82 97         320 my $req_headers = $c->req->headers;
83 97         320 my $match = $req_headers->if_none_match;
84 97 100 100     311 return undef unless (my $since = $req_headers->if_modified_since) || $match;
85              
86             # If-None-Match
87 21   100     64 $etag //= $res_headers->etag // '';
      100        
88 21 100 100     81 return undef if $match && !grep { $_ eq $etag || "W/$_" eq $etag } map { trim($_) } split /,/, $match;
  23 100       151  
  23         63  
89              
90             # If-Modified-Since
91 17 100 100     84 return !!$match unless ($last //= $res_headers->last_modified) && $since;
      100        
92 6   50     42 return _epoch($last) <= (_epoch($since) // 0);
93             }
94              
95             sub serve {
96 779     779 1 1881 my ($self, $c, $rel) = @_;
97 779 100       2083 return undef unless my $asset = $self->file($rel);
98 73         291 $c->app->types->content_type($c, {file => $rel});
99 73         386 return !!$self->serve_asset($c, $asset);
100             }
101              
102             sub serve_asset {
103 77     77 1 197 my ($self, $c, $asset) = @_;
104              
105             # Content-Type
106 77 100       336 $c->app->types->content_type($c, {file => $asset->path}) if $asset->is_file;
107              
108             # Last-Modified and ETag
109 77         321 my $res = $c->res;
110 77         342 $res->code(200)->headers->accept_ranges('bytes');
111 77         350 my $mtime = $asset->mtime;
112 77         717 my $options = {etag => md5_sum($mtime), last_modified => $mtime};
113 77 100       357 return $res->code(304) if $self->is_fresh($c, $options);
114              
115             # Range
116 74 100       252 return $res->content->asset($asset) unless my $range = $c->req->headers->range;
117              
118             # Not satisfiable
119 16 100       59 return $res->code(416) unless my $size = $asset->size;
120 15 50       110 return $res->code(416) unless $range =~ /^bytes=(\d+)?-(\d+)?/;
121 15 100 100     154 my ($start, $end) = ($1 // 0, defined $2 && $2 < $size ? $2 : $size - 1);
      100        
122 15 100       52 return $res->code(416) if $start > $end;
123              
124             # Satisfiable
125 12         50 $res->code(206)->headers->content_length($end - $start + 1)->content_range("bytes $start-$end/$size");
126 12         37 return $res->content->asset($asset->start_range($start)->end_range($end));
127             }
128              
129             sub warmup {
130 66     66 1 159 my $self = shift;
131              
132             # DATA sections
133 66         238 my $index = $self->{index} = {};
134 66         147 for my $class (reverse @{$self->classes}) { $index->{$_} = $class for keys %{data_section $class} }
  66         303  
  91         181  
  91         349  
135              
136             # Static assets
137 66         284 my $assets = $self->{assets} = {};
138 66         347 my $asset_dir = $self->asset_dir;
139 66         162 for my $path (@{$self->paths}) {
  66         253  
140 78         361 my $asset_path = path($path, $asset_dir);
141 78 100       1022 next unless -d $asset_path;
142              
143 41         457 for my $asset_file ($asset_path->list_tree({recursive => 1})->each) {
144 287         962 my $parts = $asset_file->to_rel($asset_path)->to_array;
145 287         1814 my $filename = pop @$parts;
146 287         690 my $prefix = join '/', @$parts;
147              
148 287 100       1863 next unless $filename =~ /^([^.]+)\.([^.]+)\.(.+)$/;
149 246         1447 my $checksum = $2;
150 246 100       1013 my $short = $prefix eq '' ? "/$1.$3" : "/$prefix/$1.$3";
151 246         709 my $long = '/' . join('/', @$parts, $filename);
152              
153 246 50 66     1336 $assets->{$short} = $long if !exists($assets->{$short}) || $checksum eq 'development';
154             }
155             }
156             }
157              
158 12     12   31 sub _epoch { Mojo::Date->new(shift)->epoch }
159              
160             sub _get_data_file {
161 741     741   2055 my ($self, $rel) = @_;
162              
163             # Protect files without extensions and templates with two extensions
164 741 100 100     5191 return undef if $rel !~ /\.\w+$/ || $rel =~ /\.\w+\.\w+$/;
165              
166             # Find file
167 95         509 my @args = ($self->{index}{$rel}, $rel);
168 95 100       427 return undef unless defined(my $data = data_section(@args));
169 16 100       72 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   1639 my $path = shift;
174 50     50   521 no warnings 'newline';
  50         179  
  50         6532  
175 805 100 66     25634 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