Doc patch from initself, clarify static files dir
[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.19';
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     $config->{no_logs} = 0 if $config->{logging};
96     
97     # load up a MIME::Types object, only loading types with
98     # at least 1 file extension
99     $config->{mime_types_obj} = MIME::Types->new( only_complete => 1 );
100     
101     # preload the type index hash so it's not built on the first request
102     $config->{mime_types_obj}->create_type_index;
103 }
104
105 # Search through all included directories for the static file
106 # Based on Template Toolkit INCLUDE_PATH code
107 sub _locate_static_file {
108     my ( $c, $path, $in_static_dir ) = @_;
109     
110     $path = File::Spec->catdir(
111         File::Spec->no_upwards( File::Spec->splitdir( $path ) ) 
112     );
113     
114     my $config = $c->config->{static};
115     my @ipaths = @{ $config->{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                 # Don't ignore any files in static dirs defined with 'dirs'
136                 unless ( $in_static_dir ) {
137                     # do we need to ignore the file?
138                     for my $ignore ( @{ $config->{ignore_dirs} } ) {
139                         $ignore =~ s{(/|\\)$}{};
140                         if ( $path =~ /^$ignore(\/|\\)/ ) {
141                             $c->_debug_msg( "Ignoring directory `$ignore`" )
142                                 if $config->{debug};
143                             next DIR_CHECK;
144                         }
145                     }
146                 
147                     # do we need to ignore based on extension?
148                     for my $ignore_ext ( @{ $config->{ignore_extensions} } ) {
149                         if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
150                             $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
151                                 if $config->{debug};
152                             next DIR_CHECK;
153                         }
154                     }
155                 }
156                 
157                 $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
158                     if $config->{debug};
159                 return $c->_static_file( $dir . '/' . $path );
160             }
161         }
162     }
163     
164     return;
165 }
166
167 sub _serve_static {
168     my $c = shift;
169            
170     my $full_path = shift || $c->_static_file;
171     my $type      = $c->_ext_to_type( $full_path );
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 sub serve_static_file {
200     my ( $c, $full_path ) = @_;
201
202     my $config = $c->config->{static} ||= {};
203     
204     if ( -e $full_path ) {
205         $c->_debug_msg( "Serving static file: $full_path" )
206             if $config->{debug};
207     }
208     else {
209         $c->_debug_msg( "404: file not found: $full_path" )
210             if $config->{debug};
211         $c->res->status( 404 );
212         return;
213     }
214
215     $c->_serve_static( $full_path );
216 }
217
218 # looks up the correct MIME type for the current file extension
219 sub _ext_to_type {
220     my ( $c, $full_path ) = @_;
221     
222     my $config = $c->config->{static};
223     
224     if ( $full_path =~ /.*\.(\S{1,})$/xms ) {
225         my $ext = $1;
226         my $type = $config->{mime_types}{$ext} 
227             || $config->{mime_types_obj}->mimeTypeOf( $ext );
228         if ( $type ) {
229             $c->_debug_msg( "as $type" ) if $config->{debug};
230             return ( ref $type ) ? $type->type : $type;
231         }
232         else {
233             $c->_debug_msg( "as text/plain (unknown extension $ext)" )
234                 if $config->{debug};
235             return 'text/plain';
236         }
237     }
238     else {
239         $c->_debug_msg( 'as text/plain (no extension)' )
240             if $config->{debug};
241         return 'text/plain';
242     }
243 }
244
245 sub _debug_msg {
246     my ( $c, $msg ) = @_;
247     
248     if ( !defined $c->_static_debug_message ) {
249         $c->_static_debug_message( [] );
250     }
251     
252     if ( $msg ) {
253         push @{ $c->_static_debug_message }, $msg;
254     }
255     
256     return $c->_static_debug_message;
257 }
258
259 1;
260 __END__
261
262 =head1 NAME
263
264 Catalyst::Plugin::Static::Simple - Make serving static pages painless.
265
266 =head1 SYNOPSIS
267
268     use Catalyst;
269     MyApp->setup( qw/Static::Simple/ );
270     # that's it; static content is automatically served by Catalyst
271     # from the application's root directory, though you can configure
272     # things or bypass Catalyst entirely in a production environment
273     #
274     # one caveat: the files must be served from an absolute path
275     # (ie. /images/foo.png)
276
277 =head1 DESCRIPTION
278
279 The Static::Simple plugin is designed to make serving static content in
280 your application during development quick and easy, without requiring a
281 single line of code from you.
282
283 This plugin detects static files by looking at the file extension in the
284 URL (such as B<.css> or B<.png> or B<.js>). The plugin uses the
285 lightweight L<MIME::Types> module to map file extensions to
286 IANA-registered MIME types, and will serve your static files with the
287 correct MIME type directly to the browser, without being processed
288 through Catalyst.
289
290 Note that actions mapped to paths using periods (.) will still operate
291 properly.
292
293 Though Static::Simple is designed to work out-of-the-box, you can tweak
294 the operation by adding various configuration options. In a production
295 environment, you will probably want to use your webserver to deliver
296 static content; for an example see L<USING WITH APACHE>, below.
297
298 =head1 DEFAULT BEHAVIOR
299
300 By default, Static::Simple will deliver all files having extensions
301 (that is, bits of text following a period (C<.>)), I<except> files
302 having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
303 C<xhtml>. These files, and all files without extensions, will be
304 processed through Catalyst. If L<MIME::Types> doesn't recognize an
305 extension, it will be served as C<text/plain>.
306
307 To restate: files having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>,
308 and C<xhtml> I<will not> be served statically by default, they will be
309 processed by Catalyst. Thus if you want to use C<.html> files from
310 within a Catalyst app as static files, you need to change the
311 configuration of Static::Simple. Note also that files having any other
312 extension I<will> be served statically, so if you're using any other
313 extension for template files, you should also change the configuration.
314
315 Logging of static files is turned off by default.
316
317 =head1 ADVANCED CONFIGURATION
318
319 Configuration is completely optional and is specified within
320 C<MyApp-E<gt>config-E<gt>{static}>.  If you use any of these options,
321 this module will probably feel less "simple" to you!
322
323 =head2 Enabling request logging
324
325 Since Catalyst 5.50, logging of static requests is turned off by
326 default; static requests tend to clutter the log output and rarely
327 reveal anything useful. However, if you want to enable logging of static
328 requests, you can do so by setting
329 C<MyApp-E<gt>config-E<gt>{static}-E<gt>{logging}> to 1.
330
331 =head2 Forcing directories into static mode
332
333 Define a list of top-level directories beneath your 'root' directory
334 that should always be served in static mode.  Regular expressions may be
335 specified using C<qr//>.
336
337     MyApp->config->{static}->{dirs} = [
338         'static',
339         qr/^(images|css)/,
340     ];
341
342 =head2 Including additional directories
343
344 You may specify a list of directories in which to search for your static
345 files. The directories will be searched in order and will return the
346 first file found. Note that your root directory is B<not> automatically
347 added to the search path when you specify an C<include_path>. You should
348 use C<MyApp-E<gt>config-E<gt>{root}> to add it.
349
350     MyApp->config->{static}->{include_path} = [
351         '/path/to/overlay',
352         \&incpath_generator,
353         MyApp->config->{root}
354     ];
355     
356 With the above setting, a request for the file C</images/logo.jpg> will search
357 for the following files, returning the first one found:
358
359     /path/to/overlay/images/logo.jpg
360     /dynamic/path/images/logo.jpg
361     /your/app/home/root/images/logo.jpg
362     
363 The include path can contain a subroutine reference to dynamically return a
364 list of available directories.  This method will receive the C<$c> object as a
365 parameter and should return a reference to a list of directories.  Errors can
366 be reported using C<die()>.  This method will be called every time a file is
367 requested that appears to be a static file (i.e. it has an extension).
368
369 For example:
370
371     sub incpath_generator {
372         my $c = shift;
373         
374         if ( $c->session->{customer_dir} ) {
375             return [ $c->session->{customer_dir} ];
376         } else {
377             die "No customer dir defined.";
378         }
379     }
380     
381 =head2 Ignoring certain types of files
382
383 There are some file types you may not wish to serve as static files.
384 Most important in this category are your raw template files.  By
385 default, files with the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
386 C<xhtml> will be ignored by Static::Simple in the interest of security.
387 If you wish to define your own extensions to ignore, use the
388 C<ignore_extensions> option:
389
390     MyApp->config->{static}->{ignore_extensions} 
391         = [ qw/html asp php/ ];
392     
393 =head2 Ignoring entire directories
394
395 To prevent an entire directory from being served statically, you can use
396 the C<ignore_dirs> option.  This option contains a list of relative
397 directory paths to ignore.  If using C<include_path>, the path will be
398 checked against every included path.
399
400     MyApp->config->{static}->{ignore_dirs} = [ qw/tmpl css/ ];
401     
402 For example, if combined with the above C<include_path> setting, this
403 C<ignore_dirs> value will ignore the following directories if they exist:
404
405     /path/to/overlay/tmpl
406     /path/to/overlay/css
407     /dynamic/path/tmpl
408     /dynamic/path/css
409     /your/app/home/root/tmpl
410     /your/app/home/root/css    
411
412 =head2 Custom MIME types
413
414 To override or add to the default MIME types set by the L<MIME::Types>
415 module, you may enter your own extension to MIME type mapping.
416
417     MyApp->config->{static}->{mime_types} = {
418         jpg => 'image/jpg',
419         png => 'image/png',
420     };
421
422 =head2 Compatibility with other plugins
423
424 Since version 0.12, Static::Simple plays nice with other plugins.  It no
425 longer short-circuits the C<prepare_action> stage as it was causing too
426 many compatibility issues with other plugins.
427
428 =head2 Debugging information
429
430 Enable additional debugging information printed in the Catalyst log.  This
431 is automatically enabled when running Catalyst in -Debug mode.
432
433     MyApp->config->{static}->{debug} = 1;
434     
435 =head1 USING WITH APACHE
436
437 While Static::Simple will work just fine serving files through Catalyst in
438 mod_perl, for increased performance, you may wish to have Apache handle the
439 serving of your static files.  To do this, simply use a dedicated directory
440 for your static files and configure an Apache Location block for that
441 directory.  This approach is recommended for production installations.
442
443     <Location /static>
444         SetHandler default-handler
445     </Location>
446
447 Using this approach Apache will bypass any handling of these directories
448 through Catalyst. You can leave Static::Simple as part of your
449 application, and it will continue to function on a development server,
450 or using Catalyst's built-in server.
451
452 =head1 PUBLIC METHODS
453
454 =head2 serve_static_file $file_path
455
456 Will serve the file located in $file_path statically. This is useful when
457 you need to  autogenerate them if they don't exist, or they are stored in a model.
458
459     package MyApp::Controller::User;
460
461     sub curr_user_thumb : PathPart("my_thumbnail.png") {
462         my ( $self, $c ) = @_;
463         my $file_path = $c->user->picture_thumbnail_path;
464         $c->serve_static_file($file_path);
465     }
466
467 =head1 INTERNAL EXTENDED METHODS
468
469 Static::Simple extends the following steps in the Catalyst process.
470
471 =head2 prepare_action 
472
473 C<prepare_action> is used to first check if the request path is a static
474 file.  If so, we skip all other C<prepare_action> steps to improve
475 performance.
476
477 =head2 dispatch
478
479 C<dispatch> takes the file found during C<prepare_action> and writes it
480 to the output.
481
482 =head2 finalize
483
484 C<finalize> serves up final header information and displays any log
485 messages.
486
487 =head2 setup
488
489 C<setup> initializes all default values.
490
491 =head1 SEE ALSO
492
493 L<Catalyst>, L<Catalyst::Plugin::Static>, 
494 L<http://www.iana.org/assignments/media-types/>
495
496 =head1 AUTHOR
497
498 Andy Grundman, <andy@hybridized.org>
499
500 =head1 CONTRIBUTORS
501
502 Marcus Ramberg, <mramberg@cpan.org>
503
504 Jesse Sheidlower, <jester@panix.com>
505
506 Guillermo Roditi, <groditi@cpan.org>
507
508 =head1 THANKS
509
510 The authors of Catalyst::Plugin::Static:
511
512     Sebastian Riedel
513     Christian Hansen
514     Marcus Ramberg
515
516 For the include_path code from Template Toolkit:
517
518     Andy Wardley
519
520 =head1 COPYRIGHT
521
522 This program is free software, you can redistribute it and/or modify it under
523 the same terms as Perl itself.
524
525 =cut