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