# vim: set cindent expandtab ts=4 sw=4:
#
# Copyright (c) 1998-2005 Chi-Keung Ho. All rights reserved.
#
# This programe is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# Extmail - a high-performance webmail to maildir
# $Id$
package Ext::CGI;
# Update: 2005-02-22, major change. We should completely advoid
# multiple calling 'new Ext::CGI' crash the form data!
use strict;
use Exporter;
use Ext::Utils;
use vars qw(%IN @ISA @EXPORT @EXPORT_OK $CRLF %CGIDATA);
use vars qw($LOADED $COOKIE %COOKIE_SESS);
@ISA = qw(Exporter);
@EXPORT = qw($CRLF);
%IN = ();
local $/ = $CRLF;
sub new {
my $this = shift;
my $self = bless {@_}, ref $this || $this;
$self->init(@_);
$self; # does not need new more obj ?
}
sub init {
my $self = shift;
my %opt = @_;
# Currently for the reason to simplify, only support
# Unix CRLF, sorry for M$ or VMS user
$CRLF = "\015\012"; # \r\n
$self->read_params unless ($self->{loaded});
}
sub read_params {
my $self = shift;
unless($self->{loaded}) { # XXX hack, works under CGI/FCGI
$self->{loaded} = 1; # for class use
%IN = %{$self->_read_params};
$self->{in} = \%IN;
}
\%IN;
}
# Technology tricks: we shold not call read_params more times,
# because POST data can get only once, for later use, we should
# cache the result :)
sub _read_params
{
my $self = shift;
if ($LOADED) {
return \%CGIDATA;
}
# XXX experimental code for persistent envirement
if ($ENV{FCGI_ROLE} || $ENV{FCGI_APACHE_ROLE}) {
require Ext::FCGI;
Ext::FCGI::register_cleanup(\&cleanup);
}
%CGIDATA = ();
my $in;
my $meth = $_[1] ? $_[1] : $ENV{'REQUEST_METHOD'};
$COOKIE = $self->raw_cookie;
METHOD: {
if ($meth=~/post/i && defined $ENV{'CONTENT_TYPE'}
&& $ENV{'CONTENT_TYPE'}=~m#^multipart/form-data#) {
my($bdr) = $ENV{'CONTENT_TYPE'} =~ /boundary=\"?([^\";,]+)\"?/;
#$self->_debug_read($bdr);
$self->_read_multiparts($bdr, $ENV{'CONTENT_LENGTH'});
last METHOD;
}
if ($meth=~/post/i) { # XXX not multipart?
read(STDIN, $in, $ENV{'CONTENT_LENGTH'});
#print "in='$in'<br>\n";
}
if ($ENV{'QUERY_STRING'}) {
if ($in) { $in .= "&".$ENV{'QUERY_STRING'}; }
else { $in = $ENV{'QUERY_STRING'}; }
}
} # END of METHOD
if(!$in) {
$LOADED = 1;
return \%CGIDATA;
}
foreach my $i (split(/\&/, $in)) {
my ($k, $v) = split(/=/, $i, 2);
$k = url2str($k);
$v = url2str($v);
#print "key=$k, val=$v<br>\n";
$CGIDATA{$k} = defined($CGIDATA{$k}) ? $CGIDATA{$k}."\0".$v : $v;
}
# return a HASH reference
# $self->{in} = $a;
$LOADED = 1;
\%CGIDATA;
}
## XXX Debug use only!
sub _debug_read {
my $self = shift;
my ($boundary) = @_;
while(<STDIN>) {
print $_;
}
}
use vars qw($curline);
$curline = "";
### Multipart handers, curline and $ref will only effect inside these
# functions, and don't try to access them
sub _read_multiparts {
no warnings; # XXX ouch :-(
my $self = shift;
my ($boundary, $length) = ($_[0], $_[1]);
my $curbuf = "";
my $num = 0;
while($curbuf = _read_stdin($curline)) {
# XXX XXX XXX XXX perl will complain the following error:
# Use of uninitialized value in pattern match (m//) at line xx,
# <STDIN> line yy, currently we can only disable the warning..
#
# Upate: 2005-08-03, strange regexp error here, some null file-upload
# will cause un-match error, may be due to the special character, ouch:(
#
# The old code:
# if($curbuf=~/^--$boundary/) and $curbuf=~!/^--$boundary--/) sucks!
if($curbuf=~/^--$boundary$CRLF/) {
my $buf = <STDIN>;
if (length($buf)>0) {
if($buf=~/^Content-Disposition:\s*.*;\s*filename="(.*)"/) {
my $filename = $1;
my $seq = _gen_tmpfile($num);
# ignore null or not fill filename :)
next if (not defined $filename or $filename eq "");
my $rv = _read_mimeparts($boundary, $buf, $seq);
$curbuf = $rv;
if($rv) {
$CGIDATA{UPLOAD_FILES}->{'part'.$num} =
{
path => $seq,
filename => $filename
};
}
$num++;
}elsif($buf=~/^Content-Disposition:\s*.*;\s*name="([^:]+)"/) {
<STDIN>;
my ($k, $v) = ($1, _read_mimeparams($boundary));
#$k = url2str($k);
#$v = url2str($v);
$CGIDATA{$k} = $v; # XXX
}
} else { last; }
}
last if($curbuf=~/--$boundary--/);
}
}
sub _read_mimeparts {
my($boundary, $firstline, $seq) = @_;
my $lastline = undef;
$seq = untaint ($seq); # XXX useful under taint mode
open(FD, ">$seq") or die "Failt to open $seq, $!\n";
print FD $firstline;
while(1) {
my $buf = <STDIN>;
last if length($buf)<1;
if($buf=~/^--$boundary/) {
$curline = $buf;
$lastline =~ s/\r*\n$//g; # remove the last cr*lf
print FD $lastline;
close FD;
return $buf;
}else {
print FD (defined $lastline ? $lastline : $buf);
}
$lastline = $buf;
}
close FD;
}
sub _read_mimeparams {
my($boundary) = @_;
my $buf = "";
while(1) {
my $s = <STDIN>;
last if length($s)<1;
if($s=~/^--$boundary/) {
$curline = $s;
last;
}else { $buf .= $s; }
}
# return the correct value not include the
# last CRLF tag.
$buf =~s/$CRLF$//;
$buf =~s/$CRLF/\n/g; # ouch ?
$buf;
}
sub _read_stdin {
my $sig = shift;
if($sig) {
undef $curline;
return $sig;
}
<STDIN>;
}
sub _gen_tmpfile {
my $seq = shift;
my $sid = $CGIDATA{sid} ? $CGIDATA{sid} : "";
my $seed = time.'-'.$$.'-'.$seq.'.msg'; # XXX
if($sid) {
if(!-d '/tmp/'.$sid) {
mkdir untaint("/tmp/$sid"), 0700;
}
return '/tmp/'.$sid.'/'.$seed;
}
return '/tmp/'.$seed;
}
### END of multipar handlers
sub cgi {
my $self = shift;
my $name = $_[0];
my $in = $self->read_params;
#use Data::Dumper;
#print Dumper($in);
#printf "call from %s<br>\n", caller;
defined $in->{$name} ? $in->{$name} : "";
}
# cgi_blist - get a bunch of cgi params, return it's value
# as array.
sub cgi_blist {
my $self = shift;
my @name = @_;
my $in = $self->read_params;
foreach (0...scalar @name) {
if (defined $in->{$name[$_]}) {
$name[$_]=$in->{$name[$_]};
}else {
$name[$_]="";
}
}
@name; # return value map to the name
}
# cgi_full_names - return all name of parameters, not include
# values, use cgi_full_list if you want values
sub cgi_full_names {
my $in = shift->read_params;
my @names = ();
for(keys %$in) {
push @names, $_;
}
\@names;
}
# cgi_full_list - return the $in struct
sub cgi_full_list {
shift->read_params;
}
# XXX experimental cookie support
#
# set_cookie - set a bunch of parameters for a cookie
sub set_cookie {
my $self = shift;
my %opt = @_;
return 'Need cookie name' unless ($opt{name});
my $name = delete $opt{name};
$opt{expires} = $self->expires($opt{expires}) if defined $opt{expires};
$opt{max_age} = expire_calc($opt{expires})-time() if defined $opt{max_age};
$COOKIE_SESS{$name} = \%opt;
0;
}
sub get_cookie {
my $self = shift;
my $name = $_[0];
my $cookie = $self->{_cookie} || $self->_parse_cookie;
return unless $name;
return $cookie->{$name};
}
sub raw_cookie {
my $self = shift;
my @parms = @_;
my $cookie = $ENV{HTTP_COOKIE} || $ENV{COOKIE};
$cookie;
}
sub _parse_cookie {
my $self = shift;
my $parsed = $self->{parsed_cookie};
my %cookie = ();
return $self->{_cookie} if $parsed;
my @lists = split ("; ?", $COOKIE);
for (@lists) {
s/^\s*//;
s/\s*$//;
my ($k, $v) = split(/=/, $_, 2);
next unless defined $v;
my @values;
$k = url2str($k);
if ($v ne '') {
@values = map url2str($_), split(/[&;]/, $v.'&dmy');
pop @values;
}
$cookie{$k} = ($v eq '' ? $v : (scalar @values>1?\@values:$values[0]));
}
$self->{_cookie} = \%cookie;
}
sub expires {
my $self = shift;
my ($time, $format) = @_;
$format ||= 'http';
my(@MON)=qw/Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec/;
my(@WDAY) = qw/Sun Mon Tue Wed Thu Fri Sat/;
$time = expire_calc($time);
return $time unless $time =~ /^\d+$/;
my $sp = ' ';
$sp = '-' if ($format eq 'cookie');
my($sec,$min,$hour,$mday,$mon,$year,$wday) = gmtime($time);
$year += 1900;
return sprintf("%s, %02d$sp%s$sp%04d %02d:%02d:%02d GMT",
$WDAY[$wday],$mday,$MON[$mon],$year,$hour,$min,$sec);
}
sub cookie_string {
my $self = shift;
my $name = shift;
if (defined $COOKIE_SESS{$name}) {
my $h = $COOKIE_SESS{$name};
my @arr;
push @arr, "domain=$h->{domain}" if ($h->{domain});
push @arr, "path=$h->{path}" if ($h->{path});
push @arr, "expires=$h->{expires}" if ($h->{expires});
push @arr, "max-age=$h->{max_age}" if ($h->{max_age});
push @arr, "secure" if ($h->{secure});
$name = str2url($name);
my $cookie = join ('=', $name, join('&',map str2url($_), $h->{value}));
return join("; ", $cookie, @arr);
}
'';
}
sub send_cookie {
my $self = shift;
return if $self->{cookie_sent};
for my $key (keys %COOKIE_SESS) {
my $val = $COOKIE_SESS{$key};
print "Set-Cookie: ", $self->cookie_string($key), "\r\n";
}
$self->{cookie_sent} = 1;
1;
}
# we will do cleanup in DESTROY(), this is the last chance
# to do it in a sigle object, but if we need per request
# cleanup, please refer to the cleanup() function below
sub DESTROY {
my $self = shift;
_cleantmp();
undef %CGIDATA;
undef $COOKIE;
undef %COOKIE_SESS;
}
# per request cleanup function, called by FCGI/MOD_PERL or
# SPEEDY persistent implemention at the end of a request
sub cleanup {
# call _cleantmp to do some cleanup, but this function should be
# rewritten in the future, to adovid some race condition.
_cleantmp();
%CGIDATA = ();
$LOADED = 0;
$COOKIE = undef;
%COOKIE_SESS = ();
}
sub _cleantmp {
return unless ($CGIDATA{sid} && -d '/tmp/'.$CGIDATA{sid});
opendir DIR, '/tmp/'.$CGIDATA{sid}; # ignore error
my @f = grep { !/^\./ } readdir DIR;
unlink untaint('/tmp/'.$CGIDATA{sid}.'/'.$_) for(@f);
rmdir untaint('/tmp/'.$CGIDATA{sid});
}
1;
syntax highlighted by Code2HTML, v. 0.9.1