package Catalyst::Plugin::Static::Simple;
-
use Moose::Role;
-use File::stat;
-use File::Spec ();
-use IO::File ();
-use MIME::Types ();
-use MooseX::Types::Moose qw/ArrayRef Str/;
-use Catalyst::Utils;
use namespace::autoclean;
-our $VERSION = '0.31';
-
-has _static_file => ( is => 'rw' );
-has _static_debug_message => ( is => 'rw', isa => ArrayRef[Str] );
-
-before prepare_action => sub {
- my $c = shift;
- my $path = $c->req->path;
- my $config = $c->config->{'Plugin::Static::Simple'};
-
- $path =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
-
- # is the URI in a static-defined path?
- foreach my $dir ( @{ $config->{dirs} } ) {
- my $dir_re = quotemeta $dir;
-
- # strip trailing slashes, they'll be added in our regex
- $dir_re =~ s{/$}{};
-
- my $re;
-
- if ( $dir =~ m{^qr/}xms ) {
- $re = eval $dir;
-
- if ($@) {
- $c->error( "Error compiling static dir regex '$dir': $@" );
- }
- }
- else {
- $re = qr{^${dir_re}/};
- }
-
- if ( $path =~ $re ) {
- if ( $c->_locate_static_file( $path, 1 ) ) {
- $c->_debug_msg( 'from static directory' )
- if $config->{debug};
- } else {
- $c->_debug_msg( "404: file not found: $path" )
- if $config->{debug};
- $c->res->status( 404 );
- $c->res->content_type( 'text/html' );
- }
- }
- }
-
- # Does the path have an extension?
- if ( $path =~ /.*\.(\S{1,})$/xms ) {
- # and does it exist?
- $c->_locate_static_file( $path );
- }
-};
-
-around dispatch => sub {
- my $orig = shift;
- my $c = shift;
-
- return if ( $c->res->status != 200 );
-
- if ( $c->_static_file ) {
- if ( $c->config->{'Plugin::Static::Simple'}->{no_logs} && $c->log->can('abort') ) {
- $c->log->abort( 1 );
- }
- return $c->_serve_static;
- }
- else {
- return $c->$orig(@_);
- }
-};
+use Plack::App::File;
+use Catalyst::Utils;
-before finalize => sub {
- my $c = shift;
+use Catalyst::Plugin::Static::Simple::Middleware;
- # display all log messages
- if ( $c->config->{'Plugin::Static::Simple'}->{debug} && scalar @{$c->_debug_msg} ) {
- $c->log->debug( 'Static::Simple: ' . join q{ }, @{$c->_debug_msg} );
- }
-};
+our $VERSION = '0.32';
before setup_finalize => sub {
- my $c = shift;
+ my $app = shift;
- $c->log->warn("Deprecated 'static' config key used, please use the key 'Plugin::Static::Simple' instead")
- if exists $c->config->{static};
my $config
- = $c->config->{'Plugin::Static::Simple'}
- = $c->config->{'static'}
+ = $app->config->{'Plugin::Static::Simple'}
+ = $app->config->{'static'}
= Catalyst::Utils::merge_hashes(
- $c->config->{'Plugin::Static::Simple'} || {},
- $c->config->{static} || {}
+ $app->config->{'Plugin::Static::Simple'} || {},
+ $app->config->{static} || {}
);
$config->{dirs} ||= [];
- $config->{include_path} ||= [ $c->config->{root} ];
- $config->{mime_types} ||= {};
+ $config->{include_path} ||= [ $app->config->{root} ];
$config->{ignore_extensions} ||= [ qw/tmpl tt tt2 html xhtml/ ];
$config->{ignore_dirs} ||= [];
- $config->{debug} ||= $c->debug;
- $config->{no_logs} = 1 unless defined $config->{no_logs};
- $config->{no_logs} = 0 if $config->{logging};
+ $config->{debug} ||= $app->debug;
- # load up a MIME::Types object, only loading types with
- # at least 1 file extension
- $config->{mime_types_obj} = MIME::Types->new( only_complete => 1 );
-};
-
-# Search through all included directories for the static file
-# Based on Template Toolkit INCLUDE_PATH code
-sub _locate_static_file {
- my ( $c, $path, $in_static_dir ) = @_;
-
- $path = File::Spec->catdir(
- File::Spec->no_upwards( File::Spec->splitdir( $path ) )
- );
+ my $static_middleware = Catalyst::Plugin::Static::Simple::Middleware->new({
+ config => $config,
+ cat_app => ref($app) || $app,
+ content_type => $app->_build_content_type_callback,
+ });
- my $config = $c->config->{'Plugin::Static::Simple'};
- my @ipaths = @{ $config->{include_path} };
- my $dpaths;
- my $count = 64; # maximum number of directories to search
-
- DIR_CHECK:
- while ( @ipaths && --$count) {
- my $dir = shift @ipaths || next DIR_CHECK;
-
- if ( ref $dir eq 'CODE' ) {
- eval { $dpaths = &$dir( $c ) };
- if ($@) {
- $c->log->error( 'Static::Simple: include_path error: ' . $@ );
- } else {
- unshift @ipaths, @$dpaths;
- next DIR_CHECK;
- }
- } else {
- $dir =~ s/(\/|\\)$//xms;
- if ( -d $dir && -f $dir . '/' . $path ) {
-
- # Don't ignore any files in static dirs defined with 'dirs'
- unless ( $in_static_dir ) {
- # do we need to ignore the file?
- for my $ignore ( @{ $config->{ignore_dirs} } ) {
- $ignore =~ s{(/|\\)$}{};
- if ( $path =~ /^$ignore(\/|\\)/ ) {
- $c->_debug_msg( "Ignoring directory `$ignore`" )
- if $config->{debug};
- next DIR_CHECK;
- }
- }
-
- # do we need to ignore based on extension?
- for my $ignore_ext ( @{ $config->{ignore_extensions} } ) {
- if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
- $c->_debug_msg( "Ignoring extension `$ignore_ext`" )
- if $config->{debug};
- next DIR_CHECK;
- }
- }
- }
-
- $c->_debug_msg( 'Serving ' . $dir . '/' . $path )
- if $config->{debug};
- return $c->_static_file( $dir . '/' . $path );
- }
- }
- }
+ $app->setup_middleware( $static_middleware );
+};
- return;
-}
+sub _build_content_type_callback {
+ my ( $c ) = @_;
-sub _serve_static {
- my $c = shift;
my $config = $c->config->{'Plugin::Static::Simple'};
+ return sub {
+ my $full_path = shift;
+ my $mime_type;
- my $full_path = shift || $c->_static_file;
- my $type = $c->_ext_to_type( $full_path );
- my $stat = stat $full_path;
-
- $c->res->headers->content_type( $type );
- $c->res->headers->content_length( $stat->size );
- $c->res->headers->last_modified( $stat->mtime );
- # Tell Firefox & friends its OK to cache, even over SSL:
- $c->res->headers->header('Cache-control' => 'public');
- # Optionally, set a fixed expiry time:
- if ($config->{expires}) {
- $c->res->headers->expires(time() + $config->{expires});
- }
+ if ( $config->{mime_types} && $full_path =~ /.*\.(\S{1,})$/xms ) {
+ $mime_type = $config->{mime_types}->{ $1 };
+ }
- my $fh = IO::File->new( $full_path, 'r' );
- if ( defined $fh ) {
- binmode $fh;
- $c->res->body( $fh );
+ return $mime_type || Plack::MIME->mime_type($full_path) || 'text/plain';
}
- else {
- Catalyst::Exception->throw(
- message => "Unable to open $full_path for reading" );
- }
-
- return 1;
}
sub serve_static_file {
my ( $c, $full_path ) = @_;
- my $config = $c->config->{'Plugin::Static::Simple'};
-
- if ( -e $full_path ) {
- $c->_debug_msg( "Serving static file: $full_path" )
- if $config->{debug};
- }
- else {
- $c->_debug_msg( "404: file not found: $full_path" )
- if $config->{debug};
- $c->res->status( 404 );
- $c->res->content_type( 'text/html' );
- return;
- }
-
- $c->_serve_static( $full_path );
-}
-
-# looks up the correct MIME type for the current file extension
-sub _ext_to_type {
- my ( $c, $full_path ) = @_;
-
- my $config = $c->config->{'Plugin::Static::Simple'};
-
- if ( $full_path =~ /.*\.(\S{1,})$/xms ) {
- my $ext = $1;
- my $type = $config->{mime_types}{$ext}
- || $config->{mime_types_obj}->mimeTypeOf( $ext );
- if ( $type ) {
- $c->_debug_msg( "as $type" ) if $config->{debug};
- return ( ref $type ) ? $type->type : $type;
- }
- else {
- $c->_debug_msg( "as text/plain (unknown extension $ext)" )
- if $config->{debug};
- return 'text/plain';
- }
- }
- else {
- $c->_debug_msg( 'as text/plain (no extension)' )
- if $config->{debug};
- return 'text/plain';
- }
-}
-
-sub _debug_msg {
- my ( $c, $msg ) = @_;
-
- if ( !defined $c->_static_debug_message ) {
- $c->_static_debug_message( [] );
- }
+ my $res;
- if ( $msg ) {
- push @{ $c->_static_debug_message }, $msg;
+ if(! -f $full_path ) {
+ $res = Catalyst::Plugin::Static::Simple::Middleware->return_404;
+ } else {
+ my $file_app = Plack::App::File->new( {
+ file => $full_path,
+ content_type => $c->_build_content_type_callback
+ } );
+ $res = $file_app->call($c->req->env);
}
- return $c->_static_debug_message;
+ $c->res->from_psgi_response($res);
}
1;
+
__END__
=head1 NAME
Toby Corkindale, <tjc@wintrmute.net>
+Graeme Lawton <cpan@per.ly>
+
+Mark Ellis <markellis@cpan.org>
+
=head1 THANKS
The authors of Catalyst::Plugin::Static:
--- /dev/null
+package Catalyst::Plugin::Static::Simple::Middleware;
+use strict;
+use warnings;
+
+use parent qw/Plack::Middleware/;
+
+use Plack::Util::Accessor qw/content_type config cat_app/;
+use Plack::App::File;
+use Carp;
+
+sub call {
+ my ( $self, $env ) = @_;
+
+ #wrap everything in an eval so if something goes wrong the user
+ #gets an "internal server error" message, and we get a warning
+ #instead of the user getting the warning and us getting nothing
+ my $return = eval {
+ my ( $res, $c );
+
+ if(! $self->_check_static_simple_path( $env ) ) {
+ return $self->app->($env)
+ }
+
+ my @ipaths = @{$self->config->{include_path}};
+ while ( my $ipath = shift @ipaths ) {
+ my $root;
+ if ( ref($ipath) eq 'CODE' ) {
+ $c ||= $self->cat_app->prepare(env => $env, response_cb => sub {});
+ my $paths = $ipath->( $c );
+
+ unshift @ipaths, @{$paths};
+ next;
+ } else {
+ $root = $ipath;
+ }
+
+ if(! -f $root . $env->{PATH_INFO} ) {
+ next;
+ }
+
+ my $file = Plack::App::File->new( {
+ root => $root,
+ content_type => $self->content_type
+ } );
+ $res = $file->call($env);
+
+ if( $res && ($res->[0] != 404) ) {
+ return $res;
+ }
+ }
+
+ if(! scalar ( @{$self->config->{dirs}})) {
+ $self->_debug( "Forwarding to Catalyst (or other middleware).", $env );
+ return $self->app->($env);
+ }
+
+ $self->_debug( "404: file not found: " . $env->{PATH_INFO}, $env );
+ return $self->return_404;
+ };
+
+ if ( $@ ) {
+ if($env->{'psgix.logger'} && ref($env->{'psgix.logger'}) eq 'CODE') {
+ $env->{'psgix.logger'}->({ level => 'error', message => $@ });
+ } else {
+ carp $@;
+ }
+ return $self->return_500;
+ }
+
+ return $return;
+}
+
+sub _check_static_simple_path {
+ my ( $self, $env ) = @_;
+ my $path = $env->{PATH_INFO};
+
+ for my $ignore_ext ( @{ $self->config->{ignore_extensions} } ) {
+ if ( $path =~ /.*\.${ignore_ext}$/ixms ) {
+ $self->_debug ( "Ignoring extension `$ignore_ext`", $env );
+ return undef
+ }
+ }
+
+ for my $ignore ( @{ $self->config->{ignore_dirs} } ) {
+ $ignore =~ s{(/|\\)$}{};
+
+ if ( $path =~ /^\/$ignore(\/|\\)/ ) {
+ $self->_debug( "Ignoring directory `$ignore`", $env );
+ return undef;
+ }
+ }
+
+ #we serve everything if it exists and dirs is not set
+ #we check if it exists in the middleware, once we've built the include paths
+ return 1 if ( !scalar(@{$self->config->{dirs}}) );
+
+ if( $self->_path_matches_dirs($path, $self->config->{dirs}) ) {
+ return 1;
+ }
+
+ return undef;
+}
+
+sub _path_matches_dirs {
+ my ( $self, $path, $dirs ) = @_;
+
+ $path =~ s!^/!!; #Remove leading slashes
+
+ foreach my $dir ( @$dirs ) {
+ my $re;
+ if (ref($dir) eq 'Regexp') {
+ $re = $dir;
+ } elsif ( $dir =~ m{^qr/}xms ) {
+ $re = eval $dir;
+
+ if ($@) {
+ die( "Error compiling static dir regex '$dir': $@" );
+ }
+ } else {
+ my $dir_re = quotemeta $dir;
+ $dir_re =~ s{/$}{};
+ $re = qr{^${dir_re}/};
+ }
+
+ if( $path =~ $re ) {
+ return 1;
+ }
+ }
+
+ return undef;
+}
+
+sub _debug {
+ my ( $self, $msg, $env ) = @_;
+
+ if($env && $env->{'psgix.logger'} && ref($env->{'psgix.logger'}) eq 'CODE') {
+ $env->{'psgix.logger'}->({ level => 'error', message => $@ });
+ } else {
+ warn "Static::Simple: $msg\n" if $self->config->{debug};
+ }
+}
+
+sub return_404 {
+ #for backcompat we can't use the one in Plack::App::File as it has the content-type of plain
+ return [404, ['Content-Type' => 'text/html', 'Content-Length' => 9], ['not found']];
+}
+
+sub return_500 {
+ return [500, ['Content-Type' => 'text/pain', 'Content-Length' => 9], ['internal server error']];
+}
+
+1;
\ No newline at end of file