Initial commit 4.0.5-3
[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-2012 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)],
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->full_message );
209     return "An internal RT error has occurred.  Your administrator can find more details in RT's log files.";
210 }
211
212
213 # PSGI App
214
215 use RT::Interface::Web::Handler;
216 use CGI::Emulate::PSGI;
217 use Plack::Request;
218 use Plack::Response;
219 use Plack::Util;
220 use Encode qw(encode_utf8);
221
222 sub PSGIApp {
223     my $self = shift;
224
225     # XXX: this is fucked
226     require HTML::Mason::CGIHandler;
227     require HTML::Mason::PSGIHandler::Streamy;
228     my $h = RT::Interface::Web::Handler::NewHandler('HTML::Mason::PSGIHandler::Streamy');
229
230     $self->InitSessionDir;
231
232     return sub {
233         my $env = shift;
234         RT::ConnectToDatabase() unless RT->InstallMode;
235
236         my $req = Plack::Request->new($env);
237
238         # CGI.pm normalizes .. out of paths so when you requested
239         # /NoAuth/../Ticket/Display.html we saw Ticket/Display.html
240         # PSGI doesn't normalize .. so we have to deal ourselves.
241         if ( $req->path_info =~ m{/\.} ) {
242             $RT::Logger->crit("Invalid request for ".$req->path_info." aborting");
243             my $res = Plack::Response->new(400);
244             return $self->_psgi_response_cb($res->finalize,sub { $self->CleanupRequest });
245         }
246         $env->{PATH_INFO} = $self->_mason_dir_index( $h->interp, $req->path_info);
247
248         my $ret;
249         {
250             # XXX: until we get rid of all $ENV stuff.
251             local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env));
252
253             $ret = $h->handle_psgi($env);
254         }
255
256         $RT::Logger->crit($@) if $@ && $RT::Logger;
257         warn $@ if $@ && !$RT::Logger;
258         if (ref($ret) eq 'CODE') {
259             my $orig_ret = $ret;
260             $ret = sub {
261                 my $respond = shift;
262                 local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env));
263                 $orig_ret->($respond);
264             };
265         }
266
267         return $self->_psgi_response_cb($ret,
268                                         sub {
269                                             $self->CleanupRequest()
270                                         });
271 };
272
273 sub _psgi_response_cb {
274     my $self = shift;
275     my ($ret, $cleanup) = @_;
276     Plack::Util::response_cb
277             ($ret,
278              sub {
279                  my $res = shift;
280
281                  if ( RT->Config->Get('Framebusting') ) {
282                      # XXX TODO: Do we want to make the value of this header configurable?
283                      Plack::Util::header_set($res->[1], 'X-Frame-Options' => 'DENY');
284                  }
285
286                  return sub {
287                      if (!defined $_[0]) {
288                          $cleanup->();
289                          return '';
290                      }
291                      return utf8::is_utf8($_[0]) ? encode_utf8($_[0]) : $_[0];
292                      return $_[0];
293                  };
294              });
295     }
296 }
297
298 1;