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