summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Interface
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT/Interface')
-rwxr-xr-xrt/lib/RT/Interface/Email.pm51
-rwxr-xr-xrt/lib/RT/Interface/Email/Auth/GnuPG.pm13
-rw-r--r--rt/lib/RT/Interface/Web.pm103
-rw-r--r--rt/lib/RT/Interface/Web/Handler.pm6
-rwxr-xr-xrt/lib/RT/Interface/Web/QueryBuilder/Tree.pm6
-rw-r--r--rt/lib/RT/Interface/Web/Request.pm4
-rw-r--r--rt/lib/RT/Interface/Web/Session.pm4
-rw-r--r--rt/lib/RT/Interface/Web_Vendor.pm201
8 files changed, 345 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..edb719df5 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);
@@ -1638,6 +1714,9 @@ sub _ProcessObjectCustomFieldUpdates {
$values_hash{$val} = 1 if $val;
}
+ # For Date Cfs, @values is empty when there is no changes (no datas in form input)
+ return @results if ( $cf->Type eq 'Date' && ! @values );
+
$cf_values->RedoSearch;
while ( my $cf_value = $cf_values->Next ) {
next if $values_hash{ $cf_value->id };
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,
diff --git a/rt/lib/RT/Interface/Web_Vendor.pm b/rt/lib/RT/Interface/Web_Vendor.pm
new file mode 100644
index 000000000..1999096a7
--- /dev/null
+++ b/rt/lib/RT/Interface/Web_Vendor.pm
@@ -0,0 +1,201 @@
+# Copyright (c) 2004 Ivan Kohler <ivan-rt@420.am>
+# Copyright (c) 2008 Freeside Internet Services, Inc.
+#
+# 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.
+#
+# 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.
+
+=head1 NAME
+
+RT::Interface::Web_Vendor
+
+=head1 SYNOPSIS
+
+=head1 DESCRIPTION
+
+Freeside vendor overlay for RT::Interface::Web.
+
+=begin testing
+
+use_ok(RT::Interface::Web_Vendor);
+
+=end testing
+
+=cut
+
+#package RT::Interface::Web;
+#use strict;
+
+package HTML::Mason::Commands;
+use strict;
+
+=head2 ProcessTicketCustomers
+
+=cut
+
+sub ProcessTicketCustomers {
+ my %args = (
+ TicketObj => undef,
+ ARGSRef => undef,
+ Debug => 0,
+ @_
+ );
+ my @results = ();
+
+ my $Ticket = $args{'TicketObj'};
+ my $ARGSRef = $args{'ARGSRef'};
+ my $Debug = $args{'Debug'};
+ my $me = 'ProcessTicketCustomers';
+
+ ### false laziness w/RT::Interface::Web::ProcessTicketLinks
+ # Delete links that are gone gone gone.
+ foreach my $arg ( keys %$ARGSRef ) {
+ if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
+ my $base = $1;
+ my $type = $2;
+ my $target = $3;
+
+ push @results,
+ "Trying to delete: Base: $base Target: $target Type $type";
+ my ( $val, $msg ) = $Ticket->DeleteLink( Base => $base,
+ Type => $type,
+ Target => $target );
+
+ push @results, $msg;
+
+ }
+
+ }
+ ###
+
+ ###
+ #find new customers
+ ###
+
+ my @custnums = map { /^Ticket-AddCustomer-(\d+)$/; $1 }
+ grep { /^Ticket-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
+ keys %$ARGSRef;
+
+ #my @delete_custnums =
+ # map { /^Ticket-AddCustomer-(\d+)$/; $1 }
+ # grep { /^Ticket-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
+ # keys %$ARGSRef;
+
+ ###
+ #figure out if we're going to auto-link requestors, and find them if so
+ ###
+
+ my $num_cur_cust = $Ticket->Customers->Count;
+ my $num_new_cust = scalar(@custnums);
+ warn "$me: $num_cur_cust current customers / $num_new_cust new customers\n"
+ if $Debug;
+
+ #if we're linking the first ticket to one customer
+ my $link_requestors = ( $num_cur_cust == 0 && $num_new_cust == 1 );
+ warn "$me: adding a single customer to a previously customerless".
+ " ticket, so linking customers to requestor too\n"
+ if $Debug && $link_requestors;
+
+ my @Requestors = ();
+ if ( $link_requestors ) {
+
+ #find any requestors without customers
+ @Requestors =
+ grep { ! $_->Customers->Count }
+ @{ $Ticket->Requestors->UserMembersObj->ItemsArrayRef };
+
+ warn "$me: found ". scalar(@Requestors). " requestors without".
+ " customers; linking them\n"
+ if $Debug;
+
+ }
+
+ ###
+ #link ticket (and requestors) to customers
+ ###
+
+ foreach my $custnum ( @custnums ) {
+
+ my @link = ( 'Type' => 'MemberOf',
+ 'Target' => "freeside://freeside/cust_main/$custnum",
+ );
+
+ my( $val, $msg ) = $Ticket->AddLink(@link);
+ push @results, $msg;
+
+ #add customer links to requestors
+ foreach my $Requestor ( @Requestors ) {
+ my( $val, $msg ) = $Requestor->AddLink(@link);
+ push @results, $msg;
+ warn "$me: linking requestor to custnum $custnum: $msg\n"
+ if $Debug > 1;
+ }
+
+ }
+
+ return @results;
+
+}
+
+#false laziness w/above... eventually it should go away in favor of this
+sub ProcessObjectCustomers {
+ my %args = (
+ Object => undef,
+ ARGSRef => undef,
+ @_
+ );
+ my @results = ();
+
+ my $Object = $args{'Object'};
+ my $ARGSRef = $args{'ARGSRef'};
+
+ ### false laziness w/RT::Interface::Web::ProcessTicketLinks
+ # Delete links that are gone gone gone.
+ foreach my $arg ( keys %$ARGSRef ) {
+ if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
+ my $base = $1;
+ my $type = $2;
+ my $target = $3;
+
+ push @results,
+ "Trying to delete: Base: $base Target: $target Type $type";
+ my ( $val, $msg ) = $Object->DeleteLink( Base => $base,
+ Type => $type,
+ Target => $target );
+
+ push @results, $msg;
+
+ }
+
+ }
+ ###
+
+ #my @delete_custnums =
+ # map { /^Object-AddCustomer-(\d+)$/; $1 }
+ # grep { /^Object-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
+ # keys %$ARGSRef;
+
+ my @custnums = map { /^Object-AddCustomer-(\d+)$/; $1 }
+ grep { /^Object-AddCustomer-(\d+)$/ && $ARGSRef->{$_} }
+ keys %$ARGSRef;
+
+ foreach my $custnum ( @custnums ) {
+ my( $val, $msg ) =
+ $Object->AddLink( 'Type' => 'MemberOf',
+ 'Target' => "freeside://freeside/cust_main/$custnum",
+ );
+ push @results, $msg;
+ }
+
+ return @results;
+
+}
+
+1;
+