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