File Coverage

blib/lib/Catalyst/Plugin/Static/Simple.pm
Criterion Covered Total %
statement 97 102 95.1
branch 31 44 70.4
condition 10 14 71.4
subroutine 13 13 100.0
pod 1 1 100.0
total 152 174 87.3


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::Static::Simple;
2              
3 13     13   17805739 use Moose::Role;
  13         372711  
  13         99  
4 13     13   58334 use File::stat;
  13         5507  
  13         88  
5 13     13   667 use File::Spec ();
  13         24  
  13         194  
6 13     13   436 use IO::File ();
  13         7646  
  13         224  
7 13     13   4854 use MIME::Types ();
  13         50200  
  13         284  
8 13     13   400 use MooseX::Types::Moose qw/ArrayRef Str/;
  13         49539  
  13         127  
9 13     13   54426 use Catalyst::Utils;
  13         38979  
  13         302  
10 13     13   65 use namespace::autoclean;
  13         25  
  13         101  
11              
12             our $VERSION = '0.36';
13              
14             has _static_file => ( is => 'rw' );
15             has _static_debug_message => ( is => 'rw', isa => ArrayRef[Str] );
16              
17             after setup_finalize => sub {
18               my $c = shift;
19              
20             # New: Turn off new 'autoflush' flag in logger (see Catalyst::Log).
21             # This is needed to surpress output of debug log messages for
22             # static requests:
23               $c->log->autoflush(0) if $c->log->can('autoflush');
24             };
25              
26             before prepare_action => sub {
27                 my $c = shift;
28                 my $path = $c->req->path;
29                 my $config = $c->config->{'Plugin::Static::Simple'};
30              
31                 $path =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
32              
33             # is the URI in a static-defined path?
34                 foreach my $dir ( @{ $config->{dirs} } ) {
35                     my $dir_re = quotemeta $dir;
36              
37             # strip trailing slashes, they'll be added in our regex
38                     $dir_re =~ s{/$}{};
39              
40                     my $re;
41              
42                     if ( $dir =~ m{^qr/}xms ) {
43                         $re = eval $dir;
44              
45                         if ($@) {
46                             $c->error( "Error compiling static dir regex '$dir': $@" );
47                         }
48                     }
49                     else {
50                         $re = qr{^${dir_re}/};
51                     }
52              
53                     if ( $path =~ $re ) {
54                         if ( $c->_locate_static_file( $path, 1 ) ) {
55                             $c->_debug_msg( 'from static directory' )
56                                 if $config->{debug};
57                         } else {
58                             $c->_debug_msg( "404: file not found: $path" )
59                                 if $config->{debug};
60                             $c->res->status( 404 );
61                             $c->res->content_type( 'text/html' );
62                         }
63                     }
64                 }
65              
66             # Does the path have an extension?
67                 if ( $path =~ /\.([^\/\\]+)$/m ) {
68             # and does it exist?
69                     $c->_locate_static_file( $path );
70                 }
71             };
72              
73             around dispatch => sub {
74                 my $orig = shift;
75                 my $c = shift;
76              
77                 return if ( $c->res->status != 200 );
78              
79                 if ( $c->_static_file ) {
80                     if ( $c->config->{'Plugin::Static::Simple'}->{no_logs} && $c->log->can('abort') ) {
81                        $c->log->abort( 1 );
82                     }
83                     return $c->_serve_static;
84                 }
85                 else {
86                     return $c->$orig(@_);
87                 }
88             };
89              
90             before finalize => sub {
91                 my $c = shift;
92              
93             # display all log messages
94                 if ( $c->config->{'Plugin::Static::Simple'}->{debug} && scalar @{$c->_debug_msg} ) {
95                     $c->log->debug( 'Static::Simple: ' . join q{ }, @{$c->_debug_msg} );
96                 }
97             };
98              
99             before setup_finalize => sub {
100                 my $c = shift;
101              
102                 $c->log->warn("Deprecated 'static' config key used, please use the key 'Plugin::Static::Simple' instead")
103                     if exists $c->config->{static};
104              
105                 if (exists $c->config->{static}->{include_path}) {
106                     $c->config->{'Plugin::Static::Simple'}->{include_path} = [
107                         @{$c->config->{'Plugin::Static::Simple'}->{include_path} || []},
108                         @{delete $c->config->{static}->{include_path} || []}
109                     ];
110                 }
111                 
112                 my $config
113                     = $c->config->{'Plugin::Static::Simple'}
114                     = $c->config->{'static'}
115                     = Catalyst::Utils::merge_hashes(
116                         $c->config->{'Plugin::Static::Simple'} || {},
117                         $c->config->{static} || {}
118                     );
119              
120                 $config->{dirs} ||= [];
121                 $config->{include_path} ||= [ $c->config->{root} ];
122                 $config->{mime_types} ||= {};
123                 $config->{ignore_extensions} ||= [ qw/tmpl tt tt2 html xhtml/ ];
124                 $config->{ignore_dirs} ||= [];
125                 $config->{debug} ||= $c->debug;
126                 $config->{no_logs} = 1 unless defined $config->{no_logs};
127                 $config->{no_logs} = 0 if $config->{logging};
128              
129             # load up a MIME::Types object, only loading types with
130             # at least 1 file extension
131                 $config->{mime_types_obj} = MIME::Types->new( only_complete => 1 );
132             };
133              
134             # Search through all included directories for the static file
135             # Based on Template Toolkit INCLUDE_PATH code
136             sub _locate_static_file {
137 27     27   75     my ( $c, $path, $in_static_dir ) = @_;
138              
139 27         428     $path = File::Spec->catdir(
140                     File::Spec->no_upwards( File::Spec->splitdir( $path ) )
141                 );
142              
143 27         103     my $config = $c->config->{'Plugin::Static::Simple'};
144 27         1831     my @ipaths = @{ $config->{include_path} };
  27         105  
145 27         46     my $dpaths;
146 27         48     my $count = 64; # maximum number of directories to search
147              
148                 DIR_CHECK:
149 27   66     187     while ( @ipaths && --$count) {
150 36   50     148         my $dir = shift @ipaths || next DIR_CHECK;
151              
152 36 100       203         if ( ref $dir eq 'CODE' ) {
153 2         5             eval { $dpaths = &$dir( $c ) };
  2         9  
154 2 50       207             if ($@) {
155 0         0                 $c->log->error( 'Static::Simple: include_path error: ' . $@ );
156                         } else {
157 2         7                 unshift @ipaths, @$dpaths;
158 2         9                 next DIR_CHECK;
159                         }
160                     } else {
161 34         107             $dir =~ s/(\/|\\)$//xms;
162 34 100 66     902             if ( -d $dir && -f $dir . '/' . $path ) {
163              
164             # Don't ignore any files in static dirs defined with 'dirs'
165 22 100       1139                 unless ( $in_static_dir ) {
166             # do we need to ignore the file?
167 19         29                     for my $ignore ( @{ $config->{ignore_dirs} } ) {
  19         68  
168 6         16                         $ignore =~ s{(/|\\)$}{};
169 6 100       107                         if ( $path =~ /^$ignore(\/|\\)/ ) {
170                                         $c->_debug_msg( "Ignoring directory `$ignore`" )
171 3 50       11                                 if $config->{debug};
172 3         13                             next DIR_CHECK;
173                                     }
174                                 }
175              
176             # do we need to ignore based on extension?
177 16         31                     for my $ignore_ext ( @{ $config->{ignore_extensions} } ) {
  16         38  
178 75 100       677                         if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
179                                         $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
180 3 50       17                                 if $config->{debug};
181 3         18                             next DIR_CHECK;
182                                     }
183                                 }
184                             }
185              
186                             $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
187 16 50       48                     if $config->{debug};
188 16         151                 return $c->_static_file( $dir . '/' . $path );
189                         }
190                     }
191                 }
192              
193 11         399     return;
194             }
195              
196             sub _serve_static {
197 16     16   30     my $c = shift;
198 16         45     my $config = $c->config->{'Plugin::Static::Simple'};
199              
200 16   66     1484     my $full_path = shift || $c->_static_file;
201 16         49     my $type = $c->_ext_to_type( $full_path );
202 16         106     my $stat = stat $full_path;
203              
204 16         2354     $c->res->headers->content_type( $type );
205 16         2621     $c->res->headers->content_length( $stat->size );
206 16         2323     $c->res->headers->last_modified( $stat->mtime );
207             # Tell Firefox & friends its OK to cache, even over SSL:
208 16         26029     $c->res->headers->header('Cache-control' => 'public');
209             # Optionally, set a fixed expiry time:
210 16 50       2701     if ($config->{expires}) {
211 0         0         $c->res->headers->expires(time() + $config->{expires});
212                 }
213              
214 16         94     my $fh = IO::File->new( $full_path, 'r' );
215 16 50       1598     if ( defined $fh ) {
216 16         50         binmode $fh;
217 16         54         $c->res->body( $fh );
218                 }
219                 else {
220 0         0         Catalyst::Exception->throw(
221                         message => "Unable to open $full_path for reading" );
222                 }
223              
224 16         957     return 1;
225             }
226              
227             sub serve_static_file {
228 2     2 1 4916     my ( $c, $full_path ) = @_;
229              
230 2         12     my $config = $c->config->{'Plugin::Static::Simple'};
231              
232 2 100       183     if ( -e $full_path ) {
233                     $c->_debug_msg( "Serving static file: $full_path" )
234 1 50       6             if $config->{debug};
235                 }
236                 else {
237                     $c->_debug_msg( "404: file not found: $full_path" )
238 1 50       8             if $config->{debug};
239 1         4         $c->res->status( 404 );
240 1         111         $c->res->content_type( 'text/html' );
241 1         199         return;
242                 }
243              
244 1         4     $c->_serve_static( $full_path );
245             }
246              
247             # looks up the correct MIME type for the current file extension
248             sub _ext_to_type {
249 16     16   39     my ( $c, $full_path ) = @_;
250              
251 16         60     my $config = $c->config->{'Plugin::Static::Simple'};
252              
253 16 50       1064     if ( $full_path =~ /.*\.(\S{1,})$/xms ) {
254 16         40         my $ext = $1;
255                     my $type = $config->{mime_types}{$ext}
256 16   100     95             || $config->{mime_types_obj}->mimeTypeOf( $ext );
257 16 100       1201         if ( $type ) {
258 14 50       104             $c->_debug_msg( "as $type" ) if $config->{debug};
259 14 100       68             return ( ref $type ) ? $type->type : $type;
260                     }
261                     else {
262                         $c->_debug_msg( "as text/plain (unknown extension $ext)" )
263 2 50       7                 if $config->{debug};
264 2         6             return 'text/plain';
265                     }
266                 }
267                 else {
268                     $c->_debug_msg( 'as text/plain (no extension)' )
269 0 0       0             if $config->{debug};
270 0         0         return 'text/plain';
271                 }
272             }
273              
274             sub _debug_msg {
275 3     3   6     my ( $c, $msg ) = @_;
276              
277 3 100       108     if ( !defined $c->_static_debug_message ) {
278 1         24         $c->_static_debug_message( [] );
279                 }
280              
281 3 100       7     if ( $msg ) {
282 1         2         push @{ $c->_static_debug_message }, $msg;
  1         28  
283                 }
284              
285 3         85     return $c->_static_debug_message;
286             }
287              
288             1;
289             __END__
290            
291             =head1 NAME
292            
293             Catalyst::Plugin::Static::Simple - Make serving static pages painless.
294            
295             =head1 SYNOPSIS
296            
297             package MyApp;
298             use Catalyst qw/ Static::Simple /;
299             MyApp->setup;
300             # that's it; static content is automatically served by Catalyst
301             # from the application's root directory, though you can configure
302             # things or bypass Catalyst entirely in a production environment
303             #
304             # one caveat: the files must be served from an absolute path
305             # (i.e. /images/foo.png)
306            
307             =head1 DESCRIPTION
308            
309             The Static::Simple plugin is designed to make serving static content in
310             your application during development quick and easy, without requiring a
311             single line of code from you.
312            
313             This plugin detects static files by looking at the file extension in the
314             URL (such as B<.css> or B<.png> or B<.js>). The plugin uses the
315             lightweight L<MIME::Types> module to map file extensions to
316             IANA-registered MIME types, and will serve your static files with the
317             correct MIME type directly to the browser, without being processed
318             through Catalyst.
319            
320             Note that actions mapped to paths using periods (.) will still operate
321             properly.
322            
323             If the plugin can not find the file, the request is dispatched to your
324             application instead. This means you are responsible for generating a
325             C<404> error if your application can not process the request:
326            
327             # handled by static::simple, not dispatched to your application
328             /images/exists.png
329            
330             # static::simple will not find the file and let your application
331             # handle the request. You are responsible for generating a file
332             # or returning a 404 error
333             /images/does_not_exist.png
334            
335             Though Static::Simple is designed to work out-of-the-box, you can tweak
336             the operation by adding various configuration options. In a production
337             environment, you will probably want to use your webserver to deliver
338             static content; for an example see L<USING WITH APACHE>, below.
339            
340             =head1 DEFAULT BEHAVIOUR
341            
342             By default, Static::Simple will deliver all files having extensions
343             (that is, bits of text following a period (C<.>)), I<except> files
344             having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
345             C<xhtml>. These files, and all files without extensions, will be
346             processed through Catalyst. If L<MIME::Types> doesn't recognize an
347             extension, it will be served as C<text/plain>.
348            
349             To restate: files having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>,
350             and C<xhtml> I<will not> be served statically by default, they will be
351             processed by Catalyst. Thus if you want to use C<.html> files from
352             within a Catalyst app as static files, you need to change the
353             configuration of Static::Simple. Note also that files having any other
354             extension I<will> be served statically, so if you're using any other
355             extension for template files, you should also change the configuration.
356            
357             Logging of static files is turned off by default.
358            
359             =head1 ADVANCED CONFIGURATION
360            
361             Configuration is completely optional and is specified within
362             C<MyApp-E<gt>config-E<gt>{Plugin::Static::Simple}>. If you use any of these options,
363             this module will probably feel less "simple" to you!
364            
365             =head2 Enabling request logging
366            
367             Since Catalyst 5.50, logging of static requests is turned off by
368             default; static requests tend to clutter the log output and rarely
369             reveal anything useful. However, if you want to enable logging of static
370             requests, you can do so by setting
371             C<MyApp-E<gt>config-E<gt>{Plugin::Static::Simple}-E<gt>{logging}> to 1.
372            
373             =head2 Forcing directories into static mode
374            
375             Define a list of top-level directories beneath your 'root' directory
376             that should always be served in static mode. Regular expressions may be
377             specified using C<qr//>.
378            
379             MyApp->config(
380             'Plugin::Static::Simple' => {
381             dirs => [
382             'static',
383             qr/^(images|css)/,
384             ],
385             }
386             );
387            
388             =head2 Including additional directories
389            
390             You may specify a list of directories in which to search for your static
391             files. The directories will be searched in order and will return the
392             first file found. Note that your root directory is B<not> automatically
393             added to the search path when you specify an C<include_path>. You should
394             use C<MyApp-E<gt>config-E<gt>{root}> to add it.
395            
396             MyApp->config(
397             'Plugin::Static::Simple' => {
398             include_path => [
399             '/path/to/overlay',
400             \&incpath_generator,
401             MyApp->config->{root},
402             ],
403             },
404             );
405            
406             With the above setting, a request for the file C</images/logo.jpg> will search
407             for the following files, returning the first one found:
408            
409             /path/to/overlay/images/logo.jpg
410             /dynamic/path/images/logo.jpg
411             /your/app/home/root/images/logo.jpg
412            
413             The include path can contain a subroutine reference to dynamically return a
414             list of available directories. This method will receive the C<$c> object as a
415             parameter and should return a reference to a list of directories. Errors can
416             be reported using C<die()>. This method will be called every time a file is
417             requested that appears to be a static file (i.e. it has an extension).
418            
419             For example:
420            
421             sub incpath_generator {
422             my $c = shift;
423            
424             if ( $c->session->{customer_dir} ) {
425             return [ $c->session->{customer_dir} ];
426             } else {
427             die "No customer dir defined.";
428             }
429             }
430            
431             =head2 Ignoring certain types of files
432            
433             There are some file types you may not wish to serve as static files.
434             Most important in this category are your raw template files. By
435             default, files with the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
436             C<xhtml> will be ignored by Static::Simple in the interest of security.
437             If you wish to define your own extensions to ignore, use the
438             C<ignore_extensions> option:
439            
440             MyApp->config(
441             'Plugin::Static::Simple' => {
442             ignore_extensions => [ qw/html asp php/ ],
443             },
444             );
445            
446             =head2 Ignoring entire directories
447            
448             To prevent an entire directory from being served statically, you can use
449             the C<ignore_dirs> option. This option contains a list of relative
450             directory paths to ignore. If using C<include_path>, the path will be
451             checked against every included path.
452            
453             MyApp->config(
454             'Plugin::Static::Simple' => {
455             ignore_dirs => [ qw/tmpl css/ ],
456             },
457             );
458            
459             For example, if combined with the above C<include_path> setting, this
460             C<ignore_dirs> value will ignore the following directories if they exist:
461            
462             /path/to/overlay/tmpl
463             /path/to/overlay/css
464             /dynamic/path/tmpl
465             /dynamic/path/css
466             /your/app/home/root/tmpl
467             /your/app/home/root/css
468            
469             =head2 Custom MIME types
470            
471             To override or add to the default MIME types set by the L<MIME::Types>
472             module, you may enter your own extension to MIME type mapping.
473            
474             MyApp->config(
475             'Plugin::Static::Simple' => {
476             mime_types => {
477             jpg => 'image/jpg',
478             png => 'image/png',
479             },
480             },
481             );
482            
483             =head2 Controlling caching with Expires header
484            
485             The files served by Static::Simple will have a Last-Modified header set,
486             which allows some browsers to cache them for a while. However if you want
487             to explicitly set an Expires header, such as to allow proxies to cache your
488             static content, then you can do so by setting the "expires" config option.
489            
490             The value indicates the number of seconds after access time to allow caching.
491             So a value of zero really means "don't cache at all", and any higher values
492             will keep the file around for that long.
493            
494             MyApp->config(
495             'Plugin::Static::Simple' => {
496             expires => 3600, # Caching allowed for one hour.
497             },
498             );
499            
500             =head2 Compatibility with other plugins
501            
502             Since version 0.12, Static::Simple plays nice with other plugins. It no
503             longer short-circuits the C<prepare_action> stage as it was causing too
504             many compatibility issues with other plugins.
505            
506             =head2 Debugging information
507            
508             Enable additional debugging information printed in the Catalyst log. This
509             is automatically enabled when running Catalyst in -Debug mode.
510            
511             MyApp->config(
512             'Plugin::Static::Simple' => {
513             debug => 1,
514             },
515             );
516            
517             =head1 USING WITH APACHE
518            
519             While Static::Simple will work just fine serving files through Catalyst
520             in mod_perl, for increased performance you may wish to have Apache
521             handle the serving of your static files directly. To do this, simply use
522             a dedicated directory for your static files and configure an Apache
523             Location block for that directory This approach is recommended for
524             production installations.
525            
526             <Location /myapp/static>
527             SetHandler default-handler
528             </Location>
529            
530             Using this approach Apache will bypass any handling of these directories
531             through Catalyst. You can leave Static::Simple as part of your
532             application, and it will continue to function on a development server,
533             or using Catalyst's built-in server.
534            
535             In practice, your Catalyst application is probably (i.e. should be)
536             structured in the recommended way (i.e., that generated by bootstrapping
537             the application with the C<catalyst.pl> script, with a main directory
538             under which is a C<lib/> directory for module files and a C<root/>
539             directory for templates and static files). Thus, unless you break up
540             this structure when deploying your app by moving the static files to a
541             different location in your filesystem, you will need to use an Alias
542             directive in Apache to point to the right place. You will then need to
543             add a Directory block to give permission for Apache to serve these
544             files. The final configuration will look something like this:
545            
546             Alias /myapp/static /filesystem/path/to/MyApp/root/static
547             <Directory /filesystem/path/to/MyApp/root/static>
548             allow from all
549             </Directory>
550             <Location /myapp/static>
551             SetHandler default-handler
552             </Location>
553            
554             If you are running in a VirtualHost, you can just set the DocumentRoot
555             location to the location of your root directory; see
556             L<Catalyst::Engine::Apache2::MP20>.
557            
558             =head1 PUBLIC METHODS
559            
560             =head2 serve_static_file $file_path
561            
562             Will serve the file located in $file_path statically. This is useful when
563             you need to autogenerate them if they don't exist, or they are stored in a model.
564            
565             package MyApp::Controller::User;
566            
567             sub curr_user_thumb : PathPart("my_thumbnail.png") {
568             my ( $self, $c ) = @_;
569             my $file_path = $c->user->picture_thumbnail_path;
570             $c->serve_static_file($file_path);
571             }
572            
573             =head1 INTERNAL EXTENDED METHODS
574            
575             Static::Simple extends the following steps in the Catalyst process.
576            
577             =head2 prepare_action
578            
579             C<prepare_action> is used to first check if the request path is a static
580             file. If so, we skip all other C<prepare_action> steps to improve
581             performance.
582            
583             =head2 dispatch
584            
585             C<dispatch> takes the file found during C<prepare_action> and writes it
586             to the output.
587            
588             =head2 finalize
589            
590             C<finalize> serves up final header information and displays any log
591             messages.
592            
593             =head2 setup
594            
595             C<setup> initializes all default values.
596            
597             =head1 DEPRECATIONS
598            
599             The old style of configuration using the C<'static'> config key was deprecated
600             in version 0.30. A warning will be issued if this is used, and the contents of
601             the config at this key will be merged with the newer C<'Plugin::Static::Simple'>
602             key.
603            
604             Be aware that if the C<'include_path'> key under C<'static'> exists at all, it
605             will be merged with any content of the same key under
606             C<'Plugin::Static::Simple'>. Be careful not to set this to a non-arrayref,
607             therefore.
608            
609             =head1 SEE ALSO
610            
611             L<Catalyst>, L<Catalyst::Plugin::Static>,
612             L<http://www.iana.org/assignments/media-types/>
613            
614             =head1 AUTHOR
615            
616             Andy Grundman, <andy@hybridized.org>
617            
618             =head1 CONTRIBUTORS
619            
620             Marcus Ramberg, <mramberg@cpan.org>
621            
622             Jesse Sheidlower, <jester@panix.com>
623            
624             Guillermo Roditi, <groditi@cpan.org>
625            
626             Florian Ragwitz, <rafl@debian.org>
627            
628             Tomas Doran, <bobtfish@bobtfish.net>
629            
630             Justin Wheeler (dnm)
631            
632             Matt S Trout, <mst@shadowcat.co.uk>
633            
634             Toby Corkindale, <tjc@wintrmute.net>
635            
636             =head1 THANKS
637            
638             The authors of Catalyst::Plugin::Static:
639            
640             Sebastian Riedel
641             Christian Hansen
642             Marcus Ramberg
643            
644             For the include_path code from Template Toolkit:
645            
646             Andy Wardley
647            
648             =head1 COPYRIGHT
649            
650             Copyright (c) 2005 - 2011
651             the Catalyst::Plugin::Static::Simple L</AUTHOR> and L</CONTRIBUTORS>
652             as listed above.
653            
654             =head1 LICENSE
655            
656             This program is free software, you can redistribute it and/or modify it under
657             the same terms as Perl itself.
658            
659             =cut
660