Be less fugly
[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
a5d909f1 354 MyApp->config(
355 static => {
356 dirs => [
357 'static',
358 qr/^(images|css)/,
359 ],
360 }
361 );
b1d96e3e 362
fa43d6b5 363=head2 Including additional directories
b1d96e3e 364
365You may specify a list of directories in which to search for your static
bc5b1283 366files. The directories will be searched in order and will return the
367first file found. Note that your root directory is B<not> automatically
368added to the search path when you specify an C<include_path>. You should
369use C<MyApp-E<gt>config-E<gt>{root}> to add it.
b1d96e3e 370
a5d909f1 371 MyApp->config(
372 static => {
373 include_path => [
374 '/path/to/overlay',
375 \&incpath_generator,
376 MyApp->config->{root},
377 ],
378 },
379 );
b108737b 380
bc5b1283 381With the above setting, a request for the file C</images/logo.jpg> will search
b1d96e3e 382for the following files, returning the first one found:
383
384 /path/to/overlay/images/logo.jpg
385 /dynamic/path/images/logo.jpg
386 /your/app/home/root/images/logo.jpg
b108737b 387
b1d96e3e 388The include path can contain a subroutine reference to dynamically return a
bc5b1283 389list of available directories. This method will receive the C<$c> object as a
b1d96e3e 390parameter and should return a reference to a list of directories. Errors can
bc5b1283 391be reported using C<die()>. This method will be called every time a file is
b1d96e3e 392requested that appears to be a static file (i.e. it has an extension).
393
394For example:
395
396 sub incpath_generator {
397 my $c = shift;
a5d909f1 398
b1d96e3e 399 if ( $c->session->{customer_dir} ) {
400 return [ $c->session->{customer_dir} ];
401 } else {
402 die "No customer dir defined.";
403 }
404 }
b108737b 405
8cc672a2 406=head2 Ignoring certain types of files
407
bc5b1283 408There are some file types you may not wish to serve as static files.
409Most important in this category are your raw template files. By
410default, files with the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
411C<xhtml> will be ignored by Static::Simple in the interest of security.
412If you wish to define your own extensions to ignore, use the
413C<ignore_extensions> option:
8cc672a2 414
a5d909f1 415 MyApp->config(
416 static => {
417 ignore_extensions => [ qw/html asp php/ ],
418 },
419 );
b108737b 420
8cc672a2 421=head2 Ignoring entire directories
422
bc5b1283 423To prevent an entire directory from being served statically, you can use
424the C<ignore_dirs> option. This option contains a list of relative
425directory paths to ignore. If using C<include_path>, the path will be
426checked against every included path.
8cc672a2 427
a5d909f1 428 MyApp->config(
429 static => {
430 ignore_dirs => [ qw/tmpl css/ ],
431 },
432 );
b108737b 433
bc5b1283 434For example, if combined with the above C<include_path> setting, this
435C<ignore_dirs> value will ignore the following directories if they exist:
8cc672a2 436
437 /path/to/overlay/tmpl
438 /path/to/overlay/css
439 /dynamic/path/tmpl
440 /dynamic/path/css
441 /your/app/home/root/tmpl
b108737b 442 /your/app/home/root/css
b1d96e3e 443
2268e329 444=head2 Custom MIME types
b1d96e3e 445
bc5b1283 446To override or add to the default MIME types set by the L<MIME::Types>
447module, you may enter your own extension to MIME type mapping.
b1d96e3e 448
a5d909f1 449 MyApp->config(
450 static => {
451 mime_types => {
452 jpg => 'image/jpg',
453 png => 'image/png',
454 },
455 },
456 );
2268e329 457
d38d0ed6 458=head2 Compatibility with other plugins
b1d96e3e 459
d38d0ed6 460Since version 0.12, Static::Simple plays nice with other plugins. It no
bc5b1283 461longer short-circuits the C<prepare_action> stage as it was causing too
462many compatibility issues with other plugins.
b1d96e3e 463
2268e329 464=head2 Debugging information
b1d96e3e 465
466Enable additional debugging information printed in the Catalyst log. This
467is automatically enabled when running Catalyst in -Debug mode.
468
a5d909f1 469 MyApp->config(
470 static => {
471 debug => 1,
472 },
473 );
b108737b 474
2cb3d585 475=head1 USING WITH APACHE
476
6e89d83c 477While Static::Simple will work just fine serving files through Catalyst
478in mod_perl, for increased performance you may wish to have Apache
479handle the serving of your static files directly. To do this, simply use
480a dedicated directory for your static files and configure an Apache
481Location block for that directory This approach is recommended for
482production installations.
2cb3d585 483
6e89d83c 484 <Location /myapp/static>
2cb3d585 485 SetHandler default-handler
486 </Location>
b1d96e3e 487
bc5b1283 488Using this approach Apache will bypass any handling of these directories
489through Catalyst. You can leave Static::Simple as part of your
490application, and it will continue to function on a development server,
491or using Catalyst's built-in server.
492
6e89d83c 493In practice, your Catalyst application is probably (i.e. should be)
494structured in the recommended way (i.e., that generated by bootstrapping
495the application with the C<catalyst.pl> script, with a main directory
496under which is a C<lib/> directory for module files and a C<root/>
497directory for templates and static files). Thus, unless you break up
498this structure when deploying your app by moving the static files to a
499different location in your filesystem, you will need to use an Alias
500directive in Apache to point to the right place. You will then need to
501add a Directory block to give permission for Apache to serve these
502files. The final configuration will look something like this:
503
504 Alias /myapp/static /filesystem/path/to/MyApp/root/static
505 <Directory /filesystem/path/to/MyApp/root/static>
506 allow from all
507 </Directory>
508 <Location /myapp/static>
509 SetHandler default-handler
510 </Location>
511
071c0042 512If you are running in a VirtualHost, you can just set the DocumentRoot
b108737b 513location to the location of your root directory; see
071c0042 514L<Catalyst::Engine::Apache2::MP20>.
515
ab02ca0d 516=head1 PUBLIC METHODS
517
518=head2 serve_static_file $file_path
519
520Will serve the file located in $file_path statically. This is useful when
521you need to autogenerate them if they don't exist, or they are stored in a model.
522
523 package MyApp::Controller::User;
524
525 sub curr_user_thumb : PathPart("my_thumbnail.png") {
526 my ( $self, $c ) = @_;
527 my $file_path = $c->user->picture_thumbnail_path;
528 $c->serve_static_file($file_path);
529 }
530
033a7581 531=head1 INTERNAL EXTENDED METHODS
532
533Static::Simple extends the following steps in the Catalyst process.
534
b108737b 535=head2 prepare_action
033a7581 536
bc5b1283 537C<prepare_action> is used to first check if the request path is a static
538file. If so, we skip all other C<prepare_action> steps to improve
539performance.
033a7581 540
541=head2 dispatch
542
bc5b1283 543C<dispatch> takes the file found during C<prepare_action> and writes it
544to the output.
033a7581 545
546=head2 finalize
547
bc5b1283 548C<finalize> serves up final header information and displays any log
549messages.
033a7581 550
551=head2 setup
552
bc5b1283 553C<setup> initializes all default values.
033a7581 554
d6d29b9b 555=head1 SEE ALSO
556
b108737b 557L<Catalyst>, L<Catalyst::Plugin::Static>,
b1d96e3e 558L<http://www.iana.org/assignments/media-types/>
d6d29b9b 559
560=head1 AUTHOR
561
b1d96e3e 562Andy Grundman, <andy@hybridized.org>
d6d29b9b 563
fa43d6b5 564=head1 CONTRIBUTORS
565
566Marcus Ramberg, <mramberg@cpan.org>
ab02ca0d 567
bc5b1283 568Jesse Sheidlower, <jester@panix.com>
fa43d6b5 569
ab02ca0d 570Guillermo Roditi, <groditi@cpan.org>
571
9936ddfa 572Florian Ragwitz, <rafl@debian.org>
573
574Tomas Doran, <bobtfish@bobtfish.net>
575
576Justin Wheeler (dnm)
b108737b 577
d6d29b9b 578=head1 THANKS
579
580The authors of Catalyst::Plugin::Static:
581
582 Sebastian Riedel
583 Christian Hansen
584 Marcus Ramberg
585
586For the include_path code from Template Toolkit:
587
588 Andy Wardley
589
590=head1 COPYRIGHT
591
1cc75f96 592Copyright (c) 2005 - 2009
593the Catalyst::Plugin::Static::Simple L</AUTHOR> and L</CONTRIBUTORS>
594as listed above.
595
596=head1 LICENSE
597
d6d29b9b 598This program is free software, you can redistribute it and/or modify it under
599the same terms as Perl itself.
600
601=cut