X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FInterface%2FWeb%2FHandler.pm;h=ad1068bac1a10718571f0aaee2691d7d70ca21cb;hb=7322f2afedcc2f427e997d1535a503613a83f088;hp=260a3b3195eaa06a5009e3717cfc83609a7216a6;hpb=2dfda73eeb3eae2d4f894099754794ef07d060dd;p=freeside.git diff --git a/rt/lib/RT/Interface/Web/Handler.pm b/rt/lib/RT/Interface/Web/Handler.pm index 260a3b319..ad1068bac 100644 --- a/rt/lib/RT/Interface/Web/Handler.pm +++ b/rt/lib/RT/Interface/Web/Handler.pm @@ -1,40 +1,40 @@ # BEGIN BPS TAGGED BLOCK {{{ -# +# # COPYRIGHT: -# -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC -# -# +# +# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC +# +# # (Except where explicitly superseded by other copyright notices) -# -# +# +# # LICENSE: -# +# # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. -# +# # This work 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 # General Public License for more details. -# +# # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# +# +# # CONTRIBUTION SUBMISSION POLICY: -# +# # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) -# +# # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that @@ -43,75 +43,59 @@ # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. -# +# # 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 ], - [ 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::DevelMode ? '0' : '1'), - use_object_files => ($RT::DevelMode ? '0' : '1'), - autoflush => 0 + 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': '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 ) { - goto &NewApacheHandler; - } - elsif ($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. # Note that mysql uses DB for sessions, so there's no need to do this. - unless ( $RT::DatabaseType =~ /(?:mysql|Pg)/ ) { + unless ( RT->Config->Get('DatabaseType') =~ /(?:mysql|Pg)/ ) { # Clean up our umask to protect session files umask(0077); - if ($CGI::MOD_PERL) { local $@; eval { + if ($CGI::MOD_PERL and $CGI::MOD_PERL < 1.9908 ) { chown( Apache->server->uid, Apache->server->gid, $RT::MasonSessionDir ) - }} + if Apache->server->can('uid'); + } # Die if WebSessionDir doesn't exist or we can't write to it stat($RT::MasonSessionDir); @@ -121,76 +105,65 @@ 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 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::EscapeHTML ); + $handler->interp->set_escape( u => \&RT::Interface::Web::EscapeURI ); + $handler->interp->set_escape( j => \&RT::Interface::Web::EscapeJS ); + return($handler); } -# }}} - -# {{{ sub NewApache2Handler - -=head2 NewApache2Handler - - Takes extra options to pass to MasonX::Apache2Handler->new - Returns a new MasonX::Apache2Handler object +=head2 _mason_dir_index =cut -sub NewApache2Handler { - require MasonX::Apache2Handler; - return NewHandler('MasonX::Apache2Handler', args_method => "CGI", @_); +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; } -# }}} -# {{{ sub NewCGIHandler +=head2 CleanupRequest -=head2 NewCGIHandler +Clean ups globals, caches and other things that could be still +there from previous requests: - Returns a new Mason::CGIHandler object +=over 4 -=cut +=item Rollback any uncommitted transaction(s) -sub NewCGIHandler { - require HTML::Mason::CGIHandler; - return NewHandler('HTML::Mason::CGIHandler', @_); -} +=item Flush the ACL cache -sub NewHandler { - my $class = shift; - my $handler = $class->new( - DefaultHandlerArgs(), - @_ - ); - - $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 ); - $handler->interp->set_escape( u => \&RT::Interface::Web::EscapeURI ); - return($handler); -} +=item Flush records cache of the L if +WebFlushDbCacheEveryRequest option is enabled, what is true by default +and is not recommended to change. -=head2 CleanupRequest +=item Clean up state of RT::Action::SendEmail using 'CleanSlate' method + +=item Flush tmp crypt key preferences -Rollback any uncommitted transaction. -Flush the ACL cache -Flush the searchbuilder query cache +=back =cut sub CleanupRequest { - if ( $RT::Handle->TransactionDepth ) { + if ( $RT::Handle && $RT::Handle->TransactionDepth ) { $RT::Handle->ForceRollback; $RT::Logger->crit( "Transaction not committed. Usually indicates a software fault." @@ -201,11 +174,239 @@ sub CleanupRequest { # Consistency is imprived, too. RT::Principal->InvalidateACLCache(); DBIx::SearchBuilder::Record::Cachable->FlushCache - if ( $RT::WebFlushDbCacheEveryRequest + if ( RT->Config->Get('WebFlushDbCacheEveryRequest') and UNIVERSAL::can( 'DBIx::SearchBuilder::Record::Cachable' => 'FlushCache' ) ); + # cleanup global squelching of the mails + require RT::Action::SendEmail; + RT::Action::SendEmail->CleanSlate; + + 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. 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'}; + + +} + + +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;