File Coverage

blib/lib/Catalyst/Plugin/Static/Simple.pm
Criterion Covered Total %
statement 116 137 84.7
branch 41 64 64.1
condition 17 33 51.5
subroutine 14 15 93.3
pod 4 4 100.0
total 192 253 75.9


line stmt bran cond sub pod time code
1             package Catalyst::Plugin::Static::Simple;
2              
3 8     8   114 use strict;
  8         77  
  8         127  
4 8     8   130 use warnings;
  8         77  
  8         125  
5 8     8   122 use base qw/Class::Accessor::Fast Class::Data::Inheritable/;
  8         73  
  8         120  
6 8     8   130 use File::stat;
  8         74  
  8         126  
7 8     8   122 use File::Spec ();
  8         74  
  8         76  
8 8     8   117 use IO::File ();
  8         98  
  8         77  
9 8     8   406 use MIME::Types ();
  8         84  
  8         87  
10              
11             our $VERSION = '0.15';
12              
13             __PACKAGE__->mk_accessors( qw/_static_file _static_debug_message/ );
14              
15             sub prepare_action {
16 17     17 1 1429     my $c = shift;
17 17         199     my $path = $c->req->path;
18 17         1156     my $config = $c->config->{static};
19                 
20 17         962     $path =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  0         0  
21              
22             # is the URI in a static-defined path?
23 17         161     foreach my $dir ( @{ $config->{dirs} } ) {
  17         232  
24 12         2322         my $dir_re = quotemeta $dir;
25 12 100       629         my $re = ( $dir =~ m{^qr/}xms ) ? eval $dir : qr/^${dir_re}/;
26 12 50       129         if ($@) {
27 0         0             $c->error( "Error compiling static dir regex '$dir': $@" );
28                     }
29 12 100       174         if ( $path =~ $re ) {
30 3 100       52             if ( $c->_locate_static_file( $path ) ) {
31 2 50       112                 $c->_debug_msg( 'from static directory' )
32                                 if $config->{debug};
33                         } else {
34 1 50       15                 $c->_debug_msg( "404: file not found: $path" )
35                                 if $config->{debug};
36 1         335                 $c->res->status( 404 );
37                         }
38                     }
39                 }
40                 
41             # Does the path have an extension?
42 17 100       375     if ( $path =~ /.*\.(\S{1,})$/xms ) {
43             # and does it exist?
44 16         270         $c->_locate_static_file( $path );
45                 }
46                 
47 17         777     return $c->NEXT::ACTUAL::prepare_action(@_);
48             }
49              
50             sub dispatch {
51 17     17 1 327     my $c = shift;
52                 
53 17 100       698     return if ( $c->res->status != 200 );
54                 
55 16 100       247     if ( $c->_static_file ) {
56 10 50 33     485         if ( $c->config->{static}{no_logs} && $c->log->can('abort') ) {
57 10         833            $c->log->abort( 1 );
58                     }
59 10         448         return $c->_serve_static;
60                 }
61                 else {
62 6         212         return $c->NEXT::ACTUAL::dispatch(@_);
63                 }
64             }
65              
66             sub finalize {
67 17     17 1 170     my $c = shift;
68                 
69             # display all log messages
70 17 50 33     308     if ( $c->config->{static}{debug} && scalar @{$c->_debug_msg} ) {
  0         0  
71 0         0         $c->log->debug( 'Static::Simple: ' . join q{ }, @{$c->_debug_msg} );
  0         0  
72                 }
73                 
74 17 50       1072     if ( $c->res->status =~ /^(1\d\d|[23]04)$/xms ) {
75 0         0         $c->res->headers->remove_content_headers;
76 0         0         return $c->finalize_headers;
77                 }
78                 
79 17         386     return $c->NEXT::ACTUAL::finalize(@_);
80             }
81              
82             sub setup {
83 7     7 1 80     my $c = shift;
84                 
85 7         192     $c->NEXT::setup(@_);
86                 
87 7 50       219     if ( Catalyst->VERSION le '5.33' ) {
88 0         0         require File::Slurp;
89                 }
90                 
91 7   50     211     my $config = $c->config->{static} ||= {};
92                 
93 7   50     100     $config->{dirs} ||= [];
94 7   50     118     $config->{include_path} ||= [ $c->config->{root} ];
95 7   50     107     $config->{mime_types} ||= {};
96 7   50     112     $config->{ignore_extensions} ||= [ qw/tmpl tt tt2 html xhtml/ ];
97 7   50     140     $config->{ignore_dirs} ||= [];
98 7   33     246     $config->{debug} ||= $c->debug;
99 7 50       100     $config->{no_logs} = 1 unless defined $config->{no_logs};
100                 
101             # load up a MIME::Types object, only loading types with
102             # at least 1 file extension
103 7         121     $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 7         105     $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 19     19   213     my ( $c, $path ) = @_;
113                 
114 19         549     $path = File::Spec->catdir(
115                     File::Spec->no_upwards( File::Spec->splitdir( $path ) )
116                 );
117                 
118 19         545     my $config = $c->config->{static};
119 19         1526     my @ipaths = @{ $config->{include_path} };
  19         417  
120 19         183     my $dpaths;
121 19         247     my $count = 64; # maximum number of directories to search
122                 
123                 DIR_CHECK:
124 19   66     282     while ( @ipaths && --$count) {
125 27   33     341         my $dir = shift @ipaths || next DIR_CHECK;
126                     
127 27 100       727         if ( ref $dir eq 'CODE' ) {
128 2         21             eval { $dpaths = &$dir( $c ) };
  2         30  
129 2 50       42             if ($@) {
130 0         0                 $c->log->error( 'Static::Simple: include_path error: ' . $@ );
131                         } else {
132 2         22                 unshift @ipaths, @$dpaths;
133 2         33                 next DIR_CHECK;
134                         }
135                     } else {
136 25         432             $dir =~ s/(\/|\\)$//xms;
137 25 100 66     2198             if ( -d $dir && -f $dir . '/' . $path ) {
138                             
139             # do we need to ignore the file?
140 16         454                 for my $ignore ( @{ $config->{ignore_dirs} } ) {
  16         198  
141 6         59                     $ignore =~ s{(/|\\)$}{};
142 6 100       456                     if ( $path =~ /^$ignore(\/|\\)/ ) {
143 3 50       34                         $c->_debug_msg( "Ignoring directory `$ignore`" )
144                                         if $config->{debug};
145 3         43                         next DIR_CHECK;
146                                 }
147                             }
148                             
149             # do we need to ignore based on extension?
150 13         122                 for my $ignore_ext ( @{ $config->{ignore_extensions} } ) {
  13         194  
151 61 100       1668                     if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
152 2 50       22                         $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
153                                         if $config->{debug};
154 2         30                         next DIR_CHECK;
155                                 }
156                             }
157                             
158 11 50       132                 $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
159                                 if $config->{debug};
160 11         306                 return $c->_static_file( $dir . '/' . $path );
161                         }
162                     }
163                 }
164                 
165 8         286     return;
166             }
167              
168             sub _serve_static {
169 10     10   96     my $c = shift;
170                        
171 10         120     my $full_path = $c->_static_file;
172 10         248     my $type = $c->_ext_to_type( $full_path );
173 10         193     my $stat = stat $full_path;
174              
175 10         446     $c->res->headers->content_type( $type );
176 10         1042     $c->res->headers->content_length( $stat->size );
177 10         709     $c->res->headers->last_modified( $stat->mtime );
178              
179 10 50       3763     if ( Catalyst->VERSION le '5.33' ) {
180             # old File::Slurp method
181 0         0         my $content = File::Slurp::read_file( $full_path );
182 0         0         $c->res->body( $content );
183                 }
184                 else {
185             # new method, pass an IO::File object to body
186 10         210         my $fh = IO::File->new( $full_path, 'r' );
187 10 50       6757         if ( defined $fh ) {
188 10         133             binmode $fh;
189 10         202             $c->res->body( $fh );
190                     }
191                     else {
192 0         0             Catalyst::Exception->throw(
193                             message => "Unable to open $full_path for reading" );
194                     }
195                 }
196                 
197 10         233     return 1;
198             }
199              
200             # looks up the correct MIME type for the current file extension
201             sub _ext_to_type {
202 10     10   101     my ( $c, $full_path ) = @_;
203                 
204 10         119     my $config = $c->config->{static};
205                 
206 10 50       547     if ( $full_path =~ /.*\.(\S{1,})$/xms ) {
207 10         126         my $ext = $1;
208 10   100     180         my $type = $config->{mime_types}{$ext}
209                         || $config->{mime_types_obj}->mimeTypeOf( $ext );
210 10 100       177         if ( $type ) {
211 8 50       143             $c->_debug_msg( "as $type" ) if $config->{debug};
212 8 100       114             return ( ref $type ) ? $type->type : $type;
213                     }
214                     else {
215 2 50       28             $c->_debug_msg( "as text/plain (unknown extension $ext)" )
216                             if $config->{debug};
217 2         24             return 'text/plain';
218                     }
219                 }
220                 else {
221 0 0                 $c->_debug_msg( 'as text/plain (no extension)' )
222                         if $config->{debug};
223 0                   return 'text/plain';
224                 }
225             }
226              
227             sub _debug_msg {
228 0     0         my ( $c, $msg ) = @_;
229                 
230 0 0             if ( !defined $c->_static_debug_message ) {
231 0                   $c->_static_debug_message( [] );
232                 }
233                 
234 0 0             if ( $msg ) {
235 0                   push @{ $c->_static_debug_message }, $msg;
  0            
236                 }
237                 
238 0               return $c->_static_debug_message;
239             }
240              
241             1;
242             __END__
243            
244             =head1 NAME
245            
246             Catalyst::Plugin::Static::Simple - Make serving static pages painless.
247            
248             =head1 SYNOPSIS
249            
250             use Catalyst;
251             MyApp->setup( qw/Static::Simple/ );
252             # that's it; static content is automatically served by
253             # Catalyst, though you can configure things or bypass
254             # Catalyst entirely in a production environment
255            
256             =head1 DESCRIPTION
257            
258             The Static::Simple plugin is designed to make serving static content in
259             your application during development quick and easy, without requiring a
260             single line of code from you.
261            
262             This plugin detects static files by looking at the file extension in the
263             URL (such as B<.css> or B<.png> or B<.js>). The plugin uses the
264             lightweight L<MIME::Types> module to map file extensions to
265             IANA-registered MIME types, and will serve your static files with the
266             correct MIME type directly to the browser, without being processed
267             through Catalyst.
268            
269             Note that actions mapped to paths using periods (.) will still operate
270             properly.
271            
272             Though Static::Simple is designed to work out-of-the-box, you can tweak
273             the operation by adding various configuration options. In a production
274             environment, you will probably want to use your webserver to deliver
275             static content; for an example see L<USING WITH APACHE>, below.
276            
277             =head1 DEFAULT BEHAVIOR
278            
279             By default, Static::Simple will deliver all files having extensions
280             (that is, bits of text following a period (C<.>)), I<except> files
281             having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>, and
282             C<xhtml>. These files, and all files without extensions, will be
283             processed through Catalyst. If L<MIME::Types> doesn't recognize an
284             extension, it will be served as C<text/plain>.
285            
286             To restate: files having the extensions C<tmpl>, C<tt>, C<tt2>, C<html>,
287             and C<xhtml> I<will not> be served statically by default, they will be
288             processed by Catalyst. Thus if you want to use C<.html> files from
289             within a Catalyst app as static files, you need to change the
290             configuration of Static::Simple. Note also that files having any other
291             extension I<will> be served statically, so if you're using any other
292             extension for template files, you should also change the configuration.
293            
294             Logging of static files is turned off by default.
295            
296             =head1 ADVANCED CONFIGURATION
297            
298             Configuration is completely optional and is specified within
299             C<MyApp-E<gt>config-E<gt>{static}>. If you use any of these options,
300             this module will probably feel less "simple" to you!
301            
302             =head2 Enabling request logging
303            
304             Since Catalyst 5.50, logging of static requests is turned off by
305             default; static requests tend to clutter the log output and rarely
306             reveal anything useful. However, if you want to enable logging of static
307             requests, you can do so by setting
308             C<MyApp-E<gt>config-E<gt>{static}-E<gt>{no_logs}> to 0.
309            
310             =head2 Forcing directories into static mode
311            
312             Define a list of top-level directories beneath your 'root' directory
313             that should always be served in static mode. Regular expressions may be
314             specified using C<qr//>.
315            
316