Static::Simple: Added serve_static_file patch from groditi, plus tests
[catagits/Catalyst-Plugin-Static-Simple.git] / lib / Catalyst / Plugin / Static / Simple.pm
CommitLineData
d6d29b9b 1package Catalyst::Plugin::Static::Simple;
2
3use strict;
b06be085 4use warnings;
d6d29b9b 5use base qw/Class::Accessor::Fast Class::Data::Inheritable/;
d6d29b9b 6use File::stat;
bdf5afa1 7use File::Spec ();
8use IO::File ();
9use MIME::Types ();
d6d29b9b 10
ab02ca0d 11our $VERSION = '0.17';
d6d29b9b 12
bdf5afa1 13__PACKAGE__->mk_accessors( qw/_static_file _static_debug_message/ );
d6d29b9b 14
b1d96e3e 15sub prepare_action {
d6d29b9b 16 my $c = shift;
d6d29b9b 17 my $path = $c->req->path;
bdf5afa1 18 my $config = $c->config->{static};
792411e6 19
20 $path =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
b1d96e3e 21
d6d29b9b 22 # is the URI in a static-defined path?
bdf5afa1 23 foreach my $dir ( @{ $config->{dirs} } ) {
9d557523 24 my $dir_re = quotemeta $dir;
25 my $re = ( $dir =~ m{^qr/}xms ) ? eval $dir : qr/^${dir_re}/;
b1d96e3e 26 if ($@) {
27 $c->error( "Error compiling static dir regex '$dir': $@" );
28 }
d6d29b9b 29 if ( $path =~ $re ) {
c8ee8fd2 30 if ( $c->_locate_static_file( $path, 1 ) ) {
b06be085 31 $c->_debug_msg( 'from static directory' )
bdf5afa1 32 if $config->{debug};
d6d29b9b 33 } else {
34 $c->_debug_msg( "404: file not found: $path" )
bdf5afa1 35 if $config->{debug};
d6d29b9b 36 $c->res->status( 404 );
d6d29b9b 37 }
38 }
39 }
40
41 # Does the path have an extension?
b1d96e3e 42 if ( $path =~ /.*\.(\S{1,})$/xms ) {
d6d29b9b 43 # and does it exist?
792411e6 44 $c->_locate_static_file( $path );
d6d29b9b 45 }
46
82239955 47 return $c->NEXT::ACTUAL::prepare_action(@_);
d6d29b9b 48}
49
b1d96e3e 50sub dispatch {
51 my $c = shift;
52
2268e329 53 return if ( $c->res->status != 200 );
b1d96e3e 54
55 if ( $c->_static_file ) {
bdf5afa1 56 if ( $c->config->{static}{no_logs} && $c->log->can('abort') ) {
a28d35e9 57 $c->log->abort( 1 );
58 }
b1d96e3e 59 return $c->_serve_static;
60 }
61 else {
82239955 62 return $c->NEXT::ACTUAL::dispatch(@_);
b1d96e3e 63 }
64}
65
d6d29b9b 66sub finalize {
67 my $c = shift;
68
69 # display all log messages
bdf5afa1 70 if ( $c->config->{static}{debug} && scalar @{$c->_debug_msg} ) {
be327929 71 $c->log->debug( 'Static::Simple: ' . join q{ }, @{$c->_debug_msg} );
d6d29b9b 72 }
73
82239955 74 return $c->NEXT::ACTUAL::finalize(@_);
d6d29b9b 75}
76
77sub setup {
78 my $c = shift;
79
80 $c->NEXT::setup(@_);
81
033a7581 82 if ( Catalyst->VERSION le '5.33' ) {
83 require File::Slurp;
84 }
85
bdf5afa1 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};
d6d29b9b 95
96 # load up a MIME::Types object, only loading types with
97 # at least 1 file extension
bdf5afa1 98 $config->{mime_types_obj} = MIME::Types->new( only_complete => 1 );
2268e329 99
d6d29b9b 100 # preload the type index hash so it's not built on the first request
bdf5afa1 101 $config->{mime_types_obj}->create_type_index;
d6d29b9b 102}
103
104# Search through all included directories for the static file
105# Based on Template Toolkit INCLUDE_PATH code
106sub _locate_static_file {
c8ee8fd2 107 my ( $c, $path, $in_static_dir ) = @_;
d6d29b9b 108
bdf5afa1 109 $path = File::Spec->catdir(
110 File::Spec->no_upwards( File::Spec->splitdir( $path ) )
111 );
d6d29b9b 112
bdf5afa1 113 my $config = $c->config->{static};
114 my @ipaths = @{ $config->{include_path} };
d6d29b9b 115 my $dpaths;
116 my $count = 64; # maximum number of directories to search
117
8cc672a2 118 DIR_CHECK:
d6d29b9b 119 while ( @ipaths && --$count) {
8cc672a2 120 my $dir = shift @ipaths || next DIR_CHECK;
d6d29b9b 121
122 if ( ref $dir eq 'CODE' ) {
123 eval { $dpaths = &$dir( $c ) };
124 if ($@) {
b06be085 125 $c->log->error( 'Static::Simple: include_path error: ' . $@ );
d6d29b9b 126 } else {
b06be085 127 unshift @ipaths, @$dpaths;
8cc672a2 128 next DIR_CHECK;
d6d29b9b 129 }
130 } else {
48791b66 131 $dir =~ s/(\/|\\)$//xms;
d6d29b9b 132 if ( -d $dir && -f $dir . '/' . $path ) {
8cc672a2 133
c8ee8fd2 134 # Don't ignore any files in static dirs defined with 'dirs'
135 unless ( $in_static_dir ) {
136 # do we need to ignore the file?
137 for my $ignore ( @{ $config->{ignore_dirs} } ) {
138 $ignore =~ s{(/|\\)$}{};
139 if ( $path =~ /^$ignore(\/|\\)/ ) {
140 $c->_debug_msg( "Ignoring directory `$ignore`" )
141 if $config->{debug};
142 next DIR_CHECK;
143 }
8cc672a2 144 }
8cc672a2 145
c8ee8fd2 146 # do we need to ignore based on extension?
147 for my $ignore_ext ( @{ $config->{ignore_extensions} } ) {
148 if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
149 $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
150 if $config->{debug};
151 next DIR_CHECK;
152 }
bdf5afa1 153 }
8cc672a2 154 }
155
156 $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
bdf5afa1 157 if $config->{debug};
d6d29b9b 158 return $c->_static_file( $dir . '/' . $path );
159 }
160 }
161 }
162
2268e329 163 return;
d6d29b9b 164}
165
d6d29b9b 166sub _serve_static {
167 my $c = shift;
792411e6 168
ab02ca0d 169 my $full_path = shift || $c->_static_file;
792411e6 170 my $type = $c->_ext_to_type( $full_path );
171 my $stat = stat $full_path;
d6d29b9b 172
d6d29b9b 173 $c->res->headers->content_type( $type );
174 $c->res->headers->content_length( $stat->size );
175 $c->res->headers->last_modified( $stat->mtime );
2cb3d585 176
177 if ( Catalyst->VERSION le '5.33' ) {
178 # old File::Slurp method
179 my $content = File::Slurp::read_file( $full_path );
5224ce15 180 $c->res->body( $content );
2cb3d585 181 }
182 else {
5224ce15 183 # new method, pass an IO::File object to body
184 my $fh = IO::File->new( $full_path, 'r' );
185 if ( defined $fh ) {
033a7581 186 binmode $fh;
5224ce15 187 $c->res->body( $fh );
188 }
189 else {
190 Catalyst::Exception->throw(
2cb3d585 191 message => "Unable to open $full_path for reading" );
2cb3d585 192 }
2cb3d585 193 }
194
b1d96e3e 195 return 1;
196}
197
ab02ca0d 198sub serve_static_file {
199 my ( $c, $full_path ) = @_;
200
201 my $config = $c->config->{static} ||= {};
202
203 if ( -e $full_path ) {
204 $c->_debug_msg( "Serving static file: $full_path" )
205 if $config->{debug};
206 }
207 else {
208 $c->_debug_msg( "404: file not found: $full_path" )
209 if $config->{debug};
210 $c->res->status( 404 );
211 return;
212 }
213
214 $c->_serve_static( $full_path );
215}
216
b1d96e3e 217# looks up the correct MIME type for the current file extension
218sub _ext_to_type {
792411e6 219 my ( $c, $full_path ) = @_;
b1d96e3e 220
bdf5afa1 221 my $config = $c->config->{static};
222
792411e6 223 if ( $full_path =~ /.*\.(\S{1,})$/xms ) {
b1d96e3e 224 my $ext = $1;
bdf5afa1 225 my $type = $config->{mime_types}{$ext}
226 || $config->{mime_types_obj}->mimeTypeOf( $ext );
2268e329 227 if ( $type ) {
bdf5afa1 228 $c->_debug_msg( "as $type" ) if $config->{debug};
5224ce15 229 return ( ref $type ) ? $type->type : $type;
b1d96e3e 230 }
231 else {
232 $c->_debug_msg( "as text/plain (unknown extension $ext)" )
bdf5afa1 233 if $config->{debug};
b1d96e3e 234 return 'text/plain';
235 }
236 }
237 else {
238 $c->_debug_msg( 'as text/plain (no extension)' )
bdf5afa1 239 if $config->{debug};
b1d96e3e 240 return 'text/plain';
241 }
d6d29b9b 242}
243
244sub _debug_msg {
245 my ( $c, $msg ) = @_;
246
2268e329 247 if ( !defined $c->_static_debug_message ) {
248 $c->_static_debug_message( [] );
b1d96e3e 249 }
d6d29b9b 250
b1d96e3e 251 if ( $msg ) {
2268e329 252 push @{ $c->_static_debug_message }, $msg;
b1d96e3e 253 }
d6d29b9b 254
2268e329 255 return $c->_static_debug_message;
d6d29b9b 256}
b1d96e3e 257
2581;
259__END__
260
261=head1 NAME
262
263Catalyst::Plugin::Static::Simple - Make serving static pages painless.
264
265=head1 SYNOPSIS
266
267 use Catalyst;
268 MyApp->setup( qw/Static::Simple/ );
bc5b1283 269 # that's it; static content is automatically served by
270 # Catalyst, though you can configure things or bypass
271 # Catalyst entirely in a production environment
b1d96e3e 272
273=head1 DESCRIPTION
274
bc5b1283 275The Static::Simple plugin is designed to make serving static content in
276your application during development quick and easy, without requiring a
277single line of code from you.
b1d96e3e 278
bc5b1283 279This plugin detects static files by looking at the file extension in the
280URL (such as B<.css> or B<.png> or B<.js>). The plugin uses the
281lightweight L<MIME::Types> module to map file extensions to
282IANA-registered MIME types, and will serve your static files with the
283correct MIME type directly to the browser, without being processed
284through Catalyst.
b1d96e3e 285
286Note that actions mapped to paths using periods (.) will still operate
287properly.
288
bc5b1283 289Though Static::Simple is designed to work out-of-the-box, you can tweak
290the operation by adding various configuration options. In a production
291environment, you will probably want to use your webserver to deliver
292static content; for an example see L<USING WITH APACHE>, below.
293
294=head1 DEFAULT BEHAVIOR
295
296By default, Static::Simple will deliver all files having extensions
297(that is, bits of text following a period (C<.>)), I<except> files
298having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
299C<xhtml>. These files, and all files without extensions, will be
300processed through Catalyst. If L<MIME::Types> doesn't recognize an
301extension, it will be served as C<text/plain>.
302
303To restate: files having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>,
304and C<xhtml> I<will not> be served statically by default, they will be
305processed by Catalyst. Thus if you want to use C<.html> files from
306within a Catalyst app as static files, you need to change the
307configuration of Static::Simple. Note also that files having any other
308extension I<will> be served statically, so if you're using any other
309extension for template files, you should also change the configuration.
310
311Logging of static files is turned off by default.
b1d96e3e 312
313=head1 ADVANCED CONFIGURATION
314
bc5b1283 315Configuration is completely optional and is specified within
316C<MyApp-E<gt>config-E<gt>{static}>. If you use any of these options,
317this module will probably feel less "simple" to you!
b1d96e3e 318
bc5b1283 319=head2 Enabling request logging
2de14076 320
bc5b1283 321Since Catalyst 5.50, logging of static requests is turned off by
322default; static requests tend to clutter the log output and rarely
323reveal anything useful. However, if you want to enable logging of static
324requests, you can do so by setting
325C<MyApp-E<gt>config-E<gt>{static}-E<gt>{no_logs}> to 0.
2de14076 326
2268e329 327=head2 Forcing directories into static mode
b1d96e3e 328
bc5b1283 329Define a list of top-level directories beneath your 'root' directory
330that should always be served in static mode. Regular expressions may be
331specified using C<qr//>.
b1d96e3e 332
333 MyApp->config->{static}->{dirs} = [
334 'static',
335 qr/^(images|css)/,
336 ];
337
fa43d6b5 338=head2 Including additional directories
b1d96e3e 339
340You may specify a list of directories in which to search for your static
bc5b1283 341files. The directories will be searched in order and will return the
342first file found. Note that your root directory is B<not> automatically
343added to the search path when you specify an C<include_path>. You should
344use C<MyApp-E<gt>config-E<gt>{root}> to add it.
b1d96e3e 345
346 MyApp->config->{static}->{include_path} = [
347 '/path/to/overlay',
348 \&incpath_generator,
349 MyApp->config->{root}
350 ];
351
bc5b1283 352With the above setting, a request for the file C</images/logo.jpg> will search
b1d96e3e 353for the following files, returning the first one found:
354
355 /path/to/overlay/images/logo.jpg
356 /dynamic/path/images/logo.jpg
357 /your/app/home/root/images/logo.jpg
358
359The include path can contain a subroutine reference to dynamically return a
bc5b1283 360list of available directories. This method will receive the C<$c> object as a
b1d96e3e 361parameter and should return a reference to a list of directories. Errors can
bc5b1283 362be reported using C<die()>. This method will be called every time a file is
b1d96e3e 363requested that appears to be a static file (i.e. it has an extension).
364
365For example:
366
367 sub incpath_generator {
368 my $c = shift;
369
370 if ( $c->session->{customer_dir} ) {
371 return [ $c->session->{customer_dir} ];
372 } else {
373 die "No customer dir defined.";
374 }
375 }
8cc672a2 376
377=head2 Ignoring certain types of files
378
bc5b1283 379There are some file types you may not wish to serve as static files.
380Most important in this category are your raw template files. By
381default, files with the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
382C<xhtml> will be ignored by Static::Simple in the interest of security.
383If you wish to define your own extensions to ignore, use the
384C<ignore_extensions> option:
8cc672a2 385
d38d0ed6 386 MyApp->config->{static}->{ignore_extensions}
bc5b1283 387 = [ qw/html asp php/ ];
8cc672a2 388
389=head2 Ignoring entire directories
390
bc5b1283 391To prevent an entire directory from being served statically, you can use
392the C<ignore_dirs> option. This option contains a list of relative
393directory paths to ignore. If using C<include_path>, the path will be
394checked against every included path.
8cc672a2 395
396 MyApp->config->{static}->{ignore_dirs} = [ qw/tmpl css/ ];
397
bc5b1283 398For example, if combined with the above C<include_path> setting, this
399C<ignore_dirs> value will ignore the following directories if they exist:
8cc672a2 400
401 /path/to/overlay/tmpl
402 /path/to/overlay/css
403 /dynamic/path/tmpl
404 /dynamic/path/css
405 /your/app/home/root/tmpl
406 /your/app/home/root/css
b1d96e3e 407
2268e329 408=head2 Custom MIME types
b1d96e3e 409
bc5b1283 410To override or add to the default MIME types set by the L<MIME::Types>
411module, you may enter your own extension to MIME type mapping.
b1d96e3e 412
413 MyApp->config->{static}->{mime_types} = {
414 jpg => 'image/jpg',
415 png => 'image/png',
416 };
2268e329 417
d38d0ed6 418=head2 Compatibility with other plugins
b1d96e3e 419
d38d0ed6 420Since version 0.12, Static::Simple plays nice with other plugins. It no
bc5b1283 421longer short-circuits the C<prepare_action> stage as it was causing too
422many compatibility issues with other plugins.
b1d96e3e 423
2268e329 424=head2 Debugging information
b1d96e3e 425
426Enable additional debugging information printed in the Catalyst log. This
427is automatically enabled when running Catalyst in -Debug mode.
428
429 MyApp->config->{static}->{debug} = 1;
2cb3d585 430
431=head1 USING WITH APACHE
432
433While Static::Simple will work just fine serving files through Catalyst in
434mod_perl, for increased performance, you may wish to have Apache handle the
435serving of your static files. To do this, simply use a dedicated directory
436for your static files and configure an Apache Location block for that
437directory. This approach is recommended for production installations.
438
439 <Location /static>
440 SetHandler default-handler
441 </Location>
b1d96e3e 442
bc5b1283 443Using this approach Apache will bypass any handling of these directories
444through Catalyst. You can leave Static::Simple as part of your
445application, and it will continue to function on a development server,
446or using Catalyst's built-in server.
447
ab02ca0d 448=head1 PUBLIC METHODS
449
450=head2 serve_static_file $file_path
451
452Will serve the file located in $file_path statically. This is useful when
453you need to autogenerate them if they don't exist, or they are stored in a model.
454
455 package MyApp::Controller::User;
456
457 sub curr_user_thumb : PathPart("my_thumbnail.png") {
458 my ( $self, $c ) = @_;
459 my $file_path = $c->user->picture_thumbnail_path;
460 $c->serve_static_file($file_path);
461 }
462
033a7581 463=head1 INTERNAL EXTENDED METHODS
464
465Static::Simple extends the following steps in the Catalyst process.
466
467=head2 prepare_action
468
bc5b1283 469C<prepare_action> is used to first check if the request path is a static
470file. If so, we skip all other C<prepare_action> steps to improve
471performance.
033a7581 472
473=head2 dispatch
474
bc5b1283 475C<dispatch> takes the file found during C<prepare_action> and writes it
476to the output.
033a7581 477
478=head2 finalize
479
bc5b1283 480C<finalize> serves up final header information and displays any log
481messages.
033a7581 482
483=head2 setup
484
bc5b1283 485C<setup> initializes all default values.
033a7581 486
d6d29b9b 487=head1 SEE ALSO
488
b1d96e3e 489L<Catalyst>, L<Catalyst::Plugin::Static>,
490L<http://www.iana.org/assignments/media-types/>
d6d29b9b 491
492=head1 AUTHOR
493
b1d96e3e 494Andy Grundman, <andy@hybridized.org>
d6d29b9b 495
fa43d6b5 496=head1 CONTRIBUTORS
497
498Marcus Ramberg, <mramberg@cpan.org>
ab02ca0d 499
bc5b1283 500Jesse Sheidlower, <jester@panix.com>
fa43d6b5 501
ab02ca0d 502Guillermo Roditi, <groditi@cpan.org>
503
d6d29b9b 504=head1 THANKS
505
506The authors of Catalyst::Plugin::Static:
507
508 Sebastian Riedel
509 Christian Hansen
510 Marcus Ramberg
511
512For the include_path code from Template Toolkit:
513
514 Andy Wardley
515
516=head1 COPYRIGHT
517
518This program is free software, you can redistribute it and/or modify it under
519the same terms as Perl itself.
520
521=cut