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