Handler.pm 12.6 KB
Newer Older
1
# BEGIN BPS TAGGED BLOCK {{{
Jesse Vincent's avatar
Jesse Vincent committed
2
#
3
# COPYRIGHT:
Jesse Vincent's avatar
Jesse Vincent committed
4
#
5
# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
Kevin Falcone's avatar
Kevin Falcone committed
6
#                                          <sales@bestpractical.com>
Jesse Vincent's avatar
Jesse Vincent committed
7
#
8
# (Except where explicitly superseded by other copyright notices)
Jesse Vincent's avatar
Jesse Vincent committed
9
10
#
#
11
# LICENSE:
Jesse Vincent's avatar
Jesse Vincent committed
12
#
13
14
15
16
# 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.
Jesse Vincent's avatar
Jesse Vincent committed
17
#
18
19
20
21
# 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.
Jesse Vincent's avatar
Jesse Vincent committed
22
#
23
24
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
Jesse Vincent's avatar
Jesse Vincent committed
25
26
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
Ruslan Zakirov's avatar
Ruslan Zakirov committed
27
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
Jesse Vincent's avatar
Jesse Vincent committed
28
29
#
#
30
# CONTRIBUTION SUBMISSION POLICY:
Jesse Vincent's avatar
Jesse Vincent committed
31
#
32
33
34
35
36
# (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.)
Jesse Vincent's avatar
Jesse Vincent committed
37
#
38
39
40
41
42
43
44
45
# 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
# you are the copyright holder for those contributions and you grant
# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
Jesse Vincent's avatar
Jesse Vincent committed
46
#
47
# END BPS TAGGED BLOCK }}}
48

49
package RT::Interface::Web::Handler;
50
51
use warnings;
use strict;
52

Jesse Vincent's avatar
Jesse Vincent committed
53
54
55
56
57
58
use CGI qw/-private_tempfiles/;
use MIME::Entity;
use Text::Wrapper;
use CGI::Cookie;
use Time::HiRes;
use HTML::Scrubber;
59
use RT::Interface::Web;
60
use RT::Interface::Web::Request;
Jesse Vincent's avatar
Jesse Vincent committed
61
62
63
use File::Path qw( rmtree );
use File::Glob qw( bsd_glob );
use File::Spec::Unix;
64
65
66
use HTTP::Message::PSGI;
use HTTP::Request;
use HTTP::Response;
Jesse Vincent's avatar
Jesse Vincent committed
67

Jesse Vincent's avatar
Jesse Vincent committed
68
sub DefaultHandlerArgs  { (
69
70
    comp_root            => [
        RT::Interface::Web->ComponentRoots( Names => 1 ),
71
72
73
    ],
    default_escape_flags => 'h',
    data_dir             => "$RT::MasonDataDir",
74
    allow_globals        => [qw(%session $DECODED_ARGS)],
75
    # Turn off static source if we're in developer mode.
76
77
    static_source        => (RT->Config->Get('DevelMode') ? '0' : '1'), 
    use_object_files     => (RT->Config->Get('DevelMode') ? '0' : '1'), 
78
    autoflush            => 0,
79
    error_format         => (RT->Config->Get('DevelMode') ? 'html': 'rt_error'),
80
    request_class        => 'RT::Interface::Web::Request',
81
    named_component_subs => $INC{'Devel/Cover.pm'} ? 1 : 0,
Jesse Vincent's avatar
Jesse Vincent committed
82
) };
83
84
85
86
87

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.
88
    unless ( RT->Config->Get('DatabaseType') =~ /(?:mysql|Pg)/ ) {
89
90
91
92

        # Clean up our umask to protect session files
        umask(0077);

93
        if ($CGI::MOD_PERL and $CGI::MOD_PERL < 1.9908 ) {
Jesse Vincent's avatar
Jesse Vincent committed
94

95
            chown( Apache->server->uid, Apache->server->gid,
96
                $RT::MasonSessionDir )
97
98
99
100
101
102
103
104
105
106
107
108
109
110
            if Apache->server->can('uid');
        }

        # Die if WebSessionDir doesn't exist or we can't write to it
        stat($RT::MasonSessionDir);
        die "Can't read and write $RT::MasonSessionDir"
        unless ( ( -d _ ) and ( -r _ ) and ( -w _ ) );
    }

}


sub NewHandler {
    my $class = shift;
111
    $class->require or die $!;
112
    my $handler = $class->new(
Jesse Vincent's avatar
Jesse Vincent committed
113
        DefaultHandlerArgs(),
114
        RT->Config->Get('MasonParameters'),
115
116
117
        @_
    );
  
118
    $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeHTML );
Jesse Vincent's avatar
Jesse Vincent committed
119
    $handler->interp->set_escape( u => \&RT::Interface::Web::EscapeURI  );
120
    $handler->interp->set_escape( j => \&RT::Interface::Web::EscapeJS   );
121
122
123
    return($handler);
}

124
125
126
127
128
129
=head2 _mason_dir_index

=cut

sub _mason_dir_index {
    my ($self, $interp, $path) = @_;
sunnavy's avatar
sunnavy committed
130
    $path =~ s!/$!!;
131
132
133
134
135
136
137
138
    if (   !$interp->comp_exists( $path )
         && $interp->comp_exists( $path . "/index.html" ) )
    {
        return $path . "/index.html";
    }

    return $path;
}
139
140


141
142
=head2 CleanupRequest

Ruslan Zakirov's avatar
Ruslan Zakirov committed
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
Clean ups globals, caches and other things that could be still
there from previous requests:

=over 4

=item Rollback any uncommitted transaction(s)

=item Flush the ACL cache

=item Flush records cache of the L<DBIx::SearchBuilder> if
WebFlushDbCacheEveryRequest option is enabled, what is true by default
and is not recommended to change.

=item Clean up state of RT::Action::SendEmail using 'CleanSlate' method

158
=item Flush tmp crypt key preferences
Ruslan Zakirov's avatar
Ruslan Zakirov committed
159
160

=back
161
162
163
164
165

=cut

sub CleanupRequest {

Ruslan Zakirov's avatar
Ruslan Zakirov committed
166
    if ( $RT::Handle && $RT::Handle->TransactionDepth ) {
167
168
169
170
171
172
173
174
175
176
        $RT::Handle->ForceRollback;
        $RT::Logger->crit(
            "Transaction not committed. Usually indicates a software fault."
            . "Data loss may have occurred" );
    }

    # Clean out the ACL cache. the performance impact should be marginal.
    # Consistency is imprived, too.
    RT::Principal->InvalidateACLCache();
    DBIx::SearchBuilder::Record::Cachable->FlushCache
177
      if ( RT->Config->Get('WebFlushDbCacheEveryRequest')
178
179
180
        and UNIVERSAL::can(
            'DBIx::SearchBuilder::Record::Cachable' => 'FlushCache' ) );

Ruslan Zakirov's avatar
Ruslan Zakirov committed
181
182
    # cleanup global squelching of the mails
    require RT::Action::SendEmail;
183
    RT::Action::SendEmail->CleanSlate;
Ruslan Zakirov's avatar
Ruslan Zakirov committed
184
    
185
    if (RT->Config->Get('Crypt')->{'Enable'}) {
186
187
        RT::Crypt->UseKeyForEncryption();
        RT::Crypt->UseKeyForSigning( undef );
188
    }
189
190

    %RT::Ticket::MERGE_CACHE = ( effective => {}, merged => {} );
Ruslan Zakirov's avatar
Ruslan Zakirov committed
191
    %RT::User::PREFERENCES_CACHE = ();
192

193
194
195
196
197
    # 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};

198
    # Explicitly remove any tmpfiles that GPG opened, and close their
199
200
201
202
203
    # 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'};


204
}
205

Chia-liang Kao's avatar
Chia-liang Kao committed
206

207
208
sub HTML::Mason::Exception::as_rt_error {
    my ($self) = @_;
209
    $RT::Logger->error( $self->as_text );
210
    return "An internal RT error has occurred.  Your administrator can find more details in RT's log files.";
211
212
}

213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
=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;

229
230
231
    return unless defined $handler && $handler eq 'perl-script';

    $RT::Logger->critical(<<MODPERL);
232
233
234
235
236
237
238
239
240
241
RT has problems when SetHandler is set to perl-script.
Change SetHandler in your in httpd.conf to:

    SetHandler modperl

For a complete example mod_perl configuration, see:

https://bestpractical.com/rt/docs/@{[$RT::VERSION =~ /^(\d\.\d)/]}/web_deployment.html#mod_perl-2.xx
MODPERL

242
243
244
245
    my $res = Plack::Response->new(500);
    $res->content_type("text/plain");
    $res->body("Server misconfiguration; see error log for details");
    return $res;
246
}
247

Chia-liang Kao's avatar
Chia-liang Kao committed
248
249
250
251
# PSGI App

use RT::Interface::Web::Handler;
use CGI::Emulate::PSGI;
252
use Plack::Builder;
Chia-liang Kao's avatar
Chia-liang Kao committed
253
use Plack::Request;
254
use Plack::Response;
Chia-liang Kao's avatar
Chia-liang Kao committed
255
use Plack::Util;
256
use Encode;
Chia-liang Kao's avatar
Chia-liang Kao committed
257
258

sub PSGIApp {
259
260
    my $self = shift;

Chia-liang Kao's avatar
Chia-liang Kao committed
261
    # XXX: this is fucked
Chia-liang Kao's avatar
Chia-liang Kao committed
262
    require HTML::Mason::CGIHandler;
Chia-liang Kao's avatar
Chia-liang Kao committed
263
264
    require HTML::Mason::PSGIHandler::Streamy;
    my $h = RT::Interface::Web::Handler::NewHandler('HTML::Mason::PSGIHandler::Streamy');
265

266
267
    $self->InitSessionDir;

268
    my $mason = sub {
Chia-liang Kao's avatar
Chia-liang Kao committed
269
        my $env = shift;
270

271
272
273
274
        {
            my $res = $self->CheckModPerlHandler($env);
            return $self->_psgi_response_cb( $res->finalize ) if $res;
        }
275

276
277
278
279
280
281
282
283
        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 } );
            }
        }
Chia-liang Kao's avatar
Chia-liang Kao committed
284
285
286

        my $req = Plack::Request->new($env);

287
288
289
        # 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.
Alex Vandiver's avatar
Alex Vandiver committed
290
        if ( $req->path_info =~ m{(^|/)\.\.?(/|$)} ) {
291
292
293
294
            $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 });
        }
295
        $env->{PATH_INFO} = $self->_mason_dir_index( $h->interp, $req->path_info);
Chia-liang Kao's avatar
Chia-liang Kao committed
296
297
298
299
300
301
302
303

        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);
        }
304

Chia-liang Kao's avatar
Chia-liang Kao committed
305
306
        $RT::Logger->crit($@) if $@ && $RT::Logger;
        warn $@ if $@ && !$RT::Logger;
Chia-liang Kao's avatar
Chia-liang Kao committed
307
308
309
310
311
312
313
        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);
            };
Chia-liang Kao's avatar
Chia-liang Kao committed
314
        }
Chia-liang Kao's avatar
Chia-liang Kao committed
315
316
317
318
319

        return $self->_psgi_response_cb($ret,
                                        sub {
                                            $self->CleanupRequest()
                                        });
320
    };
321
322
323
324
325
326
327
328

    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;
329
}
330

331
332
333
334
sub StaticWrap {
    my $self    = shift;
    my $app     = shift;
    my $builder = Plack::Builder->new;
Alex Vandiver's avatar
Alex Vandiver committed
335

336
337
    my $headers = RT::Interface::Web::GetStaticHeaders(Time => 'forever');

338
339
    for my $static ( RT->Config->Get('StaticRoots') ) {
        if ( ref $static && ref $static eq 'HASH' ) {
340
341
342
343
344
            $builder->add_middleware(
                '+RT::Interface::Web::Middleware::StaticHeaders',
                path => $static->{'path'},
                headers => $headers,
            );
345
346
347
348
349
            $builder->add_middleware(
                'Plack::Middleware::Static',
                pass_through => 1,
                %$static
            );
350
351
352
353
354
355
356
        }
        else {
            $RT::Logger->error(
                "Invalid config StaticRoots: item can only be a hashref" );
        }
    }

357
358
359
360
361
362
    my $path = sub { s!^/static/!! };
    $builder->add_middleware(
        '+RT::Interface::Web::Middleware::StaticHeaders',
        path => $path,
        headers => $headers,
    );
363
    for my $root (RT::Interface::Web->StaticRoots) {
364
365
        $builder->add_middleware(
            'Plack::Middleware::Static',
366
            path         => $path,
367
368
369
370
            root         => $root,
            pass_through => 1,
        );
    }
371
    return $builder->to_app($app);
372
}
Chia-liang Kao's avatar
Chia-liang Kao committed
373

Chia-liang Kao's avatar
Chia-liang Kao committed
374
375
376
377
378
379
sub _psgi_response_cb {
    my $self = shift;
    my ($ret, $cleanup) = @_;
    Plack::Util::response_cb
            ($ret,
             sub {
380
                 my $res = shift;
381

382
                 if ( RT->Config->Get('Framebusting') ) {
383
384
385
386
                     # XXX TODO: Do we want to make the value of this header configurable?
                     Plack::Util::header_set($res->[1], 'X-Frame-Options' => 'DENY');
                 }

Chia-liang Kao's avatar
Chia-liang Kao committed
387
388
389
390
391
                 return sub {
                     if (!defined $_[0]) {
                         $cleanup->();
                         return '';
                     }
392
                     return utf8::is_utf8($_[0]) ? Encode::encode( "UTF-8", $_[0]) : $_[0];
Chia-liang Kao's avatar
Chia-liang Kao committed
393
394
395
396
397
                     return $_[0];
                 };
             });
}

398
399
400
401
402
403
404
405
406
407
408
409
410
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;
}

411
1;