Added details of Aliasing filesystem paths to Using with Apache docs section
[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
6e89d83c 281 # (i.e. /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
6e89d83c 443While Static::Simple will work just fine serving files through Catalyst
444in mod_perl, for increased performance you may wish to have Apache
445handle the serving of your static files directly. To do this, simply use
446a dedicated directory for your static files and configure an Apache
447Location block for that directory This approach is recommended for
448production installations.
2cb3d585 449
6e89d83c 450 <Location /myapp/static>
2cb3d585 451 SetHandler default-handler
452 </Location>
b1d96e3e 453
bc5b1283 454Using this approach Apache will bypass any handling of these directories
455through Catalyst. You can leave Static::Simple as part of your
456application, and it will continue to function on a development server,
457or using Catalyst's built-in server.
458
6e89d83c 459In practice, your Catalyst application is probably (i.e. should be)
460structured in the recommended way (i.e., that generated by bootstrapping
461the application with the C<catalyst.pl> script, with a main directory
462under which is a C<lib/> directory for module files and a C<root/>
463directory for templates and static files). Thus, unless you break up
464this structure when deploying your app by moving the static files to a
465different location in your filesystem, you will need to use an Alias
466directive in Apache to point to the right place. You will then need to
467add a Directory block to give permission for Apache to serve these
468files. The final configuration will look something like this:
469
470 Alias /myapp/static /filesystem/path/to/MyApp/root/static
471 <Directory /filesystem/path/to/MyApp/root/static>
472 allow from all
473 </Directory>
474 <Location /myapp/static>
475 SetHandler default-handler
476 </Location>
477
ab02ca0d 478=head1 PUBLIC METHODS
479
480=head2 serve_static_file $file_path
481
482Will serve the file located in $file_path statically. This is useful when
483you need to autogenerate them if they don't exist, or they are stored in a model.
484
485 package MyApp::Controller::User;
486
487 sub curr_user_thumb : PathPart("my_thumbnail.png") {
488 my ( $self, $c ) = @_;
489 my $file_path = $c->user->picture_thumbnail_path;
490 $c->serve_static_file($file_path);
491 }
492
033a7581 493=head1 INTERNAL EXTENDED METHODS
494
495Static::Simple extends the following steps in the Catalyst process.
496
497=head2 prepare_action
498
bc5b1283 499C<prepare_action> is used to first check if the request path is a static
500file. If so, we skip all other C<prepare_action> steps to improve
501performance.
033a7581 502
503=head2 dispatch
504
bc5b1283 505C<dispatch> takes the file found during C<prepare_action> and writes it
506to the output.
033a7581 507
508=head2 finalize
509
bc5b1283 510C<finalize> serves up final header information and displays any log
511messages.
033a7581 512
513=head2 setup
514
bc5b1283 515C<setup> initializes all default values.
033a7581 516
d6d29b9b 517=head1 SEE ALSO
518
b1d96e3e 519L<Catalyst>, L<Catalyst::Plugin::Static>,
520L<http://www.iana.org/assignments/media-types/>
d6d29b9b 521
522=head1 AUTHOR
523
b1d96e3e 524Andy Grundman, <andy@hybridized.org>
d6d29b9b 525
fa43d6b5 526=head1 CONTRIBUTORS
527
528Marcus Ramberg, <mramberg@cpan.org>
ab02ca0d 529
bc5b1283 530Jesse Sheidlower, <jester@panix.com>
fa43d6b5 531
ab02ca0d 532Guillermo Roditi, <groditi@cpan.org>
533
d6d29b9b 534=head1 THANKS
535
536The authors of Catalyst::Plugin::Static:
537
538 Sebastian Riedel
539 Christian Hansen
540 Marcus Ramberg
541
542For the include_path code from Template Toolkit:
543
544 Andy Wardley
545
546=head1 COPYRIGHT
547
548This program is free software, you can redistribute it and/or modify it under
549the same terms as Perl itself.
550
551=cut