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