package Apache::ASP::Request; use Apache::ASP::Collection; use strict; sub new { my $asp = shift; my $r = $asp->{r}; my $self = bless { asp => $asp, # content => undef, # Cookies => undef, # FileUpload => undef, # Form => undef, # QueryString => undef, # ServerVariables => undef, Method => $r->method || 'GET', TotalBytes => 0, }; # calculate whether to read POST data here my $request_binary_read = &config($asp, 'RequestBinaryRead', undef, 1); $asp->{request_binary_read} = $request_binary_read; # set up the environment, including authentication info my $env = { %{$r->subprocess_env}, %ENV }; if(&config($asp, 'AuthServerVariables')) { if(defined $r->get_basic_auth_pw) { my $c = $r->connection; #X: this needs to be extended to support Digest authentication $env->{AUTH_TYPE} = $c->auth_type; $env->{AUTH_USER} = $c->user; $env->{AUTH_NAME} = $r->auth_name; $env->{REMOTE_USER} = $c->user; $env->{AUTH_PASSWD} = $r->get_basic_auth_pw; } } $self->{'ServerVariables'} = bless $env, 'Apache::ASP::Collection'; # assign no matter what so Form is always defined my $form = {}; my %upload; my $headers_in = $self->{asp}{headers_in}; if($self->{Method} eq 'POST' and $request_binary_read) { $self->{TotalBytes} = defined($ENV{CONTENT_LENGTH}) ? $ENV{CONTENT_LENGTH} : $headers_in->get('Content-Length'); if($headers_in->get('Content-Type') =~ m|^multipart/form-data|) { # do the logic here so that the normal form POST processing will not # occur either $asp->{file_upload_process} = &config($asp, 'FileUploadProcess', undef, 1); if($asp->{file_upload_process}) { if($asp->{file_upload_temp} = &config($asp, 'FileUploadTemp')) { eval "use CGI;"; } else { # default leaves no temp files for prying eyes eval "use CGI qw(-private_tempfiles);"; } if($@) { $self->{asp}->Error("can't use file upload without CGI.pm: $@"); goto ASP_REQUEST_POST_READ_DONE; } # new behavior for file uploads when FileUploadMax is exceeded, # before it used to error abruptly, now it will simply skip the file # upload data local $CGI::DISABLE_UPLOADS = $CGI::DISABLE_UPLOADS; if($asp->{file_upload_max} = &config($asp, 'FileUploadMax')) { if($self->{TotalBytes} > $asp->{file_upload_max} ) { $CGI::DISABLE_UPLOADS = 1; } } $asp->{dbg} && $asp->Debug("using CGI.pm version ". (eval { CGI->VERSION } || $CGI::VERSION). " for file upload support" ); my %form; my $q = $self->{cgi} = new CGI; $asp->Debug($q->param); for(my @names = $q->param) { my @params = $q->param($_); $form{$_} = @params > 1 ? [ @params ] : $params[0]; if(ref($form{$_}) eq 'Fh') { my $fh = $form{$_}; binmode $fh if $asp->{win32}; $upload{$_} = $q->uploadInfo($fh); if($asp->{file_upload_temp}) { $upload{$_}{TempFile} = $q->tmpFileName($fh); $upload{$_}{TempFile} =~ s|^/+|/|; } $upload{$_}{BrowserFile} = "$fh"; $upload{$_}{FileHandle} = $fh; $upload{$_}{ContentType} = $upload{$_}{'Content-Type'}; # tie the file upload reference to a collection... %upload # may be many file uploads note. $upload{$_} = bless $upload{$_}, 'Apache::ASP::Collection'; $asp->{dbg} && $asp->Debug("file upload field processed for \$Request->{FileUpload}{$_}", $upload{$_}); } } $form = \%form; } else { $self->{asp}->Debug("FileUploadProcess is disabled, file upload data in \$Request->BinaryRead"); } } else { # Only tie to STDIN if we have cached contents # don't untie *STDIN until DESTROY, so filtered handlers # have an opportunity to use any cached contents that may exist if(my $len = $self->{TotalBytes}) { $self->{content} = $self->BinaryRead($len) || ''; tie(*STDIN, 'Apache::ASP::Request', $self); if($headers_in->get('Content-Type') eq 'application/x-www-form-urlencoded') { $form = &ParseParams($self, \$self->{content}); } else { $form = {}; } } } } ASP_REQUEST_POST_READ_DONE: $self->{'Form'} = bless $form, 'Apache::ASP::Collection'; $self->{'FileUpload'} = bless \%upload, 'Apache::ASP::Collection'; my $query = $r->args(); my $parsed_query = $query ? &ParseParams($self, \$query) : {}; $self->{'QueryString'} = bless $parsed_query, 'Apache::ASP::Collection'; if(&config($asp, 'RequestParams')) { $self->{'Params'} = bless { %$parsed_query, %$form }, 'Apache::ASP::Collection'; } # do cookies now my %cookies; if(my $cookie = $headers_in->get('Cookie')) { my @parts = split(/;\s*/, ($cookie || '')); for(@parts) { my($name, $value) = split(/\=/, $_, 2); $name = &Unescape($self, $name); next if ($name eq $Apache::ASP::SessionCookieName); next if $cookies{$name}; # skip dup's $cookies{$name} = ($value =~ /\=/) ? &ParseParams($self, $value) : &Unescape($self, $value); } } $self->{Cookies} = bless \%cookies, 'Apache::ASP::Collection'; $self; } sub DESTROY { my $self = shift; if($self->{cgi}) { # make sure CGI file handles are freed $self->{cgi}->DESTROY(); $self->{cgi} = undef; } for(keys %{$self->{FileUpload}}) { my $upload = $self->{FileUpload}{$_}; $self->{Form}{$_} = undef; if($upload->{FileHandle}) { close $upload->{FileHandle}; # $self->{asp}->Debug("closing fh $upload->{FileHandle}"); } $self->{FileUpload}{$_} = undef; } %$self = (); } # just returns itself sub TIEHANDLE { $_[1] }; # just spill the cache into the scalar, so multiple reads are # fine... whoever is reading from the cached contents must # be reading the whole thing just once for this to work, # which is fine for CGI.pm sub READ { my $self = $_[0]; $_[1] ||= ''; $_[1] .= $self->{content}; $self->{ServerVariables}{CONTENT_LENGTH}; } sub BINMODE { }; # COLLECTIONS, normal, Cookies are special, with the dictionary lookup # directly aliased as this should be faster than autoloading sub Form { shift->{Form}->Item(@_) } sub FileUpload { shift->{FileUpload}->Item(@_) } sub QueryString { shift->{QueryString}->Item(@_) } sub ServerVariables { shift->{ServerVariables}->Item(@_) } sub Params { my $self = shift; $self->{Params} || die("\$Request->Params object does not exist, enable with 'PerlSetVar RequestParams 1'"); $self->{Params}->Item(@_); } sub BinaryRead { my($self, $length) = @_; my $data; return undef unless $self->{TotalBytes}; if(ref(tied(*STDIN)) && tied(*STDIN)->isa('Apache::ASP::Request')) { if($self->{TotalBytes}) { if(defined $length) { return substr($self->{content}, 0, $length); } else { return $self->{content} } } else { return undef; } } else { defined($length) || ( $length = $self->{TotalBytes} ); my $asp = $self->{asp}; my $r = $asp->{r}; if(! $ENV{MOD_PERL}) { my $rv = sysread(*STDIN, $data, $length, 0); $asp->{dbg} && $asp->Debug("read $rv bytes from STDIN for CGI mode, tried $length bytes"); } else { $r->read($data, $length); $asp->{dbg} && $asp->Debug("read ".length($data)." bytes, tried $length bytes"); } return $data; } } sub Cookies { my($self, $name, $key) = @_; if(! $name) { $self->{Cookies}; } elsif($key) { $self->{Cookies}{$name}{$key}; } else { # when we just have the name, are we expecting a dictionary or not my $cookie = $self->{Cookies}{$name}; if(ref $cookie && wantarray) { return %$cookie; } else { # CollectionItem support here one day, to not return # an undef object, CollectionItem needs tied hash support return $cookie; } } } sub ParseParams { my($self, $string) = @_; ($string = $$string) if ref($string); ## faster if we pass a ref for a big string my %params; defined($string) || return(\%params); my @params = split /[\&\;]/, $string, -1; # we have to iterate through the params here to collect multiple values for # the same param, say from a multiple select statement for my $pair (@params) { my($key, $value) = map { # inline for greater efficiency # &Unescape($self, $_) my $todecode = $_; $todecode =~ tr/+/ /; # pluses become spaces $todecode =~ s/%([0-9a-fA-F]{2})/chr(hex($1))/ge; $todecode; } split (/\=/, $pair, 2); if(defined $params{$key}) { my $collect = $params{$key}; if(ref $collect) { # we have already collected more than one param for that key push(@{$collect}, $value); } else { # this is the second value for a key we've seen, start array $params{$key} = [$collect, $value]; } } else { # normal use, one to one key value pairs, just set $params{$key} = $value; } } \%params; } # unescape URL-encoded data sub Unescape { my $todecode = $_[1]; $todecode =~ tr/+/ /; # pluses become spaces $todecode =~ s/%([0-9a-fA-F]{2})/chr(hex($1))/ge; $todecode; } *config = *Apache::ASP::config; 1;