Static::Simple, documented internal methods for coverage, fixed binmode call
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Plugin / Static / Simple.pm
CommitLineData
a9b78939 1package Catalyst::Plugin::Static::Simple;
2
3use strict;
4use warnings;
5use base qw/Class::Accessor::Fast Class::Data::Inheritable/;
6use File::stat;
04e5fb83 7use IO::File;
a9b78939 8use MIME::Types;
9use NEXT;
10
04e5fb83 11our $VERSION = '0.11';
a9b78939 12
13__PACKAGE__->mk_classdata( qw/_static_mime_types/ );
14__PACKAGE__->mk_accessors( qw/_static_file
15 _static_debug_message/ );
16
a9b78939 17sub prepare_action {
18 my $c = shift;
19 my $path = $c->req->path;
20
21 # is the URI in a static-defined path?
22 foreach my $dir ( @{ $c->config->{static}->{dirs} } ) {
23 my $re = ( $dir =~ /^qr\//xms ) ? eval $dir : qr/^${dir}/;
24 if ($@) {
25 $c->error( "Error compiling static dir regex '$dir': $@" );
26 }
27 if ( $path =~ $re ) {
28 if ( $c->_locate_static_file ) {
29 $c->_debug_msg( 'from static directory' )
30 if ( $c->config->{static}->{debug} );
31 return;
32 } else {
33 $c->_debug_msg( "404: file not found: $path" )
34 if ( $c->config->{static}->{debug} );
35 $c->res->status( 404 );
36 return;
37 }
38 }
39 }
40
41 # Does the path have an extension?
42 if ( $path =~ /.*\.(\S{1,})$/xms ) {
43 # and does it exist?
44 return if ( $c->_locate_static_file );
45 }
46
47 return $c->NEXT::ACTUAL::prepare_action(@_);
48}
49
a9b78939 50sub dispatch {
51 my $c = shift;
52
53 return if ( $c->res->status != 200 );
54
55 if ( $c->_static_file ) {
56 if ( $c->config->{static}->{no_logs} && $c->log->can('abort') ) {
57 $c->log->abort( 1 );
58 }
59 return $c->_serve_static;
60 }
61 else {
62 return $c->NEXT::ACTUAL::dispatch(@_);
63 }
64}
65
a9b78939 66sub finalize {
67 my $c = shift;
68
69 # display all log messages
70 if ( $c->config->{static}->{debug} && scalar @{$c->_debug_msg} ) {
71 $c->log->debug( 'Static::Simple: ' . join q{ }, @{$c->_debug_msg} );
72 }
73
74 if ( $c->res->status =~ /^(1\d\d|[23]04)$/xms ) {
75 $c->res->headers->remove_content_headers;
76 return $c->finalize_headers;
77 }
78
79 return $c->NEXT::ACTUAL::finalize(@_);
80}
81
82sub setup {
83 my $c = shift;
84
85 $c->NEXT::setup(@_);
86
5c19e800 87 if ( Catalyst->VERSION le '5.33' ) {
88 require File::Slurp;
89 }
90
a9b78939 91 $c->config->{static}->{dirs} ||= [];
92 $c->config->{static}->{include_path} ||= [ $c->config->{root} ];
93 $c->config->{static}->{mime_types} ||= {};
94 $c->config->{static}->{ignore_extensions} ||= [ qw/tt tt2 html xhtml/ ];
95 $c->config->{static}->{ignore_dirs} ||= [];
96 $c->config->{static}->{debug} ||= $c->debug;
97 if ( ! defined $c->config->{static}->{no_logs} ) {
98 $c->config->{static}->{no_logs} = 1;
5c19e800 99 }
a9b78939 100
101 # load up a MIME::Types object, only loading types with
102 # at least 1 file extension
103 $c->_static_mime_types( MIME::Types->new( only_complete => 1 ) );
104
105 # preload the type index hash so it's not built on the first request
106 $c->_static_mime_types->create_type_index;
107}
108
109# Search through all included directories for the static file
110# Based on Template Toolkit INCLUDE_PATH code
111sub _locate_static_file {
112 my $c = shift;
113
114 my $path = $c->req->path;
115
116 my @ipaths = @{ $c->config->{static}->{include_path} };
117 my $dpaths;
118 my $count = 64; # maximum number of directories to search
119
120 DIR_CHECK:
121 while ( @ipaths && --$count) {
122 my $dir = shift @ipaths || next DIR_CHECK;
123
124 if ( ref $dir eq 'CODE' ) {
125 eval { $dpaths = &$dir( $c ) };
126 if ($@) {
127 $c->log->error( 'Static::Simple: include_path error: ' . $@ );
128 } else {
129 unshift @ipaths, @$dpaths;
130 next DIR_CHECK;
131 }
132 } else {
133 $dir =~ s/\/$//xms;
134 if ( -d $dir && -f $dir . '/' . $path ) {
135
136 # do we need to ignore the file?
137 for my $ignore ( @{ $c->config->{static}->{ignore_dirs} } ) {
138 $ignore =~ s{/$}{};
139 if ( $path =~ /^$ignore\// ) {
140 $c->_debug_msg( "Ignoring directory `$ignore`" )
141 if ( $c->config->{static}->{debug} );
142 next DIR_CHECK;
143 }
144 }
145
146 # do we need to ignore based on extension?
147 for my $ignore_ext
148 ( @{ $c->config->{static}->{ignore_extensions} } ) {
149 if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
150 $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
151 if ( $c->config->{static}->{debug} );
152 next DIR_CHECK;
153 }
154 }
155
156 $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
157 if ( $c->config->{static}->{debug} );
158 return $c->_static_file( $dir . '/' . $path );
159 }
160 }
161 }
162
163 return;
164}
165
166sub _serve_static {
167 my $c = shift;
168
169 my $path = $c->req->path;
170 my $type = $c->_ext_to_type;
171
172 my $full_path = $c->_static_file;
173 my $stat = stat $full_path;
174
175 # the below code all from C::P::Static
176 if ( $c->req->headers->if_modified_since ) {
177 if ( $c->req->headers->if_modified_since == $stat->mtime ) {
178 $c->res->status( 304 ); # Not Modified
179 $c->res->headers->remove_content_headers;
180 return 1;
181 }
182 }
183
184 $c->res->headers->content_type( $type );
185 $c->res->headers->content_length( $stat->size );
186 $c->res->headers->last_modified( $stat->mtime );
187
188 if ( Catalyst->VERSION le '5.33' ) {
189 # old File::Slurp method
190 my $content = File::Slurp::read_file( $full_path );
04e5fb83 191 $c->res->body( $content );
a9b78939 192 }
193 else {
04e5fb83 194 # new method, pass an IO::File object to body
195 my $fh = IO::File->new( $full_path, 'r' );
196 if ( defined $fh ) {
5c19e800 197 binmode $fh;
04e5fb83 198 $c->res->body( $fh );
199 }
200 else {
201 Catalyst::Exception->throw(
a9b78939 202 message => "Unable to open $full_path for reading" );
a9b78939 203 }
a9b78939 204 }
205
206 return 1;
207}
208
209# looks up the correct MIME type for the current file extension
210sub _ext_to_type {
211 my $c = shift;
212 my $path = $c->req->path;
213
214 if ( $path =~ /.*\.(\S{1,})$/xms ) {
215 my $ext = $1;
216 my $user_types = $c->config->{static}->{mime_types};
217 my $type = $user_types->{$ext}
218 || $c->_static_mime_types->mimeTypeOf( $ext );
219 if ( $type ) {
220 $c->_debug_msg( "as $type" )
221 if ( $c->config->{static}->{debug} );
04e5fb83 222 return ( ref $type ) ? $type->type : $type;
a9b78939 223 }
224 else {
225 $c->_debug_msg( "as text/plain (unknown extension $ext)" )
226 if ( $c->config->{static}->{debug} );
227 return 'text/plain';
228 }
229 }
230 else {
231 $c->_debug_msg( 'as text/plain (no extension)' )
232 if ( $c->config->{static}->{debug} );
233 return 'text/plain';
234 }
235}
236
237sub _debug_msg {
238 my ( $c, $msg ) = @_;
239
240 if ( !defined $c->_static_debug_message ) {
241 $c->_static_debug_message( [] );
242 }
243
244 if ( $msg ) {
245 push @{ $c->_static_debug_message }, $msg;
246 }
247
248 return $c->_static_debug_message;
249}
250
2511;
252__END__
253
254=head1 NAME
255
256Catalyst::Plugin::Static::Simple - Make serving static pages painless.
257
258=head1 SYNOPSIS
259
260 use Catalyst;
261 MyApp->setup( qw/Static::Simple/ );
262
263=head1 DESCRIPTION
264
265The Static::Simple plugin is designed to make serving static content in your
266application during development quick and easy, without requiring a single
267line of code from you.
268
269It will detect static files used in your application by looking for file
270extensions in the URI. By default, you can simply load this plugin and it
271will immediately begin serving your static files with the correct MIME type.
272The light-weight MIME::Types module is used to map file extensions to
273IANA-registered MIME types.
274
275Note that actions mapped to paths using periods (.) will still operate
276properly.
277
278You may further tweak the operation by adding configuration options, described
279below.
280
281=head1 ADVANCED CONFIGURATION
282
283Configuration is completely optional and is specified within
284MyApp->config->{static}. If you use any of these options, the module will
285probably feel less "simple" to you!
286
287=head2 Aborting request logging
288
289Since Catalyst 5.50, there has been added support for dropping logging for a
290request. This is enabled by default for static files, as static requests tend
291to clutter the log output. However, if you want logging of static requests,
292you can enable it by setting MyApp->config->{static}->{no_logs} to 0.
293
294=head2 Forcing directories into static mode
295
296Define a list of top-level directories beneath your 'root' directory that
297should always be served in static mode. Regular expressions may be
298specified using qr//.
299
300 MyApp->config->{static}->{dirs} = [
301 'static',
302 qr/^(images|css)/,
303 ];
304
305=head2 Including additional directories
306
307You may specify a list of directories in which to search for your static
308files. The directories will be searched in order and will return the first
309file found. Note that your root directory is B<not> automatically added to
310the search path when you specify an include_path. You should use
311MyApp->config->{root} to add it.
312
313 MyApp->config->{static}->{include_path} = [
314 '/path/to/overlay',
315 \&incpath_generator,
316 MyApp->config->{root}
317 ];
318
319With the above setting, a request for the file /images/logo.jpg will search
320for the following files, returning the first one found:
321
322 /path/to/overlay/images/logo.jpg
323 /dynamic/path/images/logo.jpg
324 /your/app/home/root/images/logo.jpg
325
326The include path can contain a subroutine reference to dynamically return a
327list of available directories. This method will receive the $c object as a
328parameter and should return a reference to a list of directories. Errors can
329be reported using die(). This method will be called every time a file is
330requested that appears to be a static file (i.e. it has an extension).
331
332For example:
333
334 sub incpath_generator {
335 my $c = shift;
336
337 if ( $c->session->{customer_dir} ) {
338 return [ $c->session->{customer_dir} ];
339 } else {
340 die "No customer dir defined.";
341 }
342 }
343
344=head2 Ignoring certain types of files
345
346There are some file types you may not wish to serve as static files. Most
347important in this category are your raw template files. By default, files
348with the extensions tt, tt2, html, and xhtml will be ignored by Static::Simple
349in the interest of security. If you wish to define your own extensions to
350ignore, use the ignore_extensions option:
351
352 MyApp->config->{static}->{ignore_extensions} = [ qw/tt tt2 html xhtml/ ];
353
354=head2 Ignoring entire directories
355
356To prevent an entire directory from being served statically, you can use the
357ignore_dirs option. This option contains a list of relative directory paths
358to ignore. If using include_path, the path will be checked against every
359included path.
360
361 MyApp->config->{static}->{ignore_dirs} = [ qw/tmpl css/ ];
362
363For example, if combined with the above include_path setting, this
364ignore_dirs value will ignore the following directories if they exist:
365
366 /path/to/overlay/tmpl
367 /path/to/overlay/css
368 /dynamic/path/tmpl
369 /dynamic/path/css
370 /your/app/home/root/tmpl
371 /your/app/home/root/css
372
373=head2 Custom MIME types
374
375To override or add to the default MIME types set by the MIME::Types module,
376you may enter your own extension to MIME type mapping.
377
378 MyApp->config->{static}->{mime_types} = {
379 jpg => 'image/jpg',
380 png => 'image/png',
381 };
382
383=head2 Bypassing other plugins
384
385This plugin checks for a static file in the prepare_action stage. If the
386request is for a static file, it will bypass all remaining prepare_action
387steps. This means that by placing Static::Simple before all other plugins,
388they will not execute when a static file is found. This can be helpful by
389skipping session cookie checks for example. Or, if you want some plugins
390to run even on static files, list them before Static::Simple.
391
392Currently, work done by plugins in any other prepare method will execute
393normally.
394
395=head2 Debugging information
396
397Enable additional debugging information printed in the Catalyst log. This
398is automatically enabled when running Catalyst in -Debug mode.
399
400 MyApp->config->{static}->{debug} = 1;
401
402=head1 USING WITH APACHE
403
404While Static::Simple will work just fine serving files through Catalyst in
405mod_perl, for increased performance, you may wish to have Apache handle the
406serving of your static files. To do this, simply use a dedicated directory
407for your static files and configure an Apache Location block for that
408directory. This approach is recommended for production installations.
409
410 <Location /static>
411 SetHandler default-handler
412 </Location>
413
5c19e800 414=head1 INTERNAL EXTENDED METHODS
415
416Static::Simple extends the following steps in the Catalyst process.
417
418=head2 prepare_action
419
420prepare_action is used to first check if the request path is a static file.
421If so, we skip all other prepare_action steps to improve performance.
422
423=head2 dispatch
424
425dispatch takes the file found during prepare_action and writes it to the
426output.
427
428=head2 finalize
429
430finalize serves up final header information and displays any log messages.
431
432=head2 setup
433
434setup initializes all default values.
435
a9b78939 436=head1 SEE ALSO
437
438L<Catalyst>, L<Catalyst::Plugin::Static>,
439L<http://www.iana.org/assignments/media-types/>
440
441=head1 AUTHOR
442
443Andy Grundman, <andy@hybridized.org>
444
445=head1 CONTRIBUTORS
446
447Marcus Ramberg, <mramberg@cpan.org>
448
449=head1 THANKS
450
451The authors of Catalyst::Plugin::Static:
452
453 Sebastian Riedel
454 Christian Hansen
455 Marcus Ramberg
456
457For the include_path code from Template Toolkit:
458
459 Andy Wardley
460
461=head1 COPYRIGHT
462
463This program is free software, you can redistribute it and/or modify it under
464the same terms as Perl itself.
465
466=cut