File Coverage

blib/lib/Catalyst/Plugin/Static/Simple.pm
Criterion Covered Total %
statement 97 102 95.1
branch 31 44 70.4
condition 10 14 71.4
subroutine 13 13 100.0
pod 1 1 100.0
total 152 174 87.3


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