package Plack::Util; use strict; use Carp (); use Scalar::Util; use IO::Handle; use overload (); use File::Spec (); sub TRUE() { 1==1 } sub FALSE() { !TRUE } # there does not seem to be a relevant RT or perldelta entry for this use constant _SPLICE_SAME_ARRAY_SEGFAULT => $] < '5.008007'; sub load_class { my($class, $prefix) = @_; if ($prefix) { unless ($class =~ s/^\+// || $class =~ /^$prefix/) { $class = "$prefix\::$class"; } } my $file = $class; $file =~ s!::!/!g; require "$file.pm"; ## no critic return $class; } sub is_real_fh ($) { my $fh = shift; { no warnings 'uninitialized'; return FALSE if -p $fh or -c _ or -b _; } my $reftype = Scalar::Util::reftype($fh) or return; if ( $reftype eq 'IO' or $reftype eq 'GLOB' && *{$fh}{IO} ) { # if it's a blessed glob make sure to not break encapsulation with # fileno($fh) (e.g. if you are filtering output then file descriptor # based operations might no longer be valid). # then ensure that the fileno *opcode* agrees too, that there is a # valid IO object inside $fh either directly or indirectly and that it # corresponds to a real file descriptor. my $m_fileno = $fh->fileno; return FALSE unless defined $m_fileno; return FALSE unless $m_fileno >= 0; my $f_fileno = fileno($fh); return FALSE unless defined $f_fileno; return FALSE unless $f_fileno >= 0; return TRUE; } else { # anything else, including GLOBS without IO (even if they are blessed) # and non GLOB objects that look like filehandle objects cannot have a # valid file descriptor in fileno($fh) context so may break. return FALSE; } } sub set_io_path { my($fh, $path) = @_; bless $fh, 'Plack::Util::IOWithPath'; $fh->path($path); } sub content_length { my $body = shift; return unless defined $body; if (ref $body eq 'ARRAY') { my $cl = 0; for my $chunk (@$body) { $cl += length $chunk; } return $cl; } elsif ( is_real_fh($body) ) { return (-s $body) - tell($body); } return; } sub foreach { my($body, $cb) = @_; if (ref $body eq 'ARRAY') { for my $line (@$body) { $cb->($line) if length $line; } } else { local $/ = \65536 unless ref $/; while (defined(my $line = $body->getline)) { $cb->($line) if length $line; } $body->close; } } sub class_to_file { my $class = shift; $class =~ s!::!/!g; $class . ".pm"; } sub _load_sandbox { my $_file = shift; my $_package = $_file; $_package =~ s/([^A-Za-z0-9_])/sprintf("_%2x", unpack("C", $1))/eg; local $0 = $_file; # so FindBin etc. works local @ARGV = (); # Some frameworks might try to parse @ARGV return eval sprintf <<'END_EVAL', $_package; package Plack::Sandbox::%s; { my $app = do $_file; if ( !$app && ( my $error = $@ || $! )) { die $error; } $app; } END_EVAL } sub load_psgi { my $stuff = shift; local $ENV{PLACK_ENV} = $ENV{PLACK_ENV} || 'development'; my $file = $stuff =~ /^[a-zA-Z0-9\_\:]+$/ ? class_to_file($stuff) : File::Spec->rel2abs($stuff); my $app = _load_sandbox($file); die "Error while loading $file: $@" if $@; return $app; } sub run_app($$) { my($app, $env) = @_; return eval { $app->($env) } || do { my $body = "Internal Server Error"; $env->{'psgi.errors'}->print($@); [ 500, [ 'Content-Type' => 'text/plain', 'Content-Length' => length($body) ], [ $body ] ]; }; } sub headers { my $headers = shift; inline_object( iter => sub { header_iter($headers, @_) }, get => sub { header_get($headers, @_) }, set => sub { header_set($headers, @_) }, push => sub { header_push($headers, @_) }, exists => sub { header_exists($headers, @_) }, remove => sub { header_remove($headers, @_) }, headers => sub { $headers }, ); } sub header_iter { my($headers, $code) = @_; my @headers = @$headers; # copy while (my($key, $val) = splice @headers, 0, 2) { $code->($key, $val); } } sub header_get { my($headers, $key) = (shift, lc shift); return () if not @$headers; my $i = 0; if (wantarray) { return map { $key eq lc $headers->[$i++] ? $headers->[$i++] : ++$i && (); } 1 .. @$headers/2; } while ($i < @$headers) { return $headers->[$i+1] if $key eq lc $headers->[$i]; $i += 2; } (); } sub header_set { my($headers, $key, $val) = @_; @$headers = ($key, $val), return if not @$headers; my ($i, $_key) = (0, lc $key); # locate and change existing header while ($i < @$headers) { $headers->[$i+1] = $val, last if $_key eq lc $headers->[$i]; $i += 2; } if ($i > $#$headers) { # didn't find it? push @$headers, $key, $val; return; } $i += 2; # found and changed it; so, first, skip that pair return if $i > $#$headers; # anything left? # yes... so do the same thing as header_remove # but for the tail of the array only, starting at $i my $keep; my @keep = grep { $_ & 1 ? $keep : ($keep = $_key ne lc $headers->[$_]); } $i .. $#$headers; my $remainder = @$headers - $i; return if @keep == $remainder; # if we're not changing anything... splice @$headers, $i, $remainder, ( _SPLICE_SAME_ARRAY_SEGFAULT ? @{[ @$headers[@keep] ]} # force different source array : @$headers[@keep] ); (); } sub header_push { my($headers, $key, $val) = @_; push @$headers, $key, $val; } sub header_exists { my($headers, $key) = (shift, lc shift); my $check; for (@$headers) { return 1 if ($check = not $check) and $key eq lc; } return !1; } sub header_remove { my($headers, $key) = (shift, lc shift); return if not @$headers; my $keep; my @keep = grep { $_ & 1 ? $keep : ($keep = $key ne lc $headers->[$_]); } 0 .. $#$headers; @$headers = @$headers[@keep] if @keep < @$headers; (); } sub status_with_no_entity_body { my $status = shift; return $status < 200 || $status == 204 || $status == 304; } sub encode_html { my $str = shift; $str =~ s/&/&/g; $str =~ s/>/>/g; $str =~ s/($res); # If response_cb returns a callback, treat it as a $body filter if (defined $filter_cb && ref $filter_cb eq 'CODE') { Plack::Util::header_remove($res->[1], 'Content-Length'); if (defined $res->[2]) { if (ref $res->[2] eq 'ARRAY') { for my $line (@{$res->[2]}) { $line = $filter_cb->($line); } # Send EOF. my $eof = $filter_cb->( undef ); push @{ $res->[2] }, $eof if defined $eof; } else { my $body = $res->[2]; my $getline = sub { $body->getline }; $res->[2] = Plack::Util::inline_object getline => sub { $filter_cb->($getline->()) }, close => sub { $body->close }; } } else { return $filter_cb; } } }; if (ref $res eq 'ARRAY') { $body_filter->($cb, $res); return $res; } elsif (ref $res eq 'CODE') { return sub { my $respond = shift; my $cb = $cb; # To avoid the nested closure leak for 5.8.x $res->(sub { my $res = shift; my $filter_cb = $body_filter->($cb, $res); if ($filter_cb) { my $writer = $respond->($res); if ($writer) { return Plack::Util::inline_object write => sub { $writer->write($filter_cb->(@_)) }, close => sub { my $chunk = $filter_cb->(undef); $writer->write($chunk) if defined $chunk; $writer->close; }; } } else { return $respond->($res); } }); }; } return $res; } package Plack::Util::Prototype; our $AUTOLOAD; sub can { return $_[0]->{$_[1]} if Scalar::Util::blessed($_[0]); goto &UNIVERSAL::can; } sub AUTOLOAD { my $self = shift; my $attr = $AUTOLOAD; $attr =~ s/.*://; if (ref($self->{$attr}) eq 'CODE') { $self->{$attr}->(@_); } else { Carp::croak(qq/Can't locate object method "$attr" via package "Plack::Util::Prototype"/); } } sub DESTROY { } package Plack::Util::IOWithPath; use parent qw(IO::Handle); sub path { my $self = shift; if (@_) { ${*$self}{+__PACKAGE__} = shift; } ${*$self}{+__PACKAGE__}; } package Plack::Util; 1; __END__ =head1 NAME Plack::Util - Utility subroutines for Plack server and framework developers =head1 FUNCTIONS =over 4 =item TRUE, FALSE my $true = Plack::Util::TRUE; my $false = Plack::Util::FALSE; Utility constants to include when you specify boolean variables in C<$env> hash (e.g. C). =item load_class my $class = Plack::Util::load_class($class [, $prefix ]); Constructs a class name and C the class. Throws an exception if the .pm file for the class is not found, just with the built-in C. If C<$prefix> is set, the class name is prepended to the C<$class> unless C<$class> begins with C<+> sign, which means the class name is already fully qualified. my $class = Plack::Util::load_class("Foo"); # Foo my $class = Plack::Util::load_class("Baz", "Foo::Bar"); # Foo::Bar::Baz my $class = Plack::Util::load_class("+XYZ::ZZZ", "Foo::Bar"); # XYZ::ZZZ Note that this function doesn't validate (or "sanitize") the passed string, hence if you pass a user input to this function (which is an insecure thing to do in the first place) it might lead to unexpected behavior of loading files outside your C<@INC> path. If you want a generic module loading function, you should check out CPAN modules such as L. =item is_real_fh if ( Plack::Util::is_real_fh($fh) ) { } returns true if a given C<$fh> is a real file handle that has a file descriptor. It returns false if C<$fh> is PerlIO handle that is not really related to the underlying file etc. =item content_length my $cl = Plack::Util::content_length($body); Returns the length of content from body if it can be calculated. If C<$body> is an array ref it's a sum of length of each chunk, if C<$body> is a real filehandle it's a remaining size of the filehandle, otherwise returns undef. =item set_io_path Plack::Util::set_io_path($fh, "/path/to/foobar.txt"); Sets the (absolute) file path to C<$fh> filehandle object, so you can call C<< $fh->path >> on it. As a side effect C<$fh> is blessed to an internal package but it can still be treated as a normal file handle. This module doesn't normalize or absolutize the given path, and is intended to be used from Server or Middleware implementations. See also L. =item foreach Plack::Util::foreach($body, $cb); Iterate through I<$body> which is an array reference or IO::Handle-like object and pass each line (which is NOT really guaranteed to be a I) to the callback function. It internally sets the buffer length C<$/> to 65536 in case it reads the binary file, unless otherwise set in the caller's code. =item load_psgi my $app = Plack::Util::load_psgi $psgi_file_or_class; Load C file or a class name (like C) and require the file to get PSGI application handler. If the file can't be loaded (e.g. file doesn't exist or has a perl syntax error), it will throw an exception. Since version 1.0006, this function would not load PSGI files from include paths (C<@INC>) unless it looks like a class name that only consists of C<[A-Za-z0-9_:]>. For example: Plack::Util::load_psgi("app.psgi"); # ./app.psgi Plack::Util::load_psgi("/path/to/app.psgi"); # /path/to/app.psgi Plack::Util::load_psgi("MyApp::PSGI"); # MyApp/PSGI.pm from @INC B: If you give this function a class name or module name that is loadable from your system, it will load the module. This could lead to a security hole: my $psgi = ...; # user-input: consider "Moose" $app = Plack::Util::load_psgi($psgi); # this would lead to 'require "Moose.pm"'! Generally speaking, passing an external input to this function is considered very insecure. If you really want to do that, validate that a given file name contains dots (like C) and also turn it into a full path in your caller's code. =item run_app my $res = Plack::Util::run_app $app, $env; Runs the I<$app> by wrapping errors with I and if an error is found, logs it to C<< $env->{'psgi.errors'} >> and returns the template 500 Error response. =item header_get, header_exists, header_set, header_push, header_remove my $hdrs = [ 'Content-Type' => 'text/plain' ]; my $v = Plack::Util::header_get($hdrs, $key); # First found only my @v = Plack::Util::header_get($hdrs, $key); my $bool = Plack::Util::header_exists($hdrs, $key); Plack::Util::header_set($hdrs, $key, $val); # overwrites existent header Plack::Util::header_push($hdrs, $key, $val); Plack::Util::header_remove($hdrs, $key); Utility functions to manipulate PSGI response headers array reference. The methods that read existent header value handles header name as case insensitive. my $hdrs = [ 'Content-Type' => 'text/plain' ]; my $v = Plack::Util::header_get($hdrs, 'content-type'); # 'text/plain' =item headers my $headers = [ 'Content-Type' => 'text/plain' ]; my $h = Plack::Util::headers($headers); $h->get($key); if ($h->exists($key)) { ... } $h->set($key => $val); $h->push($key => $val); $h->remove($key); $h->headers; # same reference as $headers Given a header array reference, returns a convenient object that has an instance methods to access C functions with an OO interface. The object holds a reference to the original given C<$headers> argument and updates the reference accordingly when called write methods like C, C or C. It also has C method that would return the same reference. =item status_with_no_entity_body if (status_with_no_entity_body($res->[0])) { } Returns true if the given status code doesn't have any Entity body in HTTP response, i.e. it's 100, 101, 204 or 304. =item inline_object my $o = Plack::Util::inline_object( write => sub { $h->push_write(@_) }, close => sub { $h->push_shutdown }, ); $o->write(@stuff); $o->close; Creates an instant object that can react to methods passed in the constructor. Handy to create when you need to create an IO stream object for input or errors. =item encode_html my $encoded_string = Plack::Util::encode_html( $string ); Entity encodes C<<>, C<< > >>, C<&>, C<"> and C<'> in the input string and returns it. =item response_cb See L for details. =back =cut