708b2656e932d37d067a07f4b0184993c35c05bb
[catagits/Catalyst-Runtime.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     # the below code all from C::P::Static
176     if ( $c->req->headers->if_modified_since ) {
177         if ( $c->req->headers->if_modified_since == $stat->mtime ) {
178             $c->res->status( 304 ); # Not Modified
179             $c->res->headers->remove_content_headers;
180             return 1;
181         }
182     }
183     
184     $c->res->headers->content_type( $type );
185     $c->res->headers->content_length( $stat->size );
186     $c->res->headers->last_modified( $stat->mtime );
187
188     if ( Catalyst->VERSION le '5.33' ) {
189         # old File::Slurp method
190         my $content = File::Slurp::read_file( $full_path );
191         $c->res->body( $content );
192     }
193     else {
194         # new method, pass an IO::File object to body
195         my $fh = IO::File->new( $full_path, 'r' );
196         if ( defined $fh ) {
197             binmode $fh;
198             $c->res->body( $fh );
199         }
200         else {
201             Catalyst::Exception->throw( 
202                 message => "Unable to open $full_path for reading" );
203         }
204     }
205     
206     return 1;
207 }
208
209 # looks up the correct MIME type for the current file extension
210 sub _ext_to_type {
211     my $c = shift;
212     my $path = $c->req->path;
213     
214     if ( $path =~ /.*\.(\S{1,})$/xms ) {
215         my $ext = $1;
216         my $user_types = $c->config->{static}->{mime_types};
217         my $type = $user_types->{$ext} 
218                 || $c->_static_mime_types->mimeTypeOf( $ext );
219         if ( $type ) {
220             $c->_debug_msg( "as $type" )
221                 if ( $c->config->{static}->{debug} );            
222             return ( ref $type ) ? $type->type : $type;
223         }
224         else {
225             $c->_debug_msg( "as text/plain (unknown extension $ext)" )
226                 if ( $c->config->{static}->{debug} );
227             return 'text/plain';
228         }
229     }
230     else {
231         $c->_debug_msg( 'as text/plain (no extension)' )
232             if ( $c->config->{static}->{debug} );
233         return 'text/plain';
234     }
235 }
236
237 sub _debug_msg {
238     my ( $c, $msg ) = @_;
239     
240     if ( !defined $c->_static_debug_message ) {
241         $c->_static_debug_message( [] );
242     }
243     
244     if ( $msg ) {
245         push @{ $c->_static_debug_message }, $msg;
246     }
247     
248     return $c->_static_debug_message;
249 }
250
251 1;
252 __END__
253
254 =head1 NAME
255
256 Catalyst::Plugin::Static::Simple - Make serving static pages painless.
257
258 =head1 SYNOPSIS
259
260     use Catalyst;
261     MyApp->setup( qw/Static::Simple/ );
262
263 =head1 DESCRIPTION
264
265 The Static::Simple plugin is designed to make serving static content in your
266 application during development quick and easy, without requiring a single
267 line of code from you.
268
269 It will detect static files used in your application by looking for file
270 extensions in the URI.  By default, you can simply load this plugin and it
271 will immediately begin serving your static files with the correct MIME type.
272 The light-weight MIME::Types module is used to map file extensions to
273 IANA-registered MIME types.
274
275 Note that actions mapped to paths using periods (.) will still operate
276 properly.
277
278 You may further tweak the operation by adding configuration options, described
279 below.
280
281 =head1 ADVANCED CONFIGURATION
282
283 Configuration is completely optional and is specified within 
284 MyApp->config->{static}.  If you use any of these options, the module will
285 probably feel less "simple" to you!
286
287 =head2 Aborting request logging
288
289 Since Catalyst 5.50, there has been added support for dropping logging for a 
290 request. This is enabled by default for static files, as static requests tend
291 to clutter the log output.  However, if you want logging of static requests, 
292 you can enable it by setting MyApp->config->{static}->{no_logs} to 0.
293
294 =head2 Forcing directories into static mode
295
296 Define a list of top-level directories beneath your 'root' directory that
297 should always be served in static mode.  Regular expressions may be
298 specified using qr//.
299
300     MyApp->config->{static}->{dirs} = [
301         'static',
302         qr/^(images|css)/,
303     ];
304
305 =head2 Including additional directories
306
307 You may specify a list of directories in which to search for your static
308 files.  The directories will be searched in order and will return the first
309 file found.  Note that your root directory is B<not> automatically added to
310 the search path when you specify an include_path.  You should use
311 MyApp->config->{root} to add it.
312
313     MyApp->config->{static}->{include_path} = [
314         '/path/to/overlay',
315         \&incpath_generator,
316         MyApp->config->{root}
317     ];
318     
319 With the above setting, a request for the file /images/logo.jpg will search
320 for the following files, returning the first one found:
321
322     /path/to/overlay/images/logo.jpg
323     /dynamic/path/images/logo.jpg
324     /your/app/home/root/images/logo.jpg
325     
326 The include path can contain a subroutine reference to dynamically return a
327 list of available directories.  This method will receive the $c object as a
328 parameter and should return a reference to a list of directories.  Errors can
329 be reported using die().  This method will be called every time a file is
330 requested that appears to be a static file (i.e. it has an extension).
331
332 For example:
333
334     sub incpath_generator {
335         my $c = shift;
336         
337         if ( $c->session->{customer_dir} ) {
338             return [ $c->session->{customer_dir} ];
339         } else {
340             die "No customer dir defined.";
341         }
342     }
343     
344 =head2 Ignoring certain types of files
345
346 There are some file types you may not wish to serve as static files.  Most
347 important in this category are your raw template files.  By default, files
348 with the extensions tt, tt2, html, and xhtml will be ignored by Static::Simple
349 in the interest of security.  If you wish to define your own extensions to
350 ignore, use the ignore_extensions option:
351
352     MyApp->config->{static}->{ignore_extensions} = [ qw/tt tt2 html xhtml/ ];
353     
354 =head2 Ignoring entire directories
355
356 To prevent an entire directory from being served statically, you can use the
357 ignore_dirs option.  This option contains a list of relative directory paths
358 to ignore.  If using include_path, the path will be checked against every
359 included path.
360
361     MyApp->config->{static}->{ignore_dirs} = [ qw/tmpl css/ ];
362     
363 For example, if combined with the above include_path setting, this
364 ignore_dirs value will ignore the following directories if they exist:
365
366     /path/to/overlay/tmpl
367     /path/to/overlay/css
368     /dynamic/path/tmpl
369     /dynamic/path/css
370     /your/app/home/root/tmpl
371     /your/app/home/root/css    
372
373 =head2 Custom MIME types
374
375 To override or add to the default MIME types set by the MIME::Types module,
376 you may enter your own extension to MIME type mapping. 
377
378     MyApp->config->{static}->{mime_types} = {
379         jpg => 'image/jpg',
380         png => 'image/png',
381     };
382
383 =head2 Bypassing other plugins
384
385 This plugin checks for a static file in the prepare_action stage.  If the
386 request is for a static file, it will bypass all remaining prepare_action
387 steps.  This means that by placing Static::Simple before all other plugins,
388 they will not execute when a static file is found.  This can be helpful by
389 skipping session cookie checks for example.  Or, if you want some plugins
390 to run even on static files, list them before Static::Simple.
391
392 Currently, work done by plugins in any other prepare method will execute
393 normally.
394
395 =head2 Debugging information
396
397 Enable additional debugging information printed in the Catalyst log.  This
398 is automatically enabled when running Catalyst in -Debug mode.
399
400     MyApp->config->{static}->{debug} = 1;
401     
402 =head1 USING WITH APACHE
403
404 While Static::Simple will work just fine serving files through Catalyst in
405 mod_perl, for increased performance, you may wish to have Apache handle the
406 serving of your static files.  To do this, simply use a dedicated directory
407 for your static files and configure an Apache Location block for that
408 directory.  This approach is recommended for production installations.
409
410     <Location /static>
411         SetHandler default-handler
412     </Location>
413
414 =head1 INTERNAL EXTENDED METHODS
415
416 Static::Simple extends the following steps in the Catalyst process.
417
418 =head2 prepare_action 
419
420 prepare_action is used to first check if the request path is a static file.
421 If so, we skip all other prepare_action steps to improve performance.
422
423 =head2 dispatch
424
425 dispatch takes the file found during prepare_action and writes it to the
426 output.
427
428 =head2 finalize
429
430 finalize serves up final header information and displays any log messages.
431
432 =head2 setup
433
434 setup initializes all default values.
435
436 =head1 SEE ALSO
437
438 L<Catalyst>, L<Catalyst::Plugin::Static>, 
439 L<http://www.iana.org/assignments/media-types/>
440
441 =head1 AUTHOR
442
443 Andy Grundman, <andy@hybridized.org>
444
445 =head1 CONTRIBUTORS
446
447 Marcus Ramberg, <mramberg@cpan.org>
448
449 =head1 THANKS
450
451 The authors of Catalyst::Plugin::Static:
452
453     Sebastian Riedel
454     Christian Hansen
455     Marcus Ramberg
456
457 For the include_path code from Template Toolkit:
458
459     Andy Wardley
460
461 =head1 COPYRIGHT
462
463 This program is free software, you can redistribute it and/or modify it under
464 the same terms as Perl itself.
465
466 =cut