Removed the 304 Not Modified code from Static::Simple, it breaks under IE+Apache
[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 IO::File;
8 use MIME::Types;
9 use NEXT;
10
11 our $VERSION = '0.11';
12
13 __PACKAGE__->mk_classdata( qw/_static_mime_types/ );
14 __PACKAGE__->mk_accessors( qw/_static_file
15                               _static_debug_message/ );
16
17 sub prepare_action {
18     my $c = shift;
19     my $path = $c->req->path;
20
21     # is the URI in a static-defined path?
22     foreach my $dir ( @{ $c->config->{static}->{dirs} } ) {
23         my $re = ( $dir =~ /^qr\//xms ) ? eval $dir : qr/^${dir}/;
24         if ($@) {
25             $c->error( "Error compiling static dir regex '$dir': $@" );
26         }
27         if ( $path =~ $re ) {
28             if ( $c->_locate_static_file ) {
29                 $c->_debug_msg( 'from static directory' )
30                     if ( $c->config->{static}->{debug} );
31                 return;
32             } else {
33                 $c->_debug_msg( "404: file not found: $path" )
34                     if ( $c->config->{static}->{debug} );
35                 $c->res->status( 404 );
36                 return;
37             }
38         }
39     }
40     
41     # Does the path have an extension?
42     if ( $path =~ /.*\.(\S{1,})$/xms ) {
43         # and does it exist?
44         return if ( $c->_locate_static_file );
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     if ( $c->res->status =~ /^(1\d\d|[23]04)$/xms ) {
75         $c->res->headers->remove_content_headers;
76         return $c->finalize_headers;
77     }
78     
79     return $c->NEXT::ACTUAL::finalize(@_);
80 }
81
82 sub setup {
83     my $c = shift;
84     
85     $c->NEXT::setup(@_);
86     
87     if ( Catalyst->VERSION le '5.33' ) {
88         require File::Slurp;
89     }
90     
91     $c->config->{static}->{dirs} ||= [];
92     $c->config->{static}->{include_path} ||= [ $c->config->{root} ];
93     $c->config->{static}->{mime_types} ||= {};
94     $c->config->{static}->{ignore_extensions} ||= [ qw/tt tt2 html xhtml/ ];
95     $c->config->{static}->{ignore_dirs} ||= [];
96     $c->config->{static}->{debug} ||= $c->debug;
97     if ( ! defined $c->config->{static}->{no_logs} ) {
98         $c->config->{static}->{no_logs} = 1;
99     }    
100     
101     # load up a MIME::Types object, only loading types with
102     # at least 1 file extension
103     $c->_static_mime_types( MIME::Types->new( only_complete => 1 ) );
104     
105     # preload the type index hash so it's not built on the first request
106     $c->_static_mime_types->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     my $c = shift;
113     
114     my $path = $c->req->path;
115     
116     my @ipaths = @{ $c->config->{static}->{include_path} };
117     my $dpaths;
118     my $count = 64; # maximum number of directories to search
119     
120     DIR_CHECK:
121     while ( @ipaths && --$count) {
122         my $dir = shift @ipaths || next DIR_CHECK;
123         
124         if ( ref $dir eq 'CODE' ) {
125             eval { $dpaths = &$dir( $c ) };
126             if ($@) {
127                 $c->log->error( 'Static::Simple: include_path error: ' . $@ );
128             } else {
129                 unshift @ipaths, @$dpaths;
130                 next DIR_CHECK;
131             }
132         } else {
133             $dir =~ s/\/$//xms;
134             if ( -d $dir && -f $dir . '/' . $path ) {
135                 
136                 # do we need to ignore the file?
137                 for my $ignore ( @{ $c->config->{static}->{ignore_dirs} } ) {
138                     $ignore =~ s{/$}{};
139                     if ( $path =~ /^$ignore\// ) {
140                         $c->_debug_msg( "Ignoring directory `$ignore`" )
141                             if ( $c->config->{static}->{debug} );
142                         next DIR_CHECK;
143                     }
144                 }
145                 
146                 # do we need to ignore based on extension?
147                 for my $ignore_ext 
148                     ( @{ $c->config->{static}->{ignore_extensions} } ) {
149                         if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
150                             $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
151                                 if ( $c->config->{static}->{debug} );
152                             next DIR_CHECK;
153                         }
154                 }
155                 
156                 $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
157                     if ( $c->config->{static}->{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 $path = $c->req->path;    
170     my $type = $c->_ext_to_type;
171     
172     my $full_path = $c->_static_file;
173     my $stat = stat $full_path;
174
175     $c->res->headers->content_type( $type );
176     $c->res->headers->content_length( $stat->size );
177     $c->res->headers->last_modified( $stat->mtime );
178
179     if ( Catalyst->VERSION le '5.33' ) {
180         # old File::Slurp method
181         my $content = File::Slurp::read_file( $full_path );
182         $c->res->body( $content );
183     }
184     else {
185         # new method, pass an IO::File object to body
186         my $fh = IO::File->new( $full_path, 'r' );
187         if ( defined $fh ) {
188             binmode $fh;
189             $c->res->body( $fh );
190         }
191         else {
192             Catalyst::Exception->throw( 
193                 message => "Unable to open $full_path for reading" );
194         }
195     }
196     
197     return 1;
198 }
199
200 # looks up the correct MIME type for the current file extension
201 sub _ext_to_type {
202     my $c = shift;
203     my $path = $c->req->path;
204     
205     if ( $path =~ /.*\.(\S{1,})$/xms ) {
206         my $ext = $1;
207         my $user_types = $c->config->{static}->{mime_types};
208         my $type = $user_types->{$ext} 
209                 || $c->_static_mime_types->mimeTypeOf( $ext );
210         if ( $type ) {
211             $c->_debug_msg( "as $type" )
212                 if ( $c->config->{static}->{debug} );            
213             return ( ref $type ) ? $type->type : $type;
214         }
215         else {
216             $c->_debug_msg( "as text/plain (unknown extension $ext)" )
217                 if ( $c->config->{static}->{debug} );
218             return 'text/plain';
219         }
220     }
221     else {
222         $c->_debug_msg( 'as text/plain (no extension)' )
223             if ( $c->config->{static}->{debug} );
224         return 'text/plain';
225     }
226 }
227
228 sub _debug_msg {
229     my ( $c, $msg ) = @_;
230     
231     if ( !defined $c->_static_debug_message ) {
232         $c->_static_debug_message( [] );
233     }
234     
235     if ( $msg ) {
236         push @{ $c->_static_debug_message }, $msg;
237     }
238     
239     return $c->_static_debug_message;
240 }
241
242 1;
243 __END__
244
245 =head1 NAME
246
247 Catalyst::Plugin::Static::Simple - Make serving static pages painless.
248
249 =head1 SYNOPSIS
250
251     use Catalyst;
252     MyApp->setup( qw/Static::Simple/ );
253
254 =head1 DESCRIPTION
255
256 The Static::Simple plugin is designed to make serving static content in your
257 application during development quick and easy, without requiring a single
258 line of code from you.
259
260 It will detect static files used in your application by looking for file
261 extensions in the URI.  By default, you can simply load this plugin and it
262 will immediately begin serving your static files with the correct MIME type.
263 The light-weight MIME::Types module is used to map file extensions to
264 IANA-registered MIME types.
265
266 Note that actions mapped to paths using periods (.) will still operate
267 properly.
268
269 You may further tweak the operation by adding configuration options, described
270 below.
271
272 =head1 ADVANCED CONFIGURATION
273
274 Configuration is completely optional and is specified within 
275 MyApp->config->{static}.  If you use any of these options, the module will
276 probably feel less "simple" to you!
277
278 =head2 Aborting request logging
279
280 Since Catalyst 5.50, there has been added support for dropping logging for a 
281 request. This is enabled by default for static files, as static requests tend
282 to clutter the log output.  However, if you want logging of static requests, 
283 you can enable it by setting MyApp->config->{static}->{no_logs} to 0.
284
285 =head2 Forcing directories into static mode
286
287 Define a list of top-level directories beneath your 'root' directory that
288 should always be served in static mode.  Regular expressions may be
289 specified using qr//.
290
291     MyApp->config->{static}->{dirs} = [
292         'static',
293         qr/^(images|css)/,
294     ];
295
296 =head2 Including additional directories
297
298 You may specify a list of directories in which to search for your static
299 files.  The directories will be searched in order and will return the first
300 file found.  Note that your root directory is B<not> automatically added to
301 the search path when you specify an include_path.  You should use
302 MyApp->config->{root} to add it.
303
304     MyApp->config->{static}->{include_path} = [
305         '/path/to/overlay',
306         \&incpath_generator,
307         MyApp->config->{root}
308     ];
309     
310 With the above setting, a request for the file /images/logo.jpg will search
311 for the following files, returning the first one found:
312
313     /path/to/overlay/images/logo.jpg
314     /dynamic/path/images/logo.jpg
315     /your/app/home/root/images/logo.jpg
316     
317 The include path can contain a subroutine reference to dynamically return a
318 list of available directories.  This method will receive the $c object as a
319 parameter and should return a reference to a list of directories.  Errors can
320 be reported using die().  This method will be called every time a file is
321 requested that appears to be a static file (i.e. it has an extension).
322
323 For example:
324
325     sub incpath_generator {
326         my $c = shift;
327         
328         if ( $c->session->{customer_dir} ) {
329             return [ $c->session->{customer_dir} ];
330         } else {
331             die "No customer dir defined.";
332         }
333     }
334     
335 =head2 Ignoring certain types of files
336
337 There are some file types you may not wish to serve as static files.  Most
338 important in this category are your raw template files.  By default, files
339 with the extensions tt, tt2, html, and xhtml will be ignored by Static::Simple
340 in the interest of security.  If you wish to define your own extensions to
341 ignore, use the ignore_extensions option:
342
343     MyApp->config->{static}->{ignore_extensions} = [ qw/tt tt2 html xhtml/ ];
344     
345 =head2 Ignoring entire directories
346
347 To prevent an entire directory from being served statically, you can use the
348 ignore_dirs option.  This option contains a list of relative directory paths
349 to ignore.  If using include_path, the path will be checked against every
350 included path.
351
352     MyApp->config->{static}->{ignore_dirs} = [ qw/tmpl css/ ];
353     
354 For example, if combined with the above include_path setting, this
355 ignore_dirs value will ignore the following directories if they exist:
356
357     /path/to/overlay/tmpl
358     /path/to/overlay/css
359     /dynamic/path/tmpl
360     /dynamic/path/css
361     /your/app/home/root/tmpl
362     /your/app/home/root/css    
363
364 =head2 Custom MIME types
365
366 To override or add to the default MIME types set by the MIME::Types module,
367 you may enter your own extension to MIME type mapping. 
368
369     MyApp->config->{static}->{mime_types} = {
370         jpg => 'image/jpg',
371         png => 'image/png',
372     };
373
374 =head2 Bypassing other plugins
375
376 This plugin checks for a static file in the prepare_action stage.  If the
377 request is for a static file, it will bypass all remaining prepare_action
378 steps.  This means that by placing Static::Simple before all other plugins,
379 they will not execute when a static file is found.  This can be helpful by
380 skipping session cookie checks for example.  Or, if you want some plugins
381 to run even on static files, list them before Static::Simple.
382
383 Currently, work done by plugins in any other prepare method will execute
384 normally.
385
386 =head2 Debugging information
387
388 Enable additional debugging information printed in the Catalyst log.  This
389 is automatically enabled when running Catalyst in -Debug mode.
390
391     MyApp->config->{static}->{debug} = 1;
392     
393 =head1 USING WITH APACHE
394
395 While Static::Simple will work just fine serving files through Catalyst in
396 mod_perl, for increased performance, you may wish to have Apache handle the
397 serving of your static files.  To do this, simply use a dedicated directory
398 for your static files and configure an Apache Location block for that
399 directory.  This approach is recommended for production installations.
400
401     <Location /static>
402         SetHandler default-handler
403     </Location>
404
405 =head1 INTERNAL EXTENDED METHODS
406
407 Static::Simple extends the following steps in the Catalyst process.
408
409 =head2 prepare_action 
410
411 prepare_action is used to first check if the request path is a static file.
412 If so, we skip all other prepare_action steps to improve performance.
413
414 =head2 dispatch
415
416 dispatch takes the file found during prepare_action and writes it to the
417 output.
418
419 =head2 finalize
420
421 finalize serves up final header information and displays any log messages.
422
423 =head2 setup
424
425 setup initializes all default values.
426
427 =head1 SEE ALSO
428
429 L<Catalyst>, L<Catalyst::Plugin::Static>, 
430 L<http://www.iana.org/assignments/media-types/>
431
432 =head1 AUTHOR
433
434 Andy Grundman, <andy@hybridized.org>
435
436 =head1 CONTRIBUTORS
437
438 Marcus Ramberg, <mramberg@cpan.org>
439
440 =head1 THANKS
441
442 The authors of Catalyst::Plugin::Static:
443
444     Sebastian Riedel
445     Christian Hansen
446     Marcus Ramberg
447
448 For the include_path code from Template Toolkit:
449
450     Andy Wardley
451
452 =head1 COPYRIGHT
453
454 This program is free software, you can redistribute it and/or modify it under
455 the same terms as Perl itself.
456
457 =cut