X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FInterface%2FWeb%2FHandler.pm;h=927d38440cff27247a2d470346ab16ba3ca3cfe0;hb=44dd00a3ff974a17999e86e64488e996edc71e3c;hp=772fab0ca257e49bdbb2d92b5617b48af38fef1c;hpb=0fb307c305e4bc2c9c27dc25a3308beae3a4d33c;p=freeside.git diff --git a/rt/lib/RT/Interface/Web/Handler.pm b/rt/lib/RT/Interface/Web/Handler.pm index 772fab0ca..927d38440 100644 --- a/rt/lib/RT/Interface/Web/Handler.pm +++ b/rt/lib/RT/Interface/Web/Handler.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) @@ -47,60 +47,40 @@ # END BPS TAGGED BLOCK }}} package RT::Interface::Web::Handler; +use warnings; +use strict; use CGI qw/-private_tempfiles/; use MIME::Entity; use Text::Wrapper; use CGI::Cookie; -use Time::ParseDate; use Time::HiRes; -use HTML::Entities; use HTML::Scrubber; -use RT::Interface::Web::Handler; +use RT::Interface::Web; use RT::Interface::Web::Request; use File::Path qw( rmtree ); use File::Glob qw( bsd_glob ); use File::Spec::Unix; +use HTTP::Message::PSGI; +use HTTP::Request; +use HTTP::Response; sub DefaultHandlerArgs { ( - comp_root => [ - [ local => $RT::MasonLocalComponentRoot ], - (map {[ "plugin-".$_->Name => $_->ComponentRoot ]} @{RT->Plugins}), - [ standard => $RT::MasonComponentRoot ] + comp_root => [ + RT::Interface::Web->ComponentRoots( Names => 1 ), ], default_escape_flags => 'h', data_dir => "$RT::MasonDataDir", - allow_globals => [qw(%session)], + allow_globals => [qw(%session $DECODED_ARGS)], # Turn off static source if we're in developer mode. static_source => (RT->Config->Get('DevelMode') ? '0' : '1'), use_object_files => (RT->Config->Get('DevelMode') ? '0' : '1'), autoflush => 0, - error_format => (RT->Config->Get('DevelMode') ? 'html': 'brief'), + error_format => (RT->Config->Get('DevelMode') ? 'html': 'rt_error'), request_class => 'RT::Interface::Web::Request', named_component_subs => $INC{'Devel/Cover.pm'} ? 1 : 0, ) }; -# {{{ sub new - -=head2 new - - Constructs a web handler of the appropriate class. - Takes options to pass to the constructor. - -=cut - -sub new { - my $class = shift; - $class->InitSessionDir; - - if ( ($mod_perl::VERSION && $mod_perl::VERSION >= 1.9908) || $CGI::MOD_PERL) { - goto &NewApacheHandler; - } - else { - goto &NewCGIHandler; - } -} - sub InitSessionDir { # Activate the following if running httpd as root (the normal case). # Resets ownership of all files created by Mason at startup. @@ -125,89 +105,39 @@ sub InitSessionDir { } -# }}} - -# {{{ sub NewApacheHandler - -=head2 NewApacheHandler - - Takes extra options to pass to HTML::Mason::ApacheHandler->new - Returns a new Mason::ApacheHandler object - -=cut - -sub NewApacheHandler { - require HTML::Mason::ApacheHandler; - return NewHandler('HTML::Mason::ApacheHandler', args_method => "CGI", @_); -} - -# }}} - -# {{{ sub NewCGIHandler - -=head2 NewCGIHandler - - Returns a new Mason::CGIHandler object - -=cut - -sub NewCGIHandler { - require HTML::Mason::CGIHandler; - return NewHandler( - 'HTML::Mason::CGIHandler', - out_method => sub { - my $m = HTML::Mason::Request->instance; - my $r = $m->cgi_request; - - # Send headers if they have not been sent by us or by user. - $r->send_http_header unless $r->http_header_sent; - - # Set up a default - $r->content_type('text/html; charset=utf-8') - unless $r->content_type; - - if ( $r->content_type =~ /charset=([\w-]+)$/ ) { - my $enc = $1; - if ( lc $enc !~ /utf-?8$/ ) { - for my $str (@_) { - next unless $str; - - # only encode perl internal strings - next unless utf8::is_utf8($str); - $str = Encode::encode( $enc, $str ); - } - } - } - - # default to utf8 encoding - for my $str (@_) { - next unless $str; - next unless utf8::is_utf8($str); - $str = Encode::encode( 'utf8', $str ); - } - - # We could perhaps install a new, faster out_method here that - # wouldn't have to keep checking whether headers have been - # sent and what the $r->method is. That would require - # additions to the Request interface, though. - print STDOUT grep {defined} @_; - }, - @_ - ); -} sub NewHandler { my $class = shift; + $class->require or die $!; my $handler = $class->new( DefaultHandlerArgs(), + RT->Config->Get('MasonParameters'), @_ ); - $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 ); + $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeHTML ); $handler->interp->set_escape( u => \&RT::Interface::Web::EscapeURI ); + $handler->interp->set_escape( j => \&RT::Interface::Web::EscapeJS ); return($handler); } +=head2 _mason_dir_index + +=cut + +sub _mason_dir_index { + my ($self, $interp, $path) = @_; + $path =~ s!/$!!; + if ( !$interp->comp_exists( $path ) + && $interp->comp_exists( $path . "/index.html" ) ) + { + return $path . "/index.html"; + } + + return $path; +} + + =head2 CleanupRequest Clean ups globals, caches and other things that could be still @@ -225,7 +155,7 @@ and is not recommended to change. =item Clean up state of RT::Action::SendEmail using 'CleanSlate' method -=item Flush tmp GnuPG key preferences +=item Flush tmp crypt key preferences =back @@ -252,18 +182,231 @@ sub CleanupRequest { require RT::Action::SendEmail; RT::Action::SendEmail->CleanSlate; - if (RT->Config->Get('GnuPG')->{'Enable'}) { - require RT::Crypt::GnuPG; - RT::Crypt::GnuPG::UseKeyForEncryption(); - RT::Crypt::GnuPG::UseKeyForSigning( undef ); + if (RT->Config->Get('Crypt')->{'Enable'}) { + RT::Crypt->UseKeyForEncryption(); + RT::Crypt->UseKeyForSigning( undef ); } %RT::Ticket::MERGE_CACHE = ( effective => {}, merged => {} ); + # RT::System persists between requests, so its attributes cache has to be + # cleared manually. Without this, for example, subject tags across multiple + # processes will remain cached incorrectly + delete $RT::System->{attributes}; + # Explicitly remove any tmpfiles that GPG opened, and close their - # filehandles. - File::Temp::cleanup; + # filehandles. unless we are doing inline psgi testing, which kills all the tmp file created by tests. + File::Temp::cleanup() + unless $INC{'Test/WWW/Mechanize/PSGI.pm'}; + + RT::ObjectCustomFieldValues::ClearOCFVCache(); +} + + +sub HTML::Mason::Exception::as_rt_error { + my ($self) = @_; + $RT::Logger->error( $self->as_text ); + return "An internal RT error has occurred. Your administrator can find more details in RT's log files."; +} + +=head1 CheckModPerlHandler + +Make sure we're not running with SetHandler perl-script. + +=cut + +sub CheckModPerlHandler{ + my $self = shift; + my $env = shift; + + # Plack::Handler::Apache2 masks MOD_PERL, so use MOD_PERL_API_VERSION + return unless( $env->{'MOD_PERL_API_VERSION'} + and $env->{'MOD_PERL_API_VERSION'} == 2); + + my $handler = $env->{'psgi.input'}->handler; + + return unless defined $handler && $handler eq 'perl-script'; + + $RT::Logger->critical(<new(500); + $res->content_type("text/plain"); + $res->body("Server misconfiguration; see error log for details"); + return $res; +} + +# PSGI App + +use RT::Interface::Web::Handler; +use CGI::Emulate::PSGI; +use Plack::Builder; +use Plack::Request; +use Plack::Response; +use Plack::Util; + +sub PSGIApp { + my $self = shift; + + # XXX: this is fucked + require HTML::Mason::CGIHandler; + require HTML::Mason::PSGIHandler::Streamy; + my $h = RT::Interface::Web::Handler::NewHandler('HTML::Mason::PSGIHandler::Streamy'); + + $self->InitSessionDir; + + my $mason = sub { + my $env = shift; + + { + my $res = $self->CheckModPerlHandler($env); + return $self->_psgi_response_cb( $res->finalize ) if $res; + } + + unless (RT->InstallMode) { + unless (eval { RT::ConnectToDatabase() }) { + my $res = Plack::Response->new(503); + $res->content_type("text/plain"); + $res->body("Database inaccessible; contact the RT administrator (".RT->Config->Get("OwnerEmail").")"); + return $self->_psgi_response_cb( $res->finalize, sub { $self->CleanupRequest } ); + } + } + + my $req = Plack::Request->new($env); + + # CGI.pm normalizes .. out of paths so when you requested + # /NoAuth/../Ticket/Display.html we saw Ticket/Display.html + # PSGI doesn't normalize .. so we have to deal ourselves. + if ( $req->path_info =~ m{(^|/)\.\.?(/|$)} ) { + $RT::Logger->crit("Invalid request for ".$req->path_info." aborting"); + my $res = Plack::Response->new(400); + return $self->_psgi_response_cb($res->finalize,sub { $self->CleanupRequest }); + } + $env->{PATH_INFO} = $self->_mason_dir_index( $h->interp, $req->path_info); + + my $ret; + { + # XXX: until we get rid of all $ENV stuff. + local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env)); + + $ret = $h->handle_psgi($env); + } + + $RT::Logger->crit($@) if $@ && $RT::Logger; + warn $@ if $@ && !$RT::Logger; + if (ref($ret) eq 'CODE') { + my $orig_ret = $ret; + $ret = sub { + my $respond = shift; + local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env)); + $orig_ret->($respond); + }; + } + + return $self->_psgi_response_cb($ret, + sub { + $self->CleanupRequest() + }); + }; + + my $app = $self->StaticWrap($mason); + for my $plugin (RT->Config->Get("Plugins")) { + my $wrap = $plugin->can("PSGIWrap") + or next; + $app = $wrap->($plugin, $app); + } + return $app; +} + +sub StaticWrap { + my $self = shift; + my $app = shift; + my $builder = Plack::Builder->new; + + my $headers = RT::Interface::Web::GetStaticHeaders(Time => 'forever'); + + for my $static ( RT->Config->Get('StaticRoots') ) { + if ( ref $static && ref $static eq 'HASH' ) { + $builder->add_middleware( + '+RT::Interface::Web::Middleware::StaticHeaders', + path => $static->{'path'}, + headers => $headers, + ); + $builder->add_middleware( + 'Plack::Middleware::Static', + pass_through => 1, + %$static + ); + } + else { + $RT::Logger->error( + "Invalid config StaticRoots: item can only be a hashref" ); + } + } + + my $path = sub { s!^/static/!! }; + $builder->add_middleware( + '+RT::Interface::Web::Middleware::StaticHeaders', + path => $path, + headers => $headers, + ); + for my $root (RT::Interface::Web->StaticRoots) { + $builder->add_middleware( + 'Plack::Middleware::Static', + path => $path, + root => $root, + pass_through => 1, + ); + } + return $builder->to_app($app); +} + +sub _psgi_response_cb { + my $self = shift; + my ($ret, $cleanup) = @_; + Plack::Util::response_cb + ($ret, + sub { + my $res = shift; + + if ( RT->Config->Get('Framebusting') ) { + # XXX TODO: Do we want to make the value of this header configurable? + Plack::Util::header_set($res->[1], 'X-Frame-Options' => 'DENY'); + } + + return sub { + if (!defined $_[0]) { + $cleanup->(); + return ''; + } + # XXX: Ideally, responses should flag if they need + # to be encoded, rather than relying on the UTF-8 + # flag + return Encode::encode("UTF-8",$_[0]) if utf8::is_utf8($_[0]); + return $_[0]; + }; + }); +} + +sub GetStatic { + my $class = shift; + my $path = shift; + my $static = $class->StaticWrap( + # Anything the static wrap doesn't handle gets 404'd. + sub { [404, [], []] } + ); + my $response = HTTP::Response->from_psgi( + $static->( HTTP::Request->new(GET => $path)->to_psgi ) + ); + return $response; } -# }}} 1;