diff options
Diffstat (limited to 'rt/lib/RT/Interface')
-rwxr-xr-x | rt/lib/RT/Interface/Email.pm | 51 | ||||
-rwxr-xr-x | rt/lib/RT/Interface/Email/Auth/GnuPG.pm | 13 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web.pm | 100 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Handler.pm | 6 | ||||
-rwxr-xr-x | rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm | 6 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Request.pm | 4 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Session.pm | 4 |
7 files changed, 141 insertions, 43 deletions
diff --git a/rt/lib/RT/Interface/Email.pm b/rt/lib/RT/Interface/Email.pm index b669b5b2f..e0815fbe2 100755 --- a/rt/lib/RT/Interface/Email.pm +++ b/rt/lib/RT/Interface/Email.pm @@ -245,16 +245,23 @@ sub MailError { level => $args{'LogLevel'}, message => $args{'Explanation'} ) if $args{'LogLevel'}; + # the colons are necessary to make ->build include non-standard headers - my $entity = MIME::Entity->build( - Type => "multipart/mixed", - From => $args{'From'}, - Bcc => $args{'Bcc'}, - To => $args{'To'}, - Subject => $args{'Subject'}, - 'Precedence:' => 'bulk', + my %entity_args = ( + Type => "multipart/mixed", + From => $args{'From'}, + Bcc => $args{'Bcc'}, + To => $args{'To'}, + Subject => $args{'Subject'}, 'X-RT-Loop-Prevention:' => RT->Config->Get('rtname'), ); + + # only set precedence if the sysadmin wants us to + if (defined(RT->Config->Get('DefaultErrorMailPrecedence'))) { + $entity_args{'Precedence:'} = RT->Config->Get('DefaultErrorMailPrecedence'); + } + + my $entity = MIME::Entity->build(%entity_args); SetInReplyTo( Message => $entity, InReplyTo => $args{'MIMEObj'} ); $entity->attach( Data => $args{'Explanation'} . "\n" ); @@ -280,6 +287,9 @@ RT's outgoing mail configuration. If C<BOUNCE> is passed, and is a true value, the message will be marked as an autogenerated error, if possible. Sets Date field of the head to now if it's not set. +If the C<X-RT-Squelch> header is set to any true value, the mail will +not be sent. One use is to let extensions easily cancel outgoing mail. + Ticket and Transaction arguments are optional. If Transaction is specified and Ticket is not then ticket of the transaction is used, but only if the transaction belongs to a ticket. @@ -343,6 +353,11 @@ sub SendEmail { return -1; } + if ($args{'Entity'}->head->get('X-RT-Squelch')) { + $RT::Logger->info( $msgid . " Squelch header found. Not sending." ); + return -1; + } + if ( $TransactionObj && !$TicketObj && $TransactionObj->ObjectType eq 'RT::Ticket' ) { @@ -565,7 +580,7 @@ sub SendEmailUsingTemplate { return -1; } - $mail->head->set( $_ => $args{ $_ } ) + $mail->head->set( $_ => Encode::encode_utf8( $args{ $_ } ) ) foreach grep defined $args{$_}, qw(To Cc Bcc From); SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} ); @@ -963,22 +978,14 @@ sub ParseCcAddressesFromHead { @_ ); - my @recipients = - map lc $_->address, + my $current_address = lc $args{'CurrentUser'}->EmailAddress; + my $user = $args{'CurrentUser'}->UserObj; + + return + grep $_ ne $current_address && !RT::EmailParser->IsRTAddress( $_ ), + map lc $user->CanonicalizeEmailAddress( $_->address ), map Email::Address->parse( $args{'Head'}->get( $_ ) ), qw(To Cc); - - my @res; - foreach my $address ( @recipients ) { - $address = $args{'CurrentUser'}->UserObj->CanonicalizeEmailAddress( $address ); - next if lc $args{'CurrentUser'}->EmailAddress eq $address; - next if lc $args{'QueueObj'}->CorrespondAddress eq $address; - next if lc $args{'QueueObj'}->CommentAddress eq $address; - next if RT::EmailParser->IsRTAddress( $address ); - - push @res, $address; - } - return @res; } diff --git a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm index df987d806..f0fe2c917 100755 --- a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm +++ b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm @@ -75,13 +75,18 @@ sub GetCurrentUser { @_ ); - $args{'Message'}->head->delete($_) - for qw(X-RT-GnuPG-Status X-RT-Incoming-Encrypton - X-RT-Incoming-Signature X-RT-Privacy); + foreach my $p ( $args{'Message'}->parts_DFS ) { + $p->head->delete($_) for qw( + X-RT-GnuPG-Status X-RT-Incoming-Encrypton + X-RT-Incoming-Signature X-RT-Privacy + ); + } my $msg = $args{'Message'}->dup; - my ($status, @res) = VerifyDecrypt( Entity => $args{'Message'} ); + my ($status, @res) = VerifyDecrypt( + Entity => $args{'Message'}, AddStatus => 1, + ); if ( $status && !@res ) { $args{'Message'}->head->add( 'X-RT-Incoming-Encryption' => 'Not encrypted' diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm index b4279fb4b..d6b854f4e 100644 --- a/rt/lib/RT/Interface/Web.pm +++ b/rt/lib/RT/Interface/Web.pm @@ -554,6 +554,58 @@ sub StaticFileHeaders { # $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = $date->RFC2616; } +=head2 PathIsSafe + +Takes a C<< Path => path >> and returns a boolean indicating that +the path is safely within RT's control or not. The path I<must> be +relative. + +This function does not consult the filesystem at all; it is merely +a logical sanity checking of the path. This explicitly does not handle +symlinks; if you have symlinks in RT's webroot pointing outside of it, +then we assume you know what you are doing. + +=cut + +sub PathIsSafe { + my $self = shift; + my %args = @_; + my $path = $args{Path}; + + # Get File::Spec to clean up extra /s, ./, etc + my $cleaned_up = File::Spec->canonpath($path); + + if (!defined($cleaned_up)) { + $RT::Logger->info("Rejecting path that canonpath doesn't understand: $path"); + return 0; + } + + # Forbid too many ..s. We can't just sum then check because + # "../foo/bar/baz" should be illegal even though it has more + # downdirs than updirs. So as soon as we get a negative score + # (which means "breaking out" of the top level) we reject the path. + + my @components = split '/', $cleaned_up; + my $score = 0; + for my $component (@components) { + if ($component eq '..') { + $score--; + if ($score < 0) { + $RT::Logger->info("Rejecting unsafe path: $path"); + return 0; + } + } + elsif ($component eq '.' || $component eq '') { + # these two have no effect on $score + } + else { + $score++; + } + } + + return 1; +} + =head2 SendStaticFile Takes a File => path and a Type => Content-type @@ -571,6 +623,12 @@ sub SendStaticFile { my %args = @_; my $file = $args{File}; my $type = $args{Type}; + my $relfile = $args{RelativeFile}; + + if (defined($relfile) && !$self->PathIsSafe(Path => $relfile)) { + $HTML::Mason::Commands::r->status(400); + $HTML::Mason::Commands::m->abort; + } $self->StaticFileHeaders(); @@ -874,7 +932,9 @@ sub CreateTicket { } foreach my $argument (qw(Encrypt Sign)) { - $MIMEObj->head->add( "X-RT-$argument" => $ARGS{$argument} ) if defined $ARGS{$argument}; + $MIMEObj->head->add( + "X-RT-$argument" => Encode::encode_utf8( $ARGS{$argument} ) + ) if defined $ARGS{$argument}; } my %create_args = ( @@ -1084,7 +1144,9 @@ sub ProcessUpdateMessage { Type => $args{ARGSRef}->{'UpdateContentType'}, ); - $Message->head->add( 'Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'}, ) ); + $Message->head->add( 'Message-ID' => Encode::encode_utf8( + RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'} ) + ) ); my $old_txn = RT::Transaction->new( $session{'CurrentUser'} ); if ( $args{ARGSRef}->{'QuoteTransaction'} ) { $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} ); @@ -1124,6 +1186,24 @@ sub ProcessUpdateMessage { TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'} ); + my @temp_squelch; + foreach my $type (qw(Cc AdminCc)) { + if (grep $_ eq $type || $_ eq ( $type . 's' ), @{ $args{ARGSRef}->{'SkipNotification'} || [] }) { + push @temp_squelch, map $_->address, Email::Address->parse( $message_args{$type} ); + push @temp_squelch, $args{TicketObj}->$type->MemberEmailAddresses; + push @temp_squelch, $args{TicketObj}->QueueObj->$type->MemberEmailAddresses; + } + } + if (grep $_ eq 'Requestor' || $_ eq 'Requestors', @{ $args{ARGSRef}->{'SkipNotification'} || [] }) { + push @temp_squelch, map $_->address, Email::Address->parse( $message_args{Requestor} ); + push @temp_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses; + } + + if (@temp_squelch) { + require RT::Action::SendEmail; + RT::Action::SendEmail->SquelchMailTo( RT::Action::SendEmail->SquelchMailTo, @temp_squelch ); + } + unless ( $args{'ARGSRef'}->{'UpdateIgnoreAddressCheckboxes'} ) { foreach my $key ( keys %{ $args{ARGSRef} } ) { next unless $key =~ /^Update(Cc|Bcc)-(.*)$/; @@ -1182,9 +1262,8 @@ sub MakeMIMEEntity { ); my $Message = MIME::Entity->build( Type => 'multipart/mixed', - Subject => $args{'Subject'} || "", - From => $args{'From'}, - Cc => $args{'Cc'}, + map { $_ => Encode::encode_utf8( $args{ $_} ) } + grep defined $args{$_}, qw(Subject From Cc) ); if ( defined $args{'Body'} && length $args{'Body'} ) { @@ -1192,12 +1271,8 @@ sub MakeMIMEEntity { # Make the update content have no 'weird' newlines in it $args{'Body'} =~ s/\r\n/\n/gs; - # MIME::Head is not happy in utf-8 domain. This only happens - # when processing an incoming email (so far observed). - no utf8; - use bytes; $Message->attach( - Type => $args{'Type'} || 'text/plain', + Type => $args{'Type'} || 'text/plain', Charset => 'UTF-8', Data => $args{'Body'}, ); @@ -1218,8 +1293,8 @@ sub MakeMIMEEntity { # Prefer the cached name first over CGI.pm stringification. my $filename = $RT::Mason::CGI::Filename; - $filename = "$filehandle" unless defined($filename); - $filename = Encode::decode_utf8($filename); + $filename = "$filehandle" unless defined $filename; + $filename = Encode::encode_utf8( $filename ); $filename =~ s{^.*[\\/]}{}; $Message->attach( @@ -1234,6 +1309,7 @@ sub MakeMIMEEntity { } $Message->make_singlepart; + RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8 return ($Message); diff --git a/rt/lib/RT/Interface/Web/Handler.pm b/rt/lib/RT/Interface/Web/Handler.pm index 8d17921cb..6a0660670 100644 --- a/rt/lib/RT/Interface/Web/Handler.pm +++ b/rt/lib/RT/Interface/Web/Handler.pm @@ -217,6 +217,12 @@ sub CleanupRequest { RT::Crypt::GnuPG::UseKeyForEncryption(); RT::Crypt::GnuPG::UseKeyForSigning( undef ); } + + %RT::Ticket::MERGE_CACHE = ( effective => {}, merged => {} ); + + # Explicitly remove any tmpfiles that GPG opened, and close their + # filehandles. + File::Temp::cleanup; } # }}} diff --git a/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm b/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm index 574ead465..e672d8e4c 100755 --- a/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm +++ b/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm @@ -268,7 +268,11 @@ sub ParseSQL { } $value =~ s/'/\\'/g; - $value = "'$value'" if $value =~ /[^0-9]/; + if ( lc $op eq 'is' || lc $op eq 'is not' ) { + $value = 'NULL'; # just fix possible mistakes here + } elsif ( $value !~ /^[+-]?[0-9]+$/ ) { + $value = "'$value'"; + } $key = "'$key'" if $key =~ /^CF./; my $clause = { Key => $key, Op => $op, Value => $value }; diff --git a/rt/lib/RT/Interface/Web/Request.pm b/rt/lib/RT/Interface/Web/Request.pm index e1794640d..ba626a091 100644 --- a/rt/lib/RT/Interface/Web/Request.pm +++ b/rt/lib/RT/Interface/Web/Request.pm @@ -160,11 +160,11 @@ sub callback { my %seen; @$callbacks = ( - sort grep defined && length, + grep defined && length, # Skip backup files, files without a leading package name, # and files we've already seen grep !$seen{$_}++ && !m{/\.} && !m{~$} && m{^/Callbacks/[^/]+\Q$page/$name\E$}, - map $self->interp->resolver->glob_path($path, $_), + map { sort $self->interp->resolver->glob_path($path, $_) } @roots ); foreach my $comp (keys %seen) { diff --git a/rt/lib/RT/Interface/Web/Session.pm b/rt/lib/RT/Interface/Web/Session.pm index 4998c34f9..1e0e6d5f0 100644 --- a/rt/lib/RT/Interface/Web/Session.pm +++ b/rt/lib/RT/Interface/Web/Session.pm @@ -111,8 +111,8 @@ new session objects. =cut sub Attributes { - - return $_[0]->Backends->{RT->Config->Get('DatabaseType')} ? { + my $class = $_[0]->Class; + return !$class->isa('Apache::Session::File') ? { Handle => $RT::Handle->dbh, LockHandle => $RT::Handle->dbh, Transaction => 1, |