# Copyright (c) 1999-2011 bivio Software, Inc. All rights reserved. # # Visit http://www.bivio.biz for more info. # # This library is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 2.1 of the # License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; If not, you may get a copy from: # http://www.opensource.org/licenses/lgpl-license.html # # $Id: Reply.pm,v 2.26 2011/10/02 03:15:35 nagler Exp $ package Bivio::Agent::HTTP::Reply; use strict; use Bivio::Base 'Agent.Reply'; our($VERSION) = sprintf('%d.%02d', q$Revision: 2.26 $ =~ /\d+/g); b_use('IO.Trace'); our($_TRACE); my($_AC) = b_use('Ext.ApacheConstants'); my($_DT) = b_use('Type.DateTime'); my($_DC) = b_use('Bivio.DieCode'); my($_D) = b_use('Bivio.Die'); my($_C) = b_use('IO.Config'); my(%_DIE_TO_HTTP_CODE); $_C->register(my $_CFG = { additional_http_headers => undef, }); sub client_redirect { my($self, $req, $named) = @_; my($r) = $self->get('r'); $self->internal_put({}); my($uri, $status) = @$named{qw(uri http_status_code)}; $status ||= 302; # have to do it the long way, there is a bug in using the REDIRECT # return value when handling a form $r->header_out(Location => $uri); $r->status($status); _send_http_header($self, $req, $r); # make it look like apache's redirect. Ignore HEAD, because this # is like an error. $r->print(<<"EOF"); $status Found

found

The document has moved here.

EOF return; } sub die_to_http_code { # (proto, Bivio.Die) : int # (proto, Bivio.DieCode, Apache.Request) : int # Translates a L<$_DC> to an L. # # If I is C, returns C<$_AC::OK>. my(undef, $die, $r) = @_; return $_AC->OK unless defined($die); $die = $die->get('code') if $_D->is_blessed($die); return $_AC->OK unless defined($die); %_DIE_TO_HTTP_CODE = ( # Keep in synch with HTTP::Dispatcher $_DC->FORBIDDEN => $_AC->FORBIDDEN, $_DC->NOT_FOUND => $_AC->NOT_FOUND, $_DC->MODEL_NOT_FOUND => $_AC->NOT_FOUND, $_DC->CLIENT_REDIRECT_TASK => $_AC->OK, $_DC->CORRUPT_QUERY => $_AC->BAD_REQUEST, $_DC->CORRUPT_FORM => $_AC->BAD_REQUEST, $_DC->INVALID_OP => $_AC->BAD_REQUEST, $_DC->INPUT_TOO_LARGE => $_AC->HTTP_REQUEST_ENTITY_TOO_LARGE, $_DC->CLIENT_ERROR => $_AC->HTTP_SERVICE_UNAVAILABLE, ) unless %_DIE_TO_HTTP_CODE; return _error($_DIE_TO_HTTP_CODE{$die}, $r) if defined($_DIE_TO_HTTP_CODE{$die}); # The rest get mapped to SERVER_ERROR b_warn($die, ": unknown $_DC") unless $_DC->is_blessed($die); return _error($_AC->SERVER_ERROR, $r); } sub handle_config { # (proto, hash) : undef # additional_http_headers : array_ref [] # # An array of [key => value] pairs to add to the http header for all # replies. my(undef, $cfg) = @_; $_CFG = $cfg; return; } sub new { # (proto, Apache.Request) : HTTP.Reply # Creates a new Reply type which uses the specified Apache::Request for # output operations. return shift->SUPER::new->put( output_type => 'text/html', r => shift, ); } sub send { # (self, Agent.Request) : undef # Sends the buffered reply data. my($self, $req) = @_; my($r, $o) = $self->unsafe_get(qw(r output)); my($is_scalar) = ref($o) eq 'SCALAR'; die('no reply generated, missing UI item on Task: ', $req->get('task_id')->get_name) unless $is_scalar || ref($o) eq 'GLOB' || UNIVERSAL::isa($o, 'IO::Handle'); my($size) = $is_scalar ? length($$o) : -s $o; # NOTE: The -s $o and the "stat(_)" below must be near each other _cookie_check($self, $req, $r); if ($is_scalar) { # Don't allow caching of dynamically generated replies, because # we don't know the contents (typically from the database) # This isn't *really* private, i.e. not setting Pragma: no-cache. # This pragma screws up Netscape on animated gifs. # Don't set it if someone else has already set Cache-Control $self->set_cache_private unless $self->unsafe_get_header('Cache-Control'); } else { $self->set_last_modified((stat(_))[9]) unless $self->unsafe_get_header('Last-Modified'); } # Don't keep the connection open on normal replies $r->header_out('Connection', 'close'); $r->header_out('Content-Length', $size); $r->content_type($self->get_output_type()); _send_http_header($self, $req, $r); # M_HEAD not defined, so can't use method_number12 if (uc($r->method) eq 'HEAD') { # No body, just header } elsif ($is_scalar) { $r->print($$o); _trace($o) if $_TRACE; } else { $r->send_fd($o, $size); close($o); } # don't let any more data be sent. Don't clear early in case # there is an error and we get called back in die_to_http_code # (then _error()). $self->internal_put({}); return; } sub set_cache_max_age { my($self, $max_age, $req) = @_; return $self unless b_use('UI.Facade') ->get_from_source($req)->get('want_local_file_cache'); return $self ->set_header('Cache-Control', "max-age=$max_age") ->set_header(Expires => $_DT->rfc822($_DT->add_seconds($_DT->now, $max_age))); } sub set_cache_private { # (self) : undef # Do not allow shared caching of this response. my($self) = @_; $self->set_header('Cache-Control', 'private'); return $self; } sub set_expire_immediately { # (self) : undef # Set the page so it will expire immediately. my($self) = @_; $self->set_header(Expires => 'Tue, 01 Apr 1980 05:00:00 GMT'); return $self; } sub set_last_modified { my($self, $value) = @_; return $self->set_header('Last-Modified', $_DT->rfc822($value)); } sub set_output { # (self, scalar_ref) : self # (self, IO.File) : self # Sets the output to the file. Output type must be set. # I or I will be owned by this method. my($self, $value) = @_; die('too many calls to set_output') if $self->has_keys('output'); die('not an IO::Handle, GLOB, or SCALAR reference') unless ref($value) eq 'SCALAR' || ref($value) eq 'GLOB' || UNIVERSAL::isa($value, 'IO::Handle'); return shift->SUPER::set_output(@_); } sub _add_additional_http_headers { # (self, Apache.Request) : undef # Adds any additional http headers from the configuration. my($self, $r) = @_; return unless $_CFG->{additional_http_headers}; foreach my $pair (@{$_CFG->{additional_http_headers}}) { my($key, $value) = @$pair; $r->header_out($key => defined($r->header_out($key)) ? $r->header_out($key) . "\r\n$key: $value" : $value); } return; } sub _cookie_check { my($self, $req, $r) = @_; $self->set_cache_private if $req->get('cookie')->header_out($req, $r); return; } sub _error { # (int, Apache.Request) : ApacheConstants.OK # Workaround for apache in error mode. Sends the reply in line. # This is due to a bug in apache which uses a form. See Req#21 my($code, $r) = @_; #TODO: Older mod_perl versions had Apache::Constants bugs when not # running in apache. If you're using 5.6.* or higher, you're # probably using a newer apache. $^V was only defined after 5.005, # so this check is good enough. return $code if defined($^V) || !exists($ENV{MOD_PERL}) || $code == $_AC->OK; $r->status($code); $r->content_type('text/html'); _send_http_header(undef, undef, $r); # make it look like apache's redirect my($uri) = $r->uri; # Ignore HEAD. There was an error, give the whole body if ($code == $_AC->NOT_FOUND) { $r->print(<<"EOF"); 404 Not Found

Not Found

The requested URL $uri was not found on this server.

EOF } elsif ($code == $_AC->FORBIDDEN) { $r->print(<<"EOF"); 403 Forbidden

Forbidden

You don't have permission to access $uri on this server.

EOF } else { $r->print(<<"EOF"); 500 Internal Server Error

Internal Server Error

The server encountered an internal error or misconfiguration and was unable to complete your request.

Please contact the server administrator, webmaster\@@{[$r->server->server_hostname]} and inform them of the time the error occurred, and anything you might have done that may have caused the error.

EOF } # This is a workaround in older Apache versions return $_AC->OK; } sub _send_http_header { # (HTTP.Reply, Agent.Request, Apache) : undef # Sends the header, turning off keep alive (if necessary) and set cookie # (if req) my($self, $req, $r) = @_; if ($req) { $r->status($self->get('status')) if $self->has_keys('status'); _cookie_check($self, $req, $r); _add_additional_http_headers($self, $r); my($h) = $self->unsafe_get('headers'); if ($h) { foreach my $k (sort(keys(%$h))) { $r->header_out($k, $h->{$k}); } } _trace($self->unsafe_get('status'), ' ', $h) if $_TRACE; } # Turn off KeepAlive if there are jobs. This is because IE doesn't # cycle connections. It goes back to exactly the same one. $r->header_out('Connection', 'close') unless b_use('AgentJob.Dispatcher')->queue_is_empty; $r->send_http_header; return; } 1;