File Coverage

blib/lib/Catalyst/Plugin/Static/Simple/ButMaintained.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


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