package Apache::ASP::Response; use Apache::ASP::Collection; use strict; no strict qw(refs); use vars qw(@ISA @Members %LinkTags $TextHTMLRegexp); @ISA = qw(Apache::ASP::Collection); use Carp qw(confess); use Data::Dumper qw(DumperX); use bytes; @Members = qw( Buffer Clean ContentType Expires ExpiresAbsolute Status ); # used for session id auto parsing %LinkTags = ( 'a' => 'href', 'area' => 'href', 'form' => 'action', 'frame' => 'src', 'iframe' => 'src', 'img' => 'src', 'input' => 'src', 'link' => 'href', ); $TextHTMLRegexp = '^text/html(;|$)'; sub new { my $asp = shift; my $r = $asp->{'r'}; my $out = ''; my $self = bless { asp => $asp, out => \$out, # internal extension allowing various scripts like Session_OnStart # to end the same response # Ended => 0, CacheControl => 'private', CH => &config($asp, 'CgiHeaders') || 0, # Charset => undef, Clean => &config($asp, 'Clean') || 0, Cookies => bless({}, 'Apache::ASP::Collection'), ContentType => 'text/html', 'Debug' => $asp->{dbg}, FormFill => &config($asp, 'FormFill'), IsClientConnected => 1, # PICS => undef, # Status => 200, # header_buffer => '', # header_done => 0, Buffer => &config($asp, 'BufferingOn', undef, 1), BinaryRef => \$out, CompressGzip => ($asp->{compressgzip} and ($asp->{headers_in}->get('Accept-Encoding') =~ /gzip/io)) ? 1 : 0, r => $r, headers_out => scalar($r->headers_out()), }; &IsClientConnected($self); # update now $self; } sub DeprecatedMemberAccess { my($self, $member, $value) = @_; $self->{asp}->Out( "\$Response->$member() deprecated. Please access member ". "directly with \$Response->{$member} notation" ); $self->{$member} = $value; } # defined the deprecated subs now, so we can loose the AUTOLOAD method # the AUTOLOAD was forcing us to keep the DESTROY around for my $member ( @Members ) { my $subdef = "sub $member { shift->DeprecatedMemberAccess('$member', shift); }"; eval $subdef; if($@) { die("error defining Apache::ASP::Response sub -- $subdef -- $@"); } } sub AddHeader { my($self, $name, $value) = @_; my $lc_name = lc($name); if($lc_name eq 'set-cookie') { $self->{r}->err_headers_out->add($name, $value); } else { # if we have a member API for this header, set that value instead # to avoid duplicate headers from being sent out if($lc_name eq 'content-type') { $self->{ContentType} = $value; } elsif($lc_name eq 'cache-control') { $self->{CacheControl} = $value; } elsif($lc_name eq 'expires') { $self->{ExpiresAbsolute} = $value; } else { $self->{headers_out}->set($name, $value); } } } sub AppendToLog { shift->{asp}->Log(@_); } sub Debug { my $self = shift; $self->{Debug} && $self->{asp}->Out("[$self->{asp}{basename}]", @_); }; sub BinaryWrite { $_[0]->Flush(); $_[0]->{asp}{dbg} && $_[0]->{asp}->Debug("binary write of ".length($_[1])." bytes"); &Write; } sub Clear { my $out = shift->{out}; $$out = ''; } sub Cookies { my($self, $name, $key, $value) = @_; if(defined($name) && defined($key) && defined($value)) { $self->{Cookies}{$name}{$key} = $value; } elsif(defined($name) && defined($key)) { # we are assigning cookie with name the value of key if(ref $key) { # if a hash, set the values in it to the keys values # we don't just assign the ref directly since for PerlScript # compatibility while(my($k, $v) = each %{$key}) { $self->{Cookies}{$name}{$k} = $v; } } else { $self->{Cookies}{$name}{Value} = $key; } } elsif(defined($name)) { # if the cookie was just stored as the name value, then we will # will convert it into its hash form now, so we can store other # things. We will probably be storing other things now, since # we are referencing the cookie directly my $cookie = $self->{Cookies}{$name} || {}; $cookie = ref($cookie) ? $cookie : { Value => $cookie }; $self->{Cookies}{$name} = bless $cookie, 'Apache::ASP::Collection'; } else { $self->{Cookies}; } } sub End { my $self = shift; # by not calling EndSoft(), but letting it be called naturally after # Execute() in hander(), we allow more natural Buffer flushing to occur # even if we are in a situation where Flush() has been made null like # in an XMLSubs or cached or trapped include # &EndSoft($self); eval { goto APACHE_ASP_EXECUTE_END; }; } sub EndSoft { my $self = shift; return if $self->{Ended}++; &Flush($self); } sub Flush { my $self = shift; my $asp = $self->{asp}; my $out = $self->{out}; local $| = 1; # Script_OnFlush event handler $asp->{GlobalASA}{'exists'} && $asp->{GlobalASA}->ScriptOnFlush(); # XSLT Processing, check for errors so PrettyError() can call Flush() if($asp->{xslt} && ! $asp->{errs}) { $asp->{dbg} && $asp->Debug("pre xslt $out length: ".length($$out)); $self->FlushXSLT; $asp->{dbg} && $asp->Debug("post xslt $out length: ".length($$out)); return if $asp->{errs}; } # FormFill if ($self->{FormFill} && ! $asp->{errs}) { $self->FormFill; return if $asp->{errs}; } if($self->{Clean} and $self->{ContentType} =~ /$TextHTMLRegexp/o) { # by checking defined, we just check once unless(defined $Apache::ASP::CleanSupport) { eval 'use HTML::Clean'; if($@) { $self->{asp}->Log("Error loading module HTML::Clean with Clean set to $self->{Clean}. ". "Make user you have HTML::Clean installed properly. Error: $@"); $Apache::ASP::CleanSupport = 0; } else { $Apache::ASP::CleanSupport = 1; } } # if we can't clean, we simply ignore if($Apache::ASP::CleanSupport) { my $h = HTML::Clean->new($out, $self->{Clean}); if($h) { $h->strip(); } else { $self->{asp}->Error("clean error: $! $@"); } } } ## Session query auto parsing for cookieless sessions if( $asp->{Session} and ! $asp->{session_cookie} and $asp->{session_url_parse} and ($self->{ContentType} =~ /^text/i) ) { $self->SessionQueryParse(); } if($self->{Ended}) { # log total request time just once at the end # and append to html like Cocoon, per user request my $total_time = sprintf('%7.5f', ( eval { &Time::HiRes::time() } || time() ) - $asp->{start_time}); $asp->{dbg} && $asp->Debug("page executed in $total_time seconds"); $asp->{total_time} = $total_time; if(&config($asp, 'TimeHiRes')) { if($self->{ContentType} =~ /$TextHTMLRegexp/o) { if(&config($asp, 'Debug')) { $$out .= "\n"; } } } } # HEADERS AFTER CLEAN, so content-length would be calculated correctly # if this is the first writing from the page, flush a newline, to # get the headers out properly if(! $self->{header_done}) { # if no headers and the script has ended, we know that the # the script has not been flushed yet, which would at least # occur with buffering on if($self->{Ended}) { # compression & content-length settings will kill filters # after Apache::ASP if(! $asp->{filter}) { # gzip the buffer if CompressGzip && browser accepts it && # the script is flushed once if($self->{CompressGzip} && $asp->LoadModule('Gzip','Compress::Zlib')) { $self->{headers_out}->set('Content-Encoding','gzip'); $$out = Compress::Zlib::memGzip($out); } $self->{headers_out}->set('Content-Length', length($$out)); } } &SendHeaders($self); } if($asp->{filter}) { print STDOUT $$out; } else { # just in case IsClientConnected is set incorrectly, still try to print # the worst thing is some extra error messages in the error_log ... # there have been spurious error reported with the IsClientConnected # code since it was introduced, and this will limit the errors ( if any are left ) # to the users explicitly using this functionality, --jc 11/29/2001 # # if($self->{IsClientConnected}) { if(! defined $self->{Status} or ($self->{Status} >= 200 and $self->{Status} < 400)) { $self->{r}->print($$out); } # } } # update after flushes only, expensive call $self->{Ended} || &IsClientConnected($self); # supposedly this is more efficient than undeffing, since # the string does not let go of its allocated memory buffer $$out = ''; 1; } sub FormFill { my $self = shift; my $asp = $self->{asp}; $asp->{dbg} && $asp->Debug("form fill begin"); $asp->LoadModule('FormFill', 'HTML::FillInForm') || return; my $ref = $self->{BinaryRef}; $$ref =~ s/(\