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