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