Putting 4.2.0 on top of 4.0.17
[usit-rt.git] / lib / RT / Interface / Web / Handler.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::Interface::Web::Handler;
50 use warnings;
51 use strict;
52
53 use CGI qw/-private_tempfiles/;
54 use MIME::Entity;
55 use Text::Wrapper;
56 use CGI::Cookie;
57 use Time::ParseDate;
58 use Time::HiRes;
59 use HTML::Scrubber;
60 use RT::Interface::Web;
61 use RT::Interface::Web::Request;
62 use File::Path qw( rmtree );
63 use File::Glob qw( bsd_glob );
64 use File::Spec::Unix;
65 use HTTP::Message::PSGI;
66 use HTTP::Request;
67 use HTTP::Response;
68
69 sub DefaultHandlerArgs  { (
70     comp_root            => [
71         RT::Interface::Web->ComponentRoots( Names => 1 ),
72     ],
73     default_escape_flags => 'h',
74     data_dir             => "$RT::MasonDataDir",
75     allow_globals        => [qw(%session $DECODED_ARGS)],
76     # Turn off static source if we're in developer mode.
77     static_source        => (RT->Config->Get('DevelMode') ? '0' : '1'), 
78     use_object_files     => (RT->Config->Get('DevelMode') ? '0' : '1'), 
79     autoflush            => 0,
80     error_format         => (RT->Config->Get('DevelMode') ? 'html': 'rt_error'),
81     request_class        => 'RT::Interface::Web::Request',
82     named_component_subs => $INC{'Devel/Cover.pm'} ? 1 : 0,
83 ) };
84
85 sub InitSessionDir {
86     # Activate the following if running httpd as root (the normal case).
87     # Resets ownership of all files created by Mason at startup.
88     # Note that mysql uses DB for sessions, so there's no need to do this.
89     unless ( RT->Config->Get('DatabaseType') =~ /(?:mysql|Pg)/ ) {
90
91         # Clean up our umask to protect session files
92         umask(0077);
93
94         if ($CGI::MOD_PERL and $CGI::MOD_PERL < 1.9908 ) {
95
96             chown( Apache->server->uid, Apache->server->gid,
97                 $RT::MasonSessionDir )
98             if Apache->server->can('uid');
99         }
100
101         # Die if WebSessionDir doesn't exist or we can't write to it
102         stat($RT::MasonSessionDir);
103         die "Can't read and write $RT::MasonSessionDir"
104         unless ( ( -d _ ) and ( -r _ ) and ( -w _ ) );
105     }
106
107 }
108
109
110 use UNIVERSAL::require;
111 sub NewHandler {
112     my $class = shift;
113     $class->require or die $!;
114     my $handler = $class->new(
115         DefaultHandlerArgs(),
116         RT->Config->Get('MasonParameters'),
117         @_
118     );
119   
120     $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeHTML );
121     $handler->interp->set_escape( u => \&RT::Interface::Web::EscapeURI  );
122     $handler->interp->set_escape( j => \&RT::Interface::Web::EscapeJS   );
123     return($handler);
124 }
125
126 =head2 _mason_dir_index
127
128 =cut
129
130 sub _mason_dir_index {
131     my ($self, $interp, $path) = @_;
132     $path =~ s!/$!!;
133     if (   !$interp->comp_exists( $path )
134          && $interp->comp_exists( $path . "/index.html" ) )
135     {
136         return $path . "/index.html";
137     }
138
139     return $path;
140 }
141
142
143 =head2 CleanupRequest
144
145 Clean ups globals, caches and other things that could be still
146 there from previous requests:
147
148 =over 4
149
150 =item Rollback any uncommitted transaction(s)
151
152 =item Flush the ACL cache
153
154 =item Flush records cache of the L<DBIx::SearchBuilder> if
155 WebFlushDbCacheEveryRequest option is enabled, what is true by default
156 and is not recommended to change.
157
158 =item Clean up state of RT::Action::SendEmail using 'CleanSlate' method
159
160 =item Flush tmp crypt key preferences
161
162 =back
163
164 =cut
165
166 sub CleanupRequest {
167
168     if ( $RT::Handle && $RT::Handle->TransactionDepth ) {
169         $RT::Handle->ForceRollback;
170         $RT::Logger->crit(
171             "Transaction not committed. Usually indicates a software fault."
172             . "Data loss may have occurred" );
173     }
174
175     # Clean out the ACL cache. the performance impact should be marginal.
176     # Consistency is imprived, too.
177     RT::Principal->InvalidateACLCache();
178     DBIx::SearchBuilder::Record::Cachable->FlushCache
179       if ( RT->Config->Get('WebFlushDbCacheEveryRequest')
180         and UNIVERSAL::can(
181             'DBIx::SearchBuilder::Record::Cachable' => 'FlushCache' ) );
182
183     # cleanup global squelching of the mails
184     require RT::Action::SendEmail;
185     RT::Action::SendEmail->CleanSlate;
186     
187     if (RT->Config->Get('Crypt')->{'Enable'}) {
188         RT::Crypt->UseKeyForEncryption();
189         RT::Crypt->UseKeyForSigning( undef );
190     }
191
192     %RT::Ticket::MERGE_CACHE = ( effective => {}, merged => {} );
193     %RT::User::PREFERENCES_CACHE = ();
194
195     # RT::System persists between requests, so its attributes cache has to be
196     # cleared manually. Without this, for example, subject tags across multiple
197     # processes will remain cached incorrectly
198     delete $RT::System->{attributes};
199
200     # Explicitly remove any tmpfiles that GPG opened, and close their
201     # filehandles.  unless we are doing inline psgi testing, which kills all the tmp file created by tests.
202     File::Temp::cleanup()
203             unless $INC{'Test/WWW/Mechanize/PSGI.pm'};
204
205
206 }
207
208
209 sub HTML::Mason::Exception::as_rt_error {
210     my ($self) = @_;
211     $RT::Logger->error( $self->as_text );
212     return "An internal RT error has occurred.  Your administrator can find more details in RT's log files.";
213 }
214
215 =head1 CheckModPerlHandler
216
217 Make sure we're not running with SetHandler perl-script.
218
219 =cut
220
221 sub CheckModPerlHandler{
222     my $self = shift;
223     my $env = shift;
224
225     # Plack::Handler::Apache2 masks MOD_PERL, so use MOD_PERL_API_VERSION
226     return unless( $env->{'MOD_PERL_API_VERSION'}
227                    and $env->{'MOD_PERL_API_VERSION'} == 2);
228
229     my $handler = $env->{'psgi.input'}->handler;
230
231     return unless defined $handler && $handler eq 'perl-script';
232
233     $RT::Logger->critical(<<MODPERL);
234 RT has problems when SetHandler is set to perl-script.
235 Change SetHandler in your in httpd.conf to:
236
237     SetHandler modperl
238
239 For a complete example mod_perl configuration, see:
240
241 https://bestpractical.com/rt/docs/@{[$RT::VERSION =~ /^(\d\.\d)/]}/web_deployment.html#mod_perl-2.xx
242 MODPERL
243
244     my $res = Plack::Response->new(500);
245     $res->content_type("text/plain");
246     $res->body("Server misconfiguration; see error log for details");
247     return $res;
248 }
249
250 # PSGI App
251
252 use RT::Interface::Web::Handler;
253 use CGI::Emulate::PSGI;
254 use Plack::Builder;
255 use Plack::Request;
256 use Plack::Response;
257 use Plack::Util;
258 use Encode qw(encode_utf8);
259
260 sub PSGIApp {
261     my $self = shift;
262
263     # XXX: this is fucked
264     require HTML::Mason::CGIHandler;
265     require HTML::Mason::PSGIHandler::Streamy;
266     my $h = RT::Interface::Web::Handler::NewHandler('HTML::Mason::PSGIHandler::Streamy');
267
268     $self->InitSessionDir;
269
270     my $mason = sub {
271         my $env = shift;
272
273         {
274             my $res = $self->CheckModPerlHandler($env);
275             return $self->_psgi_response_cb( $res->finalize ) if $res;
276         }
277
278         RT::ConnectToDatabase() unless RT->InstallMode;
279
280         my $req = Plack::Request->new($env);
281
282         # CGI.pm normalizes .. out of paths so when you requested
283         # /NoAuth/../Ticket/Display.html we saw Ticket/Display.html
284         # PSGI doesn't normalize .. so we have to deal ourselves.
285         if ( $req->path_info =~ m{/\.} ) {
286             $RT::Logger->crit("Invalid request for ".$req->path_info." aborting");
287             my $res = Plack::Response->new(400);
288             return $self->_psgi_response_cb($res->finalize,sub { $self->CleanupRequest });
289         }
290         $env->{PATH_INFO} = $self->_mason_dir_index( $h->interp, $req->path_info);
291
292         my $ret;
293         {
294             # XXX: until we get rid of all $ENV stuff.
295             local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env));
296
297             $ret = $h->handle_psgi($env);
298         }
299
300         $RT::Logger->crit($@) if $@ && $RT::Logger;
301         warn $@ if $@ && !$RT::Logger;
302         if (ref($ret) eq 'CODE') {
303             my $orig_ret = $ret;
304             $ret = sub {
305                 my $respond = shift;
306                 local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env));
307                 $orig_ret->($respond);
308             };
309         }
310
311         return $self->_psgi_response_cb($ret,
312                                         sub {
313                                             $self->CleanupRequest()
314                                         });
315     };
316     return $self->StaticWrap($mason);
317 }
318
319 sub StaticWrap {
320     my $self    = shift;
321     my $app     = shift;
322     my $builder = Plack::Builder->new;
323
324     for my $static ( RT->Config->Get('StaticRoots') ) {
325         if ( ref $static && ref $static eq 'HASH' ) {
326             $builder->add_middleware(
327                 'Plack::Middleware::Static',
328                 pass_through => 1,
329                 %$static
330             );
331         }
332         else {
333             $RT::Logger->error(
334                 "Invalid config StaticRoots: item can only be a hashref" );
335         }
336     }
337
338     for my $root (RT::Interface::Web->StaticRoots) {
339         $builder->add_middleware(
340             'Plack::Middleware::Static',
341             path         => sub { s!^/static/!! },
342             root         => $root,
343             pass_through => 1,
344         );
345     }
346     return $builder->to_app($app);
347 }
348
349 sub _psgi_response_cb {
350     my $self = shift;
351     my ($ret, $cleanup) = @_;
352     Plack::Util::response_cb
353             ($ret,
354              sub {
355                  my $res = shift;
356
357                  if ( RT->Config->Get('Framebusting') ) {
358                      # XXX TODO: Do we want to make the value of this header configurable?
359                      Plack::Util::header_set($res->[1], 'X-Frame-Options' => 'DENY');
360                  }
361
362                  return sub {
363                      if (!defined $_[0]) {
364                          $cleanup->();
365                          return '';
366                      }
367                      return utf8::is_utf8($_[0]) ? encode_utf8($_[0]) : $_[0];
368                      return $_[0];
369                  };
370              });
371 }
372
373 sub GetStatic {
374     my $class  = shift;
375     my $path   = shift;
376     my $static = $class->StaticWrap(
377         # Anything the static wrap doesn't handle gets 404'd.
378         sub { [404, [], []] }
379     );
380     my $response = HTTP::Response->from_psgi(
381         $static->( HTTP::Request->new(GET => $path)->to_psgi )
382     );
383     return $response;
384 }
385
386 1;