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