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