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