Static::Simple 0.12, play nice with other plugins
[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.12';
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             } else {
32                 $c->_debug_msg( "404: file not found: $path" )
33                     if ( $c->config->{static}->{debug} );
34                 $c->res->status( 404 );
35             }
36         }
37     }
38     
39     # Does the path have an extension?
40     if ( $path =~ /.*\.(\S{1,})$/xms ) {
41         # and does it exist?
42         $c->_locate_static_file;
43     }
44     
45     return $c->NEXT::ACTUAL::prepare_action(@_);
46 }
47
48 sub dispatch {
49     my $c = shift;
50     
51     return if ( $c->res->status != 200 );
52     
53     if ( $c->_static_file ) {
54         if ( $c->config->{static}->{no_logs} && $c->log->can('abort') ) {
55            $c->log->abort( 1 );
56         }
57         return $c->_serve_static;
58     }
59     else {
60         return $c->NEXT::ACTUAL::dispatch(@_);
61     }
62 }
63
64 sub finalize {
65     my $c = shift;
66     
67     # display all log messages
68     if ( $c->config->{static}->{debug} && scalar @{$c->_debug_msg} ) {
69         $c->log->debug( 'Static::Simple: ' . join q{ }, @{$c->_debug_msg} );
70     }
71     
72     if ( $c->res->status =~ /^(1\d\d|[23]04)$/xms ) {
73         $c->res->headers->remove_content_headers;
74         return $c->finalize_headers;
75     }
76     
77     return $c->NEXT::ACTUAL::finalize(@_);
78 }
79
80 sub setup {
81     my $c = shift;
82     
83     $c->NEXT::setup(@_);
84     
85     if ( Catalyst->VERSION le '5.33' ) {
86         require File::Slurp;
87     }
88     
89     $c->config->{static}->{dirs} ||= [];
90     $c->config->{static}->{include_path} ||= [ $c->config->{root} ];
91     $c->config->{static}->{mime_types} ||= {};
92     $c->config->{static}->{ignore_extensions} 
93         ||= [ qw/tmpl tt tt2 html xhtml/ ];
94     $c->config->{static}->{ignore_dirs} ||= [];
95     $c->config->{static}->{debug} ||= $c->debug;
96     if ( ! defined $c->config->{static}->{no_logs} ) {
97         $c->config->{static}->{no_logs} = 1;
98     }    
99     
100     # load up a MIME::Types object, only loading types with
101     # at least 1 file extension
102     $c->_static_mime_types( MIME::Types->new( only_complete => 1 ) );
103     
104     # preload the type index hash so it's not built on the first request
105     $c->_static_mime_types->create_type_index;
106 }
107
108 # Search through all included directories for the static file
109 # Based on Template Toolkit INCLUDE_PATH code
110 sub _locate_static_file {
111     my $c = shift;
112     
113     my $path = $c->req->path;
114     
115     my @ipaths = @{ $c->config->{static}->{include_path} };
116     my $dpaths;
117     my $count = 64; # maximum number of directories to search
118     
119     DIR_CHECK:
120     while ( @ipaths && --$count) {
121         my $dir = shift @ipaths || next DIR_CHECK;
122         
123         if ( ref $dir eq 'CODE' ) {
124             eval { $dpaths = &$dir( $c ) };
125             if ($@) {
126                 $c->log->error( 'Static::Simple: include_path error: ' . $@ );
127             } else {
128                 unshift @ipaths, @$dpaths;
129                 next DIR_CHECK;
130             }
131         } else {
132             $dir =~ s/\/$//xms;
133             if ( -d $dir && -f $dir . '/' . $path ) {
134                 
135                 # do we need to ignore the file?
136                 for my $ignore ( @{ $c->config->{static}->{ignore_dirs} } ) {
137                     $ignore =~ s{/$}{};
138                     if ( $path =~ /^$ignore\// ) {
139                         $c->_debug_msg( "Ignoring directory `$ignore`" )
140                             if ( $c->config->{static}->{debug} );
141                         next DIR_CHECK;
142                     }
143                 }
144                 
145                 # do we need to ignore based on extension?
146                 for my $ignore_ext 
147                     ( @{ $c->config->{static}->{ignore_extensions} } ) {
148                         if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
149                             $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
150                                 if ( $c->config->{static}->{debug} );
151                             next DIR_CHECK;
152                         }
153                 }
154                 
155                 $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
156                     if ( $c->config->{static}->{debug} );
157                 return $c->_static_file( $dir . '/' . $path );
158             }
159         }
160     }
161     
162     return;
163 }
164
165 sub _serve_static {
166     my $c = shift;
167     
168     my $path = $c->req->path;    
169     my $type = $c->_ext_to_type;
170     
171     my $full_path = $c->_static_file;
172     my $stat = stat $full_path;
173
174     $c->res->headers->content_type( $type );
175     $c->res->headers->content_length( $stat->size );
176     $c->res->headers->last_modified( $stat->mtime );
177
178     if ( Catalyst->VERSION le '5.33' ) {
179         # old File::Slurp method
180         my $content = File::Slurp::read_file( $full_path );
181         $c->res->body( $content );
182     }
183     else {
184         # new method, pass an IO::File object to body
185         my $fh = IO::File->new( $full_path, 'r' );
186         if ( defined $fh ) {
187             binmode $fh;
188             $c->res->body( $fh );
189         }
190         else {
191             Catalyst::Exception->throw( 
192                 message => "Unable to open $full_path for reading" );
193         }
194     }
195     
196     return 1;
197 }
198
199 # looks up the correct MIME type for the current file extension
200 sub _ext_to_type {
201     my $c = shift;
202     my $path = $c->req->path;
203     
204     if ( $path =~ /.*\.(\S{1,})$/xms ) {
205         my $ext = $1;
206         my $user_types = $c->config->{static}->{mime_types};
207         my $type = $user_types->{$ext} 
208                 || $c->_static_mime_types->mimeTypeOf( $ext );
209         if ( $type ) {
210             $c->_debug_msg( "as $type" )
211                 if ( $c->config->{static}->{debug} );            
212             return ( ref $type ) ? $type->type : $type;
213         }
214         else {
215             $c->_debug_msg( "as text/plain (unknown extension $ext)" )
216                 if ( $c->config->{static}->{debug} );
217             return 'text/plain';
218         }
219     }
220     else {
221         $c->_debug_msg( 'as text/plain (no extension)' )
222             if ( $c->config->{static}->{debug} );
223         return 'text/plain';
224     }
225 }
226
227 sub _debug_msg {
228     my ( $c, $msg ) = @_;
229     
230     if ( !defined $c->_static_debug_message ) {
231         $c->_static_debug_message( [] );
232     }
233     
234     if ( $msg ) {
235         push @{ $c->_static_debug_message }, $msg;
236     }
237     
238     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
253 =head1 DESCRIPTION
254
255 The Static::Simple plugin is designed to make serving static content in your
256 application during development quick and easy, without requiring a single
257 line of code from you.
258
259 It will detect static files used in your application by looking for file
260 extensions in the URI.  By default, you can simply load this plugin and it
261 will immediately begin serving your static files with the correct MIME type.
262 The light-weight MIME::Types module is used to map file extensions to
263 IANA-registered MIME types.
264
265 Note that actions mapped to paths using periods (.) will still operate
266 properly.
267
268 You may further tweak the operation by adding configuration options, described
269 below.
270
271 =head1 ADVANCED CONFIGURATION
272
273 Configuration is completely optional and is specified within 
274 MyApp->config->{static}.  If you use any of these options, the module will
275 probably feel less "simple" to you!
276
277 =head2 Aborting request logging
278
279 Since Catalyst 5.50, there has been added support for dropping logging for a 
280 request. This is enabled by default for static files, as static requests tend
281 to clutter the log output.  However, if you want logging of static requests, 
282 you can enable it by setting MyApp->config->{static}->{no_logs} to 0.
283
284 =head2 Forcing directories into static mode
285
286 Define a list of top-level directories beneath your 'root' directory that
287 should always be served in static mode.  Regular expressions may be
288 specified using qr//.
289
290     MyApp->config->{static}->{dirs} = [
291         'static',
292         qr/^(images|css)/,
293     ];
294
295 =head2 Including additional directories
296
297 You may specify a list of directories in which to search for your static
298 files.  The directories will be searched in order and will return the first
299 file found.  Note that your root directory is B<not> automatically added to
300 the search path when you specify an include_path.  You should use
301 MyApp->config->{root} to add it.
302
303     MyApp->config->{static}->{include_path} = [
304         '/path/to/overlay',
305         \&incpath_generator,
306         MyApp->config->{root}
307     ];
308     
309 With the above setting, a request for the file /images/logo.jpg will search
310 for the following files, returning the first one found:
311
312     /path/to/overlay/images/logo.jpg
313     /dynamic/path/images/logo.jpg
314     /your/app/home/root/images/logo.jpg
315     
316 The include path can contain a subroutine reference to dynamically return a
317 list of available directories.  This method will receive the $c object as a
318 parameter and should return a reference to a list of directories.  Errors can
319 be reported using die().  This method will be called every time a file is
320 requested that appears to be a static file (i.e. it has an extension).
321
322 For example:
323
324     sub incpath_generator {
325         my $c = shift;
326         
327         if ( $c->session->{customer_dir} ) {
328             return [ $c->session->{customer_dir} ];
329         } else {
330             die "No customer dir defined.";
331         }
332     }
333     
334 =head2 Ignoring certain types of files
335
336 There are some file types you may not wish to serve as static files.  Most
337 important in this category are your raw template files.  By default, files
338 with the extensions tmpl, tt, tt2, html, and xhtml will be ignored by
339 Static::Simple in the interest of security.  If you wish to define your own
340 extensions to ignore, use the ignore_extensions option:
341
342     MyApp->config->{static}->{ignore_extensions} 
343         = [ qw/tmpl 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 Compatibility with other plugins
375
376 Since version 0.12, Static::Simple plays nice with other plugins.  It no
377 longer short-circuits the prepare_action stage as it was causing too many
378 compatibility issues with other plugins.
379
380 =head2 Debugging information
381
382 Enable additional debugging information printed in the Catalyst log.  This
383 is automatically enabled when running Catalyst in -Debug mode.
384
385     MyApp->config->{static}->{debug} = 1;
386     
387 =head1 USING WITH APACHE
388
389 While Static::Simple will work just fine serving files through Catalyst in
390 mod_perl, for increased performance, you may wish to have Apache handle the
391 serving of your static files.  To do this, simply use a dedicated directory
392 for your static files and configure an Apache Location block for that
393 directory.  This approach is recommended for production installations.
394
395     <Location /static>
396         SetHandler default-handler
397     </Location>
398
399 =head1 INTERNAL EXTENDED METHODS
400
401 Static::Simple extends the following steps in the Catalyst process.
402
403 =head2 prepare_action 
404
405 prepare_action is used to first check if the request path is a static file.
406 If so, we skip all other prepare_action steps to improve performance.
407
408 =head2 dispatch
409
410 dispatch takes the file found during prepare_action and writes it to the
411 output.
412
413 =head2 finalize
414
415 finalize serves up final header information and displays any log messages.
416
417 =head2 setup
418
419 setup initializes all default values.
420
421 =head1 SEE ALSO
422
423 L<Catalyst>, L<Catalyst::Plugin::Static>, 
424 L<http://www.iana.org/assignments/media-types/>
425
426 =head1 AUTHOR
427
428 Andy Grundman, <andy@hybridized.org>
429
430 =head1 CONTRIBUTORS
431
432 Marcus Ramberg, <mramberg@cpan.org>
433
434 =head1 THANKS
435
436 The authors of Catalyst::Plugin::Static:
437
438     Sebastian Riedel
439     Christian Hansen
440     Marcus Ramberg
441
442 For the include_path code from Template Toolkit:
443
444     Andy Wardley
445
446 =head1 COPYRIGHT
447
448 This program is free software, you can redistribute it and/or modify it under
449 the same terms as Perl itself.
450
451 =cut