7cf18d1ab66f96715e38daa31fee1bfe7e2ae2ec
[freeside.git] / rt / lib / RT / Interface / Web / Handler.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 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
66 sub DefaultHandlerArgs  { (
67     comp_root            => [
68         RT::Interface::Web->ComponentRoots( Names => 1 ),
69     ],
70     default_escape_flags => 'h',
71     data_dir             => "$RT::MasonDataDir",
72     allow_globals        => [qw(%session $DECODED_ARGS)],
73     # Turn off static source if we're in developer mode.
74     static_source        => (RT->Config->Get('DevelMode') ? '0' : '1'), 
75     use_object_files     => (RT->Config->Get('DevelMode') ? '0' : '1'), 
76     autoflush            => 0,
77     error_format         => (RT->Config->Get('DevelMode') ? 'html': 'rt_error'),
78     request_class        => 'RT::Interface::Web::Request',
79     named_component_subs => $INC{'Devel/Cover.pm'} ? 1 : 0,
80 ) };
81
82 sub InitSessionDir {
83     # Activate the following if running httpd as root (the normal case).
84     # Resets ownership of all files created by Mason at startup.
85     # Note that mysql uses DB for sessions, so there's no need to do this.
86     unless ( RT->Config->Get('DatabaseType') =~ /(?:mysql|Pg)/ ) {
87
88         # Clean up our umask to protect session files
89         umask(0077);
90
91         if ($CGI::MOD_PERL and $CGI::MOD_PERL < 1.9908 ) {
92
93             chown( Apache->server->uid, Apache->server->gid,
94                 $RT::MasonSessionDir )
95             if Apache->server->can('uid');
96         }
97
98         # Die if WebSessionDir doesn't exist or we can't write to it
99         stat($RT::MasonSessionDir);
100         die "Can't read and write $RT::MasonSessionDir"
101         unless ( ( -d _ ) and ( -r _ ) and ( -w _ ) );
102     }
103
104 }
105
106
107 use UNIVERSAL::require;
108 sub NewHandler {
109     my $class = shift;
110     $class->require or die $!;
111     my $handler = $class->new(
112         DefaultHandlerArgs(),
113         RT->Config->Get('MasonParameters'),
114         @_
115     );
116   
117     $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 );
118     $handler->interp->set_escape( u => \&RT::Interface::Web::EscapeURI  );
119     $handler->interp->set_escape( j => \&RT::Interface::Web::EscapeJS   );
120     return($handler);
121 }
122
123 =head2 _mason_dir_index
124
125 =cut
126
127 sub _mason_dir_index {
128     my ($self, $interp, $path) = @_;
129     $path =~ s!/$!!;
130     if (   !$interp->comp_exists( $path )
131          && $interp->comp_exists( $path . "/index.html" ) )
132     {
133         return $path . "/index.html";
134     }
135
136     return $path;
137 }
138
139
140 =head2 CleanupRequest
141
142 Clean ups globals, caches and other things that could be still
143 there from previous requests:
144
145 =over 4
146
147 =item Rollback any uncommitted transaction(s)
148
149 =item Flush the ACL cache
150
151 =item Flush records cache of the L<DBIx::SearchBuilder> if
152 WebFlushDbCacheEveryRequest option is enabled, what is true by default
153 and is not recommended to change.
154
155 =item Clean up state of RT::Action::SendEmail using 'CleanSlate' method
156
157 =item Flush tmp GnuPG key preferences
158
159 =back
160
161 =cut
162
163 sub CleanupRequest {
164
165     if ( $RT::Handle && $RT::Handle->TransactionDepth ) {
166         $RT::Handle->ForceRollback;
167         $RT::Logger->crit(
168             "Transaction not committed. Usually indicates a software fault."
169             . "Data loss may have occurred" );
170     }
171
172     # Clean out the ACL cache. the performance impact should be marginal.
173     # Consistency is imprived, too.
174     RT::Principal->InvalidateACLCache();
175     DBIx::SearchBuilder::Record::Cachable->FlushCache
176       if ( RT->Config->Get('WebFlushDbCacheEveryRequest')
177         and UNIVERSAL::can(
178             'DBIx::SearchBuilder::Record::Cachable' => 'FlushCache' ) );
179
180     # cleanup global squelching of the mails
181     require RT::Action::SendEmail;
182     RT::Action::SendEmail->CleanSlate;
183     
184     if (RT->Config->Get('GnuPG')->{'Enable'}) {
185         require RT::Crypt::GnuPG;
186         RT::Crypt::GnuPG::UseKeyForEncryption();
187         RT::Crypt::GnuPG::UseKeyForSigning( undef );
188     }
189
190     %RT::Ticket::MERGE_CACHE = ( effective => {}, merged => {} );
191
192     # RT::System persists between requests, so its attributes cache has to be
193     # cleared manually. Without this, for example, subject tags across multiple
194     # processes will remain cached incorrectly
195     delete $RT::System->{attributes};
196
197     # Explicitly remove any tmpfiles that GPG opened, and close their
198     # filehandles.  unless we are doing inline psgi testing, which kills all the tmp file created by tests.
199     File::Temp::cleanup()
200             unless $INC{'Test/WWW/Mechanize/PSGI.pm'};
201
202
203 }
204
205
206 sub HTML::Mason::Exception::as_rt_error {
207     my ($self) = @_;
208     $RT::Logger->error( $self->as_text );
209     return "An internal RT error has occurred.  Your administrator can find more details in RT's log files.";
210 }
211
212 =head1 CheckModPerlHandler
213
214 Make sure we're not running with SetHandler perl-script.
215
216 =cut
217
218 sub CheckModPerlHandler{
219     my $self = shift;
220     my $env = shift;
221
222     # Plack::Handler::Apache2 masks MOD_PERL, so use MOD_PERL_API_VERSION
223     return unless( $env->{'MOD_PERL_API_VERSION'}
224                    and $env->{'MOD_PERL_API_VERSION'} == 2);
225
226     my $handler = $env->{'psgi.input'}->handler;
227
228     return unless defined $handler && $handler eq 'perl-script';
229
230     $RT::Logger->critical(<<MODPERL);
231 RT has problems when SetHandler is set to perl-script.
232 Change SetHandler in your in httpd.conf to:
233
234     SetHandler modperl
235
236 For a complete example mod_perl configuration, see:
237
238 https://bestpractical.com/rt/docs/@{[$RT::VERSION =~ /^(\d\.\d)/]}/web_deployment.html#mod_perl-2.xx
239 MODPERL
240
241     my $res = Plack::Response->new(500);
242     $res->content_type("text/plain");
243     $res->body("Server misconfiguration; see error log for details");
244     return $res;
245 }
246
247 # PSGI App
248
249 use RT::Interface::Web::Handler;
250 use CGI::Emulate::PSGI;
251 use Plack::Request;
252 use Plack::Response;
253 use Plack::Util;
254
255 sub PSGIApp {
256     my $self = shift;
257
258     # XXX: this is fucked
259     require HTML::Mason::CGIHandler;
260     require HTML::Mason::PSGIHandler::Streamy;
261     my $h = RT::Interface::Web::Handler::NewHandler('HTML::Mason::PSGIHandler::Streamy');
262
263     $self->InitSessionDir;
264
265     return sub {
266         my $env = shift;
267
268         {
269             my $res = $self->CheckModPerlHandler($env);
270             return $self->_psgi_response_cb( $res->finalize ) if $res;
271         }
272
273         RT::ConnectToDatabase() unless RT->InstallMode;
274
275         my $req = Plack::Request->new($env);
276
277         # CGI.pm normalizes .. out of paths so when you requested
278         # /NoAuth/../Ticket/Display.html we saw Ticket/Display.html
279         # PSGI doesn't normalize .. so we have to deal ourselves.
280         if ( $req->path_info =~ m{(^|/)\.\.?(/|$)} ) {
281             $RT::Logger->crit("Invalid request for ".$req->path_info." aborting");
282             my $res = Plack::Response->new(400);
283             return $self->_psgi_response_cb($res->finalize,sub { $self->CleanupRequest });
284         }
285         $env->{PATH_INFO} = $self->_mason_dir_index( $h->interp, $req->path_info);
286
287         my $ret;
288         {
289             # XXX: until we get rid of all $ENV stuff.
290             local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env));
291
292             $ret = $h->handle_psgi($env);
293         }
294
295         $RT::Logger->crit($@) if $@ && $RT::Logger;
296         warn $@ if $@ && !$RT::Logger;
297         if (ref($ret) eq 'CODE') {
298             my $orig_ret = $ret;
299             $ret = sub {
300                 my $respond = shift;
301                 local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env));
302                 $orig_ret->($respond);
303             };
304         }
305
306         return $self->_psgi_response_cb($ret,
307                                         sub {
308                                             $self->CleanupRequest()
309                                         });
310 };
311
312 sub _psgi_response_cb {
313     my $self = shift;
314     my ($ret, $cleanup) = @_;
315     Plack::Util::response_cb
316             ($ret,
317              sub {
318                  my $res = shift;
319
320                  if ( RT->Config->Get('Framebusting') ) {
321                      # XXX TODO: Do we want to make the value of this header configurable?
322                      Plack::Util::header_set($res->[1], 'X-Frame-Options' => 'DENY');
323                  }
324
325                  return sub {
326                      if (!defined $_[0]) {
327                          $cleanup->();
328                          return '';
329                      }
330                      # XXX: Ideally, responses should flag if they need
331                      # to be encoded, rather than relying on the UTF-8
332                      # flag
333                      return Encode::encode("UTF-8",$_[0]) if utf8::is_utf8($_[0]);
334                      return $_[0];
335                  };
336              });
337     }
338 }
339
340 1;