Merged latest Static::Simple into core
[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 File::Spec::Functions qw/catdir no_upwards splitdir/;
8 use IO::File;
9 use MIME::Types;
10 use NEXT;
11
12 our $VERSION = '0.12';
13
14 __PACKAGE__->mk_classdata( qw/_static_mime_types/ );
15 __PACKAGE__->mk_accessors( qw/_static_file
16                               _static_debug_message/ );
17
18 sub prepare_action {
19     my $c = shift;
20     my $path = $c->req->path;
21
22     # is the URI in a static-defined path?
23     foreach my $dir ( @{ $c->config->{static}->{dirs} } ) {
24         my $re = ( $dir =~ /^qr\//xms ) ? eval $dir : qr/^${dir}/;
25         if ($@) {
26             $c->error( "Error compiling static dir regex '$dir': $@" );
27         }
28         if ( $path =~ $re ) {
29             if ( $c->_locate_static_file ) {
30                 $c->_debug_msg( 'from static directory' )
31                     if ( $c->config->{static}->{debug} );
32             } else {
33                 $c->_debug_msg( "404: file not found: $path" )
34                     if ( $c->config->{static}->{debug} );
35                 $c->res->status( 404 );
36             }
37         }
38     }
39     
40     # Does the path have an extension?
41     if ( $path =~ /.*\.(\S{1,})$/xms ) {
42         # and does it exist?
43         $c->_locate_static_file;
44     }
45     
46     return $c->NEXT::ACTUAL::prepare_action(@_);
47 }
48
49 sub dispatch {
50     my $c = shift;
51     
52     return if ( $c->res->status != 200 );
53     
54     if ( $c->_static_file ) {
55         if ( $c->config->{static}->{no_logs} && $c->log->can('abort') ) {
56            $c->log->abort( 1 );
57         }
58         return $c->_serve_static;
59     }
60     else {
61         return $c->NEXT::ACTUAL::dispatch(@_);
62     }
63 }
64
65 sub finalize {
66     my $c = shift;
67     
68     # display all log messages
69     if ( $c->config->{static}->{debug} && scalar @{$c->_debug_msg} ) {
70         $c->log->debug( 'Static::Simple: ' . join q{ }, @{$c->_debug_msg} );
71     }
72     
73     if ( $c->res->status =~ /^(1\d\d|[23]04)$/xms ) {
74         $c->res->headers->remove_content_headers;
75         return $c->finalize_headers;
76     }
77     
78     return $c->NEXT::ACTUAL::finalize(@_);
79 }
80
81 sub setup {
82     my $c = shift;
83     
84     $c->NEXT::setup(@_);
85     
86     if ( Catalyst->VERSION le '5.33' ) {
87         require File::Slurp;
88     }
89     
90     $c->config->{static}->{dirs} ||= [];
91     $c->config->{static}->{include_path} ||= [ $c->config->{root} ];
92     $c->config->{static}->{mime_types} ||= {};
93     $c->config->{static}->{ignore_extensions} 
94         ||= [ qw/tmpl 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 = catdir( no_upwards( splitdir( $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 tmpl, tt, tt2, html, and xhtml will be ignored by
340 Static::Simple in the interest of security.  If you wish to define your own
341 extensions to ignore, use the ignore_extensions option:
342
343     MyApp->config->{static}->{ignore_extensions} 
344         = [ qw/tmpl tt tt2 html xhtml/ ];
345     
346 =head2 Ignoring entire directories
347
348 To prevent an entire directory from being served statically, you can use the
349 ignore_dirs option.  This option contains a list of relative directory paths
350 to ignore.  If using include_path, the path will be checked against every
351 included path.
352
353     MyApp->config->{static}->{ignore_dirs} = [ qw/tmpl css/ ];
354     
355 For example, if combined with the above include_path setting, this
356 ignore_dirs value will ignore the following directories if they exist:
357
358     /path/to/overlay/tmpl
359     /path/to/overlay/css
360     /dynamic/path/tmpl
361     /dynamic/path/css
362     /your/app/home/root/tmpl
363     /your/app/home/root/css    
364
365 =head2 Custom MIME types
366
367 To override or add to the default MIME types set by the MIME::Types module,
368 you may enter your own extension to MIME type mapping. 
369
370     MyApp->config->{static}->{mime_types} = {
371         jpg => 'image/jpg',
372         png => 'image/png',
373     };
374
375 =head2 Compatibility with other plugins
376
377 Since version 0.12, Static::Simple plays nice with other plugins.  It no
378 longer short-circuits the prepare_action stage as it was causing too many
379 compatibility issues with other plugins.
380
381 =head2 Debugging information
382
383 Enable additional debugging information printed in the Catalyst log.  This
384 is automatically enabled when running Catalyst in -Debug mode.
385
386     MyApp->config->{static}->{debug} = 1;
387     
388 =head1 USING WITH APACHE
389
390 While Static::Simple will work just fine serving files through Catalyst in
391 mod_perl, for increased performance, you may wish to have Apache handle the
392 serving of your static files.  To do this, simply use a dedicated directory
393 for your static files and configure an Apache Location block for that
394 directory.  This approach is recommended for production installations.
395
396     <Location /static>
397         SetHandler default-handler
398     </Location>
399
400 =head1 INTERNAL EXTENDED METHODS
401
402 Static::Simple extends the following steps in the Catalyst process.
403
404 =head2 prepare_action 
405
406 prepare_action is used to first check if the request path is a static file.
407 If so, we skip all other prepare_action steps to improve performance.
408
409 =head2 dispatch
410
411 dispatch takes the file found during prepare_action and writes it to the
412 output.
413
414 =head2 finalize
415
416 finalize serves up final header information and displays any log messages.
417
418 =head2 setup
419
420 setup initializes all default values.
421
422 =head1 SEE ALSO
423
424 L<Catalyst>, L<Catalyst::Plugin::Static>, 
425 L<http://www.iana.org/assignments/media-types/>
426
427 =head1 AUTHOR
428
429 Andy Grundman, <andy@hybridized.org>
430
431 =head1 CONTRIBUTORS
432
433 Marcus Ramberg, <mramberg@cpan.org>
434
435 =head1 THANKS
436
437 The authors of Catalyst::Plugin::Static:
438
439     Sebastian Riedel
440     Christian Hansen
441     Marcus Ramberg
442
443 For the include_path code from Template Toolkit:
444
445     Andy Wardley
446
447 =head1 COPYRIGHT
448
449 This program is free software, you can redistribute it and/or modify it under
450 the same terms as Perl itself.
451
452 =cut