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