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