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