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