summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Interface
diff options
context:
space:
mode:
authorivan <ivan>2009-12-31 13:16:41 +0000
committerivan <ivan>2009-12-31 13:16:41 +0000
commitb4b0c7e72d7eaee2fbfc7022022c9698323203dd (patch)
treeba4cd21399e412c32fe3737eaa8478e3271509f9 /rt/lib/RT/Interface
parent2dfda73eeb3eae2d4f894099754794ef07d060dd (diff)
import rt 3.8.7
Diffstat (limited to 'rt/lib/RT/Interface')
-rw-r--r--rt/lib/RT/Interface/CLI.pm24
-rwxr-xr-xrt/lib/RT/Interface/Email.pm1205
-rwxr-xr-xrt/lib/RT/Interface/Email/Auth/GnuPG.pm211
-rw-r--r--rt/lib/RT/Interface/Email/Auth/MailFrom.pm99
-rw-r--r--rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm8
-rw-r--r--rt/lib/RT/Interface/REST.pm58
-rw-r--r--rt/lib/RT/Interface/Web.pm1849
-rw-r--r--rt/lib/RT/Interface/Web/Handler.pm78
-rw-r--r--rt/lib/RT/Interface/Web/Menu.pm5
-rw-r--r--rt/lib/RT/Interface/Web/Menu/Item.pm5
-rwxr-xr-xrt/lib/RT/Interface/Web/QueryBuilder.pm5
-rwxr-xr-xrt/lib/RT/Interface/Web/QueryBuilder/Tree.pm180
-rw-r--r--rt/lib/RT/Interface/Web/Request.pm207
-rw-r--r--rt/lib/RT/Interface/Web/Session.pm285
-rwxr-xr-xrt/lib/RT/Interface/Web/Standalone.pm49
-rw-r--r--rt/lib/RT/Interface/Web/Standalone/PreFork.pm103
16 files changed, 3171 insertions, 1200 deletions
diff --git a/rt/lib/RT/Interface/CLI.pm b/rt/lib/RT/Interface/CLI.pm
index 5331504..a910fb4 100644
--- a/rt/lib/RT/Interface/CLI.pm
+++ b/rt/lib/RT/Interface/CLI.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,6 +45,7 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
use strict;
use RT;
@@ -53,14 +54,12 @@ package RT::Interface::CLI;
BEGIN {
- use Exporter ();
- use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
+ use base 'Exporter';
+ use vars qw ($VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS);
# set the version for version checking
- $VERSION = do { my @r = (q$Revision: 1.1.1.7 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker
-
- @ISA = qw(Exporter);
-
+ $VERSION = do { my @r = (q$Revision: 1.1.1.8 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker
+
# your exported package globals go here,
# as well as any optionally exported functions
@EXPORT_OK = qw(&CleanEnv
@@ -100,11 +99,6 @@ BEGIN {
=head1 METHODS
-=begin testing
-
-ok(require RT::Interface::CLI);
-
-=end testing
=cut
@@ -226,7 +220,7 @@ sub GetMessageContent {
if ($edit) {
unless ($ENV{'EDITOR'}) {
- $RT::Logger->crit('No $EDITOR variable defined'. "\n");
+ $RT::Logger->crit('No $EDITOR variable defined');
return undef;
}
system ($ENV{'EDITOR'}, $filename);
@@ -249,7 +243,7 @@ sub debug {
my $val = shift;
my ($debug);
if ($val) {
- $RT::Logger->debug($val."\n");
+ $RT::Logger->debug($val);
if ($debug) {
print STDERR "$val\n";
}
diff --git a/rt/lib/RT/Interface/Email.pm b/rt/lib/RT/Interface/Email.pm
index 14ae2a0..b669b5b 100755
--- a/rt/lib/RT/Interface/Email.pm
+++ b/rt/lib/RT/Interface/Email.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,24 +45,26 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
package RT::Interface::Email;
use strict;
-use Mail::Address;
+use warnings;
+
+use Email::Address;
use MIME::Entity;
use RT::EmailParser;
use File::Temp;
use UNIVERSAL::require;
+use Mail::Mailer ();
BEGIN {
- use Exporter ();
- use vars qw ( @ISA @EXPORT_OK);
+ use base 'Exporter';
+ use vars qw ( @EXPORT_OK);
# set the version for version checking
our $VERSION = 2.0;
- @ISA = qw(Exporter);
-
# your exported package globals go here,
# as well as any optionally exported functions
@EXPORT_OK = qw(
@@ -95,37 +97,41 @@ BEGIN {
=head1 DESCRIPTION
-=begin testing
-ok(require RT::Interface::Email);
-=end testing
+=head1 METHODS
+=head2 CheckForLoops HEAD
-=head1 METHODS
+Takes a HEAD object of L<MIME::Head> class and returns true if the
+message's been sent by this RT instance. Uses "X-RT-Loop-Prevention"
+field of the head for test.
=cut
-# {{{ sub CheckForLoops
-
sub CheckForLoops {
my $head = shift;
- #If this instance of RT sent it our, we don't want to take it in
+ # If this instance of RT sent it our, we don't want to take it in
my $RTLoop = $head->get("X-RT-Loop-Prevention") || "";
- chomp($RTLoop); #remove that newline
- if ( $RTLoop eq "$RT::rtname" ) {
- return (1);
+ chomp ($RTLoop); # remove that newline
+ if ( $RTLoop eq RT->Config->Get('rtname') ) {
+ return 1;
}
# TODO: We might not trap the case where RT instance A sends a mail
# to RT instance B which sends a mail to ...
- return (undef);
+ return undef;
}
-# }}}
+=head2 CheckForSuspiciousSender HEAD
+
+Takes a HEAD object of L<MIME::Head> class and returns true if sender
+is suspicious. Suspicious means mailer daemon.
+
+See also L</ParseSenderAddressFromHead>.
-# {{{ sub CheckForSuspiciousSender
+=cut
sub CheckForSuspiciousSender {
my $head = shift;
@@ -143,19 +149,24 @@ sub CheckForSuspiciousSender {
my ( $From, $junk ) = ParseSenderAddressFromHead($head);
if ( ( $From =~ /^mailer-daemon\@/i )
- or ( $From =~ /^postmaster\@/i ) )
+ or ( $From =~ /^postmaster\@/i )
+ or ( $From eq "" ))
{
return (1);
}
- return (undef);
-
+ return undef;
}
-# }}}
+=head2 CheckForAutoGenerated HEAD
+
+Takes a HEAD object of L<MIME::Head> class and returns true if message
+is autogenerated. Checks 'Precedence' and 'X-FC-Machinegenerated'
+fields of the head in tests.
+
+=cut
-# {{{ sub CheckForAutoGenerated
sub CheckForAutoGenerated {
my $head = shift;
@@ -164,6 +175,13 @@ sub CheckForAutoGenerated {
return (1);
}
+ # Per RFC3834, any Auto-Submitted header which is not "no" means
+ # it is auto-generated.
+ my $AutoSubmitted = $head->get("Auto-Submitted") || "";
+ if ( length $AutoSubmitted and $AutoSubmitted ne "no" ) {
+ return (1);
+ }
+
# First Class mailer uses this as a clue.
my $FCJunk = $head->get("X-FC-Machinegenerated") || "";
if ( $FCJunk =~ /^true/i ) {
@@ -173,9 +191,7 @@ sub CheckForAutoGenerated {
return (0);
}
-# }}}
-# {{{ sub CheckForBounce
sub CheckForBounce {
my $head = shift;
@@ -183,55 +199,40 @@ sub CheckForBounce {
return ( $ReturnPath =~ /<>/ );
}
-# }}}
-# {{{ IsRTAddress
+=head2 MailError PARAM HASH
-=head2 IsRTAddress ADDRESS
+Sends an error message. Takes a param hash:
-Takes a single parameter, an email address.
-Returns true if that address matches the $RTAddressRegexp.
-Returns false, otherwise.
+=over 4
-=cut
+=item From - sender's address, by default is 'CorrespondAddress';
-sub IsRTAddress {
- my $address = shift || '';
+=item To - recipient, by default is 'OwnerEmail';
- # Example: the following rule would tell RT not to Cc
- # "tickets@noc.example.com"
- if ( defined($RT::RTAddressRegexp)
- && $address =~ /$RT::RTAddressRegexp/i )
- {
- return (1);
- } else {
- return (undef);
- }
-}
+=item Bcc - optional Bcc recipients;
-# }}}
+=item Subject - subject of the message, default is 'There has been an error';
-# {{{ CullRTAddresses
+=item Explanation - main content of the error, default value is 'Unexplained error';
-=head2 CullRTAddresses ARRAY
+=item MIMEObj - optional MIME entity that's attached to the error mail, as well we
+add 'In-Reply-To' field to the error that points to this message.
-Takes a single argument, an array of email addresses.
-Returns the same array with any IsRTAddress()es weeded out.
+=item Attach - optional text that attached to the error as 'message/rfc822' part.
-=cut
+=item LogLevel - log level under which we should write explanation message into the
+log, by default we log it as critical.
-sub CullRTAddresses {
- return grep !IsRTAddress($_), @_;
-}
+=back
-# }}}
+=cut
-# {{{ sub MailError
sub MailError {
my %args = (
- To => $RT::OwnerEmail,
+ To => RT->Config->Get('OwnerEmail'),
Bcc => undef,
- From => $RT::CorrespondAddress,
+ From => RT->Config->Get('CorrespondAddress'),
Subject => 'There has been an error',
Explanation => 'Unexplained error',
MIMEObj => undef,
@@ -243,7 +244,7 @@ sub MailError {
$RT::Logger->log(
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",
@@ -252,16 +253,15 @@ sub MailError {
To => $args{'To'},
Subject => $args{'Subject'},
'Precedence:' => 'bulk',
- 'X-RT-Loop-Prevention:' => $RT::rtname,
- 'In-Reply-To:' => $args{'MIMEObj'} ? $args{'MIMEObj'}->head->get('Message-Id') : undef
+ 'X-RT-Loop-Prevention:' => RT->Config->Get('rtname'),
);
+ SetInReplyTo( Message => $entity, InReplyTo => $args{'MIMEObj'} );
$entity->attach( Data => $args{'Explanation'} . "\n" );
- my $mimeobj = $args{'MIMEObj'};
- if ($mimeobj) {
- $mimeobj->sync_headers();
- $entity->add_part($mimeobj);
+ if ( $args{'MIMEObj'} ) {
+ $args{'MIMEObj'}->sync_headers;
+ $entity->add_part( $args{'MIMEObj'} );
}
if ( $args{'Attach'} ) {
@@ -269,25 +269,628 @@ sub MailError {
}
- if ( $RT::MailCommand eq 'sendmailpipe' ) {
- open( MAIL,
- "|$RT::SendmailPath $RT::SendmailBounceArguments $RT::SendmailArguments"
- )
- || return (0);
- print MAIL $entity->as_string;
- close(MAIL);
+ SendEmail( Entity => $entity, Bounce => 1 );
+}
+
+
+=head2 SendEmail Entity => undef, [ Bounce => 0, Ticket => undef, Transaction => undef ]
+
+Sends an email (passed as a L<MIME::Entity> object C<ENTITY>) using
+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.
+
+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.
+
+Returns 1 on success, 0 on error or -1 if message has no recipients
+and hasn't been sent.
+
+=head3 Signing and Encrypting
+
+This function as well signs and/or encrypts the message according to
+headers of a transaction's attachment or properties of a ticket's queue.
+To get full access to the configuration Ticket and/or Transaction
+arguments must be provided, but you can force behaviour using Sign
+and/or Encrypt arguments.
+
+The following precedence of arguments are used to figure out if
+the message should be encrypted and/or signed:
+
+* if Sign or Encrypt argument is defined then its value is used
+
+* else if Transaction's first attachment has X-RT-Sign or X-RT-Encrypt
+header field then it's value is used
+
+* else properties of a queue of the Ticket are used.
+
+=cut
+
+sub SendEmail {
+ my (%args) = (
+ Entity => undef,
+ Bounce => 0,
+ Ticket => undef,
+ Transaction => undef,
+ @_,
+ );
+
+ my $TicketObj = $args{'Ticket'};
+ my $TransactionObj = $args{'Transaction'};
+
+ foreach my $arg( qw(Entity Bounce) ) {
+ next unless defined $args{ lc $arg };
+
+ $RT::Logger->warning("'". lc($arg) ."' argument is deprecated, use '$arg' instead");
+ $args{ $arg } = delete $args{ lc $arg };
+ }
+
+ unless ( $args{'Entity'} ) {
+ $RT::Logger->crit( "Could not send mail without 'Entity' object" );
+ return 0;
+ }
+
+ my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
+ chomp $msgid;
+
+ # If we don't have any recipients to send to, don't send a message;
+ unless ( $args{'Entity'}->head->get('To')
+ || $args{'Entity'}->head->get('Cc')
+ || $args{'Entity'}->head->get('Bcc') )
+ {
+ $RT::Logger->info( $msgid . " No recipients found. Not sending." );
+ return -1;
+ }
+
+ if ( $TransactionObj && !$TicketObj
+ && $TransactionObj->ObjectType eq 'RT::Ticket' )
+ {
+ $TicketObj = $TransactionObj->Object;
+ }
+
+ if ( RT->Config->Get('GnuPG')->{'Enable'} ) {
+ my %crypt;
+
+ my $attachment;
+ $attachment = $TransactionObj->Attachments->First
+ if $TransactionObj;
+
+ foreach my $argument ( qw(Sign Encrypt) ) {
+ next if defined $args{ $argument };
+
+ if ( $attachment && defined $attachment->GetHeader("X-RT-$argument") ) {
+ $crypt{$argument} = $attachment->GetHeader("X-RT-$argument");
+ } elsif ( $TicketObj ) {
+ $crypt{$argument} = $TicketObj->QueueObj->$argument();
+ }
+ }
+
+ my $res = SignEncrypt( %args, %crypt );
+ return $res unless $res > 0;
+ }
+
+ unless ( $args{'Entity'}->head->get('Date') ) {
+ require RT::Date;
+ my $date = RT::Date->new( $RT::SystemUser );
+ $date->SetToNow;
+ $args{'Entity'}->head->set( 'Date', $date->RFC2822( Timezone => 'server' ) );
+ }
+
+ my $mail_command = RT->Config->Get('MailCommand');
+
+ if ($mail_command eq 'testfile') {
+ $Mail::Mailer::testfile::config{outfile} = File::Temp->new;
+ }
+
+ # if it is a sub routine, we just return it;
+ return $mail_command->($args{'Entity'}) if UNIVERSAL::isa( $mail_command, 'CODE' );
+
+ if ( $mail_command eq 'sendmailpipe' ) {
+ my $path = RT->Config->Get('SendmailPath');
+ my $args = RT->Config->Get('SendmailArguments');
+
+ # SetOutgoingMailFrom
+ if ( RT->Config->Get('SetOutgoingMailFrom') ) {
+ my $OutgoingMailAddress;
+
+ if ($TicketObj) {
+ my $QueueName = $TicketObj->QueueObj->Name;
+ my $QueueAddressOverride = RT->Config->Get('OverrideOutgoingMailFrom')->{$QueueName};
+
+ if ($QueueAddressOverride) {
+ $OutgoingMailAddress = $QueueAddressOverride;
+ } else {
+ $OutgoingMailAddress = $TicketObj->QueueObj->CorrespondAddress;
+ }
+ }
+
+ $OutgoingMailAddress ||= RT->Config->Get('OverrideOutgoingMailFrom')->{'Default'};
+
+ $args .= " -f $OutgoingMailAddress"
+ if $OutgoingMailAddress;
+ }
+
+ # Set Bounce Arguments
+ $args .= ' '. RT->Config->Get('SendmailBounceArguments') if $args{'Bounce'};
+
+ # VERP
+ if ( $TransactionObj and
+ my $prefix = RT->Config->Get('VERPPrefix') and
+ my $domain = RT->Config->Get('VERPDomain') )
+ {
+ my $from = $TransactionObj->CreatorObj->EmailAddress;
+ $from =~ s/@/=/g;
+ $from =~ s/\s//g;
+ $args .= " -f $prefix$from\@$domain";
+ }
+
+ eval {
+ # don't ignore CHLD signal to get proper exit code
+ local $SIG{'CHLD'} = 'DEFAULT';
+
+ open my $mail, "|$path $args" or die "couldn't execute program: $!";
+
+ # if something wrong with $mail->print we will get PIPE signal, handle it
+ local $SIG{'PIPE'} = sub { die "program unexpectedly closed pipe" };
+ $args{'Entity'}->print($mail);
+
+ unless ( close $mail ) {
+ die "close pipe failed: $!" if $!; # system error
+ # sendmail exit statuses mostly errors with data not software
+ # TODO: status parsing: core dump, exit on signal or EX_*
+ my $msg = "$msgid: `$path $args` exitted with code ". ($?>>8);
+ $msg = ", interrupted by signal ". ($?&127) if $?&127;
+ $RT::Logger->error( $msg );
+ }
+ };
+ if ( $@ ) {
+ $RT::Logger->crit( "$msgid: Could not send mail with command `$path $args`: " . $@ );
+ return 0;
+ }
+ }
+ elsif ( $mail_command eq 'smtp' ) {
+ require Net::SMTP;
+ my $smtp = do { local $@; eval { Net::SMTP->new(
+ Host => RT->Config->Get('SMTPServer'),
+ Debug => RT->Config->Get('SMTPDebug'),
+ ) } };
+ unless ( $smtp ) {
+ $RT::Logger->crit( "Could not connect to SMTP server.");
+ return 0;
+ }
+
+ # duplicate head as we want drop Bcc field
+ my $head = $args{'Entity'}->head->dup;
+ my @recipients = map $_->address, map
+ Email::Address->parse($head->get($_)), qw(To Cc Bcc);
+ $head->delete('Bcc');
+
+ my $sender = RT->Config->Get('SMTPFrom')
+ || $args{'Entity'}->head->get('From');
+ chomp $sender;
+
+ my $status = $smtp->mail( $sender )
+ && $smtp->recipient( @recipients );
+
+ if ( $status ) {
+ $smtp->data;
+ my $fh = $smtp->tied_fh;
+ $head->print( $fh );
+ print $fh "\n";
+ $args{'Entity'}->print_body( $fh );
+ $smtp->dataend;
+ }
+ $smtp->quit;
+
+ unless ( $status ) {
+ $RT::Logger->crit( "$msgid: Could not send mail via SMTP." );
+ return 0;
+ }
+ }
+ else {
+ local ($ENV{'MAILADDRESS'}, $ENV{'PERL_MAILERS'});
+
+ my @mailer_args = ($mail_command);
+ if ( $mail_command eq 'sendmail' ) {
+ $ENV{'PERL_MAILERS'} = RT->Config->Get('SendmailPath');
+ push @mailer_args, split(/\s+/, RT->Config->Get('SendmailArguments'));
+ }
+ else {
+ push @mailer_args, RT->Config->Get('MailParams');
+ }
+
+ unless ( $args{'Entity'}->send( @mailer_args ) ) {
+ $RT::Logger->crit( "$msgid: Could not send mail." );
+ return 0;
+ }
+ }
+ return 1;
+}
+
+=head2 PrepareEmailUsingTemplate Template => '', Arguments => {}
+
+Loads a template. Parses it using arguments if it's not empty.
+Returns a tuple (L<RT::Template> object, error message).
+
+Note that even if a template object is returned MIMEObj method
+may return undef for empty templates.
+
+=cut
+
+sub PrepareEmailUsingTemplate {
+ my %args = (
+ Template => '',
+ Arguments => {},
+ @_
+ );
+
+ my $template = RT::Template->new( $RT::SystemUser );
+ $template->LoadGlobalTemplate( $args{'Template'} );
+ unless ( $template->id ) {
+ return (undef, "Couldn't load template '". $args{'Template'} ."'");
+ }
+ return $template if $template->IsEmpty;
+
+ my ($status, $msg) = $template->Parse( %{ $args{'Arguments'} } );
+ return (undef, $msg) unless $status;
+
+ return $template;
+}
+
+=head2 SendEmailUsingTemplate Template => '', Arguments => {}, From => CorrespondAddress, To => '', Cc => '', Bcc => ''
+
+Sends email using a template, takes name of template, arguments for it and recipients.
+
+=cut
+
+sub SendEmailUsingTemplate {
+ my %args = (
+ Template => '',
+ Arguments => {},
+ To => undef,
+ Cc => undef,
+ Bcc => undef,
+ From => RT->Config->Get('CorrespondAddress'),
+ InReplyTo => undef,
+ @_
+ );
+
+ my ($template, $msg) = PrepareEmailUsingTemplate( %args );
+ return (0, $msg) unless $template;
+
+ my $mail = $template->MIMEObj;
+ unless ( $mail ) {
+ $RT::Logger->info("Message is not sent as template #". $template->id ." is empty");
+ return -1;
+ }
+
+ $mail->head->set( $_ => $args{ $_ } )
+ foreach grep defined $args{$_}, qw(To Cc Bcc From);
+
+ SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
+
+ return SendEmail( Entity => $mail );
+}
+
+=head2 ForwardTransaction TRANSACTION, To => '', Cc => '', Bcc => ''
+
+Forwards transaction with all attachments as 'message/rfc822'.
+
+=cut
+
+sub ForwardTransaction {
+ my $txn = shift;
+ my %args = ( To => '', Cc => '', Bcc => '', @_ );
+
+ my $entity = $txn->ContentAsMIME;
+
+ return SendForward( %args, Entity => $entity, Transaction => $txn );
+}
+
+=head2 ForwardTicket TICKET, To => '', Cc => '', Bcc => ''
+
+Forwards a ticket's Create and Correspond Transactions and their Attachments as 'message/rfc822'.
+
+=cut
+
+sub ForwardTicket {
+ my $ticket = shift;
+ my %args = ( To => '', Cc => '', Bcc => '', @_ );
+
+ my $txns = $ticket->Transactions;
+ $txns->Limit(
+ FIELD => 'Type',
+ VALUE => $_,
+ ) for qw(Create Correspond);
+
+ my $entity = MIME::Entity->build(
+ Type => 'multipart/mixed',
+ );
+ $entity->add_part( $_ ) foreach
+ map $_->ContentAsMIME,
+ @{ $txns->ItemsArrayRef };
+
+ return SendForward( %args, Entity => $entity, Ticket => $ticket, Template => 'Forward Ticket' );
+}
+
+=head2 SendForward Entity => undef, Ticket => undef, Transaction => undef, Template => undef, To => '', Cc => '', Bcc => ''
+
+Forwards an Entity representing Ticket or Transaction as 'message/rfc822'. Entity is wrapped into Template.
+
+=cut
+
+sub SendForward {
+ my (%args) = (
+ Entity => undef,
+ Ticket => undef,
+ Transaction => undef,
+ Template => 'Forward',
+ To => '', Cc => '', Bcc => '',
+ @_
+ );
+
+ my $txn = $args{'Transaction'};
+ my $ticket = $args{'Ticket'};
+ $ticket ||= $txn->Object if $txn;
+
+ my $entity = $args{'Entity'};
+ unless ( $entity ) {
+ require Carp;
+ $RT::Logger->error(Carp::longmess("No entity provided"));
+ return (0, $ticket->loc("Couldn't send email"));
+ }
+
+ my ($template, $msg) = PrepareEmailUsingTemplate(
+ Template => $args{'Template'},
+ Arguments => {
+ Ticket => $ticket,
+ Transaction => $txn,
+ },
+ );
+
+ my $mail;
+ if ( $template ) {
+ $mail = $template->MIMEObj;
} else {
- $entity->send( $RT::MailCommand, $RT::MailParams );
+ $RT::Logger->warning($msg);
+ }
+ unless ( $mail ) {
+ $RT::Logger->warning("Couldn't generate email using template '$args{Template}'");
+
+ my $description;
+ unless ( $args{'Transaction'} ) {
+ $description = 'This is forward of ticket #'. $ticket->id;
+ } else {
+ $description = 'This is forward of transaction #'
+ . $txn->id ." of a ticket #". $txn->ObjectId;
+ }
+ $mail = MIME::Entity->build(
+ Type => 'text/plain',
+ Data => $description,
+ );
+ }
+
+ $mail->head->set( $_ => EncodeToMIME( String => $args{$_} ) )
+ foreach grep defined $args{$_}, qw(To Cc Bcc);
+
+ $mail->attach(
+ Type => 'message/rfc822',
+ Disposition => 'attachment',
+ Description => 'forwarded message',
+ Data => $entity->as_string,
+ );
+
+ my $from;
+ my $subject = '';
+ $subject = $txn->Subject if $txn;
+ $subject ||= $ticket->Subject if $ticket;
+ if ( RT->Config->Get('ForwardFromUser') ) {
+ $from = ($txn || $ticket)->CurrentUser->UserObj->EmailAddress;
+ } else {
+ # XXX: what if want to forward txn of other object than ticket?
+ $subject = AddSubjectTag( $subject, $ticket );
+ $from = $ticket->QueueObj->CorrespondAddress
+ || RT->Config->Get('CorrespondAddress');
+ }
+ $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) );
+ $mail->head->set( From => EncodeToMIME( String => $from ) );
+
+ my $status = RT->Config->Get('ForwardFromUser')
+ # never sign if we forward from User
+ ? SendEmail( %args, Entity => $mail, Sign => 0 )
+ : SendEmail( %args, Entity => $mail );
+ return (0, $ticket->loc("Couldn't send email")) unless $status;
+ return (1, $ticket->loc("Send email successfully"));
+}
+
+=head2 SignEncrypt Entity => undef, Sign => 0, Encrypt => 0
+
+Signs and encrypts message using L<RT::Crypt::GnuPG>, but as well
+handle errors with users' keys.
+
+If a recipient has no key or has other problems with it, then the
+unction sends a error to him using 'Error: public key' template.
+Also, notifies RT's owner using template 'Error to RT owner: public key'
+to inform that there are problems with users' keys. Then we filter
+all bad recipients and retry.
+
+Returns 1 on success, 0 on error and -1 if all recipients are bad and
+had been filtered out.
+
+=cut
+
+sub SignEncrypt {
+ my %args = (
+ Entity => undef,
+ Sign => 0,
+ Encrypt => 0,
+ @_
+ );
+ return 1 unless $args{'Sign'} || $args{'Encrypt'};
+
+ my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
+ chomp $msgid;
+
+ $RT::Logger->debug("$msgid Signing message") if $args{'Sign'};
+ $RT::Logger->debug("$msgid Encrypting message") if $args{'Encrypt'};
+
+ require RT::Crypt::GnuPG;
+ my %res = RT::Crypt::GnuPG::SignEncrypt( %args );
+ return 1 unless $res{'exit_code'};
+
+ my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+
+ my @bad_recipients;
+ foreach my $line ( @status ) {
+ # if the passphrase fails, either you have a bad passphrase
+ # or gpg-agent has died. That should get caught in Create and
+ # Update, but at least throw an error here
+ if (($line->{'Operation'}||'') eq 'PassphraseCheck'
+ && $line->{'Status'} =~ /^(?:BAD|MISSING)$/ ) {
+ $RT::Logger->error( "$line->{'Status'} PASSPHRASE: $line->{'Message'}" );
+ return 0;
+ }
+ next unless ($line->{'Operation'}||'') eq 'RecipientsCheck';
+ next if $line->{'Status'} eq 'DONE';
+ $RT::Logger->error( $line->{'Message'} );
+ push @bad_recipients, $line;
+ }
+ return 0 unless @bad_recipients;
+
+ $_->{'AddressObj'} = (Email::Address->parse( $_->{'Recipient'} ))[0]
+ foreach @bad_recipients;
+
+ foreach my $recipient ( @bad_recipients ) {
+ my $status = SendEmailUsingTemplate(
+ To => $recipient->{'AddressObj'}->address,
+ Template => 'Error: public key',
+ Arguments => {
+ %$recipient,
+ TicketObj => $args{'Ticket'},
+ TransactionObj => $args{'Transaction'},
+ },
+ );
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't send 'Error: public key'");
+ }
+ }
+
+ my $status = SendEmailUsingTemplate(
+ To => RT->Config->Get('OwnerEmail'),
+ Template => 'Error to RT owner: public key',
+ Arguments => {
+ BadRecipients => \@bad_recipients,
+ TicketObj => $args{'Ticket'},
+ TransactionObj => $args{'Transaction'},
+ },
+ );
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't send 'Error to RT owner: public key'");
+ }
+
+ DeleteRecipientsFromHead(
+ $args{'Entity'}->head,
+ map $_->{'AddressObj'}->address, @bad_recipients
+ );
+
+ unless ( $args{'Entity'}->head->get('To')
+ || $args{'Entity'}->head->get('Cc')
+ || $args{'Entity'}->head->get('Bcc') )
+ {
+ $RT::Logger->debug("$msgid No recipients that have public key, not sending");
+ return -1;
}
+
+ # redo without broken recipients
+ %res = RT::Crypt::GnuPG::SignEncrypt( %args );
+ return 0 if $res{'exit_code'};
+
+ return 1;
}
-# }}}
+use MIME::Words ();
+
+=head2 EncodeToMIME
+
+Takes a hash with a String and a Charset. Returns the string encoded
+according to RFC2047, using B (base64 based) encoding.
+
+String must be a perl string, octets are returned.
+
+If Charset is not provided then $EmailOutputEncoding config option
+is used, or "latin-1" if that is not set.
+
+=cut
+
+sub EncodeToMIME {
+ my %args = (
+ String => undef,
+ Charset => undef,
+ @_
+ );
+ my $value = $args{'String'};
+ return $value unless $value; # 0 is perfect ascii
+ my $charset = $args{'Charset'} || RT->Config->Get('EmailOutputEncoding');
+ my $encoding = 'B';
+
+ # using RFC2047 notation, sec 2.
+ # encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
+
+ # An 'encoded-word' may not be more than 75 characters long
+ #
+ # MIME encoding increases 4/3*(number of bytes), and always in multiples
+ # of 4. Thus we have to find the best available value of bytes available
+ # for each chunk.
+ #
+ # First we get the integer max which max*4/3 would fit on space.
+ # Then we find the greater multiple of 3 lower or equal than $max.
+ my $max = int(
+ ( ( 75 - length( '=?' . $charset . '?' . $encoding . '?' . '?=' ) )
+ * 3
+ ) / 4
+ );
+ $max = int( $max / 3 ) * 3;
+
+ chomp $value;
+
+ if ( $max <= 0 ) {
+
+ # gives an error...
+ $RT::Logger->crit("Can't encode! Charset or encoding too big.");
+ return ($value);
+ }
+
+ return ($value) unless $value =~ /[^\x20-\x7e]/;
+
+ $value =~ s/\s+$//;
-# {{{ Create User
+ # we need perl string to split thing char by char
+ Encode::_utf8_on($value) unless Encode::is_utf8($value);
+
+ my ( $tmp, @chunks ) = ( '', () );
+ while ( length $value ) {
+ my $char = substr( $value, 0, 1, '' );
+ my $octets = Encode::encode( $charset, $char );
+ if ( length($tmp) + length($octets) > $max ) {
+ push @chunks, $tmp;
+ $tmp = '';
+ }
+ $tmp .= $octets;
+ }
+ push @chunks, $tmp if length $tmp;
+
+ # encode an join chuncks
+ $value = join "\n ",
+ map MIME::Words::encode_mimeword( $_, $encoding, $charset ),
+ @chunks;
+ return ($value);
+}
sub CreateUser {
my ( $Username, $Address, $Name, $ErrorsTo, $entity ) = @_;
- my $NewUser = RT::User->new($RT::SystemUser);
+
+ my $NewUser = RT::User->new( $RT::SystemUser );
my ( $Val, $Message ) = $NewUser->Create(
Name => ( $Username || $Address ),
@@ -295,7 +898,7 @@ sub CreateUser {
RealName => $Name,
Password => undef,
Privileged => 0,
- Comments => 'Autocreated on ticket submission'
+ Comments => 'Autocreated on ticket submission',
);
unless ($Val) {
@@ -316,14 +919,14 @@ sub CreateUser {
Explanation =>
"User creation failed in mailgateway: $Message",
MIMEObj => $entity,
- LogLevel => 'crit'
+ LogLevel => 'crit',
);
}
}
#Load the new user object
- my $CurrentUser = RT::CurrentUser->new();
- $CurrentUser->LoadByEmail($Address);
+ my $CurrentUser = new RT::CurrentUser;
+ $CurrentUser->LoadByEmail( $Address );
unless ( $CurrentUser->id ) {
$RT::Logger->warning(
@@ -341,15 +944,13 @@ sub CreateUser {
return $CurrentUser;
}
-# }}}
-# {{{ ParseCcAddressesFromHead
-=head2 ParseCcAddressesFromHead HASHREF
+=head2 ParseCcAddressesFromHead HASH
-Takes a hashref object containing QueueObj, Head and CurrentUser objects.
-Returns a list of all email addresses in the To and Cc
-headers b<except> the current Queue\'s email addresses, the CurrentUser\'s
+Takes a hash containing QueueObj, Head and CurrentUser objects.
+Returns a list of all email addresses in the To and Cc
+headers b<except> the current Queue\'s email addresses, the CurrentUser\'s
email address and anything that the configuration sub RT::IsRTAddress matches.
=cut
@@ -362,32 +963,29 @@ sub ParseCcAddressesFromHead {
@_
);
- my (@Addresses);
+ my @recipients =
+ map lc $_->address,
+ map Email::Address->parse( $args{'Head'}->get( $_ ) ),
+ qw(To Cc);
- my @ToObjs = Mail::Address->parse( $args{'Head'}->get('To') );
- my @CcObjs = Mail::Address->parse( $args{'Head'}->get('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 );
- foreach my $AddrObj ( @ToObjs, @CcObjs ) {
- my $Address = $AddrObj->address;
- $Address = $args{'CurrentUser'}
- ->UserObj->CanonicalizeEmailAddress($Address);
- next if ( $args{'CurrentUser'}->EmailAddress =~ /^\Q$Address\E$/i );
- next if ( $args{'QueueObj'}->CorrespondAddress =~ /^\Q$Address\E$/i );
- next if ( $args{'QueueObj'}->CommentAddress =~ /^\Q$Address\E$/i );
- next if ( RT::EmailParser->IsRTAddress($Address) );
-
- push( @Addresses, $Address );
+ push @res, $address;
}
- return (@Addresses);
+ return @res;
}
-# }}}
-# {{{ ParseSenderAdddressFromHead
-=head2 ParseSenderAddressFromHead
+=head2 ParseSenderAddressFromHead HEAD
-Takes a MIME::Header object. Returns a tuple: (user@host, friendly name)
+Takes a MIME::Header object. Returns a tuple: (user@host, friendly name)
of the From (evaluated in order of Reply-To:, From:, Sender)
=cut
@@ -397,19 +995,16 @@ sub ParseSenderAddressFromHead {
#Figure out who's sending this message.
foreach my $header ('Reply-To', 'From', 'Sender') {
- my $From = $head->get($header);
- my ($addr, $name) = ParseAddressFromHeader($From);
+ my $addr_line = $head->get($header) || next;
+ my ($addr, $name) = ParseAddressFromHeader( $addr_line );
# only return if the address is not empty
return ($addr, $name) if $addr;
}
return (undef, undef);
}
-# }}}
-
-# {{{ ParseErrorsToAdddressFromHead
-=head2 ParseErrorsToAddressFromHead
+=head2 ParseErrorsToAddressFromHead HEAD
Takes a MIME::Header object. Return a single value : user@host
of the From (evaluated in order of Return-path:,Errors-To:,Reply-To:,
@@ -435,13 +1030,11 @@ sub ParseErrorsToAddressFromHead {
}
}
-# }}}
-# {{{ ParseAddressFromHeader
=head2 ParseAddressFromHeader ADDRESS
-Takes an address from $head->get('Line') and returns a tuple: user@host, friendly name
+Takes an address from C<$head->get('Line')> and returns a tuple: user@host, friendly name
=cut
@@ -449,15 +1042,15 @@ sub ParseAddressFromHeader {
my $Addr = shift;
# Some broken mailers send: ""Vincent, Jesse"" <jesse@fsck.com>. Hate
- $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g;
- my @Addresses = Mail::Address->parse($Addr);
+ $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g;
+ my @Addresses = RT::EmailParser->ParseEmailAddress($Addr);
my ($AddrObj) = grep ref $_, @Addresses;
unless ( $AddrObj ) {
return ( undef, undef );
}
- my $Name = ( $AddrObj->phrase || $AddrObj->comment || $AddrObj->address );
+ my $Name = ( $AddrObj->name || $AddrObj->phrase || $AddrObj->comment || $AddrObj->address );
#Lets take the from and load a user object.
my $Address = $AddrObj->address;
@@ -465,26 +1058,127 @@ sub ParseAddressFromHeader {
return ( $Address, $Name );
}
-# }}}
+=head2 DeleteRecipientsFromHead HEAD RECIPIENTS
-# {{{ sub ParseTicketId
+Gets a head object and list of addresses.
+Deletes addresses from To, Cc or Bcc fields.
+
+=cut
+
+sub DeleteRecipientsFromHead {
+ my $head = shift;
+ my %skip = map { lc $_ => 1 } @_;
+
+ foreach my $field ( qw(To Cc Bcc) ) {
+ $head->set( $field =>
+ join ', ', map $_->format, grep !$skip{ lc $_->address },
+ Email::Address->parse( $head->get( $field ) )
+ );
+ }
+}
+
+sub GenMessageId {
+ my %args = (
+ Ticket => undef,
+ Scrip => undef,
+ ScripAction => undef,
+ @_
+ );
+ my $org = RT->Config->Get('Organization');
+ my $ticket_id = ( ref $args{'Ticket'}? $args{'Ticket'}->id : $args{'Ticket'} ) || 0;
+ my $scrip_id = ( ref $args{'Scrip'}? $args{'Scrip'}->id : $args{'Scrip'} ) || 0;
+ my $sent = ( ref $args{'ScripAction'}? $args{'ScripAction'}->{'_Message_ID'} : 0 ) || 0;
+
+ return "<rt-". $RT::VERSION ."-". $$ ."-". CORE::time() ."-". int(rand(2000)) .'.'
+ . $ticket_id ."-". $scrip_id ."-". $sent ."@". $org .">" ;
+}
+
+sub SetInReplyTo {
+ my %args = (
+ Message => undef,
+ InReplyTo => undef,
+ Ticket => undef,
+ @_
+ );
+ return unless $args{'Message'} && $args{'InReplyTo'};
+
+ my $get_header = sub {
+ my @res;
+ if ( $args{'InReplyTo'}->isa('MIME::Entity') ) {
+ @res = $args{'InReplyTo'}->head->get( shift );
+ } else {
+ @res = $args{'InReplyTo'}->GetHeader( shift ) || '';
+ }
+ return grep length, map { split /\s+/m, $_ } grep defined, @res;
+ };
+
+ my @id = $get_header->('Message-ID');
+ #XXX: custom header should begin with X- otherwise is violation of the standard
+ my @rtid = $get_header->('RT-Message-ID');
+ my @references = $get_header->('References');
+ unless ( @references ) {
+ @references = $get_header->('In-Reply-To');
+ }
+ push @references, @id, @rtid;
+ if ( $args{'Ticket'} ) {
+ my $pseudo_ref = '<RT-Ticket-'. $args{'Ticket'}->id .'@'. RT->Config->Get('Organization') .'>';
+ push @references, $pseudo_ref unless grep $_ eq $pseudo_ref, @references;
+ }
+ @references = splice @references, 4, -6
+ if @references > 10;
+
+ my $mail = $args{'Message'};
+ $mail->head->set( 'In-Reply-To' => join ' ', @rtid? (@rtid) : (@id) ) if @id || @rtid;
+ $mail->head->set( 'References' => join ' ', @references );
+}
sub ParseTicketId {
my $Subject = shift;
- my $id;
- my $test_name = $RT::EmailSubjectTagRegex || qr/\Q$RT::rtname\E/i;
+ my $rtname = RT->Config->Get('rtname');
+ my $test_name = RT->Config->Get('EmailSubjectTagRegex') || qr/\Q$rtname\E/i;
+ my $id;
if ( $Subject =~ s/\[$test_name\s+\#(\d+)\s*\]//i ) {
- my $id = $1;
- $RT::Logger->debug("Found a ticket ID. It's $id");
- return ($id);
+ $id = $1;
} else {
- return (undef);
+ foreach my $tag ( RT->System->SubjectTag ) {
+ next unless $Subject =~ s/\[\Q$tag\E\s+\#(\d+)\s*\]//i;
+ $id = $1;
+ last;
+ }
}
+ return undef unless $id;
+
+ $RT::Logger->debug("Found a ticket ID. It's $id");
+ return $id;
+}
+
+sub AddSubjectTag {
+ my $subject = shift;
+ my $ticket = shift;
+ unless ( ref $ticket ) {
+ my $tmp = RT::Ticket->new( $RT::SystemUser );
+ $tmp->Load( $ticket );
+ $ticket = $tmp;
+ }
+ my $id = $ticket->id;
+ my $queue_tag = $ticket->QueueObj->SubjectTag;
+
+ my $tag_re = RT->Config->Get('EmailSubjectTagRegex');
+ unless ( $tag_re ) {
+ my $tag = $queue_tag || RT->Config->Get('rtname');
+ $tag_re = qr/\Q$tag\E/;
+ } elsif ( $queue_tag ) {
+ $tag_re = qr/$tag_re|\Q$queue_tag\E/;
+ }
+ return $subject if $subject =~ /\[$tag_re\s+#$id\]/;
+
+ $subject =~ s/(\r\n|\n|\s)/ /gi;
+ chomp $subject;
+ return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject";
}
-# }}}
=head2 Gateway ARGSREF
@@ -506,22 +1200,49 @@ any ticket id found in the subject.
Returns:
An array of:
-
+
(status code, message, optional ticket object)
status code is a numeric value.
for temporary failures, the status code should be -75
- for permanent failures which are handled by RT, the status code
+ for permanent failures which are handled by RT, the status code
should be 0
-
+
for succces, the status code should be 1
=cut
+sub _LoadPlugins {
+ my @mail_plugins = @_;
+
+ my @res;
+ foreach my $plugin (@mail_plugins) {
+ if ( ref($plugin) eq "CODE" ) {
+ push @res, $plugin;
+ } elsif ( !ref $plugin ) {
+ my $Class = $plugin;
+ $Class = "RT::Interface::Email::" . $Class
+ unless $Class =~ /^RT::Interface::Email::/;
+ $Class->require or
+ do { $RT::Logger->error("Couldn't load $Class: $@"); next };
+
+ no strict 'refs';
+ unless ( defined *{ $Class . "::GetCurrentUser" }{CODE} ) {
+ $RT::Logger->crit( "No GetCurrentUser code found in $Class module");
+ next;
+ }
+ push @res, $Class;
+ } else {
+ $RT::Logger->crit( "$plugin - is not class name or code reference");
+ }
+ }
+ return @res;
+}
+
sub Gateway {
my $argsref = shift;
my %args = (
@@ -549,12 +1270,15 @@ sub Gateway {
}
my $parser = RT::EmailParser->new();
- $parser->SmartParseMIMEEntityFromScalar( Message => $args{'message'} );
- my $Message = $parser->Entity();
+ $parser->SmartParseMIMEEntityFromScalar(
+ Message => $args{'message'},
+ Decode => 0,
+ Exact => 1,
+ );
+ my $Message = $parser->Entity();
unless ($Message) {
MailError(
- To => $RT::OwnerEmail,
Subject => "RT Bounce: Unparseable message",
Explanation => "RT couldn't process the message below",
Attach => $args{'message'}
@@ -565,12 +1289,50 @@ sub Gateway {
);
}
- my $head = $Message->head;
+ my @mail_plugins = grep $_, RT->Config->Get('MailPlugins');
+ push @mail_plugins, "Auth::MailFrom" unless @mail_plugins;
+ @mail_plugins = _LoadPlugins( @mail_plugins );
+
+ my %skip_plugin;
+ foreach my $class( grep !ref, @mail_plugins ) {
+ # check if we should apply filter before decoding
+ my $check_cb = do {
+ no strict 'refs';
+ *{ $class . "::ApplyBeforeDecode" }{CODE};
+ };
+ next unless defined $check_cb;
+ next unless $check_cb->(
+ Message => $Message,
+ RawMessageRef => \$args{'message'},
+ );
- my $ErrorsTo = ParseErrorsToAddressFromHead($head);
+ $skip_plugin{ $class }++;
+
+ my $Code = do {
+ no strict 'refs';
+ *{ $class . "::GetCurrentUser" }{CODE};
+ };
+ my ($status, $msg) = $Code->(
+ Message => $Message,
+ RawMessageRef => \$args{'message'},
+ );
+ next if $status > 0;
+
+ if ( $status == -2 ) {
+ return (1, $msg, undef);
+ } elsif ( $status == -1 ) {
+ return (0, $msg, undef);
+ }
+ }
+ @mail_plugins = grep !$skip_plugin{"$_"}, @mail_plugins;
+ $parser->_DecodeBodies;
+ $parser->_PostProcessNewEntity;
+
+ my $head = $Message->head;
+ my $ErrorsTo = ParseErrorsToAddressFromHead( $head );
my $MessageId = $head->get('Message-ID')
- || "<no-message-id-" . time . rand(2000) . "\@.$RT::Organization>";
+ || "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
#Pull apart the subject line
my $Subject = $head->get('Subject') || '';
@@ -591,10 +1353,11 @@ sub Gateway {
if ($IsALoop && !$should_store_machine_generated_message) {
return ( 0, $result, undef );
}
+ # }}}
- $args{'ticket'} ||= ParseTicketId($Subject);
+ $args{'ticket'} ||= ParseTicketId( $Subject );
- $SystemTicket = RT::Ticket->new($RT::SystemUser);
+ $SystemTicket = RT::Ticket->new( $RT::SystemUser );
$SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ;
if ( $SystemTicket->id ) {
$Right = 'ReplyToTicket';
@@ -603,7 +1366,7 @@ sub Gateway {
}
#Set up a queue object
- my $SystemQueueObj = RT::Queue->new($RT::SystemUser);
+ my $SystemQueueObj = RT::Queue->new( $RT::SystemUser );
$SystemQueueObj->Load( $args{'queue'} );
# We can safely have no queue of we have a known-good ticket
@@ -611,67 +1374,15 @@ sub Gateway {
return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef );
}
- # Authentication Level ($AuthStat)
- # -1 - Get out. this user has been explicitly declined
- # 0 - User may not do anything (Not used at the moment)
- # 1 - Normal user
- # 2 - User is allowed to specify status updates etc. a la enhanced-mailgate
- my ( $CurrentUser, $AuthStat, $error );
-
- # Initalize AuthStat so comparisons work correctly
- $AuthStat = -9999999;
-
- push @RT::MailPlugins, "Auth::MailFrom" unless @RT::MailPlugins;
-
- # if plugin returns AuthStat -2 we skip action
- # NOTE: this is experimental API and it would be changed
- my %skip_action = ();
-
- # Since this needs loading, no matter what
- foreach (@RT::MailPlugins) {
- my ($Code, $NewAuthStat);
- if ( ref($_) eq "CODE" ) {
- $Code = $_;
- } else {
- my $Class = $_;
- $Class = "RT::Interface::Email::" . $Class
- unless $Class =~ /^RT::Interface::Email::/;
- $Class->require or
- do { $RT::Logger->error("Couldn't load $Class: $@"); next };
-
- no strict 'refs';
- unless ( defined( $Code = *{ $Class . "::GetCurrentUser" }{CODE} ) ) {
- $RT::Logger->crit( "No 'GetCurrentUser' function found in '$Class' module");
- next;
- }
- }
-
- foreach my $action (@actions) {
- ( $CurrentUser, $NewAuthStat ) = $Code->(
- Message => $Message,
- RawMessageRef => \$args{'message'},
- CurrentUser => $CurrentUser,
- AuthLevel => $AuthStat,
- Action => $action,
- Ticket => $SystemTicket,
- Queue => $SystemQueueObj
- );
-
-# You get the highest level of authentication you were assigned, unless you get the magic -1
-# If a module returns a "-1" then we discard the ticket, so.
- $AuthStat = $NewAuthStat
- if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 );
-
- last if $AuthStat == -1;
- $skip_action{$action}++ if $AuthStat == -2;
- }
-
- # strip actions we should skip
- @actions = grep !$skip_action{$_}, @actions if $AuthStat == -2;
- last unless @actions;
+ my ($AuthStat, $CurrentUser, $error) = GetAuthenticationLevel(
+ MailPlugins => \@mail_plugins,
+ Actions => \@actions,
+ Message => $Message,
+ RawMessageRef => \$args{message},
+ SystemTicket => $SystemTicket,
+ SystemQueue => $SystemQueueObj,
+ );
- last if $AuthStat == -1;
- }
# {{{ If authentication fails and no new user was created, get out.
if ( !$CurrentUser || !$CurrentUser->id || $AuthStat == -1 ) {
@@ -710,7 +1421,7 @@ sub Gateway {
unless ($should_store_machine_generated_message) {
return ( 0, $result, undef );
}
-
+
# if plugin's updated SystemTicket then update arguments
$args{'ticket'} = $SystemTicket->Id if $SystemTicket && $SystemTicket->Id;
@@ -722,7 +1433,7 @@ sub Gateway {
my @Cc;
my @Requestors = ( $CurrentUser->id );
- if ($RT::ParseNewMessageForTicketCcs) {
+ if (RT->Config->Get('ParseNewMessageForTicketCcs')) {
@Cc = ParseCcAddressesFromHead(
Head => $head,
CurrentUser => $CurrentUser,
@@ -772,6 +1483,8 @@ sub Gateway {
}
# }}}
+
+ my $unsafe_actions = RT->Config->Get('UnsafeEmailCommands');
foreach my $action (@actions) {
# If the action is comment, add a comment.
@@ -789,7 +1502,7 @@ sub Gateway {
);
return ( 0, "Message not recorded: $msg", $Ticket );
}
- } elsif ($RT::UnsafeEmailCommands) {
+ } elsif ($unsafe_actions) {
my ( $status, $msg ) = _RunUnsafeAction(
Action => $action,
ErrorsTo => $ErrorsTo,
@@ -803,6 +1516,79 @@ sub Gateway {
return ( 1, "Success", $Ticket );
}
+=head2 GetAuthenticationLevel
+
+ # Authentication Level
+ # -1 - Get out. this user has been explicitly declined
+ # 0 - User may not do anything (Not used at the moment)
+ # 1 - Normal user
+ # 2 - User is allowed to specify status updates etc. a la enhanced-mailgate
+
+=cut
+
+sub GetAuthenticationLevel {
+ my %args = (
+ MailPlugins => [],
+ Actions => [],
+ Message => undef,
+ RawMessageRef => undef,
+ SystemTicket => undef,
+ SystemQueue => undef,
+ @_,
+ );
+
+ my ( $CurrentUser, $AuthStat, $error );
+
+ # Initalize AuthStat so comparisons work correctly
+ $AuthStat = -9999999;
+
+ # if plugin returns AuthStat -2 we skip action
+ # NOTE: this is experimental API and it would be changed
+ my %skip_action = ();
+
+ # Since this needs loading, no matter what
+ foreach (@{ $args{MailPlugins} }) {
+ my ($Code, $NewAuthStat);
+ if ( ref($_) eq "CODE" ) {
+ $Code = $_;
+ } else {
+ no strict 'refs';
+ $Code = *{ $_ . "::GetCurrentUser" }{CODE};
+ }
+
+ foreach my $action (@{ $args{Actions} }) {
+ ( $CurrentUser, $NewAuthStat ) = $Code->(
+ Message => $args{Message},
+ RawMessageRef => $args{RawMessageRef},
+ CurrentUser => $CurrentUser,
+ AuthLevel => $AuthStat,
+ Action => $action,
+ Ticket => $args{SystemTicket},
+ Queue => $args{SystemQueue},
+ );
+
+# You get the highest level of authentication you were assigned, unless you get the magic -1
+# If a module returns a "-1" then we discard the ticket, so.
+ $AuthStat = $NewAuthStat
+ if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 );
+
+ last if $AuthStat == -1;
+ $skip_action{$action}++ if $AuthStat == -2;
+ }
+
+ # strip actions we should skip
+ @{$args{Actions}} = grep !$skip_action{$_}, @{$args{Actions}}
+ if $AuthStat == -2;
+ last unless @{$args{Actions}};
+
+ last if $AuthStat == -1;
+ }
+
+ return $AuthStat if !wantarray;
+
+ return ($AuthStat, $CurrentUser, $error);
+}
+
sub _RunUnsafeAction {
my %args = (
Action => undef,
@@ -860,7 +1646,7 @@ sub _NoAuthorizedUserFound {
# Notify the RT Admin of the failure.
MailError(
- To => $RT::OwnerEmail,
+ To => RT->Config->Get('OwnerEmail'),
Subject => "Could not load a valid user",
Explanation => <<EOT,
RT could not load a valid user, and RT's configuration does not allow
@@ -875,7 +1661,7 @@ EOT
);
# Also notify the requestor that his request has been dropped.
- if ($args{'Requestor'} ne $RT::OwnerEmail) {
+ if ($args{'Requestor'} ne RT->Config->Get('OwnerEmail')) {
MailError(
To => $args{'Requestor'},
Subject => "Could not load a valid user",
@@ -918,11 +1704,13 @@ sub _HandleMachineGeneratedMail {
my $SquelchReplies = 0;
+ my $owner_mail = RT->Config->Get('OwnerEmail');
+
#If the message is autogenerated, we need to know, so we can not
# send mail to the sender
if ( $IsBounce || $IsSuspiciousSender || $IsAutoGenerated || $IsALoop ) {
$SquelchReplies = 1;
- $ErrorsTo = $RT::OwnerEmail;
+ $ErrorsTo = $owner_mail;
}
# Warn someone if it's a loop, before we drop it on the ground
@@ -930,9 +1718,9 @@ sub _HandleMachineGeneratedMail {
$RT::Logger->crit("RT Received mail (".$args{MessageId}.") from itself.");
#Should we mail it to RTOwner?
- if ($RT::LoopsToRTOwner) {
+ if ( RT->Config->Get('LoopsToRTOwner') ) {
MailError(
- To => $RT::OwnerEmail,
+ To => $owner_mail,
Subject => "RT Bounce: ".$args{'Subject'},
Explanation => "RT thinks this message may be a bounce",
MIMEObj => $args{Message}
@@ -940,7 +1728,8 @@ sub _HandleMachineGeneratedMail {
}
#Do we actually want to store it?
- return ( 0, $ErrorsTo, "Message Bounced", $IsALoop ) unless ($RT::StoreLoops);
+ return ( 0, $ErrorsTo, "Message Bounced", $IsALoop )
+ unless RT->Config->Get('StoreLoops');
}
# Squelch replies if necessary
@@ -977,7 +1766,7 @@ sub IsCorrectAction {
my $action = shift;
my @actions = grep $_, split /-/, $action;
return ( 0, '(no value)' ) unless @actions;
- foreach (@actions) {
+ foreach ( @actions ) {
return ( 0, $_ ) unless /^(?:comment|correspond|take|resolve)$/;
}
return ( 1, @actions );
diff --git a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm
index e543c4b..df987d8 100755
--- a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm
+++ b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,70 +45,204 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
-#
+
package RT::Interface::Email::Auth::GnuPG;
-use Mail::GnuPG;
+
+use strict;
+use warnings;
=head2 GetCurrentUser
To use the gnupg-secured mail gateway, you need to do the following:
-Set up a gnupgp key directory with a pubring containing only the keys
+Set up a GnuPG key directory with a pubring containing only the keys
you care about and specify the following in your SiteConfig.pm
-Set($RT::GPGKeyDir, "/path/to/keyring-directory");
-@RT::MailPlugins = qw(Auth::MailFrom Auth::GnuPG Filter::TakeAction);
-
-
+ Set(%GnuPGOptions, homedir => '/opt/rt3/var/data/GnuPG');
+ Set(@MailPlugins, 'Auth::MailFrom', 'Auth::GnuPG', ...other filter...);
=cut
+sub ApplyBeforeDecode { return 1 }
+use RT::Crypt::GnuPG;
+use RT::EmailParser ();
sub GetCurrentUser {
my %args = (
- Message => undef,
- RawMessageRef => undef,
- CurrentUser => undef,
- AuthLevel => undef,
- Ticket => undef,
- Queue => undef,
- Action => undef,
+ Message => undef,
+ RawMessageRef => undef,
@_
);
- my ( $val, $key, $address,$gpg );
+ $args{'Message'}->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'} );
+ if ( $status && !@res ) {
+ $args{'Message'}->head->add(
+ 'X-RT-Incoming-Encryption' => 'Not encrypted'
+ );
+
+ return 1;
+ }
+
+ # FIXME: Check if the message is encrypted to the address of
+ # _this_ queue. send rejecting mail otherwise.
+
+ unless ( $status ) {
+ $RT::Logger->error("Had a problem during decrypting and verifying");
+ my $reject = HandleErrors( Message => $args{'Message'}, Result => \@res );
+ return (0, 'rejected because of problems during decrypting and verifying')
+ if $reject;
+ }
+
+ # attach the original encrypted message
+ $args{'Message'}->attach(
+ Type => 'application/x-rt-original-message',
+ Disposition => 'inline',
+ Data => ${ $args{'RawMessageRef'} },
+ );
+
+ $args{'Message'}->head->add( 'X-RT-Privacy' => 'PGP' );
- eval {
+ foreach my $part ( $args{'Message'}->parts_DFS ) {
+ my $decrypted;
- my $parser = RT::EmailParser->new();
- $parser->SmartParseMIMEEntityFromScalar(Message => ${$args{'RawMessageRef'}}, Decode => 0);
- $gpg = Mail::GnuPG->new( keydir => $RT::GPGKeyDir );
- my $entity = $parser->Entity;
- ( $val, $key, $address ) = $gpg->verify( $parser->Entity);
- $RT::Logger->crit("Got $val - $key - $address");
- };
-
- if ($@) {
- $RT::Logger->crit($@);
+ my $status = $part->head->get( 'X-RT-GnuPG-Status' );
+ if ( $status ) {
+ for ( RT::Crypt::GnuPG::ParseStatus( $status ) ) {
+ if ( $_->{Operation} eq 'Decrypt' && $_->{Status} eq 'DONE' ) {
+ $decrypted = 1;
+ }
+ if ( $_->{Operation} eq 'Verify' && $_->{Status} eq 'DONE' ) {
+ $part->head->add(
+ 'X-RT-Incoming-Signature' => $_->{UserString}
+ );
+ }
+ }
}
- unless ($address) {
- $RT::Logger->crit( "Couldn't find a valid signature" . join ( "\n", @{ $gpg->{'last_message'} } ) );
- return ( $args{'CurrentUser'}, $args{'AuthLevel'} );
+ $part->head->add(
+ 'X-RT-Incoming-Encryption' =>
+ $decrypted ? 'Success' : 'Not encrypted'
+ );
}
- my @addrs = Mail::Address->parse($address);
- $address = $addrs[0]->address();
+ return 1;
+}
+
+sub HandleErrors {
+ my %args = (
+ Message => undef,
+ Result => [],
+ @_
+ );
- my $CurrentUser = RT::CurrentUser->new();
- $CurrentUser->LoadByEmail($address);
+ my $reject = 0;
- if ( $CurrentUser->Id ) {
- $RT::Logger->crit($address . " authenticated via PGP signature");
- return ( $CurrentUser, 2 );
+ my %sent_once = ();
+ foreach my $run ( @{ $args{'Result'} } ) {
+ my @status = RT::Crypt::GnuPG::ParseStatus( $run->{'status'} );
+ unless ( $sent_once{'NoPrivateKey'} ) {
+ unless ( CheckNoPrivateKey( Message => $args{'Message'}, Status => \@status ) ) {
+ $sent_once{'NoPrivateKey'}++;
+ $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnMissingPrivateKey'};
+ }
+ }
+ unless ( $sent_once{'BadData'} ) {
+ unless ( CheckBadData( Message => $args{'Message'}, Status => \@status ) ) {
+ $sent_once{'BadData'}++;
+ $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnBadData'};
+ }
+ }
}
+ return $reject;
+}
+
+sub CheckNoPrivateKey {
+ my %args = (Message => undef, Status => [], @_ );
+ my @status = @{ $args{'Status'} };
+ my @decrypts = grep $_->{'Operation'} eq 'Decrypt', @status;
+ return 1 unless @decrypts;
+ foreach my $action ( @decrypts ) {
+ # if at least one secrete key exist then it's another error
+ return 1 if
+ grep !$_->{'User'}{'SecretKeyMissing'},
+ @{ $action->{'EncryptedTo'} };
+ }
+
+ $RT::Logger->error("Couldn't decrypt a message: have no private key");
+
+ my $address = (RT::Interface::Email::ParseSenderAddressFromHead( $args{'Message'}->head ))[0];
+ my ($status) = RT::Interface::Email::SendEmailUsingTemplate(
+ To => $address,
+ Template => 'Error: no private key',
+ Arguments => {
+ Message => $args{'Message'},
+ TicketObj => $args{'Ticket'},
+ },
+ InReplyTo => $args{'Message'},
+ );
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't send 'Error: no private key'");
+ }
+ return 0;
+}
+
+sub CheckBadData {
+ my %args = (Message => undef, Status => [], @_ );
+ my @bad_data_messages =
+ map $_->{'Message'},
+ grep $_->{'Status'} ne 'DONE' && $_->{'Operation'} eq 'Data',
+ @{ $args{'Status'} };
+ return 1 unless @bad_data_messages;
+
+ $RT::Logger->error("Couldn't process a message: ". join ', ', @bad_data_messages );
+
+ my $address = (RT::Interface::Email::ParseSenderAddressFromHead( $args{'Message'}->head ))[0];
+ my ($status) = RT::Interface::Email::SendEmailUsingTemplate(
+ To => $address,
+ Template => 'Error: bad GnuPG data',
+ Arguments => {
+ Messages => [ @bad_data_messages ],
+ TicketObj => $args{'Ticket'},
+ },
+ InReplyTo => $args{'Message'},
+ );
+ unless ( $status ) {
+ $RT::Logger->error("Couldn't send 'Error: bad GnuPG data'");
+ }
+ return 0;
+}
+
+sub VerifyDecrypt {
+ my %args = (
+ Entity => undef,
+ @_
+ );
+
+ my @res = RT::Crypt::GnuPG::VerifyDecrypt( %args );
+ unless ( @res ) {
+ $RT::Logger->debug("No more encrypted/signed parts");
+ return 1;
+ }
+
+ $RT::Logger->debug('Found GnuPG protected parts');
+
+ # return on any error
+ if ( grep $_->{'exit_code'}, @res ) {
+ $RT::Logger->debug("Error during verify/decrypt operation");
+ return (0, @res);
+ }
+
+ # nesting
+ my ($status, @nested) = VerifyDecrypt( %args );
+ return $status, @res, @nested;
}
eval "require RT::Interface::Email::Auth::GnuPG_Vendor";
@@ -121,3 +255,4 @@ die $@
&& $@ !~ qr{^Can't locate RT/Interface/Email/Auth/GnuPG_Local.pm} );
1;
+
diff --git a/rt/lib/RT/Interface/Email/Auth/MailFrom.pm b/rt/lib/RT/Interface/Email/Auth/MailFrom.pm
index 71cdf60..0673c73 100644
--- a/rt/lib/RT/Interface/Email/Auth/MailFrom.pm
+++ b/rt/lib/RT/Interface/Email/Auth/MailFrom.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,6 +45,7 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
package RT::Interface::Email::Auth::MailFrom;
use RT::Interface::Email qw(ParseSenderAddressFromHead CreateUser);
@@ -62,84 +63,74 @@ sub GetCurrentUser {
# We don't need to do any external lookups
my ( $Address, $Name ) = ParseSenderAddressFromHead( $args{'Message'}->head );
-
- unless ($Address) {
+ unless ( $Address ) {
+ $RT::Logger->error("Couldn't find sender's address");
return ( $args{'CurrentUser'}, -1 );
}
- my $CurrentUser = RT::CurrentUser->new();
- $CurrentUser->LoadByEmail($Address);
-
- unless ( $CurrentUser->Id ) {
- $CurrentUser->LoadByName($Address);
- }
-
+ my $CurrentUser = new RT::CurrentUser;
+ $CurrentUser->LoadByEmail( $Address );
+ $CurrentUser->LoadByName( $Address ) unless $CurrentUser->Id;
if ( $CurrentUser->Id ) {
+ $RT::Logger->debug("Mail from user #". $CurrentUser->Id ." ($Address)" );
return ( $CurrentUser, 1 );
}
-
-
# If the user can't be loaded, we may need to create one. Figure out the acl situation.
- my $unpriv = RT::Group->new($RT::SystemUser);
+ my $unpriv = RT::Group->new( $RT::SystemUser );
$unpriv->LoadSystemInternalGroup('Unprivileged');
unless ( $unpriv->Id ) {
- $RT::Logger->crit( "Auth::MailFrom couldn't find the 'Unprivileged' internal group" );
+ $RT::Logger->crit("Couldn't find the 'Unprivileged' internal group");
return ( $args{'CurrentUser'}, -1 );
}
- my $everyone = RT::Group->new($RT::SystemUser);
+ my $everyone = RT::Group->new( $RT::SystemUser );
$everyone->LoadSystemInternalGroup('Everyone');
unless ( $everyone->Id ) {
- $RT::Logger->crit( "Auth::MailFrom couldn't find the 'Everyone' internal group");
+ $RT::Logger->crit("Couldn't find the 'Everyone' internal group");
return ( $args{'CurrentUser'}, -1 );
}
+ $RT::Logger->debug("Going to create user with address '$Address'" );
+
# but before we do that, we need to make sure that the created user would have the right
# to do what we're doing.
if ( $args{'Ticket'} && $args{'Ticket'}->Id ) {
+ my $qname = $args{'Queue'}->Name;
# We have a ticket. that means we're commenting or corresponding
if ( $args{'Action'} =~ /^comment$/i ) {
# check to see whether "Everyone" or "Unprivileged users" can comment on tickets
- unless ( $everyone->PrincipalObj->HasRight(
- Object => $args{'Queue'},
- Right => 'CommentOnTicket'
- )
- || $unpriv->PrincipalObj->HasRight(
- Object => $args{'Queue'},
- Right => 'CommentOnTicket'
- )
- ) {
+ unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'},
+ Right => 'CommentOnTicket' )
+ || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'},
+ Right => 'CommentOnTicket' ) )
+ {
+ $RT::Logger->debug("Unprivileged users have no right to comment on ticket in queue '$qname'");
return ( $args{'CurrentUser'}, 0 );
}
}
elsif ( $args{'Action'} =~ /^correspond$/i ) {
# check to see whether "Everybody" or "Unprivileged users" can correspond on tickets
- unless ( $everyone->PrincipalObj->HasRight(Object => $args{'Queue'},
- Right => 'ReplyToTicket'
- )
- || $unpriv->PrincipalObj->HasRight(
- Object => $args{'Queue'},
- Right => 'ReplyToTicket'
- )
- ) {
+ unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'},
+ Right => 'ReplyToTicket' )
+ || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'},
+ Right => 'ReplyToTicket' ) )
+ {
+ $RT::Logger->debug("Unprivileged users have no right to reply to ticket in queue '$qname'");
return ( $args{'CurrentUser'}, 0 );
}
-
}
elsif ( $args{'Action'} =~ /^take$/i ) {
# check to see whether "Everybody" or "Unprivileged users" can correspond on tickets
- unless ( $everyone->PrincipalObj->HasRight(Object => $args{'Queue'},
- Right => 'OwnTicket'
- )
- || $unpriv->PrincipalObj->HasRight(
- Object => $args{'Queue'},
- Right => 'OwnTicket'
- )
- ) {
+ unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'},
+ Right => 'OwnTicket' )
+ || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'},
+ Right => 'OwnTicket' ) )
+ {
+ $RT::Logger->debug("Unprivileged users have no right to own ticket in queue '$qname'");
return ( $args{'CurrentUser'}, 0 );
}
@@ -147,33 +138,35 @@ sub GetCurrentUser {
elsif ( $args{'Action'} =~ /^resolve$/i ) {
# check to see whether "Everybody" or "Unprivileged users" can correspond on tickets
- unless ( $everyone->PrincipalObj->HasRight(Object => $args{'Queue'},
- Right => 'ModifyTicket'
- )
- || $unpriv->PrincipalObj->HasRight(
- Object => $args{'Queue'},
- Right => 'ModifyTicket'
- )
- ) {
+ unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'},
+ Right => 'ModifyTicket' )
+ || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'},
+ Right => 'ModifyTicket' ) )
+ {
+ $RT::Logger->debug("Unprivileged users have no right to resolve ticket in queue '$qname'");
return ( $args{'CurrentUser'}, 0 );
}
}
else {
+ $RT::Logger->warning("Action '". ($args{'Action'}||'') ."' is unknown");
return ( $args{'CurrentUser'}, 0 );
}
}
# We're creating a ticket
elsif ( $args{'Queue'} && $args{'Queue'}->Id ) {
+ my $qname = $args{'Queue'}->Name;
# check to see whether "Everybody" or "Unprivileged users" can create tickets in this queue
unless ( $everyone->PrincipalObj->HasRight( Object => $args{'Queue'},
Right => 'CreateTicket' )
- ) {
+ || $unpriv->PrincipalObj->HasRight( Object => $args{'Queue'},
+ Right => 'CreateTicket' ) )
+ {
+ $RT::Logger->debug("Unprivileged users have no right to create ticket in queue '$qname'");
return ( $args{'CurrentUser'}, 0 );
}
-
}
$CurrentUser = CreateUser( undef, $Address, $Name, $Address, $args{'Message'} );
diff --git a/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm b/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm
index 176b394..49e89c5 100644
--- a/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm
+++ b/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,6 +45,7 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
package RT::Interface::Email::Filter::SpamAssassin;
use Mail::SpamAssassin;
@@ -77,7 +78,8 @@ RT::Interface::Email::Filter::SpamAssassin - Spam filter for RT
=head1 SYNOPSIS
- @RT::MailPlugins = ("Filter::SpamAssassin", ...);
+ # in RT config
+ Set(@MailPlugins, 'Filter::SpamAssassin', ...other filters...);
=head1 DESCRIPTION
diff --git a/rt/lib/RT/Interface/REST.pm b/rt/lib/RT/Interface/REST.pm
index 9fef6f8..e7689f4 100644
--- a/rt/lib/RT/Interface/REST.pm
+++ b/rt/lib/RT/Interface/REST.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,6 +45,7 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
# lib/RT/Interface/REST.pm
#
@@ -53,16 +54,39 @@ use strict;
use RT;
BEGIN {
- use Exporter ();
- use vars qw($VERSION @ISA @EXPORT);
+ use base 'Exporter';
+ use vars qw($VERSION @EXPORT);
- $VERSION = do { my @r = (q$Revision: 1.1.1.7 $ =~ /\d+/g); sprintf "%d."."%02d"x$#r, @r };
+ $VERSION = do { my @r = (q$Revision: 1.1.1.8 $ =~ /\d+/g); sprintf "%d."."%02d"x$#r, @r };
- @ISA = qw(Exporter);
@EXPORT = qw(expand_list form_parse form_compose vpush vsplit);
}
-my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-(?:[a-z0-9_ -]|\s)+)';
+sub custom_field_spec {
+ my $self = shift;
+ my $capture = shift;
+
+ my $CF_char = '[\sa-z0-9_ :()/-]';
+ my $CF_name = $CF_char . '+';
+ $CF_name = '(' . $CF_name . ')' if $capture;
+
+ my $new_style = 'CF\.\{'.$CF_name.'\}';
+ my $old_style = 'C(?:ustom)?F(?:ield)?-'.$CF_name;
+
+ return '(?i:' . join('|', $new_style, $old_style) . ')';
+}
+
+sub field_spec {
+ my $self = shift;
+ my $capture = shift;
+
+ my $field = '[a-z][a-z0-9_-]*';
+ $field = '(' . $field . ')' if $capture;
+
+ my $custom_field = __PACKAGE__->custom_field_spec($capture);
+
+ return '(?i:' . join('|', $field, $custom_field) . ')';
+}
# WARN: this code is duplicated in bin/rt.in,
# change both functions at once
@@ -95,6 +119,7 @@ sub form_parse {
my @forms = ();
my @lines = split /\n/, $_[0];
my ($c, $o, $k, $e) = ("", [], {}, "");
+ my $field = __PACKAGE__->field_spec;
LINE:
while (@lines) {
@@ -148,7 +173,7 @@ sub form_parse {
$state = 1;
}
- elsif ($line !~ /^#/) {
+ elsif ($line =~ /^#/) {
# We've found a syntax error, so we'll reconstruct the
# form parsed thus far, and add an error marker. (>>)
$state = -1;
@@ -201,7 +226,8 @@ sub form_compose {
$sp = " "x4 if length($sp) > 16;
foreach $v (@values) {
- if ($v =~ /\n/) {
+ $v = '' unless defined $v;
+ if ( $v =~ /\n/) {
$v =~ s/^/$sp/gm;
$v =~ s/^$sp//;
@@ -269,9 +295,9 @@ sub vsplit {
my ($val) = @_;
my ($line, $word, @words);
- foreach $line (map {split /\n/} (ref $val eq 'ARRAY') ? @$val : $val)
+ foreach $line (map {split /\n/} (ref $val eq 'ARRAY') ? @$val : ($val||''))
{
- # XXX: This should become a real parser, à la Text::ParseWords.
+ # XXX: This should become a real parser, ? la Text::ParseWords.
$line =~ s/^\s+//;
$line =~ s/\s+$//;
push @words, split /\s*,\s*/, $line;
@@ -280,6 +306,16 @@ sub vsplit {
return \@words;
}
+eval "require RT::Interface::REST_Vendor";
+if ($@ && $@ !~ qr{^Can't locate RT/Interface/REST_Vendor.pm}) {
+ die $@;
+};
+
+eval "require RT::Interface::REST_Local";
+if ($@ && $@ !~ qr{^Can't locate RT/Interface/REST_Local.pm}) {
+ die $@;
+};
+
1;
=head1 NAME
diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm
index 4e5fca1..b4279fb 100644
--- a/rt/lib/RT/Interface/Web.pm
+++ b/rt/lib/RT/Interface/Web.pm
@@ -1,40 +1,40 @@
# BEGIN BPS TAGGED BLOCK {{{
-#
+#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
-#
+#
# (Except where explicitly superseded by other copyright notices)
-#
-#
+#
+#
# LICENSE:
-#
+#
# 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.
-#
+#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
-#
-#
+#
+#
# CONTRIBUTION SUBMISSION POLICY:
-#
+#
# (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.)
-#
+#
# 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
@@ -43,34 +43,31 @@
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
-#
+#
# END BPS TAGGED BLOCK }}}
+
## Portions Copyright 2000 Tobias Brox <tobix@fsck.com>
## This is a library of static subs to be used by the Mason web
## interface to RT
-
=head1 NAME
RT::Interface::Web
-=begin testing
-
-use_ok(RT::Interface::Web);
-
-=end testing
=cut
-
use strict;
use warnings;
package RT::Interface::Web;
-use HTTP::Date;
+
use RT::SavedSearches;
-use URI;
+use URI qw();
+use RT::Interface::Web::Session;
+use Digest::MD5 ();
+use Encode qw();
# {{{ EscapeUTF8
@@ -80,22 +77,17 @@ does a css-busting but minimalist escaping of whatever html you're passing in.
=cut
-sub EscapeUTF8 {
- my $ref = shift;
- return unless defined $$ref;
- my $val = $$ref;
- use bytes;
- $val =~ s/&/&#38;/g;
- $val =~ s/</&lt;/g;
- $val =~ s/>/&gt;/g;
- $val =~ s/\(/&#40;/g;
- $val =~ s/\)/&#41;/g;
- $val =~ s/"/&#34;/g;
- $val =~ s/'/&#39;/g;
- $$ref = $val;
- Encode::_utf8_on($$ref);
-
-
+sub EscapeUTF8 {
+ my $ref = shift;
+ return unless defined $$ref;
+
+ $$ref =~ s/&/&#38;/g;
+ $$ref =~ s/</&lt;/g;
+ $$ref =~ s/>/&gt;/g;
+ $$ref =~ s/\(/&#40;/g;
+ $$ref =~ s/\)/&#41;/g;
+ $$ref =~ s/"/&#34;/g;
+ $$ref =~ s/'/&#39;/g;
}
# }}}
@@ -108,12 +100,12 @@ Escapes URI component according to RFC2396
=cut
-use Encode qw();
sub EscapeURI {
my $ref = shift;
- $$ref = Encode::encode_utf8( $$ref );
+ return unless defined $$ref;
+
+ use bytes;
$$ref =~ s/([^a-zA-Z0-9_.!~*'()-])/uc sprintf("%%%02X", ord($1))/eg;
- Encode::_utf8_on( $$ref );
}
# }}}
@@ -129,13 +121,7 @@ just downcase $ENV{'REMOTE_USER'}
=cut
sub WebCanonicalizeInfo {
- my $user;
-
- if ( defined $ENV{'REMOTE_USER'} ) {
- $user = lc ( $ENV{'REMOTE_USER'} ) if( length($ENV{'REMOTE_USER'}) );
- }
-
- return $user;
+ return $ENV{'REMOTE_USER'} ? lc $ENV{'REMOTE_USER'} : $ENV{'REMOTE_USER'};
}
# }}}
@@ -155,19 +141,21 @@ sub WebExternalAutoInfo {
# default to making Privileged users, even if they specify
# some other default Attributes
- if (!$RT::AutoCreate ||
- ( ref($RT::AutoCreate) && not exists $RT::AutoCreate->{Privileged})) {
+ if ( !$RT::AutoCreate
+ || ( ref($RT::AutoCreate) && not exists $RT::AutoCreate->{Privileged} ) )
+ {
$user_info{'Privileged'} = 1;
}
- if ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
+ if ( $^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/ ) {
+
# Populate fields with information from Unix /etc/passwd
- my ($comments, $realname) = (getpwnam($user))[5, 6];
+ my ( $comments, $realname ) = ( getpwnam($user) )[ 5, 6 ];
$user_info{'Comments'} = $comments if defined $comments;
$user_info{'RealName'} = $realname if defined $realname;
- }
- elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
+ } elsif ( $^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1' ) {
+
# Populate fields with information from NT domain controller
}
@@ -177,7 +165,323 @@ sub WebExternalAutoInfo {
# }}}
+sub HandleRequest {
+ my $ARGS = shift;
+
+ $HTML::Mason::Commands::r->content_type("text/html; charset=utf-8");
+
+ $HTML::Mason::Commands::m->{'rt_base_time'} = [ Time::HiRes::gettimeofday() ];
+
+ # Roll back any dangling transactions from a previous failed connection
+ $RT::Handle->ForceRollback() if $RT::Handle->TransactionDepth;
+
+ MaybeEnableSQLStatementLog();
+
+ # avoid reentrancy, as suggested by masonbook
+ local *HTML::Mason::Commands::session unless $HTML::Mason::Commands::m->is_subrequest;
+
+ $HTML::Mason::Commands::m->autoflush( $HTML::Mason::Commands::m->request_comp->attr('AutoFlush') )
+ if ( $HTML::Mason::Commands::m->request_comp->attr_exists('AutoFlush') );
+
+ DecodeARGS($ARGS);
+ PreprocessTimeUpdates($ARGS);
+
+ MaybeShowInstallModePage();
+
+ $HTML::Mason::Commands::m->comp( '/Elements/SetupSessionCookie', %$ARGS );
+ SendSessionCookie();
+ $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new() unless _UserLoggedIn();
+
+ MaybeShowNoAuthPage($ARGS);
+
+ AttemptExternalAuth($ARGS) if RT->Config->Get('WebExternalAuthContinuous') or not _UserLoggedIn();
+
+ _ForceLogout() unless _UserLoggedIn();
+
+ # Process per-page authentication callbacks
+ $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Auth', CallbackPage => '/autohandler' );
+
+ unless ( _UserLoggedIn() ) {
+ _ForceLogout();
+
+ # If the user is logging in, let's authenticate
+ if ( defined $ARGS->{user} && defined $ARGS->{pass} ) {
+ AttemptPasswordAuthentication($ARGS);
+ } else {
+ # if no credentials then show him login page
+ $HTML::Mason::Commands::m->comp( '/Elements/Login', %$ARGS );
+ $HTML::Mason::Commands::m->abort;
+ }
+ }
+
+ # now it applies not only to home page, but any dashboard that can be used as a workspace
+ $HTML::Mason::Commands::session{'home_refresh_interval'} = $ARGS->{'HomeRefreshInterval'}
+ if ( $ARGS->{'HomeRefreshInterval'} );
+
+ # Process per-page global callbacks
+ $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Default', CallbackPage => '/autohandler' );
+
+ ShowRequestedPage($ARGS);
+ LogRecordedSQLStatements();
+}
+
+sub _ForceLogout {
+
+ delete $HTML::Mason::Commands::session{'CurrentUser'};
+}
+
+sub _UserLoggedIn {
+ if ( $HTML::Mason::Commands::session{CurrentUser} && $HTML::Mason::Commands::session{'CurrentUser'}->id ) {
+ return 1;
+ } else {
+ return undef;
+ }
+
+}
+
+=head2 MaybeShowInstallModePage
+
+This function, called exclusively by RT's autohandler, dispatches
+a request to RT's Installation workflow, only if Install Mode is enabled in the configuration file.
+
+If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler
+
+=cut
+
+sub MaybeShowInstallModePage {
+ return unless RT->InstallMode;
+
+ my $m = $HTML::Mason::Commands::m;
+ if ( $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex') ) {
+ $m->call_next();
+ } elsif ( $m->request_comp->path !~ '^(/+)Install/' ) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "Install/index.html" );
+ } else {
+ $m->call_next();
+ }
+ $m->abort();
+}
+
+=head2 MaybeShowNoAuthPage \%ARGS
+
+This function, called exclusively by RT's autohandler, dispatches
+a request to the page a user requested (but only if it matches the "noauth" regex.
+
+If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler
+
+=cut
+sub MaybeShowNoAuthPage {
+ my $ARGS = shift;
+
+ my $m = $HTML::Mason::Commands::m;
+
+ return unless $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex');
+
+ # If it's a noauth file, don't ask for auth.
+ SendSessionCookie();
+ $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
+ $m->abort;
+}
+
+=head2 ShowRequestedPage \%ARGS
+
+This function, called exclusively by RT's autohandler, dispatches
+a request to the page a user requested (making sure that unpriviled users
+can only see self-service pages.
+
+=cut
+
+sub ShowRequestedPage {
+ my $ARGS = shift;
+
+ my $m = $HTML::Mason::Commands::m;
+
+ SendSessionCookie();
+
+ # If the user isn't privileged, they can only see SelfService
+ unless ( $HTML::Mason::Commands::session{'CurrentUser'}->Privileged ) {
+
+ # if the user is trying to access a ticket, redirect them
+ if ( $m->request_comp->path =~ '^(/+)Ticket/Display.html' && $ARGS->{'id'} ) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/Display.html?id=" . $ARGS->{'id'} );
+ }
+
+ # otherwise, drop the user at the SelfService default page
+ elsif ( $m->base_comp->path !~ RT->Config->Get('SelfServiceRegex') ) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/" );
+ }
+
+ # if user is in SelfService dir let him do anything
+ else {
+ $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
+ }
+ } else {
+ $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
+ }
+
+}
+
+sub AttemptExternalAuth {
+ my $ARGS = shift;
+
+ return unless ( RT->Config->Get('WebExternalAuth') );
+
+ my $user = $ARGS->{user};
+ my $m = $HTML::Mason::Commands::m;
+
+ # If RT is configured for external auth, let's go through and get REMOTE_USER
+
+ # do we actually have a REMOTE_USER equivlent?
+ if ( RT::Interface::Web::WebCanonicalizeInfo() ) {
+ my $orig_user = $user;
+
+ $user = RT::Interface::Web::WebCanonicalizeInfo();
+ my $load_method = RT->Config->Get('WebExternalGecos') ? 'LoadByGecos' : 'Load';
+
+ if ( $^O eq 'MSWin32' and RT->Config->Get('WebExternalGecos') ) {
+ my $NodeName = Win32::NodeName();
+ $user =~ s/^\Q$NodeName\E\\//i;
+ }
+
+ InstantiateNewSession() unless _UserLoggedIn;
+ $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
+ $HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user);
+
+ if ( RT->Config->Get('WebExternalAuto') and not _UserLoggedIn() ) {
+
+ # Create users on-the-fly
+ my $UserObj = RT::User->new($RT::SystemUser);
+ my ( $val, $msg ) = $UserObj->Create(
+ %{ ref RT->Config->Get('AutoCreate') ? RT->Config->Get('AutoCreate') : {} },
+ Name => $user,
+ Gecos => $user,
+ );
+
+ if ($val) {
+
+ # now get user specific information, to better create our user.
+ my $new_user_info = RT::Interface::Web::WebExternalAutoInfo($user);
+
+ # set the attributes that have been defined.
+ foreach my $attribute ( $UserObj->WritableAttributes ) {
+ $m->callback(
+ Attribute => $attribute,
+ User => $user,
+ UserInfo => $new_user_info,
+ CallbackName => 'NewUser',
+ CallbackPage => '/autohandler'
+ );
+ my $method = "Set$attribute";
+ $UserObj->$method( $new_user_info->{$attribute} ) if defined $new_user_info->{$attribute};
+ }
+ $HTML::Mason::Commands::session{'CurrentUser'}->Load($user);
+ } else {
+
+ # we failed to successfully create the user. abort abort abort.
+ delete $HTML::Mason::Commands::session{'CurrentUser'};
+ $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc( 'Cannot create user: [_1]', $msg ) )
+ if RT->Config->Get('WebFallbackToInternalAuth');;
+ $m->abort();
+ }
+ }
+
+ if ( _UserLoggedIn() ) {
+ $m->callback( %$ARGS, CallbackName => 'ExternalAuthSuccessfulLogin', CallbackPage => '/autohandler' );
+ } else {
+ delete $HTML::Mason::Commands::session{'CurrentUser'};
+ $user = $orig_user;
+
+ if ( RT->Config->Get('WebExternalOnly') ) {
+ $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') );
+ $m->abort();
+ }
+ }
+ } elsif ( RT->Config->Get('WebFallbackToInternalAuth') ) {
+ unless ( defined $HTML::Mason::Commands::session{'CurrentUser'} ) {
+ # XXX unreachable due to prior defaulting in HandleRequest (check c34d108)
+ $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') );
+ $m->abort();
+ }
+ } else {
+
+ # WebExternalAuth is set, but we don't have a REMOTE_USER. abort
+ # XXX: we must return AUTH_REQUIRED status or we fallback to
+ # internal auth here too.
+ delete $HTML::Mason::Commands::session{'CurrentUser'}
+ if defined $HTML::Mason::Commands::session{'CurrentUser'};
+ }
+}
+
+sub AttemptPasswordAuthentication {
+ my $ARGS = shift;
+ my $user_obj = RT::CurrentUser->new();
+ $user_obj->Load( $ARGS->{user} );
+
+ my $m = $HTML::Mason::Commands::m;
+
+ unless ( $user_obj->id && $user_obj->IsPassword( $ARGS->{pass} ) ) {
+ $RT::Logger->error("FAILED LOGIN for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
+ $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('Your username or password is incorrect'), );
+ $m->callback( %$ARGS, CallbackName => 'FailedLogin', CallbackPage => '/autohandler' );
+ $m->abort;
+ }
+
+ $RT::Logger->info("Successful login for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
+ InstantiateNewSession();
+ $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
+ $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
+}
+
+=head2 LoadSessionFromCookie
+
+Load or setup a session cookie for the current user.
+
+=cut
+
+sub _SessionCookieName {
+ my $cookiename = "RT_SID_" . RT->Config->Get('rtname');
+ $cookiename .= "." . $ENV{'SERVER_PORT'} if $ENV{'SERVER_PORT'};
+ return $cookiename;
+}
+
+sub LoadSessionFromCookie {
+
+ my %cookies = CGI::Cookie->fetch;
+ my $cookiename = _SessionCookieName();
+ my $SessionCookie = ( $cookies{$cookiename} ? $cookies{$cookiename}->value : undef );
+ tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', $SessionCookie;
+ unless ( $SessionCookie && $HTML::Mason::Commands::session{'_session_id'} eq $SessionCookie ) {
+ undef $cookies{$cookiename};
+ }
+ if ( int RT->Config->Get('AutoLogoff') ) {
+ my $now = int( time / 60 );
+ my $last_update = $HTML::Mason::Commands::session{'_session_last_update'} || 0;
+
+ if ( $last_update && ( $now - $last_update - RT->Config->Get('AutoLogoff') ) > 0 ) {
+ InstantiateNewSession();
+ }
+
+ # save session on each request when AutoLogoff is turned on
+ $HTML::Mason::Commands::session{'_session_last_update'} = $now if $now != $last_update;
+ }
+}
+
+sub InstantiateNewSession {
+ tied(%HTML::Mason::Commands::session)->delete if tied(%HTML::Mason::Commands::session);
+ tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', undef;
+}
+
+sub SendSessionCookie {
+ my $cookie = CGI::Cookie->new(
+ -name => _SessionCookieName(),
+ -value => $HTML::Mason::Commands::session{_session_id},
+ -path => RT->Config->Get('WebPath'),
+ -secure => ( RT->Config->Get('WebSecureCookies') ? 1 : 0 )
+ );
+
+ $HTML::Mason::Commands::r->err_headers_out->{'Set-Cookie'} = $cookie->as_string;
+}
=head2 Redirect URL
@@ -188,27 +492,42 @@ a cached DBI statement handle twice at the same time.
=cut
-
sub Redirect {
my $redir_to = shift;
untie $HTML::Mason::Commands::session;
- my $uri = URI->new($redir_to);
- my $server_uri = URI->new($RT::WebURL);
+ my $uri = URI->new($redir_to);
+ my $server_uri = URI->new( RT->Config->Get('WebURL') );
# If the user is coming in via a non-canonical
# hostname, don't redirect them to the canonical host,
# it will just upset them (and invalidate their credentials)
- if ($uri->host eq $server_uri->host &&
- $uri->port eq $server_uri->port) {
- $uri->host($ENV{'HTTP_HOST'});
- $uri->port($ENV{'SERVER_PORT'});
+ # don't do this if $RT::CanoniaclRedirectURLs is true
+ if ( !RT->Config->Get('CanonicalizeRedirectURLs')
+ && $uri->host eq $server_uri->host
+ && $uri->port eq $server_uri->port )
+ {
+ if ( defined $ENV{HTTPS} and $ENV{'HTTPS'} eq 'on' ) {
+ $uri->scheme('https');
+ } else {
+ $uri->scheme('http');
}
- $HTML::Mason::Commands::m->redirect($uri->canonical);
+ # [rt3.fsck.com #12716] Apache recommends use of $SERVER_HOST
+ $uri->host( $ENV{'SERVER_HOST'} || $ENV{'HTTP_HOST'} );
+ $uri->port( $ENV{'SERVER_PORT'} );
+ }
+
+ # not sure why, but on some systems without this call mason doesn't
+ # set status to 302, but 200 instead and people see blank pages
+ $HTML::Mason::Commands::r->status(302);
+
+ # Perlbal expects a status message, but Mason's default redirect status
+ # doesn't provide one. See also rt.cpan.org #36689.
+ $HTML::Mason::Commands::m->redirect( $uri->canonical, "302 Found" );
+
$HTML::Mason::Commands::m->abort;
}
-
=head2 StaticFileHeaders
Send the browser a few headers to try to get it to (somewhat agressively)
@@ -219,23 +538,196 @@ This routine could really use _accurate_ heuristics. (XXX TODO)
=cut
sub StaticFileHeaders {
+ my $date = RT::Date->new($RT::SystemUser);
+
# make cache public
$HTML::Mason::Commands::r->headers_out->{'Cache-Control'} = 'max-age=259200, public';
# Expire things in a month.
- $HTML::Mason::Commands::r->headers_out->{'Expires'} = HTTP::Date::time2str( time() + 2592000 );
+ $date->Set( Value => time + 30 * 24 * 60 * 60 );
+ $HTML::Mason::Commands::r->headers_out->{'Expires'} = $date->RFC2616;
# if we set 'Last-Modified' then browser request a comp using 'If-Modified-Since'
# request, but we don't handle it and generate full reply again
# Last modified at server start time
- #$HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = HTTP::Date::time2str($^T);
+ # $date->Set( Value => $^T );
+ # $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = $date->RFC2616;
+}
+
+=head2 SendStaticFile
+
+Takes a File => path and a Type => Content-type
+
+If Type isn't provided and File is an image, it will
+figure out a sane Content-type, otherwise it will
+send application/octet-stream
+
+Will set caching headers using StaticFileHeaders
+
+=cut
+
+sub SendStaticFile {
+ my $self = shift;
+ my %args = @_;
+ my $file = $args{File};
+ my $type = $args{Type};
+
+ $self->StaticFileHeaders();
+
+ unless ($type) {
+ if ( $file =~ /\.(gif|png|jpe?g)$/i ) {
+ $type = "image/$1";
+ $type =~ s/jpg/jpeg/gi;
+ }
+ $type ||= "application/octet-stream";
+ }
+ $HTML::Mason::Commands::r->content_type($type);
+ open my $fh, "<$file" or die "couldn't open file: $!";
+ binmode($fh);
+ {
+ local $/ = \16384;
+ $HTML::Mason::Commands::m->out($_) while (<$fh>);
+ $HTML::Mason::Commands::m->flush_buffer;
+ }
+ close $fh;
+}
+
+sub StripContent {
+ my %args = @_;
+ my $content = $args{Content};
+ return '' unless $content;
+
+ # Make the content have no 'weird' newlines in it
+ $content =~ s/\r+\n/\n/g;
+
+ my $return_content = $content;
+
+ my $html = $args{ContentType} && $args{ContentType} eq "text/html";
+ my $sigonly = $args{StripSignature};
+
+ # massage content to easily detect if there's any real content
+ $content =~ s/\s+//g; # yes! remove all the spaces
+ if ( $html ) {
+ # remove html version of spaces and newlines
+ $content =~ s!&nbsp;!!g;
+ $content =~ s!<br/?>!!g;
+ }
+ # Filter empty content when type is text/html
+ return '' if $html && $content !~ /\S/;
+
+ # If we aren't supposed to strip the sig, just bail now.
+ return $return_content unless $sigonly;
+
+ # Find the signature
+ my $sig = $args{'CurrentUser'}->UserObj->Signature || '';
+ $sig =~ s/\s+//g;
+
+ # Check for plaintext sig
+ return '' if not $html and $content =~ /^(--)?\Q$sig\E$/;
+
+ # Check for html-formatted sig
+ RT::Interface::Web::EscapeUTF8( \$sig );
+ return ''
+ if $html
+ and $content =~ m{^(?:<p>)?(--)?\Q$sig\E(?:</p>)?$}s;
+
+ # Pass it through
+ return $return_content;
+}
+
+sub DecodeARGS {
+ my $ARGS = shift;
+
+ %{$ARGS} = map {
+
+ # if they've passed multiple values, they'll be an array. if they've
+ # passed just one, a scalar whatever they are, mark them as utf8
+ my $type = ref($_);
+ ( !$type )
+ ? Encode::is_utf8($_)
+ ? $_
+ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ )
+ : ( $type eq 'ARRAY' )
+ ? [ map { ( ref($_) or Encode::is_utf8($_) ) ? $_ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) }
+ @$_ ]
+ : ( $type eq 'HASH' )
+ ? { map { ( ref($_) or Encode::is_utf8($_) ) ? $_ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) }
+ %$_ }
+ : $_
+ } %$ARGS;
}
+sub PreprocessTimeUpdates {
+ my $ARGS = shift;
+
+ # Later in the code we use
+ # $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %ARGS );
+ # instead of $m->call_next to avoid problems with UTF8 keys in arguments.
+ # The call_next method pass through original arguments and if you have
+ # an argument with unicode key then in a next component you'll get two
+ # records in the args hash: one with key without UTF8 flag and another
+ # with the flag, which may result into errors. "{ base_comp => $m->request_comp }"
+ # is copied from mason's source to get the same results as we get from
+ # call_next method, this feature is not documented, so we just leave it
+ # here to avoid possible side effects.
+
+ # This code canonicalizes time inputs in hours into minutes
+ foreach my $field ( keys %$ARGS ) {
+ next unless $field =~ /^(.*)-TimeUnits$/i && $ARGS->{$1};
+ my $local = $1;
+ $ARGS->{$local} =~ s{\b (?: (\d+) \s+ )? (\d+)/(\d+) \b}
+ {($1 || 0) + $3 ? $2 / $3 : 0}xe;
+ if ( $ARGS->{$field} && $ARGS->{$field} =~ /hours/i ) {
+ $ARGS->{$local} *= 60;
+ }
+ delete $ARGS->{$field};
+ }
+
+}
+
+sub MaybeEnableSQLStatementLog {
+
+ my $log_sql_statements = RT->Config->Get('StatementLog');
+
+ if ($log_sql_statements) {
+ $RT::Handle->ClearSQLStatementLog;
+ $RT::Handle->LogSQLStatements(1);
+ }
+
+}
+
+sub LogRecordedSQLStatements {
+ my $log_sql_statements = RT->Config->Get('StatementLog');
+
+ return unless ($log_sql_statements);
+
+ my @log = $RT::Handle->SQLStatementLog;
+ $RT::Handle->ClearSQLStatementLog;
+ for my $stmt (@log) {
+ my ( $time, $sql, $bind, $duration ) = @{$stmt};
+ my @bind;
+ if ( ref $bind ) {
+ @bind = @{$bind};
+ } else {
+
+ # Older DBIx-SB
+ $duration = $bind;
+ }
+ $RT::Logger->log(
+ level => $log_sql_statements,
+ message => "SQL("
+ . sprintf( "%.6f", $duration )
+ . "s): $sql;"
+ . ( @bind ? " [ bound values: @{[map{qq|'$_'|} @bind]} ]" : "" )
+ );
+ }
+
+}
package HTML::Mason::Commands;
-use vars qw/$r $m %session/;
+use vars qw/$r $m %session/;
# {{{ loc
@@ -250,14 +742,19 @@ through
sub loc {
- if ($session{'CurrentUser'} &&
- UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
- return($session{'CurrentUser'}->loc(@_));
- }
- elsif ( my $u = eval { RT::CurrentUser->new($RT::SystemUser->Id) } ) {
- return ($u->loc(@_));
- }
- else {
+ if ( $session{'CurrentUser'}
+ && UNIVERSAL::can( $session{'CurrentUser'}, 'loc' ) )
+ {
+ return ( $session{'CurrentUser'}->loc(@_) );
+ } elsif (
+ my $u = eval {
+ RT::CurrentUser->new();
+ }
+ )
+ {
+ return ( $u->loc(@_) );
+ } else {
+
# pathetic case -- SystemUser is gone.
return $_[0];
}
@@ -265,7 +762,6 @@ sub loc {
# }}}
-
# {{{ loc_fuzzy
=head2 loc_fuzzy STRING
@@ -279,40 +775,41 @@ inside the lexicon file.
=cut
sub loc_fuzzy {
- my $msg = shift;
-
- if ($session{'CurrentUser'} &&
- UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
- return($session{'CurrentUser'}->loc_fuzzy($msg));
- }
- else {
- my $u = RT::CurrentUser->new($RT::SystemUser->Id);
- return ($u->loc_fuzzy($msg));
+ my $msg = shift;
+
+ if ( $session{'CurrentUser'}
+ && UNIVERSAL::can( $session{'CurrentUser'}, 'loc' ) )
+ {
+ return ( $session{'CurrentUser'}->loc_fuzzy($msg) );
+ } else {
+ my $u = RT::CurrentUser->new( $RT::SystemUser->Id );
+ return ( $u->loc_fuzzy($msg) );
}
}
# }}}
-
# {{{ sub Abort
# Error - calls Error and aborts
sub Abort {
+ my $why = shift;
+ my %args = @_;
- if ($session{'ErrorDocument'} &&
- $session{'ErrorDocumentType'}) {
- $r->content_type($session{'ErrorDocumentType'});
- $m->comp($session{'ErrorDocument'} , Why => shift);
+ if ( $session{'ErrorDocument'}
+ && $session{'ErrorDocumentType'} )
+ {
+ $r->content_type( $session{'ErrorDocumentType'} );
+ $m->comp( $session{'ErrorDocument'}, Why => $why, %args );
$m->abort;
- }
- else {
- $m->comp("/Elements/Error" , Why => shift);
+ } else {
+ $m->comp( "/Elements/Error", Why => $why, %args );
$m->abort;
}
}
# }}}
-# {{{ sub CreateTicket
+# {{{ sub CreateTicket
=head2 CreateTicket ARGS
@@ -336,17 +833,30 @@ sub CreateTicket {
Abort('You have no permission to create tickets in that queue.');
}
- my $due = new RT::Date( $session{'CurrentUser'} );
- $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
- my $starts = new RT::Date( $session{'CurrentUser'} );
- $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
+ my $due;
+ if ( defined $ARGS{'Due'} and $ARGS{'Due'} =~ /\S/ ) {
+ $due = new RT::Date( $session{'CurrentUser'} );
+ $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
+ }
+ my $starts;
+ if ( defined $ARGS{'Starts'} and $ARGS{'Starts'} =~ /\S/ ) {
+ $starts = new RT::Date( $session{'CurrentUser'} );
+ $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
+ }
+
+ my $sigless = RT::Interface::Web::StripContent(
+ Content => $ARGS{Content},
+ ContentType => $ARGS{ContentType},
+ StripSignature => 1,
+ CurrentUser => $session{'CurrentUser'},
+ );
my $MIMEObj = MakeMIMEEntity(
- Subject => $ARGS{'Subject'},
- From => $ARGS{'From'},
- Cc => $ARGS{'Cc'},
- Body => $ARGS{'Content'},
- Type => $ARGS{'ContentType'},
+ Subject => $ARGS{'Subject'},
+ From => $ARGS{'From'},
+ Cc => $ARGS{'Cc'},
+ Body => $sigless,
+ Type => $ARGS{'ContentType'},
);
if ( $ARGS{'Attachments'} ) {
@@ -354,8 +864,8 @@ sub CreateTicket {
$RT::Logger->error("Couldn't make multipart message")
if !$rv || $rv !~ /^(?:DONE|ALREADY)$/;
- foreach ( values %{$ARGS{'Attachments'}} ) {
- unless ( $_ ) {
+ foreach ( values %{ $ARGS{'Attachments'} } ) {
+ unless ($_) {
$RT::Logger->error("Couldn't add empty attachemnt");
next;
}
@@ -363,121 +873,120 @@ sub CreateTicket {
}
}
+ foreach my $argument (qw(Encrypt Sign)) {
+ $MIMEObj->head->add( "X-RT-$argument" => $ARGS{$argument} ) if defined $ARGS{$argument};
+ }
+
my %create_args = (
- Type => $ARGS{'Type'} || 'ticket',
- Queue => $ARGS{'Queue'},
- Owner => $ARGS{'Owner'},
+ Type => $ARGS{'Type'} || 'ticket',
+ Queue => $ARGS{'Queue'},
+ Owner => $ARGS{'Owner'},
+
+ # note: name change
+ Requestor => $ARGS{'Requestors'},
+ Cc => $ARGS{'Cc'},
+ AdminCc => $ARGS{'AdminCc'},
InitialPriority => $ARGS{'InitialPriority'},
FinalPriority => $ARGS{'FinalPriority'},
TimeLeft => $ARGS{'TimeLeft'},
- TimeEstimated => $ARGS{'TimeEstimated'},
+ TimeEstimated => $ARGS{'TimeEstimated'},
TimeWorked => $ARGS{'TimeWorked'},
Subject => $ARGS{'Subject'},
Status => $ARGS{'Status'},
- Due => $due->ISO,
- Starts => $starts->ISO,
+ Due => $due ? $due->ISO : undef,
+ Starts => $starts ? $starts->ISO : undef,
MIMEObj => $MIMEObj
);
my @temp_squelch;
- foreach my $type (qw(Requestors Cc AdminCc)) {
- my @tmp = map { $_->format } grep { $_->address} Mail::Address->parse( $ARGS{ $type } );
+ foreach my $type (qw(Requestor Cc AdminCc)) {
+ push @temp_squelch, map $_->address, Email::Address->parse( $create_args{$type} )
+ if grep $_ eq $type || $_ eq ( $type . 's' ), @{ $ARGS{'SkipNotification'} || [] };
+
+ }
- $create_args{ $type } = [
- grep $_, map {
- my $user = RT::User->new( $RT::SystemUser );
- $user->LoadOrCreateByEmail( $_ );
- # convert to ids to avoid work later
- $user->id;
- } @tmp
- ];
- $RT::Logger->debug(
- "$type got ".join(',',@{$create_args{ $type }}) );
+ if (@temp_squelch) {
+ require RT::Action::SendEmail;
+ RT::Action::SendEmail->SquelchMailTo( RT::Action::SendEmail->SquelchMailTo, @temp_squelch );
+ }
+ if ( $ARGS{'AttachTickets'} ) {
+ require RT::Action::SendEmail;
+ RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets,
+ ref $ARGS{'AttachTickets'}
+ ? @{ $ARGS{'AttachTickets'} }
+ : ( $ARGS{'AttachTickets'} ) );
}
- # XXX: workaround for name conflict :(
- $create_args{'Requestor'} = delete $create_args{'Requestors'};
- foreach my $arg (keys %ARGS) {
+ foreach my $arg ( keys %ARGS ) {
next if $arg =~ /-(?:Magic|Category)$/;
- if ($arg =~ /^Object-RT::Transaction--CustomField-/) {
+ if ( $arg =~ /^Object-RT::Transaction--CustomField-/ ) {
$create_args{$arg} = $ARGS{$arg};
}
+
# Object-RT::Ticket--CustomField-3-Values
- elsif ($arg =~ /^Object-RT::Ticket--CustomField-(\d+)(.*?)$/) {
+ elsif ( $arg =~ /^Object-RT::Ticket--CustomField-(\d+)/ ) {
my $cfid = $1;
- my $cf = RT::CustomField->new( $session{'CurrentUser'});
- $cf->Load($cfid);
- if ( $cf->Type eq 'Freeform' && ! $cf->SingleValue) {
- $ARGS{$arg} =~ s/\r\n/\n/g;
- $ARGS{$arg} = [split('\n', $ARGS{$arg})];
- }
-
- if ( $cf->Type =~ /text/i) { # Catch both Text and Wikitext
- $ARGS{$arg} =~ s/\r//g;
+ my $cf = RT::CustomField->new( $session{'CurrentUser'} );
+ $cf->Load($cfid);
+ unless ( $cf->id ) {
+ $RT::Logger->error( "Couldn't load custom field #" . $cfid );
+ next;
}
if ( $arg =~ /-Upload$/ ) {
- $create_args{"CustomField-".$cfid} = _UploadedFile($arg);
- }
- else {
- $create_args{"CustomField-".$cfid} = $ARGS{"$arg"};
+ $create_args{"CustomField-$cfid"} = _UploadedFile($arg);
+ next;
}
- }
- }
+ my $type = $cf->Type;
- # XXX TODO This code should be about six lines. and badly needs refactoring.
-
- # {{{ turn new link lists into arrays, and pass in the proper arguments
- my (@dependson, @dependedonby, @parents, @children, @refersto, @referredtoby);
-
- foreach my $luri ( split ( / /, $ARGS{"new-DependsOn"} ) ) {
- $luri =~ s/\s*$//; # Strip trailing whitespace
- push @dependson, $luri;
- }
- $create_args{'DependsOn'} = \@dependson;
-
- foreach my $luri ( split ( / /, $ARGS{"DependsOn-new"} ) ) {
- push @dependedonby, $luri;
- }
- $create_args{'DependedOnBy'} = \@dependedonby;
+ my @values = ();
+ if ( ref $ARGS{$arg} eq 'ARRAY' ) {
+ @values = @{ $ARGS{$arg} };
+ } elsif ( $type =~ /text/i ) {
+ @values = ( $ARGS{$arg} );
+ } else {
+ no warnings 'uninitialized';
+ @values = split /\r*\n/, $ARGS{$arg};
+ }
+ @values = grep length, map {
+ s/\r+\n/\n/g;
+ s/^\s+//;
+ s/\s+$//;
+ $_;
+ }
+ grep defined, @values;
- foreach my $luri ( split ( / /, $ARGS{"new-MemberOf"} ) ) {
- $luri =~ s/\s*$//; # Strip trailing whitespace
- push @parents, $luri;
+ $create_args{"CustomField-$cfid"} = \@values;
+ }
}
- $create_args{'Parents'} = \@parents;
- foreach my $luri ( split ( / /, $ARGS{"MemberOf-new"} ) ) {
- push @children, $luri;
- }
- $create_args{'Children'} = \@children;
+ # turn new link lists into arrays, and pass in the proper arguments
+ my %map = (
+ 'new-DependsOn' => 'DependsOn',
+ 'DependsOn-new' => 'DependedOnBy',
+ 'new-MemberOf' => 'Parents',
+ 'MemberOf-new' => 'Children',
+ 'new-RefersTo' => 'RefersTo',
+ 'RefersTo-new' => 'ReferredToBy',
+ );
+ foreach my $key ( keys %map ) {
+ next unless $ARGS{$key};
+ $create_args{ $map{$key} } = [ grep $_, split ' ', $ARGS{$key} ];
- foreach my $luri ( split ( / /, $ARGS{"new-RefersTo"} ) ) {
- $luri =~ s/\s*$//; # Strip trailing whitespace
- push @refersto, $luri;
}
- $create_args{'RefersTo'} = \@refersto;
- foreach my $luri ( split ( / /, $ARGS{"RefersTo-new"} ) ) {
- push @referredtoby, $luri;
- }
- $create_args{'ReferredToBy'} = \@referredtoby;
- # }}}
-
-
my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
- unless ( $id ) {
+ unless ($id) {
Abort($ErrMsg);
}
- push ( @Actions, split("\n", $ErrMsg) );
+ push( @Actions, split( "\n", $ErrMsg ) );
unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
- Abort( "No permission to view newly created ticket #"
- . $Ticket->id . "." );
+ Abort( "No permission to view newly created ticket #" . $Ticket->id . "." );
}
return ( $Ticket, @Actions );
@@ -519,99 +1028,130 @@ sub LoadTicket {
# {{{ sub ProcessUpdateMessage
+=head2 ProcessUpdateMessage
+
+Takes paramhash with fields ARGSRef, TicketObj and SkipSignatureOnly.
+
+Don't write message if it only contains current user's signature and
+SkipSignatureOnly argument is true. Function anyway adds attachments
+and updates time worked field even if skips message. The default value
+is true.
+
+=cut
+
sub ProcessUpdateMessage {
- #TODO document what else this takes.
my %args = (
- ARGSRef => undef,
- Actions => undef,
- TicketObj => undef,
+ ARGSRef => undef,
+ TicketObj => undef,
+ SkipSignatureOnly => 1,
@_
);
- #Make the update content have no 'weird' newlines in it
- if ( $args{ARGSRef}->{'UpdateTimeWorked'}
- || $args{ARGSRef}->{'UpdateContent'}
- || $args{ARGSRef}->{'UpdateAttachments'} )
+ if ( $args{ARGSRef}->{'UpdateAttachments'}
+ && !keys %{ $args{ARGSRef}->{'UpdateAttachments'} } )
{
+ delete $args{ARGSRef}->{'UpdateAttachments'};
+ }
- if (
- $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() )
- {
- $args{ARGSRef}->{'UpdateSubject'} = undef;
+ # Strip the signature
+ $args{ARGSRef}->{UpdateContent} = RT::Interface::Web::StripContent(
+ Content => $args{ARGSRef}->{UpdateContent},
+ ContentType => $args{ARGSRef}->{UpdateContentType},
+ StripSignature => $args{SkipSignatureOnly},
+ CurrentUser => $args{'TicketObj'}->CurrentUser,
+ );
+
+ # If, after stripping the signature, we have no message, move the
+ # UpdateTimeWorked into adjusted TimeWorked, so that a later
+ # ProcessBasics can deal -- then bail out.
+ if ( not $args{ARGSRef}->{'UpdateAttachments'}
+ and not length $args{ARGSRef}->{'UpdateContent'} )
+ {
+ if ( $args{ARGSRef}->{'UpdateTimeWorked'} ) {
+ $args{ARGSRef}->{TimeWorked} = $args{TicketObj}->TimeWorked + delete $args{ARGSRef}->{'UpdateTimeWorked'};
}
+ return;
+ }
- my $Message = MakeMIMEEntity(
- Subject => $args{ARGSRef}->{'UpdateSubject'},
- Body => $args{ARGSRef}->{'UpdateContent'},
- Type => $args{ARGSRef}->{'UpdateContentType'},
- );
+ if ( $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject ) {
+ $args{ARGSRef}->{'UpdateSubject'} = undef;
+ }
- $Message->head->add( 'Message-ID' =>
- "<rt-"
- . $RT::VERSION . "-"
- . $$ . "-"
- . CORE::time() . "-"
- . int(rand(2000)) . "."
- . $args{'TicketObj'}->id . "-"
- . "0" . "-" # Scrip
- . "0" . "@" # Email sent
- . $RT::Organization
- . ">" );
- my $old_txn = RT::Transaction->new( $session{'CurrentUser'} );
- if ( $args{ARGSRef}->{'QuoteTransaction'} ) {
- $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} );
- }
- else {
- $old_txn = $args{TicketObj}->Transactions->First();
- }
+ my $Message = MakeMIMEEntity(
+ Subject => $args{ARGSRef}->{'UpdateSubject'},
+ Body => $args{ARGSRef}->{'UpdateContent'},
+ Type => $args{ARGSRef}->{'UpdateContentType'},
+ );
- if ( $old_txn->Message && $old_txn->Message->First ) {
- my @in_reply_to = split(/\s+/m, $old_txn->Message->First->GetHeader('In-Reply-To') || '');
- my @references = split(/\s+/m, $old_txn->Message->First->GetHeader('References') || '' );
- my @msgid = split(/\s+/m,$old_txn->Message->First->GetHeader('Message-ID') || '');
- my @rtmsgid = split(/\s+/m,$old_txn->Message->First->GetHeader('RT-Message-ID') || '');
+ $Message->head->add( 'Message-ID' => 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'} );
+ } else {
+ $old_txn = $args{TicketObj}->Transactions->First();
+ }
- $Message->head->replace( 'In-Reply-To', join (' ', @rtmsgid ? @rtmsgid : @msgid));
- $Message->head->replace( 'References', join(' ', @references, @msgid, @rtmsgid));
- }
+ if ( my $msg = $old_txn->Message->First ) {
+ RT::Interface::Email::SetInReplyTo(
+ Message => $Message,
+ InReplyTo => $msg
+ );
+ }
if ( $args{ARGSRef}->{'UpdateAttachments'} ) {
$Message->make_multipart;
- $Message->add_part($_)
- foreach values %{ $args{ARGSRef}->{'UpdateAttachments'} };
+ $Message->add_part($_) foreach values %{ $args{ARGSRef}->{'UpdateAttachments'} };
+ }
+
+ if ( $args{ARGSRef}->{'AttachTickets'} ) {
+ require RT::Action::SendEmail;
+ RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets,
+ ref $args{ARGSRef}->{'AttachTickets'}
+ ? @{ $args{ARGSRef}->{'AttachTickets'} }
+ : ( $args{ARGSRef}->{'AttachTickets'} ) );
+ }
+
+ my $bcc = $args{ARGSRef}->{'UpdateBcc'};
+ my $cc = $args{ARGSRef}->{'UpdateCc'};
+
+ my %message_args = (
+ CcMessageTo => $cc,
+ BccMessageTo => $bcc,
+ Sign => $args{ARGSRef}->{'Sign'},
+ Encrypt => $args{ARGSRef}->{'Encrypt'},
+ MIMEObj => $Message,
+ TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
+ );
+
+ unless ( $args{'ARGSRef'}->{'UpdateIgnoreAddressCheckboxes'} ) {
+ foreach my $key ( keys %{ $args{ARGSRef} } ) {
+ next unless $key =~ /^Update(Cc|Bcc)-(.*)$/;
+
+ my $var = ucfirst($1) . 'MessageTo';
+ my $value = $2;
+ if ( $message_args{$var} ) {
+ $message_args{$var} .= ", $value";
+ } else {
+ $message_args{$var} = $value;
+ }
+ }
}
- ## TODO: Implement public comments
+ my @results;
if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
- my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(
- CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
- BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
- MIMEObj => $Message,
- TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
- );
- push( @{ $args{Actions} }, $Description );
+ my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(%message_args);
+ push( @results, $Description );
$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
- }
- elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
- my ( $Transaction, $Description, $Object ) =
- $args{TicketObj}->Correspond(
- CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
- BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
- MIMEObj => $Message,
- TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
- );
- push( @{ $args{Actions} }, $Description );
+ } elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
+ my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Correspond(%message_args);
+ push( @results, $Description );
$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
+ } else {
+ push( @results,
+ loc("Update type was neither correspondence nor comment.") . " " . loc("Update not recorded.") );
}
- else {
- push(
- @{ $args{'Actions'} },
- loc("Update type was neither correspondence nor comment.") . " "
- . loc("Update not recorded.")
- );
- }
-}
+ return @results;
}
# }}}
@@ -638,301 +1178,65 @@ sub MakeMIMEEntity {
Body => undef,
AttachmentFieldName => undef,
Type => undef,
-# map Encode::encode_utf8($_), @_,
@_,
);
+ my $Message = MIME::Entity->build(
+ Type => 'multipart/mixed',
+ Subject => $args{'Subject'} || "",
+ From => $args{'From'},
+ Cc => $args{'Cc'},
+ );
- #Make the update content have no 'weird' newlines in it
+ if ( defined $args{'Body'} && length $args{'Body'} ) {
+
+ # Make the update content have no 'weird' newlines in it
+ $args{'Body'} =~ s/\r\n/\n/gs;
- $args{'Body'} =~ s/\r\n/\n/gs if $args{'Body'};
- my $Message;
- {
# 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 = MIME::Entity->build(
- Subject => $args{'Subject'} || "",
- From => $args{'From'},
- Cc => $args{'Cc'},
- Type => $args{'Type'} || 'text/plain',
- 'Charset:' => 'utf8',
- Data => [ $args{'Body'} ]
+ $Message->attach(
+ Type => $args{'Type'} || 'text/plain',
+ Charset => 'UTF-8',
+ Data => $args{'Body'},
);
}
- my $cgi_object = $m->cgi_object;
+ if ( $args{'AttachmentFieldName'} ) {
- if (my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) {
+ my $cgi_object = $m->cgi_object;
+ if ( my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) {
-
- use File::Temp qw(tempfile tempdir);
-
- #foreach my $filehandle (@filenames) {
-
- my ( $fh, $temp_file );
- for ( 1 .. 10 ) {
- # on NFS and NTFS, it is possible that tempfile() conflicts
- # with other processes, causing a race condition. we try to
- # accommodate this by pausing and retrying.
- last if ($fh, $temp_file) = eval { tempfile( UNLINK => 1) };
- sleep 1;
- }
-
- binmode $fh; #thank you, windows
- my ($buffer);
- while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
- print $fh $buffer;
- }
-
- my $uploadinfo = $cgi_object->uploadInfo($filehandle);
-
- # Prefer the cached name first over CGI.pm stringification.
- my $filename = $RT::Mason::CGI::Filename;
- $filename = "$filehandle" unless defined($filename);
-
- $filename =~ s#^.*[\\/]##;
-
- $Message->attach(
- Path => $temp_file,
- Filename => Encode::decode_utf8($filename),
- Type => $uploadinfo->{'Content-Type'},
- );
- close($fh);
-
- # }
-
- }
-
- $Message->make_singlepart();
- RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
-
- return ($Message);
-
-}
-
-# }}}
-
-# {{{ sub ProcessSearchQuery
-
-=head2 ProcessSearchQuery
-
- Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand.
-
-TODO Doc exactly what comes in the paramhash
-
-
-=cut
-
-sub ProcessSearchQuery {
- my %args = @_;
-
- ## TODO: The only parameter here is %ARGS. Maybe it would be
- ## cleaner to load this parameter as $ARGS, and use $ARGS->{...}
- ## instead of $args{ARGS}->{...} ? :)
-
- #Searches are sticky.
- if ( defined $session{'tickets'} ) {
-
- # Reset the old search
- $session{'tickets'}->GotoFirstItem;
- }
- else {
-
- # Init a new search
- $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} );
- }
-
- #Import a bookmarked search if we have one
- if ( defined $args{ARGS}->{'Bookmark'} ) {
- $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} );
- }
-
- # {{{ Goto next/prev page
- if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) {
- $session{'tickets'}->NextPage;
- }
- elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) {
- $session{'tickets'}->PrevPage;
- }
- elsif ( $args{ARGS}->{'GotoPage'} > 0 ) {
- $session{'tickets'}->GotoPage( $args{ARGS}->{GotoPage} - 1 );
- }
-
- # }}}
-
- # {{{ Deal with limiting the search
-
- if ( $args{ARGS}->{'RefreshSearchInterval'} ) {
- $session{'tickets_refresh_interval'} =
- $args{ARGS}->{'RefreshSearchInterval'};
- }
-
- if ( $args{ARGS}->{'TicketsSortBy'} ) {
- $session{'tickets_sort_by'} = $args{ARGS}->{'TicketsSortBy'};
- $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'};
- $session{'tickets'}->OrderBy(
- FIELD => $args{ARGS}->{'TicketsSortBy'},
- ORDER => $args{ARGS}->{'TicketsSortOrder'}
- );
- }
-
- # }}}
-
- # {{{ Set the query limit
- if ( defined $args{ARGS}->{'RowsPerPage'} ) {
- $RT::Logger->debug(
- "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" );
-
- $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'};
- $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} );
- }
-
- # }}}
- # {{{ Limit priority
- if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) {
- $session{'tickets'}->LimitPriority(
- VALUE => $args{ARGS}->{'ValueOfPriority'},
- OPERATOR => $args{ARGS}->{'PriorityOp'}
- );
- }
-
- # }}}
- # {{{ Limit owner
- if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) {
- $session{'tickets'}->LimitOwner(
- VALUE => $args{ARGS}->{'ValueOfOwner'},
- OPERATOR => $args{ARGS}->{'OwnerOp'}
- );
- }
-
- # }}}
- # {{{ Limit requestor email
- if ( $args{ARGS}->{'ValueOfWatcherRole'} ne '' ) {
- $session{'tickets'}->LimitWatcher(
- TYPE => $args{ARGS}->{'WatcherRole'},
- VALUE => $args{ARGS}->{'ValueOfWatcherRole'},
- OPERATOR => $args{ARGS}->{'WatcherRoleOp'},
-
- );
- }
-
- # }}}
- # {{{ Limit Queue
- if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) {
- $session{'tickets'}->LimitQueue(
- VALUE => $args{ARGS}->{'ValueOfQueue'},
- OPERATOR => $args{ARGS}->{'QueueOp'}
- );
- }
-
- # }}}
- # {{{ Limit Status
- if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) {
- if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) {
- foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) {
- $session{'tickets'}->LimitStatus(
- VALUE => $value,
- OPERATOR => $args{ARGS}->{'StatusOp'},
- );
+ my ( @content, $buffer );
+ while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
+ push @content, $buffer;
}
- }
- else {
- $session{'tickets'}->LimitStatus(
- VALUE => $args{ARGS}->{'ValueOfStatus'},
- OPERATOR => $args{ARGS}->{'StatusOp'},
- );
- }
- }
+ my $uploadinfo = $cgi_object->uploadInfo($filehandle);
- # }}}
- # {{{ Limit Subject
- if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) {
- my $val = $args{ARGS}->{'ValueOfSubject'};
- if ($args{ARGS}->{'SubjectOp'} =~ /like/) {
- $val = "%".$val."%";
- }
- $session{'tickets'}->LimitSubject(
- VALUE => $val,
- OPERATOR => $args{ARGS}->{'SubjectOp'},
- );
- }
+ # 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 =~ s{^.*[\\/]}{};
- # }}}
- # {{{ Limit Dates
- if ( $args{ARGS}->{'ValueOfDate'} ne '' ) {
- my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} );
- $args{ARGS}->{'DateType'} =~ s/_Date$//;
-
- if ( $args{ARGS}->{'DateType'} eq 'Updated' ) {
- $session{'tickets'}->LimitTransactionDate(
- VALUE => $date,
- OPERATOR => $args{ARGS}->{'DateOp'},
+ $Message->attach(
+ Type => $uploadinfo->{'Content-Type'},
+ Filename => $filename,
+ Data => \@content,
);
- }
- else {
- $session{'tickets'}->LimitDate( FIELD => $args{ARGS}->{'DateType'},
- VALUE => $date,
- OPERATOR => $args{ARGS}->{'DateOp'},
- );
- }
- }
-
- # }}}
- # {{{ Limit Content
- if ( $args{ARGS}->{'ValueOfAttachmentField'} ne '' ) {
- my $val = $args{ARGS}->{'ValueOfAttachmentField'};
- if ($args{ARGS}->{'AttachmentFieldOp'} =~ /like/) {
- $val = "%".$val."%";
- }
- $session{'tickets'}->Limit(
- FIELD => $args{ARGS}->{'AttachmentField'},
- VALUE => $val,
- OPERATOR => $args{ARGS}->{'AttachmentFieldOp'},
- );
- }
-
- # }}}
-
- # {{{ Limit CustomFields
-
- foreach my $arg ( keys %{ $args{ARGS} } ) {
- my $id;
- if ( $arg =~ /^CustomField(\d+)$/ ) {
- $id = $1;
- }
- else {
- next;
- }
- next unless ( $args{ARGS}->{$arg} );
-
- my $form = $args{ARGS}->{$arg};
- my $oper = $args{ARGS}->{ "CustomFieldOp" . $id };
- foreach my $value ( ref($form) ? @{$form} : ($form) ) {
- my $quote = 1;
- if ($oper =~ /like/i) {
- $value = "%".$value."%";
- }
- if ( $value =~ /^null$/i ) {
-
- #Don't quote the string 'null'
- $quote = 0;
-
- # Convert the operator to something apropriate for nulls
- $oper = 'IS' if ( $oper eq '=' );
- $oper = 'IS NOT' if ( $oper eq '!=' );
+ if ( !$args{'Subject'} && !( defined $args{'Body'} && length $args{'Body'} ) ) {
+ $Message->head->set( 'Subject' => $filename );
}
- $session{'tickets'}->LimitCustomField( CUSTOMFIELD => $id,
- OPERATOR => $oper,
- QUOTEVALUE => $quote,
- VALUE => $value );
}
}
- # }}}
+ $Message->make_singlepart;
+ RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
+ return ($Message);
}
@@ -950,7 +1254,7 @@ Returns an ISO date and time in GMT
sub ParseDateToISO {
my $date = shift;
- my $date_obj = RT::Date->new($session{'CurrentUser'});
+ my $date_obj = RT::Date->new( $session{'CurrentUser'} );
$date_obj->Set(
Format => 'unknown',
Value => $date
@@ -965,72 +1269,51 @@ sub ParseDateToISO {
sub ProcessACLChanges {
my $ARGSref = shift;
- my %ARGS = %$ARGSref;
-
- my ( $ACL, @results );
-
+ #XXX: why don't we get ARGSref like in other Process* subs?
- foreach my $arg (keys %ARGS) {
- if ($arg =~ /GrantRight-(\d+)-(.*?)-(\d+)$/) {
- my $principal_id = $1;
- my $object_type = $2;
- my $object_id = $3;
- my $rights = $ARGS{$arg};
+ my @results;
- my $principal = RT::Principal->new($session{'CurrentUser'});
- $principal->Load($principal_id);
+ foreach my $arg ( keys %$ARGSref ) {
+ next unless ( $arg =~ /^(GrantRight|RevokeRight)-(\d+)-(.+?)-(\d+)$/ );
- my $obj;
+ my ( $method, $principal_id, $object_type, $object_id ) = ( $1, $2, $3, $4 );
- if ($object_type eq 'RT::System') {
- $obj = $RT::System;
- } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
- $obj = $object_type->new($session{'CurrentUser'});
- $obj->Load($object_id);
- } else {
- push (@results, loc("System Error"). ': '.
- loc("Rights could not be granted for [_1]", $object_type));
- next;
- }
-
- my @rights = ref($ARGS{$arg}) eq 'ARRAY' ? @{$ARGS{$arg}} : ($ARGS{$arg});
- foreach my $right (@rights) {
- next unless ($right);
- my ($val, $msg) = $principal->GrantRight(Object => $obj, Right => $right);
- push (@results, $msg);
- }
+ my @rights;
+ if ( UNIVERSAL::isa( $ARGSref->{$arg}, 'ARRAY' ) ) {
+ @rights = @{ $ARGSref->{$arg} };
+ } else {
+ @rights = $ARGSref->{$arg};
}
- elsif ($arg =~ /RevokeRight-(\d+)-(.*?)-(\d+)-(.*?)$/) {
- my $principal_id = $1;
- my $object_type = $2;
- my $object_id = $3;
- my $right = $4;
-
- my $principal = RT::Principal->new($session{'CurrentUser'});
- $principal->Load($principal_id);
- next unless ($right);
- my $obj;
-
- if ($object_type eq 'RT::System') {
- $obj = $RT::System;
- } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
- $obj = $object_type->new($session{'CurrentUser'});
- $obj->Load($object_id);
- } else {
- push (@results, loc("System Error"). ': '.
- loc("Rights could not be revoked for [_1]", $object_type));
+ @rights = grep $_, @rights;
+ next unless @rights;
+
+ my $principal = RT::Principal->new( $session{'CurrentUser'} );
+ $principal->Load($principal_id);
+
+ my $obj;
+ if ( $object_type eq 'RT::System' ) {
+ $obj = $RT::System;
+ } elsif ( $RT::ACE::OBJECT_TYPES{$object_type} ) {
+ $obj = $object_type->new( $session{'CurrentUser'} );
+ $obj->Load($object_id);
+ unless ( $obj->id ) {
+ $RT::Logger->error("couldn't load $object_type #$object_id");
next;
}
- my ($val, $msg) = $principal->RevokeRight(Object => $obj, Right => $right);
- push (@results, $msg);
+ } else {
+ $RT::Logger->error("object type '$object_type' is incorrect");
+ push( @results, loc("System Error") . ': ' . loc( "Rights could not be granted for [_1]", $object_type ) );
+ next;
}
-
+ foreach my $right (@rights) {
+ my ( $val, $msg ) = $principal->$method( Object => $obj, Right => $right );
+ push( @results, $msg );
+ }
}
return (@results);
-
- }
+}
# }}}
@@ -1046,18 +1329,19 @@ Returns an array of success/failure messages
sub UpdateRecordObject {
my %args = (
- ARGSRef => undef,
- AttributesRef => undef,
- Object => undef,
+ ARGSRef => undef,
+ AttributesRef => undef,
+ Object => undef,
AttributePrefix => undef,
@_
);
- my $Object = $args{'Object'};
- my @results = $Object->Update(AttributesRef => $args{'AttributesRef'},
- ARGSRef => $args{'ARGSRef'},
- AttributePrefix => $args{'AttributePrefix'}
- );
+ my $Object = $args{'Object'};
+ my @results = $Object->Update(
+ AttributesRef => $args{'AttributesRef'},
+ ARGSRef => $args{'ARGSRef'},
+ AttributePrefix => $args{'AttributePrefix'},
+ );
return (@results);
}
@@ -1076,44 +1360,40 @@ sub ProcessCustomFieldUpdates {
my $Object = $args{'CustomFieldObj'};
my $ARGSRef = $args{'ARGSRef'};
- my @attribs = qw( Name Type Description Queue SortOrder);
+ my @attribs = qw(Name Type Description Queue SortOrder);
my @results = UpdateRecordObject(
AttributesRef => \@attribs,
Object => $Object,
ARGSRef => $ARGSRef
);
- if ( $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" } ) {
-
+ my $prefix = "CustomField-" . $Object->Id;
+ if ( $ARGSRef->{"$prefix-AddValue-Name"} ) {
my ( $addval, $addmsg ) = $Object->AddValue(
- Name =>
- $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" },
- Description => $ARGSRef->{ "CustomField-"
- . $Object->Id
- . "-AddValue-Description" },
- SortOrder => $ARGSRef->{ "CustomField-"
- . $Object->Id
- . "-AddValue-SortOrder" },
+ Name => $ARGSRef->{"$prefix-AddValue-Name"},
+ Description => $ARGSRef->{"$prefix-AddValue-Description"},
+ SortOrder => $ARGSRef->{"$prefix-AddValue-SortOrder"},
);
- push ( @results, $addmsg );
+ push( @results, $addmsg );
}
- my @delete_values = (
- ref $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } eq
- 'ARRAY' )
- ? @{ $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } }
- : ( $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } );
+
+ my @delete_values
+ = ( ref $ARGSRef->{"$prefix-DeleteValue"} eq 'ARRAY' )
+ ? @{ $ARGSRef->{"$prefix-DeleteValue"} }
+ : ( $ARGSRef->{"$prefix-DeleteValue"} );
+
foreach my $id (@delete_values) {
next unless defined $id;
my ( $err, $msg ) = $Object->DeleteValue($id);
- push ( @results, $msg );
+ push( @results, $msg );
}
my $vals = $Object->Values();
- while (my $cfv = $vals->Next()) {
- if (my $so = $ARGSRef->{ 'CustomField-' . $Object->Id . '-SortOrder' . $cfv->Id }) {
- if ($cfv->SortOrder != $so) {
+ while ( my $cfv = $vals->Next() ) {
+ if ( my $so = $ARGSRef->{ "$prefix-SortOrder" . $cfv->Id } ) {
+ if ( $cfv->SortOrder != $so ) {
my ( $err, $msg ) = $cfv->SetSortOrder($so);
- push ( @results, $msg );
+ push( @results, $msg );
}
}
}
@@ -1142,37 +1422,35 @@ sub ProcessTicketBasics {
my $TicketObj = $args{'TicketObj'};
my $ARGSRef = $args{'ARGSRef'};
- # {{{ Set basic fields
+ # {{{ Set basic fields
my @attribs = qw(
- Subject
- FinalPriority
- Priority
- TimeEstimated
- TimeWorked
- TimeLeft
- Type
- Status
- Queue
+ Subject
+ FinalPriority
+ Priority
+ TimeEstimated
+ TimeWorked
+ TimeLeft
+ Type
+ Status
+ Queue
);
-
if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
my $tempqueue = RT::Queue->new($RT::SystemUser);
$tempqueue->Load( $ARGSRef->{'Queue'} );
if ( $tempqueue->id ) {
- $ARGSRef->{'Queue'} = $tempqueue->Id();
+ $ARGSRef->{'Queue'} = $tempqueue->id;
}
}
+ # Status isn't a field that can be set to a null value.
+ # RT core complains if you try
+ delete $ARGSRef->{'Status'} unless $ARGSRef->{'Status'};
- # Status isn't a field that can be set to a null value.
- # RT core complains if you try
- delete $ARGSRef->{'Status'} unless ($ARGSRef->{'Status'});
-
my @results = UpdateRecordObject(
AttributesRef => \@attribs,
Object => $TicketObj,
- ARGSRef => $ARGSRef
+ ARGSRef => $ARGSRef,
);
# We special case owner changing, so we can use ForceOwnerChange
@@ -1180,14 +1458,12 @@ sub ProcessTicketBasics {
my ($ChownType);
if ( $ARGSRef->{'ForceOwnerChange'} ) {
$ChownType = "Force";
- }
- else {
+ } else {
$ChownType = "Give";
}
- my ( $val, $msg ) =
- $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
- push ( @results, $msg );
+ my ( $val, $msg ) = $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
+ push( @results, $msg );
}
# }}}
@@ -1205,58 +1481,59 @@ sub ProcessTicketCustomFieldUpdates {
# Build up a list of objects that we want to work with
my %custom_fields_to_mod;
foreach my $arg ( keys %$ARGSRef ) {
- if ( $arg =~ /^Ticket-(\d+-.*)/) {
+ if ( $arg =~ /^Ticket-(\d+-.*)/ ) {
$ARGSRef->{"Object-RT::Ticket-$1"} = delete $ARGSRef->{$arg};
- }
- elsif ( $arg =~ /^CustomField-(\d+-.*)/) {
+ } elsif ( $arg =~ /^CustomField-(\d+-.*)/ ) {
$ARGSRef->{"Object-RT::Ticket--$1"} = delete $ARGSRef->{$arg};
}
}
- return ProcessObjectCustomFieldUpdates(%args, ARGSRef => $ARGSRef);
+ return ProcessObjectCustomFieldUpdates( %args, ARGSRef => $ARGSRef );
}
sub ProcessObjectCustomFieldUpdates {
- my %args = @_;
+ my %args = @_;
my $ARGSRef = $args{'ARGSRef'};
my @results;
# Build up a list of objects that we want to work with
my %custom_fields_to_mod;
foreach my $arg ( keys %$ARGSRef ) {
+
# format: Object-<object class>-<object id>-CustomField-<CF id>-<commands>
next unless $arg =~ /^Object-([\w:]+)-(\d*)-CustomField-(\d+)-(.*)$/;
# For each of those objects, find out what custom fields we want to work with.
- $custom_fields_to_mod{ $1 }{ $2 || 0 }{ $3 }{ $4 } = $ARGSRef->{ $arg };
+ $custom_fields_to_mod{$1}{ $2 || 0 }{$3}{$4} = $ARGSRef->{$arg};
}
# For each of those objects
foreach my $class ( keys %custom_fields_to_mod ) {
- foreach my $id ( keys %{$custom_fields_to_mod{$class}} ) {
+ foreach my $id ( keys %{ $custom_fields_to_mod{$class} } ) {
my $Object = $args{'Object'};
$Object = $class->new( $session{'CurrentUser'} )
unless $Object && ref $Object eq $class;
- $Object->Load( $id ) unless ($Object->id || 0) == $id;
+ $Object->Load($id) unless ( $Object->id || 0 ) == $id;
unless ( $Object->id ) {
$RT::Logger->warning("Couldn't load object $class #$id");
next;
}
- foreach my $cf ( keys %{ $custom_fields_to_mod{ $class }{ $id } } ) {
+ foreach my $cf ( keys %{ $custom_fields_to_mod{$class}{$id} } ) {
my $CustomFieldObj = RT::CustomField->new( $session{'CurrentUser'} );
- $CustomFieldObj->LoadById( $cf );
+ $CustomFieldObj->LoadById($cf);
unless ( $CustomFieldObj->id ) {
- $RT::Logger->warning("Couldn't load custom field #$id");
+ $RT::Logger->warning("Couldn't load custom field #$cf");
next;
}
- push @results, _ProcessObjectCustomFieldUpdates(
+ push @results,
+ _ProcessObjectCustomFieldUpdates(
Prefix => "Object-$class-$id-CustomField-$cf-",
Object => $Object,
CustomField => $CustomFieldObj,
ARGS => $custom_fields_to_mod{$class}{$id}{$cf},
- );
+ );
}
}
}
@@ -1264,20 +1541,32 @@ sub ProcessObjectCustomFieldUpdates {
}
sub _ProcessObjectCustomFieldUpdates {
- my %args = @_;
- my $cf = $args{'CustomField'};
+ my %args = @_;
+ my $cf = $args{'CustomField'};
my $cf_type = $cf->Type;
+ # Remove blank Values since the magic field will take care of this. Sometimes
+ # the browser gives you a blank value which causes CFs to be processed twice
+ if ( defined $args{'ARGS'}->{'Values'}
+ && !length $args{'ARGS'}->{'Values'}
+ && $args{'ARGS'}->{'Values-Magic'} )
+ {
+ delete $args{'ARGS'}->{'Values'};
+ }
+
my @results;
foreach my $arg ( keys %{ $args{'ARGS'} } ) {
-
- next if $arg =~ /Category$/;
+
+ # skip category argument
+ next if $arg eq 'Category';
# since http won't pass in a form element with a null value, we need
# to fake it
if ( $arg eq 'Values-Magic' ) {
+
# We don't care about the magic, if there's really a values element;
- next if $args{'ARGS'}->{'Value'} || $args{'ARGS'}->{'Values'};
+ next if defined $args{'ARGS'}->{'Value'} && length $args{'ARGS'}->{'Value'};
+ next if defined $args{'ARGS'}->{'Values'} && length $args{'ARGS'}->{'Values'};
# "Empty" values does not mean anything for Image and Binary fields
next if $cf_type =~ /^(?:Image|Binary)$/;
@@ -1287,18 +1576,21 @@ sub _ProcessObjectCustomFieldUpdates {
}
my @values = ();
- if ( ref $args{'ARGS'}->{ $arg } eq 'ARRAY' ) {
+ if ( ref $args{'ARGS'}->{$arg} eq 'ARRAY' ) {
@values = @{ $args{'ARGS'}->{$arg} };
- } elsif ( $cf_type =~ /text/i ) { # Both Text and Wikitext
- @values = ($args{'ARGS'}->{$arg});
- } elsif ( defined( $args{'ARGS'}->{ $arg } ) ) {
- @values = split /\n/, $args{'ARGS'}->{ $arg };
- }
-
- if ( ( $cf_type eq 'Freeform' && !$cf->SingleValue ) || $cf_type =~ /text/i ) {
- s/\r//g foreach @values;
+ } elsif ( $cf_type =~ /text/i ) { # Both Text and Wikitext
+ @values = ( $args{'ARGS'}->{$arg} );
+ } else {
+ @values = split /\r*\n/, $args{'ARGS'}->{$arg}
+ if defined $args{'ARGS'}->{$arg};
}
- @values = grep defined && $_ ne '', @values;
+ @values = grep length, map {
+ s/\r+\n/\n/g;
+ s/^\s+//;
+ s/\s+$//;
+ $_;
+ }
+ grep defined, @values;
if ( $arg eq 'AddValue' || $arg eq 'Value' ) {
foreach my $value (@values) {
@@ -1306,69 +1598,63 @@ sub _ProcessObjectCustomFieldUpdates {
Field => $cf->id,
Value => $value
);
- push ( @results, $msg );
+ push( @results, $msg );
}
- }
- elsif ( $arg eq 'Upload' ) {
+ } elsif ( $arg eq 'Upload' ) {
my $value_hash = _UploadedFile( $args{'Prefix'} . $arg ) or next;
- my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
- %$value_hash,
- Field => $cf,
- );
- push ( @results, $msg );
- }
- elsif ( $arg eq 'DeleteValues' ) {
- foreach my $value ( @values ) {
+ my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( %$value_hash, Field => $cf, );
+ push( @results, $msg );
+ } elsif ( $arg eq 'DeleteValues' ) {
+ foreach my $value (@values) {
my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
Field => $cf,
Value => $value,
);
- push ( @results, $msg );
+ push( @results, $msg );
}
- }
- elsif ( $arg eq 'DeleteValueIds' ) {
- foreach my $value ( @values ) {
+ } elsif ( $arg eq 'DeleteValueIds' ) {
+ foreach my $value (@values) {
my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
Field => $cf,
ValueId => $value,
);
- push ( @results, $msg );
+ push( @results, $msg );
}
- }
- elsif ( $arg eq 'Values' && !$cf->Repeated ) {
+ } elsif ( $arg eq 'Values' && !$cf->Repeated ) {
my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
my %values_hash;
- foreach my $value ( @values ) {
- # build up a hash of values that the new set has
- $values_hash{$value} = 1;
- next if $cf_values->HasEntry( $value );
+ foreach my $value (@values) {
+ if ( my $entry = $cf_values->HasEntry($value) ) {
+ $values_hash{ $entry->id } = 1;
+ next;
+ }
my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
Field => $cf,
Value => $value
);
- push ( @results, $msg );
+ push( @results, $msg );
+ $values_hash{$val} = 1 if $val;
}
$cf_values->RedoSearch;
while ( my $cf_value = $cf_values->Next ) {
- next if $values_hash{ $cf_value->Content };
+ next if $values_hash{ $cf_value->id };
my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
- Field => $cf,
- Value => $cf_value->Content
+ Field => $cf,
+ ValueId => $cf_value->id
);
- push ( @results, $msg);
+ push( @results, $msg );
}
- }
- elsif ( $arg eq 'Values' ) {
+ } elsif ( $arg eq 'Values' ) {
my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
# keep everything up to the point of difference, delete the rest
my $delete_flag;
- foreach my $old_cf (@{$cf_values->ItemsArrayRef}) {
- if (!$delete_flag and @values and $old_cf->Content eq $values[0]) {
+ foreach my $old_cf ( @{ $cf_values->ItemsArrayRef } ) {
+ if ( !$delete_flag and @values and $old_cf->Content eq $values[0] ) {
shift @values;
next;
}
@@ -1378,18 +1664,20 @@ sub _ProcessObjectCustomFieldUpdates {
}
# now add/replace extra things, if any
- foreach my $value ( @values ) {
+ foreach my $value (@values) {
my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
Field => $cf,
Value => $value
);
- push ( @results, $msg );
+ push( @results, $msg );
}
- }
- else {
- push ( @results,
+ } else {
+ push(
+ @results,
loc("User asked for an unknown update type for custom field [_1] for [_2] object #[_3]",
- $cf->Name, ref $args{'Object'}, $args{'Object'}->id )
+ $cf->Name, ref $args{'Object'},
+ $args{'Object'}->id
+ )
);
}
}
@@ -1420,8 +1708,7 @@ sub ProcessTicketWatchers {
foreach my $key ( keys %$ARGSRef ) {
# Delete deletable watchers
- if ( ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) )
- {
+ if ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) {
my ( $code, $msg ) = $Ticket->DeleteWatcher(
PrincipalId => $2,
Type => $1
@@ -1439,8 +1726,8 @@ sub ProcessTicketWatchers {
}
# Add new wathchers by email address
- elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
- and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) )
+ elsif ( ( $ARGSRef->{$key} || '' ) =~ /^(?:AdminCc|Cc|Requestor)$/
+ and $key =~ /^WatcherTypeEmail(\d*)$/ )
{
#They're in this order because otherwise $1 gets clobbered :/
@@ -1463,7 +1750,7 @@ sub ProcessTicketWatchers {
# Add new watchers by owner
elsif ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) {
my $principal_id = $1;
- my $form = $ARGSRef->{$key};
+ my $form = $ARGSRef->{$key};
foreach my $value ( ref($form) ? @{$form} : ($form) ) {
next unless $value =~ /^(?:AdminCc|Cc|Requestor)$/i;
@@ -1503,33 +1790,33 @@ sub ProcessTicketDates {
# {{{ Set date fields
my @date_fields = qw(
- Told
- Resolved
- Starts
- Started
- Due
+ Told
+ Resolved
+ Starts
+ Started
+ Due
);
#Run through each field in this list. update the value if apropriate
foreach my $field (@date_fields) {
+ next unless exists $ARGSRef->{ $field . '_Date' };
+ next if $ARGSRef->{ $field . '_Date' } eq '';
+
my ( $code, $msg );
my $DateObj = RT::Date->new( $session{'CurrentUser'} );
+ $DateObj->Set(
+ Format => 'unknown',
+ Value => $ARGSRef->{ $field . '_Date' }
+ );
- #If it's something other than just whitespace
- if ( $ARGSRef->{ $field . '_Date' } && ($ARGSRef->{ $field . '_Date' } ne '') ) {
- $DateObj->Set(
- Format => 'unknown',
- Value => $ARGSRef->{ $field . '_Date' }
- );
- my $obj = $field . "Obj";
- if ( ( defined $DateObj->Unix )
- and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) )
- {
- my $method = "Set$field";
- my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
- push @results, "$msg";
- }
+ my $obj = $field . "Obj";
+ if ( ( defined $DateObj->Unix )
+ and ( $DateObj->Unix != $Ticket->$obj()->Unix() ) )
+ {
+ my $method = "Set$field";
+ my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
+ push @results, "$msg";
}
}
@@ -1548,21 +1835,21 @@ Returns an array of results messages.
=cut
sub ProcessTicketLinks {
- my %args = ( TicketObj => undef,
- ARGSRef => undef,
- @_ );
+ my %args = (
+ TicketObj => undef,
+ ARGSRef => undef,
+ @_
+ );
my $Ticket = $args{'TicketObj'};
my $ARGSRef = $args{'ARGSRef'};
-
- my (@results) = ProcessRecordLinks(RecordObj => $Ticket,
- ARGSRef => $ARGSRef);
+ my (@results) = ProcessRecordLinks( RecordObj => $Ticket, ARGSRef => $ARGSRef );
#Merge if we need to
if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
- my ( $val, $msg ) =
- $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
+ $ARGSRef->{ $Ticket->Id . "-MergeInto" } =~ s/\s+//g;
+ my ( $val, $msg ) = $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
push @results, $msg;
}
@@ -1572,9 +1859,11 @@ sub ProcessTicketLinks {
# }}}
sub ProcessRecordLinks {
- my %args = ( RecordObj => undef,
- ARGSRef => undef,
- @_ );
+ my %args = (
+ RecordObj => undef,
+ ARGSRef => undef,
+ @_
+ );
my $Record = $args{'RecordObj'};
my $ARGSRef = $args{'ARGSRef'};
@@ -1588,11 +1877,11 @@ sub ProcessRecordLinks {
my $type = $2;
my $target = $3;
- push @results,
- "Trying to delete: Base: $base Target: $target Type $type";
- my ( $val, $msg ) = $Record->DeleteLink( Base => $base,
- Type => $type,
- Target => $target );
+ my ( $val, $msg ) = $Record->DeleteLink(
+ Base => $base,
+ Type => $type,
+ Target => $target
+ );
push @results, $msg;
@@ -1604,28 +1893,38 @@ sub ProcessRecordLinks {
foreach my $linktype (@linktypes) {
if ( $ARGSRef->{ $Record->Id . "-$linktype" } ) {
- for my $luri ( split ( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) {
- $luri =~ s/\s*$//; # Strip trailing whitespace
- my ( $val, $msg ) = $Record->AddLink( Target => $luri,
- Type => $linktype );
+ $ARGSRef->{ $Record->Id . "-$linktype" } = join( ' ', @{ $ARGSRef->{ $Record->Id . "-$linktype" } } )
+ if ref( $ARGSRef->{ $Record->Id . "-$linktype" } );
+
+ for my $luri ( split( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) {
+ next unless $luri;
+ $luri =~ s/\s+$//; # Strip trailing whitespace
+ my ( $val, $msg ) = $Record->AddLink(
+ Target => $luri,
+ Type => $linktype
+ );
push @results, $msg;
}
}
if ( $ARGSRef->{ "$linktype-" . $Record->Id } ) {
-
- for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) {
- my ( $val, $msg ) = $Record->AddLink( Base => $luri,
- Type => $linktype );
+ $ARGSRef->{ "$linktype-" . $Record->Id } = join( ' ', @{ $ARGSRef->{ "$linktype-" . $Record->Id } } )
+ if ref( $ARGSRef->{ "$linktype-" . $Record->Id } );
+
+ for my $luri ( split( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) {
+ next unless $luri;
+ my ( $val, $msg ) = $Record->AddLink(
+ Base => $luri,
+ Type => $linktype
+ );
push @results, $msg;
}
- }
+ }
}
return (@results);
}
-
=head2 _UploadedFile ( $arg );
Takes a CGI parameter name; if a file is uploaded under that name,
@@ -1637,9 +1936,9 @@ Returns C<undef> if no files were uploaded in the C<$arg> field.
=cut
sub _UploadedFile {
- my $arg = shift;
- my $cgi_object = $m->cgi_object;
- my $fh = $cgi_object->upload($arg) or return undef;
+ my $arg = shift;
+ my $cgi_object = $m->cgi_object;
+ my $fh = $cgi_object->upload($arg) or return undef;
my $upload_info = $cgi_object->uploadInfo($fh);
my $filename = "$fh";
@@ -1647,12 +1946,50 @@ sub _UploadedFile {
binmode($fh);
return {
- Value => $filename,
+ Value => $filename,
LargeContent => do { local $/; scalar <$fh> },
- ContentType => $upload_info->{'Content-Type'},
+ ContentType => $upload_info->{'Content-Type'},
};
}
+sub GetColumnMapEntry {
+ my %args = ( Map => {}, Name => '', Attribute => undef, @_ );
+
+ # deal with the simplest thing first
+ if ( $args{'Map'}{ $args{'Name'} } ) {
+ return $args{'Map'}{ $args{'Name'} }{ $args{'Attribute'} };
+ }
+
+ # complex things
+ elsif ( my ( $mainkey, $subkey ) = $args{'Name'} =~ /^(.*?)\.{(.+)}$/ ) {
+ return undef unless $args{'Map'}->{$mainkey};
+ return $args{'Map'}{$mainkey}{ $args{'Attribute'} }
+ unless ref $args{'Map'}{$mainkey}{ $args{'Attribute'} } eq 'CODE';
+
+ return sub { $args{'Map'}{$mainkey}{ $args{'Attribute'} }->( @_, $subkey ) };
+ }
+ return undef;
+}
+
+sub ProcessColumnMapValue {
+ my $value = shift;
+ my %args = ( Arguments => [], Escape => 1, @_ );
+
+ if ( ref $value ) {
+ if ( UNIVERSAL::isa( $value, 'CODE' ) ) {
+ my @tmp = $value->( @{ $args{'Arguments'} } );
+ return ProcessColumnMapValue( ( @tmp > 1 ? \@tmp : $tmp[0] ), %args );
+ } elsif ( UNIVERSAL::isa( $value, 'ARRAY' ) ) {
+ return join '', map ProcessColumnMapValue( $_, %args ), @$value;
+ } elsif ( UNIVERSAL::isa( $value, 'SCALAR' ) ) {
+ return $$value;
+ }
+ }
+
+ return $m->interp->apply_escapes( $value, 'h' ) if $args{'Escape'};
+ return $value;
+}
+
=head2 _load_container_object ( $type, $id );
Instantiate container object for saving searches.
@@ -1660,8 +1997,8 @@ Instantiate container object for saving searches.
=cut
sub _load_container_object {
- my ($obj_type, $obj_id) = @_;
- return RT::SavedSearch->new($session{'CurrentUser'})->_load_privacy_object($obj_type, $obj_id);
+ my ( $obj_type, $obj_id ) = @_;
+ return RT::SavedSearch->new( $session{'CurrentUser'} )->_load_privacy_object( $obj_type, $obj_id );
}
=head2 _parse_saved_search ( $arg );
@@ -1674,19 +2011,19 @@ container object and the search id.
sub _parse_saved_search {
my $spec = shift;
return unless $spec;
- if ($spec !~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/ ) {
+ if ( $spec !~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/ ) {
return;
}
my $obj_type = $1;
my $obj_id = $2;
my $search_id = $3;
- return (_load_container_object ($obj_type, $obj_id), $search_id);
+ return ( _load_container_object( $obj_type, $obj_id ), $search_id );
}
eval "require RT::Interface::Web_Vendor";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm});
+die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm} );
eval "require RT::Interface::Web_Local";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm});
+die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm} );
1;
diff --git a/rt/lib/RT/Interface/Web/Handler.pm b/rt/lib/RT/Interface/Web/Handler.pm
index 260a3b3..8d17921 100644
--- a/rt/lib/RT/Interface/Web/Handler.pm
+++ b/rt/lib/RT/Interface/Web/Handler.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,6 +45,7 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
package RT::Interface::Web::Handler;
use CGI qw/-private_tempfiles/;
@@ -56,6 +57,7 @@ use Time::HiRes;
use HTML::Entities;
use HTML::Scrubber;
use RT::Interface::Web::Handler;
+use RT::Interface::Web::Request;
use File::Path qw( rmtree );
use File::Glob qw( bsd_glob );
use File::Spec::Unix;
@@ -63,15 +65,19 @@ use File::Spec::Unix;
sub DefaultHandlerArgs { (
comp_root => [
[ local => $RT::MasonLocalComponentRoot ],
+ (map {[ "plugin-".$_->Name => $_->ComponentRoot ]} @{RT->Plugins}),
[ standard => $RT::MasonComponentRoot ]
],
default_escape_flags => 'h',
data_dir => "$RT::MasonDataDir",
allow_globals => [qw(%session)],
# Turn off static source if we're in developer mode.
- static_source => ($RT::DevelMode ? '0' : '1'),
- use_object_files => ($RT::DevelMode ? '0' : '1'),
- autoflush => 0
+ static_source => (RT->Config->Get('DevelMode') ? '0' : '1'),
+ use_object_files => (RT->Config->Get('DevelMode') ? '0' : '1'),
+ autoflush => 0,
+ error_format => (RT->Config->Get('DevelMode') ? 'html': 'brief'),
+ request_class => 'RT::Interface::Web::Request',
+ named_component_subs => $INC{'Devel/Cover.pm'} ? 1 : 0,
) };
# {{{ sub new
@@ -87,10 +93,7 @@ sub new {
my $class = shift;
$class->InitSessionDir;
- if ( $mod_perl::VERSION && $mod_perl::VERSION >= 1.9908 ) {
- goto &NewApacheHandler;
- }
- elsif ($CGI::MOD_PERL) {
+ if ( ($mod_perl::VERSION && $mod_perl::VERSION >= 1.9908) || $CGI::MOD_PERL) {
goto &NewApacheHandler;
}
else {
@@ -102,16 +105,17 @@ 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.
- unless ( $RT::DatabaseType =~ /(?:mysql|Pg)/ ) {
+ unless ( RT->Config->Get('DatabaseType') =~ /(?:mysql|Pg)/ ) {
# Clean up our umask to protect session files
umask(0077);
- if ($CGI::MOD_PERL) { local $@; eval {
+ if ($CGI::MOD_PERL and $CGI::MOD_PERL < 1.9908 ) {
chown( Apache->server->uid, Apache->server->gid,
$RT::MasonSessionDir )
- }}
+ if Apache->server->can('uid');
+ }
# Die if WebSessionDir doesn't exist or we can't write to it
stat($RT::MasonSessionDir);
@@ -139,22 +143,6 @@ sub NewApacheHandler {
# }}}
-# {{{ sub NewApache2Handler
-
-=head2 NewApache2Handler
-
- Takes extra options to pass to MasonX::Apache2Handler->new
- Returns a new MasonX::Apache2Handler object
-
-=cut
-
-sub NewApache2Handler {
- require MasonX::Apache2Handler;
- return NewHandler('MasonX::Apache2Handler', args_method => "CGI", @_);
-}
-
-# }}}
-
# {{{ sub NewCGIHandler
=head2 NewCGIHandler
@@ -182,15 +170,30 @@ sub NewHandler {
=head2 CleanupRequest
-Rollback any uncommitted transaction.
-Flush the ACL cache
-Flush the searchbuilder query cache
+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
+
+=item Flush tmp GnuPG key preferences
+
+=back
=cut
sub CleanupRequest {
- if ( $RT::Handle->TransactionDepth ) {
+ if ( $RT::Handle && $RT::Handle->TransactionDepth ) {
$RT::Handle->ForceRollback;
$RT::Logger->crit(
"Transaction not committed. Usually indicates a software fault."
@@ -201,10 +204,19 @@ sub CleanupRequest {
# Consistency is imprived, too.
RT::Principal->InvalidateACLCache();
DBIx::SearchBuilder::Record::Cachable->FlushCache
- if ( $RT::WebFlushDbCacheEveryRequest
+ if ( RT->Config->Get('WebFlushDbCacheEveryRequest')
and UNIVERSAL::can(
'DBIx::SearchBuilder::Record::Cachable' => 'FlushCache' ) );
+ # cleanup global squelching of the mails
+ require RT::Action::SendEmail;
+ RT::Action::SendEmail->CleanSlate;
+
+ if (RT->Config->Get('GnuPG')->{'Enable'}) {
+ require RT::Crypt::GnuPG;
+ RT::Crypt::GnuPG::UseKeyForEncryption();
+ RT::Crypt::GnuPG::UseKeyForSigning( undef );
+ }
}
# }}}
diff --git a/rt/lib/RT/Interface/Web/Menu.pm b/rt/lib/RT/Interface/Web/Menu.pm
index de479da..3569942 100644
--- a/rt/lib/RT/Interface/Web/Menu.pm
+++ b/rt/lib/RT/Interface/Web/Menu.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,6 +45,7 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
package RT::Interface::Web::Menu;
diff --git a/rt/lib/RT/Interface/Web/Menu/Item.pm b/rt/lib/RT/Interface/Web/Menu/Item.pm
index 4149a0b..8eb4120 100644
--- a/rt/lib/RT/Interface/Web/Menu/Item.pm
+++ b/rt/lib/RT/Interface/Web/Menu/Item.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,6 +45,7 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
package RT::Interface::Web::Menu::Item;
diff --git a/rt/lib/RT/Interface/Web/QueryBuilder.pm b/rt/lib/RT/Interface/Web/QueryBuilder.pm
index f93c415..29d12b4 100755
--- a/rt/lib/RT/Interface/Web/QueryBuilder.pm
+++ b/rt/lib/RT/Interface/Web/QueryBuilder.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,6 +45,7 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
package RT::Interface::Web::QueryBuilder;
use strict;
diff --git a/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm b/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm
index b605206..574ead4 100755
--- a/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm
+++ b/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,11 +45,13 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
+
package RT::Interface::Web::QueryBuilder::Tree;
use strict;
use warnings;
+use Tree::Simple qw/use_weak_refs/;
use base qw/Tree::Simple/;
=head1 NAME
@@ -77,13 +79,15 @@ on the root node passed to it.)
sub TraversePrePost {
my ($self, $prefunc, $postfunc) = @_;
- $prefunc->($self);
-
+ # XXX: if pre or post action changes siblings (delete or adds)
+ # we could have problems
+ $prefunc->($self) if $prefunc;
+
foreach my $child ($self->getAllChildren()) {
$child->TraversePrePost($prefunc, $postfunc);
}
- $postfunc->($self);
+ $postfunc->($self) if $postfunc;
}
=head2 GetReferencedQueues
@@ -103,10 +107,11 @@ sub GetReferencedQueues {
my $node = shift;
return if $node->isRoot;
+ return unless $node->isLeaf;
my $clause = $node->getNodeValue();
-
- if ( ref($clause) and $clause->{Key} eq 'Queue' ) {
+
+ if ( $clause->{Key} eq 'Queue' ) {
$queues->{ $clause->{Value} } = 1;
};
}
@@ -133,55 +138,13 @@ sub GetQueryAndOptionList {
my $self = shift;
my $selected_nodes = shift;
- my $optionlist = [];
-
- my $i = 0;
-
- $self->TraversePrePost(
- sub { # This is called before recursing to the node's children.
- my $node = shift;
-
- return if $node->isRoot or $node->getParent->isRoot;
-
- my $clause = $node->getNodeValue();
- my $str = ' ';
- my $aggregator_context = $node->getParent()->getNodeValue();
- $str = $aggregator_context . " " if $node->getIndex() > 0;
-
- if ( ref($clause) ) { # ie, it's a leaf
- $str .=
- $clause->{Key} . " " . $clause->{Op} . " " . $clause->{Value};
- }
-
- unless ($node->getParent->getParent->isRoot) {
- # used to check !ref( $parent->getNodeValue() ) )
- if ( $node->getIndex() == 0 ) {
- $str = '( ' . $str;
- }
- }
-
- push @$optionlist, {
- TEXT => $str,
- INDEX => $i,
- SELECTED => (grep { $_ == $node } @$selected_nodes) ? 'SELECTED' : '',
- DEPTH => $node->getDepth() - 1,
- };
+ my $list = $self->__LinearizeTree;
+ foreach my $e( @$list ) {
+ $e->{'DEPTH'} = $e->{'NODE'}->getDepth;
+ $e->{'SELECTED'} = (grep $_ == $e->{'NODE'}, @$selected_nodes)? qq[ selected="selected"] : '';
+ }
- $i++;
- }, sub {
- # This is called after recursing to the node's children.
- my $node = shift;
-
- return if $node->isRoot or $node->getParent->isRoot or $node->getParent->getParent->isRoot;
-
- # Only do this for the rightmost child.
- return unless $node->getIndex == $node->getParent->getChildCount - 1;
-
- $optionlist->[-1]{TEXT} .= ' )';
- }
- );
-
- return (join ' ', map { $_->{TEXT} } @$optionlist), $optionlist;
+ return (join ' ', map $_->{'TEXT'}, @$list), $list;
}
=head2 PruneChildLessAggregators
@@ -195,23 +158,18 @@ sub PruneChildlessAggregators {
my $self = shift;
$self->TraversePrePost(
- sub {
- },
+ undef,
sub {
my $node = shift;
+ return unless $node->isLeaf;
- return if $node->isRoot or $node->getParent->isRoot;
-
# We're only looking for aggregators (AND/OR)
return if ref $node->getNodeValue;
-
- return if $node->getChildCount != 0;
-
+
+ return if $node->isRoot;
+
# OK, this is a childless aggregator. Remove self.
-
$node->getParent->removeChild($node);
-
- # Deal with circular refs
$node->DESTROY;
}
);
@@ -226,18 +184,102 @@ In fact, it's all of them but the root and its child.
=cut
sub GetDisplayedNodes {
+ return map $_->{NODE}, @{ (shift)->__LinearizeTree };
+}
+
+
+sub __LinearizeTree {
my $self = shift;
- my @lines;
- $self->traverse(sub {
+ my ($list, $i) = ([], 0);
+
+ $self->TraversePrePost( sub {
my $node = shift;
+ return if $node->isRoot;
+
+ my $str = '';
+ if( $node->getIndex > 0 ) {
+ $str .= " ". $node->getParent->getNodeValue ." ";
+ }
+
+ unless( $node->isLeaf ) {
+ $str .= '( ';
+ } else {
+
+ my $clause = $node->getNodeValue;
+ $str .= $clause->{Key};
+ $str .= " ". $clause->{Op};
+ $str .= " ". $clause->{Value};
- push @lines, $node unless $node->isRoot or $node->getParent->isRoot;
+ }
+ $str =~ s/^\s+|\s+$//;
+
+ push @$list, {
+ NODE => $node,
+ TEXT => $str,
+ INDEX => $i,
+ };
+
+ $i++;
+ }, sub {
+ my $node = shift;
+ return if $node->isRoot;
+ return if $node->isLeaf;
+ $list->[-1]->{'TEXT'} .= ' )';
});
- return @lines;
+ return $list;
}
+sub ParseSQL {
+ my $self = shift;
+ my %args = (
+ Query => '',
+ CurrentUser => '', #XXX: Hack
+ @_
+ );
+ my $string = $args{'Query'};
+
+ my @results;
+
+ my %field = %{ RT::Tickets->new( $args{'CurrentUser'} )->FIELDS };
+ my %lcfield = map { ( lc($_) => $_ ) } keys %field;
+
+ my $node = $self;
+
+ my %callback;
+ $callback{'OpenParen'} = sub {
+ $node = __PACKAGE__->new( 'AND', $node );
+ };
+ $callback{'CloseParen'} = sub { $node = $node->getParent };
+ $callback{'EntryAggregator'} = sub { $node->setNodeValue( $_[0] ) };
+ $callback{'Condition'} = sub {
+ my ($key, $op, $value) = @_;
+
+ my ($main_key) = split /[.]/, $key;
+
+ my $class;
+ if ( exists $lcfield{ lc $main_key } ) {
+ $class = $field{ $main_key }->[0];
+ $key =~ s/^[^.]+/ $lcfield{ lc $main_key } /e;
+ }
+ unless( $class ) {
+ push @results, [ $args{'CurrentUser'}->loc("Unknown field: [_1]", $key), -1 ]
+ }
+
+ $value =~ s/'/\\'/g;
+ $value = "'$value'" if $value =~ /[^0-9]/;
+ $key = "'$key'" if $key =~ /^CF./;
+
+ my $clause = { Key => $key, Op => $op, Value => $value };
+ $node->addChild( __PACKAGE__->new( $clause ) );
+ };
+ $callback{'Error'} = sub { push @results, @_ };
+
+ require RT::SQL;
+ RT::SQL::Parse($string, \%callback);
+ return @results;
+}
eval "require RT::Interface::Web::QueryBuilder::Tree_Vendor";
die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web/QueryBuilder/Tree_Vendor.pm});
diff --git a/rt/lib/RT/Interface/Web/Request.pm b/rt/lib/RT/Interface/Web/Request.pm
new file mode 100644
index 0000000..e179464
--- /dev/null
+++ b/rt/lib/RT/Interface/Web/Request.pm
@@ -0,0 +1,207 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+# <jesse@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (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.)
+#
+# 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.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Interface::Web::Request;
+
+use strict;
+use warnings;
+
+our $VERSION = '0.30';
+use base qw(HTML::Mason::Request);
+
+sub new {
+ my $class = shift;
+
+ my $new_class = $HTML::Mason::ApacheHandler::VERSION ?
+ 'HTML::Mason::Request::ApacheHandler' :
+ $HTML::Mason::CGIHandler::VERSION ?
+ 'HTML::Mason::Request::CGI' :
+ 'HTML::Mason::Request';
+
+ $class->alter_superclass( $new_class );
+ $class->valid_params( %{ $new_class->valid_params } );
+ return $class->SUPER::new(@_);
+}
+
+# XXX TODO: This alter_superclass replaces teh funcitonality in Mason 1.39
+# with code which doesn't trigger a bug in Perl 5.10.
+# (Perl 5.10.0 does NOT take kindly to having its @INC entries changed)
+# http://rt.perl.org/rt3/Public/Bug/Display.html?id=54566
+#
+# This routine can be removed when either:
+# * RT depends on a version of mason which contains this fix
+# * Perl 5.10.0 is not supported for running RT
+sub alter_superclass {
+ my $class = shift;
+ my $new_super = shift;
+ my $isa_ref;
+ { no strict 'refs'; my @entries = @{$class."::ISA"}; $isa_ref = \@entries; }
+
+ # handles multiple inheritance properly and preserve
+ # inheritance order
+ for ( my $x = 0; $x <= $#{$isa_ref} ; $x++ ) {
+ if ( $isa_ref->[$x]->isa('HTML::Mason::Request') ) {
+ my $old_super = $isa_ref->[$x];
+ $isa_ref->[$x] = $new_super
+ if ( $old_super ne $new_super );
+ last;
+ }
+ }
+
+ { no strict 'refs'; @{$class."::ISA"} = @$isa_ref; }
+ $class->valid_params( %{ $class->valid_params } );
+}
+
+
+=head2 callback
+
+Method replaces deprecated component C<Element/Callback>.
+
+Takes hash with optional C<CallbackPage>, C<CallbackName>
+and C<CallbackOnce> arguments, other arguments are passed
+throught to callback components.
+
+=over 4
+
+=item CallbackPage
+
+Page path relative to the root, leading slash is mandatory.
+By default is equal to path of the caller component.
+
+=item CallbackName
+
+Name of the callback. C<Default> is used unless specified.
+
+=item CallbackOnce
+
+By default is false, otherwise runs callbacks only once per
+process of the server. Such callbacks can be used to fill
+structures.
+
+=back
+
+Searches for callback components in
+F<< /Callbacks/<any dir>/CallbackPage/CallbackName >>, for
+example F</Callbacks/MyExtension/autohandler/Default> would
+be called as default callback for F</autohandler>.
+
+=cut
+
+{
+my %cache = ();
+my %called = ();
+sub callback {
+ my ($self, %args) = @_;
+
+ my $name = delete $args{'CallbackName'} || 'Default';
+ my $page = delete $args{'CallbackPage'} || $self->callers(0)->path;
+ unless ( $page ) {
+ $RT::Logger->error("Couldn't get a page name for callbacks");
+ return;
+ }
+
+ my $CacheKey = "$page--$name";
+ return 1 if delete $args{'CallbackOnce'} && $called{ $CacheKey };
+ $called{ $CacheKey } = 1;
+
+ my $callbacks = $cache{ $CacheKey };
+ unless ( $callbacks ) {
+ $callbacks = [];
+ my $path = "/Callbacks/*$page/$name";
+ my @roots = map $_->[1],
+ $HTML::Mason::VERSION <= 1.28
+ ? $self->interp->resolver->comp_root_array
+ : $self->interp->comp_root_array;
+
+ my %seen;
+ @$callbacks = (
+ sort 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, $_),
+ @roots
+ );
+ foreach my $comp (keys %seen) {
+ next unless $seen{$comp} > 1;
+ $RT::Logger->error("Found more than one occurrence of the $comp callback. This may cause only one of the callbacks to run. Look for the duplicate Callback in your @roots");
+ }
+
+ $cache{ $CacheKey } = $callbacks unless RT->Config->Get('DevelMode');
+ }
+
+ my @rv;
+ foreach my $cb ( @$callbacks ) {
+ push @rv, scalar $self->comp( $cb, %args );
+ }
+ return @rv;
+}
+}
+
+=head2 request_path
+
+Returns path of the request.
+
+Very close to C<< $m->request_comp->path >>, but if called in a dhandler returns
+path of the request without dhandler name, but with dhandler arguments instead.
+
+=cut
+
+sub request_path {
+ my $self = shift;
+
+ my $path = $self->request_comp->path;
+ # disabled dhandlers, not RT case, but anyway
+ return $path unless my $dh_name = $self->dhandler_name;
+ # not a dhandler
+ return $path unless substr($path, -length("/$dh_name")) eq "/$dh_name";
+ substr($path, -length $dh_name) = $self->dhandler_arg;
+ return $path;
+}
+
+1;
diff --git a/rt/lib/RT/Interface/Web/Session.pm b/rt/lib/RT/Interface/Web/Session.pm
new file mode 100644
index 0000000..4998c34
--- /dev/null
+++ b/rt/lib/RT/Interface/Web/Session.pm
@@ -0,0 +1,285 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+# <jesse@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (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.)
+#
+# 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.
+#
+# END BPS TAGGED BLOCK }}}
+
+package RT::Interface::Web::Session;
+use warnings;
+use strict;
+
+use RT::CurrentUser;
+
+=head1 NAME
+
+RT::Interface::Web::Session - RT web session class
+
+=head1 SYNOPSYS
+
+
+=head1 DESCRIPTION
+
+RT session class and utilities.
+
+CLASS METHODS can be used without creating object instances,
+it's mainly utilities to clean unused session records.
+
+Object is tied hash and can be used to access session data.
+
+=head1 METHODS
+
+=head2 CLASS METHODS
+
+=head3 Class
+
+Returns name of the class that is used as sessions storage.
+
+=cut
+
+sub Class {
+ my $self = shift;
+
+ my $class = RT->Config->Get('WebSessionClass')
+ || $self->Backends->{RT->Config->Get('DatabaseType')}
+ || 'Apache::Session::File';
+ eval "require $class";
+ die $@ if $@;
+ return $class;
+}
+
+=head3 Backends
+
+Returns hash reference with names of the databases as keys and
+sessions class names as values.
+
+=cut
+
+sub Backends {
+ return {
+ mysql => 'Apache::Session::MySQL',
+ Pg => 'Apache::Session::Postgres',
+ };
+}
+
+=head3 Attributes
+
+Returns hash reference with attributes that are used to create
+new session objects.
+
+=cut
+
+sub Attributes {
+
+ return $_[0]->Backends->{RT->Config->Get('DatabaseType')} ? {
+ Handle => $RT::Handle->dbh,
+ LockHandle => $RT::Handle->dbh,
+ Transaction => 1,
+ } : {
+ Directory => $RT::MasonSessionDir,
+ LockDirectory => $RT::MasonSessionDir,
+ Transaction => 1,
+ };
+}
+
+=head3 Ids
+
+Returns array ref with list of the session IDs.
+
+=cut
+
+sub Ids {
+ my $self = shift || __PACKAGE__;
+ my $attributes = $self->Attributes;
+ if( $attributes->{Directory} ) {
+ return $self->_IdsDir( $attributes->{Directory} );
+ } else {
+ return $self->_IdsDB( $RT::Handle->dbh );
+ }
+}
+
+sub _IdsDir {
+ my ($self, $dir) = @_;
+ require File::Find;
+ my %file;
+ File::Find::find(
+ sub { return unless /^[a-zA-Z0-9]+$/;
+ $file{$_} = (stat($_))[9];
+ },
+ $dir,
+ );
+
+ return [ sort { $file{$a} <=> $file{$b} } keys %file ];
+}
+
+sub _IdsDB {
+ my ($self, $dbh) = @_;
+ my $ids = $dbh->selectcol_arrayref("SELECT id FROM sessions ORDER BY LastUpdated DESC");
+ die "couldn't get ids: ". $dbh->errstr if $dbh->errstr;
+ return $ids;
+}
+
+=head3 ClearOld
+
+Takes seconds and deletes all sessions that are older.
+
+=cut
+
+sub ClearOld {
+ my $class = shift || __PACKAGE__;
+ my $attributes = $class->Attributes;
+ if( $attributes->{Directory} ) {
+ return $class->_CleariOldDir( $attributes->{Directory}, @_ );
+ } else {
+ return $class->_ClearOldDB( $RT::Handle->dbh, @_ );
+ }
+}
+
+sub _ClearOldDB {
+ my ($self, $dbh, $older_than) = @_;
+ my $rows;
+ unless( int $older_than ) {
+ $rows = $dbh->do("DELETE FROM sessions");
+ die "couldn't delete sessions: ". $dbh->errstr unless defined $rows;
+ } else {
+ require POSIX;
+ my $date = POSIX::strftime("%Y-%m-%d %H:%M", localtime( time - int $older_than ) );
+
+ my $sth = $dbh->prepare("DELETE FROM sessions WHERE LastUpdated < ?");
+ die "couldn't prepare query: ". $dbh->errstr unless $sth;
+ $rows = $sth->execute( $date );
+ die "couldn't execute query: ". $dbh->errstr unless defined $rows;
+ }
+
+ $RT::Logger->info("successfuly deleted $rows sessions");
+ return;
+}
+
+sub _ClearOldDir {
+ my ($self, $dir, $older_than) = @_;
+
+ require File::Spec if int $older_than;
+
+ my $now = time;
+ my $class = $self->Class;
+ my $attrs = $self->Attributes;
+
+ foreach my $id( @{ $self->Ids } ) {
+ if( int $older_than ) {
+ my $ctime = (stat(File::Spec->catfile($dir,$id)))[9];
+ if( $ctime > $now - $older_than ) {
+ $RT::Logger->debug("skipped session '$id', isn't old");
+ next;
+ }
+ }
+
+ my %session;
+ local $@;
+ eval { tie %session, $class, $id, $attrs };
+ if( $@ ) {
+ $RT::Logger->debug("skipped session '$id', couldn't load: $@");
+ next;
+ }
+ tied(%session)->delete;
+ $RT::Logger->info("successfuly deleted session '$id'");
+ }
+ return;
+}
+
+=head3 ClearByUser
+
+Checks all sessions and if user has more then one session
+then leave only the latest one.
+
+=cut
+
+sub ClearByUser {
+ my $self = shift || __PACKAGE__;
+ my $class = $self->Class;
+ my $attrs = $self->Attributes;
+
+ my %seen = ();
+ foreach my $id( @{ $self->Ids } ) {
+ my %session;
+ local $@;
+ eval { tie %session, $class, $id, $attrs };
+ if( $@ ) {
+ $RT::Logger->debug("skipped session '$id', couldn't load: $@");
+ next;
+ }
+ if( $session{'CurrentUser'} && $session{'CurrentUser'}->id ) {
+ unless( $seen{ $session{'CurrentUser'}->id }++ ) {
+ $RT::Logger->debug("skipped session '$id', first user's session");
+ next;
+ }
+ }
+ tied(%session)->delete;
+ $RT::Logger->info("successfuly deleted session '$id'");
+ }
+}
+
+sub TIEHASH {
+ my $self = shift;
+ my $id = shift;
+
+ my $class = $self->Class;
+ my $attrs = $self->Attributes;
+
+ my %session;
+
+ local $@;
+ eval { tie %session, $class, $id, $attrs };
+ eval { tie %session, $class, undef, $attrs } if $@;
+ if ( $@ ) {
+ die loc("RT couldn't store your session.") . "\n"
+ . loc("This may mean that that the directory '[_1]' isn't writable or a database table is missing or corrupt.",
+ $RT::MasonSessionDir)
+ . "\n\n"
+ . $@;
+ }
+
+ return tied %session;
+}
+
+1;
diff --git a/rt/lib/RT/Interface/Web/Standalone.pm b/rt/lib/RT/Interface/Web/Standalone.pm
index f625dd8..12bd276 100755
--- a/rt/lib/RT/Interface/Web/Standalone.pm
+++ b/rt/lib/RT/Interface/Web/Standalone.pm
@@ -1,8 +1,8 @@
# BEGIN BPS TAGGED BLOCK {{{
#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -45,12 +45,15 @@
# those contributions and any derivatives thereof.
#
# END BPS TAGGED BLOCK }}}
-package RT::Interface::Web::Standalone;
use strict;
+use warnings;
+package RT::Interface::Web::Standalone;
+
use base 'HTTP::Server::Simple::Mason';
use RT::Interface::Web::Handler;
use RT::Interface::Web;
+use URI;
sub handler_class { "RT::Interface::Web::Handler" }
@@ -64,7 +67,7 @@ sub setup_escapes {
}
sub default_mason_config {
- return @RT::MasonParameters;
+ return RT->Config->Get('MasonParameters');
}
sub handle_request {
@@ -72,13 +75,43 @@ sub handle_request {
my $self = shift;
my $cgi = shift;
- Module::Refresh->refresh if $RT::DevelMode;
-
+ Module::Refresh->refresh if RT->Config->Get('DevelMode');
+ RT::ConnectToDatabase() unless RT->InstallMode;
$self->SUPER::handle_request($cgi);
- $RT::Logger->crit($@) if ($@);
-
+ $RT::Logger->crit($@) if $@ && $RT::Logger;
+ warn $@ if $@ && !$RT::Logger;
RT::Interface::Web::Handler->CleanupRequest();
+}
+
+sub net_server {
+ my $self = shift;
+ $self->{rt_net_server} = shift if @_;
+ return $self->{rt_net_server};
+}
+
+
+=head2 print_banner
+
+This routine prints a banner before the server request-handling loop
+starts.
+
+Methods below this point are probably not terribly useful to define
+yourself in subclasses.
+
+=cut
+
+sub print_banner {
+ my $self = shift;
+
+ my $url = URI->new( RT->Config->Get('WebBaseURL'));
+ $url->host('127.0.0.1') if ($url->host() eq 'localhost');
+ $url->port($self->port);
+ print(
+ "You can connect to your server at "
+ . $url->canonical
+ . "\n" );
}
+
1;
diff --git a/rt/lib/RT/Interface/Web/Standalone/PreFork.pm b/rt/lib/RT/Interface/Web/Standalone/PreFork.pm
new file mode 100644
index 0000000..c00f8cd
--- /dev/null
+++ b/rt/lib/RT/Interface/Web/Standalone/PreFork.pm
@@ -0,0 +1,103 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+# <jesse@bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# 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.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (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.)
+#
+# 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.
+#
+# END BPS TAGGED BLOCK }}}
+
+use warnings;
+use strict;
+
+package RT::Interface::Web::Standalone::PreFork;
+use base qw/Net::Server::PreFork/;
+
+my %option_map = (
+ min_servers => 'StandaloneMinServers',
+ max_servers => 'StandaloneMaxServers',
+ min_spare_servers => 'StandaloneMinSpareServers',
+ max_spare_servers => 'StandaloneMaxSpareServers',
+ max_requests => 'StandaloneMaxRequests',
+);
+
+=head2 default_values
+
+Produces the default values for L<Net::Server> configuration from RT's config
+files.
+
+=cut
+
+sub default_values {
+ my %forking = (
+ map { $_ => RT->Config->Get( $option_map{$_} ) }
+ grep { defined( RT->Config->Get( $option_map{$_} ) ) }
+ keys %option_map,
+ );
+
+ return {
+ %forking,
+ log_level => 1,
+ RT->Config->Get('NetServerOptions')
+ };
+}
+
+=head2 post_bind_hook
+
+After binding to the specified ports, let the user know that the server is
+prepared to handle connections.
+
+=cut
+
+sub post_bind_hook {
+ my $self = shift;
+ my @ports = @{ $self->{server}->{port} };
+
+ print $0
+ . ": You can connect to your server at "
+ . (join ' , ', map { "http://localhost:$_/" } @ports)
+ . "\n";
+
+ $self->SUPER::post_bind_hook(@_);
+}
+
+1;