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