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