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