File Coverage

blib/lib/Catalyst/Plugin/Static/Simple.pm
Criterion Covered Total %
statement 94 99 94.9
branch 31 44 70.4
condition 10 14 71.4
subroutine 12 12 100.0
pod 1 1 100.0
total 148 170 87.0


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