File Coverage

blib/lib/Catalyst/Plugin/Static/Simple.pm
Criterion Covered Total %
statement 116 137 84.7
branch 41 64 64.1
condition 17 33 51.5
subroutine 14 15 93.3
pod 4 4 100.0
total 192 253 75.9


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::Static::Simple;
2              
3 8     8   114 use strict;
  8         77  
  8         127  
4 8     8   130 use warnings;
  8         77  
  8         125  
5 8     8   122 use base qw/Class::Accessor::Fast Class::Data::Inheritable/;
  8         73  
  8         120  
6 8     8   130 use File::stat;
  8         74  
  8         126  
7 8     8   122 use File::Spec ();
  8         74  
  8         76  
8 8     8   117 use IO::File ();
  8         98  
  8         77  
9 8     8   406 use MIME::Types ();
  8         84  
  8         87  
10              
11             our $VERSION = '0.15';
12              
13             __PACKAGE__->mk_accessors( qw/_static_file _static_debug_message/ );
14              
15             sub prepare_action {
16 17     17 1 1429     my $c = shift;
17 17         199     my $path = $c->req->path;
18 17         1156     my $config = $c->config->{static};
19                 
20 17         962     $path =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  0         0  
21              
22             # is the URI in a static-defined path?
23 17         161     foreach my $dir ( @{ $config->{dirs} } ) {
  17         232  
24 12         2322         my $dir_re = quotemeta $dir;
25 12 100       629         my $re = ( $dir =~ m{^qr/}xms ) ? eval $dir : qr/^${dir_re}/;
26 12 50       129         if ($@) {
27 0         0             $c->error( "Error compiling static dir regex '$dir': $@" );
28                     }
29 12 100       174         if ( $path =~ $re ) {
30 3 100       52             if ( $c->_locate_static_file( $path ) ) {
31 2 50       112                 $c->_debug_msg( 'from static directory' )
32                                 if $config->{debug};
33                         } else {
34 1 50       15                 $c->_debug_msg( "404: file not found: $path" )
35                                 if $config->{debug};
36 1         335                 $c->res->status( 404 );
37                         }
38                     }
39                 }
40                 
41             # Does the path have an extension?
42 17 100       375     if ( $path =~ /.*\.(\S{1,})$/xms ) {
43             # and does it exist?
44 16         270         $c->_locate_static_file( $path );
45                 }
46                 
47 17         777     return $c->NEXT::ACTUAL::prepare_action(@_);
48             }
49              
50             sub dispatch {
51 17     17 1 327     my $c = shift;
52                 
53 17 100       698     return if ( $c->res->status != 200 );
54                 
55 16 100       247     if ( $c->_static_file ) {
56 10 50 33     485         if ( $c->config->{static}{no_logs} && $c->log->can('abort') ) {
57 10         833            $c->log->abort( 1 );
58                     }
59 10         448         return $c->_serve_static;
60                 }
61                 else {
62 6         212         return $c->NEXT::ACTUAL::dispatch(@_);
63                 }
64             }
65              
66             sub finalize {
67 17     17 1 170     my $c = shift;
68                 
69             # display all log messages
70 17 50 33     308     if ( $c->config->{static}{debug} && scalar @{$c->_debug_msg} ) {
  0         0  
71 0         0         $c->log->debug( 'Static::Simple: ' . join q{ }, @{$c->_debug_msg} );
  0         0  
72                 }
73                 
74 17 50       1072     if ( $c->res->status =~ /^(1\d\d|[23]04)$/xms ) {
75 0         0         $c->res->headers->remove_content_headers;
76 0         0         return $c->finalize_headers;
77                 }
78                 
79 17         386     return $c->NEXT::ACTUAL::finalize(@_);
80             }
81              
82             sub setup {
83 7     7 1 80     my $c = shift;
84                 
85 7         192     $c->NEXT::setup(@_);
86                 
87 7 50       219     if ( Catalyst->VERSION le '5.33' ) {
88 0         0         require File::Slurp;
89                 }
90                 
91 7   50     211     my $config = $c->config->{static} ||= {};
92                 
93 7   50     100     $config->{dirs} ||= [];
94 7   50     118     $config->{include_path} ||= [ $c->config->{root} ];
95 7   50     107     $config->{mime_types} ||= {};
96 7   50     112     $config->{ignore_extensions} ||= [ qw/tmpl tt tt2 html xhtml/ ];
97 7   50     140     $config->{ignore_dirs} ||= [];
98 7   33     246     $config->{debug} ||= $c->debug;
99 7 50       100     $config->{no_logs} = 1 unless defined $config->{no_logs};
100                 
101             # load up a MIME::Types object, only loading types with
102             # at least 1 file extension
103 7         121     $config->{mime_types_obj} = MIME::Types->new( only_complete => 1 );
104                 
105             # preload the type index hash so it's not built on the first request
106 7         105     $config->{mime_types_obj}->create_type_index;
107             }
108              
109             # Search through all included directories for the static file
110             # Based on Template Toolkit INCLUDE_PATH code
111             sub _locate_static_file {
112 19     19   213     my ( $c, $path ) = @_;
113                 
114 19         549     $path = File::Spec->catdir(
115                     File::Spec->no_upwards( File::Spec->splitdir( $path ) )
116                 );
117                 
118 19         545     my $config = $c->config->{static};
119 19         1526     my @ipaths = @{ $config->{include_path} };
  19         417  
120 19         183     my $dpaths;
121 19         247     my $count = 64; # maximum number of directories to search
122                 
123                 DIR_CHECK:
124 19   66     282     while ( @ipaths && --$count) {
125 27   33     341         my $dir = shift @ipaths || next DIR_CHECK;
126                     
127 27 100       727         if ( ref $dir eq 'CODE' ) {
128 2         21             eval { $dpaths = &$dir( $c ) };
  2         30  
129 2 50       42             if ($@) {
130 0         0                 $c->log->error( 'Static::Simple: include_path error: ' . $@ );
131                         } else {
132 2         22                 unshift @ipaths, @$dpaths;
133 2         33                 next DIR_CHECK;
134                         }
135                     } else {
136 25         432             $dir =~ s/(\/|\\)$//xms;
137 25 100 66     2198             if ( -d $dir && -f $dir . '/' . $path ) {
138                             
139             # do we need to ignore the file?
140 16         454                 for my $ignore ( @{ $config->{ignore_dirs} } ) {
  16         198  
141 6         59                     $ignore =~ s{(/|\\)$}{};
142 6 100       456                     if ( $path =~ /^$ignore(\/|\\)/ ) {
143 3 50       34                         $c->_debug_msg( "Ignoring directory `$ignore`" )
144                                         if $config->{debug};
145 3         43                         next DIR_CHECK;
146                                 }
147                             }
148                             
149             # do we need to ignore based on extension?
150 13         122                 for my $ignore_ext ( @{ $config->{ignore_extensions} } ) {
  13         194  
151 61 100       1668                     if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
152 2 50       22                         $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
153                                         if $config->{debug};
154 2         30                         next DIR_CHECK;
155                                 }
156                             }
157                             
158 11 50       132                 $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
159                                 if $config->{debug};
160 11         306                 return $c->_static_file( $dir . '/' . $path );
161                         }
162                     }
163                 }
164                 
165 8         286     return;
166             }
167              
168             sub _serve_static {
169 10     10   96     my $c = shift;
170                        
171 10         120     my $full_path = $c->_static_file;
172 10         248     my $type = $c->_ext_to_type( $full_path );
173 10         193     my $stat = stat $full_path;
174              
175 10         446     $c->res->headers->content_type( $type );
176 10         1042     $c->res->headers->content_length( $stat->size );
177 10         709     $c->res->headers->last_modified( $stat->mtime );
178              
179 10 50       3763     if ( Catalyst->VERSION le '5.33' ) {
180             # old File::Slurp method
181 0         0         my $content = File::Slurp::read_file( $full_path );
182 0         0         $c->res->body( $content );
183                 }
184                 else {
185             # new method, pass an IO::File object to body
186 10         210         my $fh = IO::File->new( $full_path, 'r' );
187 10 50       6757         if ( defined $fh ) {
188 10         133             binmode $fh;
189 10         202             $c->res->body( $fh );
190                     }
191                     else {
192 0         0             Catalyst::Exception->throw(
193                             message => "Unable to open $full_path for reading" );
194                     }
195                 }
196                 
197 10         233     return 1;
198             }
199              
200             # looks up the correct MIME type for the current file extension
201             sub _ext_to_type {
202 10     10   101     my ( $c, $full_path ) = @_;
203                 
204 10         119     my $config = $c->config->{static};
205                 
206 10 50       547     if ( $full_path =~ /.*\.(\S{1,})$/xms ) {
207 10         126         my $ext = $1;
208 10   100     180         my $type = $config->{mime_types}{$ext}
209                         || $config->{mime_types_obj}->mimeTypeOf( $ext );
210 10 100       177         if ( $type ) {
211 8 50       143             $c->_debug_msg( "as $type" ) if $config->{debug};
212 8 100       114             return ( ref $type ) ? $type->type : $type;
213                     }
214                     else {
215 2 50       28             $c->_debug_msg( "as text/plain (unknown extension $ext)" )
216                             if $config->{debug};
217 2         24             return 'text/plain';
218                     }
219                 }
220                 else {
221 0 0                 $c->_debug_msg( 'as text/plain (no extension)' )
222                         if $config->{debug};
223 0                   return 'text/plain';
224                 }
225             }
226              
227             sub _debug_msg {
228 0     0         my ( $c, $msg ) = @_;
229                 
230 0 0             if ( !defined $c->_static_debug_message ) {
231 0                   $c->_static_debug_message( [] );
232                 }
233                 
234 0 0             if ( $msg ) {
235 0                   push @{ $c->_static_debug_message }, $msg;
  0            
236                 }
237                 
238 0               return $c->_static_debug_message;
239             }
240              
241             1;
242             __END__
243            
244             =head1 NAME
245            
246             Catalyst::Plugin::Static::Simple - Make serving static pages painless.
247            
248             =head1 SYNOPSIS
249            
250             use Catalyst;
251             MyApp->setup( qw/Static::Simple/ );
252             # that's it; static content is automatically served by
253             # Catalyst, though you can configure things or bypass
254             # Catalyst entirely in a production environment
255            
256             =head1 DESCRIPTION
257            
258             The Static::Simple plugin is designed to make serving static content in
259             your application during development quick and easy, without requiring a
260             single line of code from you.
261            
262             This plugin detects static files by looking at the file extension in the
263             URL (such as B<.css> or B<.png> or B<.js>). The plugin uses the
264             lightweight L<MIME::Types> module to map file extensions to
265             IANA-registered MIME types, and will serve your static files with the
266             correct MIME type directly to the browser, without being processed
267             through Catalyst.
268            
269             Note that actions mapped to paths using periods (.) will still operate
270             properly.
271            
272             Though Static::Simple is designed to work out-of-the-box, you can tweak
273             the operation by adding various configuration options. In a production
274             environment, you will probably want to use your webserver to deliver
275             static content; for an example see L<USING WITH APACHE>, below.
276            
277             =head1 DEFAULT BEHAVIOR
278            
279             By default, Static::Simple will deliver all files having extensions
280             (that is, bits of text following a period (C<.>)), I<except> files
281             having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
282             C<xhtml>. These files, and all files without extensions, will be
283             processed through Catalyst. If L<MIME::Types> doesn't recognize an
284             extension, it will be served as C<text/plain>.
285            
286             To restate: files having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>,
287             and C<xhtml> I<will not> be served statically by default, they will be
288             processed by Catalyst. Thus if you want to use C<.html> files from
289             within a Catalyst app as static files, you need to change the
290             configuration of Static::Simple. Note also that files having any other
291             extension I<will> be served statically, so if you're using any other
292             extension for template files, you should also change the configuration.
293            
294             Logging of static files is turned off by default.
295            
296             =head1 ADVANCED CONFIGURATION
297            
298             Configuration is completely optional and is specified within
299             C<MyApp-E<gt>config-E<gt>{static}>. If you use any of these options,
300             this module will probably feel less "simple" to you!
301            
302             =head2 Enabling request logging
303            
304             Since Catalyst 5.50, logging of static requests is turned off by
305             default; static requests tend to clutter the log output and rarely
306             reveal anything useful. However, if you want to enable logging of static
307             requests, you can do so by setting
308             C<MyApp-E<gt>config-E<gt>{static}-E<gt>{no_logs}> to 0.
309            
310             =head2 Forcing directories into static mode
311            
312             Define a list of top-level directories beneath your 'root' directory
313             that should always be served in static mode. Regular expressions may be
314             specified using C<qr//>.
315            
316             MyApp->config->{static}->{dirs} = [
317             'static',
318             qr/^(images|css)/,
319             ];
320            
321             =head2 Including additional directories
322            
323             You may specify a list of directories in which to search for your static
324             files. The directories will be searched in order and will return the
325             first file found. Note that your root directory is B<not> automatically
326             added to the search path when you specify an C<include_path>. You should
327             use C<MyApp-E<gt>config-E<gt>{root}> to add it.
328            
329             MyApp->config->{static}->{include_path} = [
330             '/path/to/overlay',
331             \&incpath_generator,
332             MyApp->config->{root}
333             ];
334            
335             With the above setting, a request for the file C</images/logo.jpg> will search
336             for the following files, returning the first one found:
337            
338             /path/to/overlay/images/logo.jpg
339             /dynamic/path/images/logo.jpg
340             /your/app/home/root/images/logo.jpg
341            
342             The include path can contain a subroutine reference to dynamically return a
343             list of available directories. This method will receive the C<$c> object as a
344             parameter and should return a reference to a list of directories. Errors can
345             be reported using C<die()>. This method will be called every time a file is
346             requested that appears to be a static file (i.e. it has an extension).
347            
348             For example:
349            
350             sub incpath_generator {
351             my $c = shift;
352            
353             if ( $c->session->{customer_dir} ) {
354             return [ $c->session->{customer_dir} ];
355             } else {
356             die "No customer dir defined.";
357             }
358             }
359            
360             =head2 Ignoring certain types of files
361            
362             There are some file types you may not wish to serve as static files.
363             Most important in this category are your raw template files. By
364             default, files with the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
365             C<xhtml> will be ignored by Static::Simple in the interest of security.
366             If you wish to define your own extensions to ignore, use the
367             C<ignore_extensions> option:
368            
369             MyApp->config->{static}->{ignore_extensions}
370             = [ qw/html asp php/ ];
371            
372             =head2 Ignoring entire directories
373            
374             To prevent an entire directory from being served statically, you can use
375             the C<ignore_dirs> option. This option contains a list of relative
376             directory paths to ignore. If using C<include_path>, the path will be
377             checked against every included path.
378            
379             MyApp->config->{static}->{ignore_dirs} = [ qw/tmpl css/ ];
380            
381             For example, if combined with the above C<include_path> setting, this
382             C<ignore_dirs> value will ignore the following directories if they exist:
383            
384             /path/to/overlay/tmpl
385             /path/to/overlay/css
386             /dynamic/path/tmpl
387             /dynamic/path/css
388             /your/app/home/root/tmpl
389             /your/app/home/root/css
390            
391             =head2 Custom MIME types
392            
393             To override or add to the default MIME types set by the L<MIME::Types>
394             module, you may enter your own extension to MIME type mapping.
395            
396             MyApp->config->{static}->{mime_types} = {
397             jpg => 'image/jpg',
398             png => 'image/png',
399             };
400            
401             =head2 Compatibility with other plugins
402            
403             Since version 0.12, Static::Simple plays nice with other plugins. It no
404             longer short-circuits the C<prepare_action> stage as it was causing too
405             many compatibility issues with other plugins.
406            
407             =head2 Debugging information
408            
409             Enable additional debugging information printed in the Catalyst log. This
410             is automatically enabled when running Catalyst in -Debug mode.
411            
412             MyApp->config->{static}->{debug} = 1;
413            
414             =head1 USING WITH APACHE
415            
416             While Static::Simple will work just fine serving files through Catalyst in
417             mod_perl, for increased performance, you may wish to have Apache handle the
418             serving of your static files. To do this, simply use a dedicated directory
419             for your static files and configure an Apache Location block for that
420             directory. This approach is recommended for production installations.
421            
422             <Location /static>
423             SetHandler default-handler
424             </Location>
425            
426             Using this approach Apache will bypass any handling of these directories
427             through Catalyst. You can leave Static::Simple as part of your
428             application, and it will continue to function on a development server,
429             or using Catalyst's built-in server.
430            
431             =head1 INTERNAL EXTENDED METHODS
432            
433             Static::Simple extends the following steps in the Catalyst process.
434            
435             =head2 prepare_action
436            
437             C<prepare_action> is used to first check if the request path is a static
438             file. If so, we skip all other C<prepare_action> steps to improve
439             performance.
440            
441             =head2 dispatch
442            
443             C<dispatch> takes the file found during C<prepare_action> and writes it
444             to the output.
445            
446             =head2 finalize
447            
448             C<finalize> serves up final header information and displays any log
449             messages.
450            
451             =head2 setup
452            
453             C<setup> initializes all default values.
454            
455             =head1 SEE ALSO
456            
457             L<Catalyst>, L<Catalyst::Plugin::Static>,
458             L<http://www.iana.org/assignments/media-types/>
459            
460             =head1 AUTHOR
461            
462             Andy Grundman, <andy@hybridized.org>
463            
464             =head1 CONTRIBUTORS
465            
466             Marcus Ramberg, <mramberg@cpan.org>
467             Jesse Sheidlower, <jester@panix.com>
468            
469             =head1 THANKS
470            
471             The authors of Catalyst::Plugin::Static:
472            
473             Sebastian Riedel
474             Christian Hansen
475             Marcus Ramberg
476            
477             For the include_path code from Template Toolkit:
478            
479             Andy Wardley
480            
481             =head1 COPYRIGHT
482            
483             This program is free software, you can redistribute it and/or modify it under
484             the same terms as Perl itself.
485            
486             =cut
487