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