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