summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Interface
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT/Interface')
-rw-r--r--rt/lib/RT/Interface/CLI.pm195
-rwxr-xr-xrt/lib/RT/Interface/Email.pm533
-rwxr-xr-xrt/lib/RT/Interface/Email.pm.orig81
-rw-r--r--[-rwxr-xr-x]rt/lib/RT/Interface/Email/Auth/Crypt.pm (renamed from rt/lib/RT/Interface/Email/Auth/GnuPG.pm)189
-rw-r--r--rt/lib/RT/Interface/REST.pm59
-rw-r--r--rt/lib/RT/Interface/Web.pm1358
-rw-r--r--rt/lib/RT/Interface/Web.pm.orig3454
-rw-r--r--rt/lib/RT/Interface/Web/Handler.pm96
-rw-r--r--rt/lib/RT/Interface/Web/Menu.pm14
-rw-r--r--rt/lib/RT/Interface/Web/Middleware/StaticHeaders.pm80
-rwxr-xr-xrt/lib/RT/Interface/Web/QueryBuilder/Tree.pm7
-rw-r--r--rt/lib/RT/Interface/Web/Request.pm3
-rw-r--r--rt/lib/RT/Interface/Web/Session.pm30
13 files changed, 1755 insertions, 4344 deletions
diff --git a/rt/lib/RT/Interface/CLI.pm b/rt/lib/RT/Interface/CLI.pm
index 5faa8ca..f992f16 100644
--- a/rt/lib/RT/Interface/CLI.pm
+++ b/rt/lib/RT/Interface/CLI.pm
@@ -49,10 +49,11 @@
package RT::Interface::CLI;
use strict;
use warnings;
-use RT;
+
+use RT::Base;
use base 'Exporter';
-our @EXPORT_OK = qw(CleanEnv GetCurrentUser GetMessageContent debug loc);
+our @EXPORT_OK = qw(CleanEnv GetCurrentUser debug loc Init);
=head1 NAME
@@ -60,27 +61,18 @@ our @EXPORT_OK = qw(CleanEnv GetCurrentUser GetMessageContent debug loc);
=head1 SYNOPSIS
- use lib "/path/to/rt/libraries/";
-
- use RT::Interface::CLI qw(CleanEnv
- GetCurrentUser GetMessageContent loc);
-
- #Clean out all the nasties from the environment
- CleanEnv();
+ use lib "/opt/rt4/local/lib", "/opt/rt4/lib";
- #let's talk to RT'
- use RT;
+ use RT::Interface::CLI qw(GetCurrentUser Init loc);
- #Load RT's config file
- RT::LoadConfig();
+ # Process command-line arguments, load the configuration, and connect
+ # to the database
+ Init();
- # Connect to the database. set up loggign
- RT::Init();
-
- #Get the current user all loaded
+ # Get the current user all loaded
my $CurrentUser = GetCurrentUser();
- print loc('Hello!'); # Synonym of $CuurentUser->loc('Hello!');
+ print loc('Hello!'); # Synonym of $CurrentUser->loc('Hello!');
=head1 DESCRIPTION
@@ -98,11 +90,13 @@ Removes some of the nastiest nasties from the user's environment.
=cut
sub CleanEnv {
+ RT->Deprecated( Remove => "4.4" );
+
$ENV{'PATH'} = '/bin:/usr/bin'; # or whatever you need
$ENV{'CDPATH'} = '' if defined $ENV{'CDPATH'};
$ENV{'SHELL'} = '/bin/sh' if defined $ENV{'SHELL'};
$ENV{'ENV'} = '' if defined $ENV{'ENV'};
- $ENV{'IFS'} = '' if defined $ENV{'IFS'};
+ $ENV{'IFS'} = '' if defined $ENV{'IFS'};
}
@@ -121,28 +115,26 @@ loaded with that user. if the current user isn't found, returns a copy of RT::N
=cut
sub GetCurrentUser {
-
+
require RT::CurrentUser;
-
+
#Instantiate a user object
-
- my $Gecos= ($^O eq 'MSWin32') ? Win32::LoginName() : (getpwuid($<))[0];
+
+ my $Gecos= (getpwuid($<))[0];
#If the current user is 0, then RT will assume that the User object
#is that of the currentuser.
$CurrentUser = RT::CurrentUser->new();
$CurrentUser->LoadByGecos($Gecos);
-
+
unless ($CurrentUser->Id) {
- $RT::Logger->debug("No user with a unix login of '$Gecos' was found. ");
+ $RT::Logger->error("No user with a GECOS (unix login) of '$Gecos' was found.");
}
return($CurrentUser);
}
-
-
=head2 loc
Synonym of $CurrentUser->loc().
@@ -156,82 +148,9 @@ sub loc {
}
-
-
-=head2 GetMessageContent
-
-Takes two arguments a source file and a boolean "edit". If the source file
-is undef or "", assumes an empty file. Returns an edited file as an
-array of lines.
-
-=cut
-
-sub GetMessageContent {
- my %args = ( Source => undef,
- Content => undef,
- Edit => undef,
- CurrentUser => undef,
- @_);
- my $source = $args{'Source'};
-
- my $edit = $args{'Edit'};
-
- my $currentuser = $args{'CurrentUser'};
- my @lines;
-
- use File::Temp qw/ tempfile/;
-
- #Load the sourcefile, if it's been handed to us
- if ($source) {
- open( SOURCE, '<', $source ) or die $!;
- @lines = (<SOURCE>) or die $!;
- close (SOURCE) or die $!;
- }
- elsif ($args{'Content'}) {
- @lines = split('\n',$args{'Content'});
- }
- #get us a tempfile.
- my ($fh, $filename) = tempfile();
-
- #write to a tmpfile
- for (@lines) {
- print $fh $_;
- }
- close ($fh) or die $!;
-
- #Edit the file if we need to
- if ($edit) {
-
- unless ($ENV{'EDITOR'}) {
- $RT::Logger->crit('No $EDITOR variable defined');
- return undef;
- }
- system ($ENV{'EDITOR'}, $filename);
- }
-
- open( READ, '<', $filename ) or die $!;
- my @newlines = (<READ>);
- close (READ) or die $!;
-
- unlink ($filename) unless (debug());
- return(\@newlines);
-
-}
-
-
-
sub debug {
- my $val = shift;
- my ($debug);
- if ($val) {
- $RT::Logger->debug($val);
- if ($debug) {
- print STDERR "$val\n";
- }
- }
- if ($debug) {
- return(1);
- }
+ RT->Deprecated( Remove => "4.4", Instead => '$RT::Logger->debug' );
+ $RT::Logger->debug(@_);
}
sub ShowHelp {
@@ -249,6 +168,78 @@ sub ShowHelp {
);
}
+=head2 Init
+
+A shim for L<Getopt::Long/GetOptions> which automatically adds a
+C<--help> option if it is not supplied. It then calls L<RT/LoadConfig>
+and L<RT/Init>.
+
+It sets the C<LogToSTDERR> setting to C<warning>, to ensure that the
+user sees all relevant warnings. It also adds C<--quiet> and
+C<--verbose> options, which adjust the C<LogToSTDERR> value to C<error>
+or C<debug>, respectively.
+
+=cut
+
+sub Init {
+ require Getopt::Long;
+ require Pod::Usage;
+
+ my %exists;
+ my @args;
+ my $hash;
+ if (ref $_[0]) {
+ $hash = shift(@_);
+ for (@_) {
+ m/^([a-zA-Z0-9-]+)/;
+ $exists{$1}++;
+ push @args, $_ => \($hash->{$1});
+ }
+ } else {
+ $hash = {};
+ @args = @_;
+ while (@_) {
+ my $key = shift(@_);
+ $exists{$key}++;
+ shift(@_);
+ }
+ }
+
+ push @args, "help|h!" => \($hash->{help})
+ unless $exists{help};
+
+ push @args, "verbose|v!" => \($hash->{verbose})
+ unless $exists{verbose};
+
+ push @args, "quiet|q!" => \($hash->{quiet})
+ unless $exists{quiet};
+
+ my $ok = Getopt::Long::GetOptions( @args );
+ Pod::Usage::pod2usage(1) if not $ok and not defined wantarray;
+
+ return unless $ok;
+
+ Pod::Usage::pod2usage({ verbose => 2})
+ if not $exists{help} and $hash->{help};
+
+ require RT;
+ RT::LoadConfig();
+
+ if (not $exists{quiet} and $hash->{quiet}) {
+ RT->Config->Set(LogToSTDERR => "error");
+ } elsif (not $exists{verbose} and $hash->{verbose}) {
+ RT->Config->Set(LogToSTDERR => "debug");
+ } else {
+ RT->Config->Set(LogToSTDERR => "warning");
+ }
+
+ RT::Init();
+
+ $| = 1;
+
+ return $ok;
+}
+
RT::Base->_ImportOverlays();
1;
diff --git a/rt/lib/RT/Interface/Email.pm b/rt/lib/RT/Interface/Email.pm
index f860461..87e24e7 100755
--- a/rt/lib/RT/Interface/Email.pm
+++ b/rt/lib/RT/Interface/Email.pm
@@ -50,27 +50,25 @@ package RT::Interface::Email;
use strict;
use warnings;
+use 5.010;
use Email::Address;
use MIME::Entity;
use RT::EmailParser;
use File::Temp;
-use UNIVERSAL::require;
use Mail::Mailer ();
use Text::ParseWords qw/shellwords/;
+use RT::Util 'safe_run_child';
+use File::Spec;
BEGIN {
use base 'Exporter';
use vars qw ( @EXPORT_OK);
- # set the version for version checking
- our $VERSION = 2.0;
-
# your exported package globals go here,
# as well as any optionally exported functions
@EXPORT_OK = qw(
&CreateUser
- &GetMessageContent
&CheckForLoops
&CheckForSuspiciousSender
&CheckForAutoGenerated
@@ -165,17 +163,16 @@ sub CheckForSuspiciousSender {
=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.
+Takes a HEAD object of L<MIME::Head> class and returns true if message is
+autogenerated. Checks C<Precedence>, C<Auto-Submitted>, and
+C<X-FC-Machinegenerated> fields of the head in tests.
=cut
sub CheckForAutoGenerated {
my $head = shift;
- my $Precedence = $head->get("Precedence") || "";
- if ( $Precedence =~ /^(bulk|junk)/i ) {
+ if (grep { /^(bulk|junk)/i } $head->get_all("Precedence")) {
return (1);
}
@@ -331,7 +328,7 @@ sub WillSignEncrypt {
my $attachment = delete $args{Attachment};
my $ticket = delete $args{Ticket};
- if ( not RT->Config->Get('GnuPG')->{'Enable'} ) {
+ if ( not RT->Config->Get('Crypt')->{'Enable'} ) {
$args{Sign} = $args{Encrypt} = 0;
return wantarray ? %args : 0;
}
@@ -367,13 +364,6 @@ sub SendEmail {
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;
@@ -396,13 +386,35 @@ sub SendEmail {
return -1;
}
+ if (my $precedence = RT->Config->Get('DefaultMailPrecedence')
+ and !$args{'Entity'}->head->get("Precedence")
+ ) {
+ $args{'Entity'}->head->replace( 'Precedence', Encode::encode("UTF-8",$precedence) );
+ }
+
if ( $TransactionObj && !$TicketObj
&& $TransactionObj->ObjectType eq 'RT::Ticket' )
{
$TicketObj = $TransactionObj->Object;
}
- if ( RT->Config->Get('GnuPG')->{'Enable'} ) {
+ my $head = $args{'Entity'}->head;
+ unless ( $head->get('Date') ) {
+ require RT::Date;
+ my $date = RT::Date->new( RT->SystemUser );
+ $date->SetToNow;
+ $head->replace( 'Date', Encode::encode("UTF-8",$date->RFC2822( Timezone => 'server' ) ) );
+ }
+ unless ( $head->get('MIME-Version') ) {
+ # We should never have to set the MIME-Version header
+ $head->replace( 'MIME-Version', '1.0' );
+ }
+ unless ( $head->get('Content-Transfer-Encoding') ) {
+ # fsck.com #5959: Since RT sends 8bit mail, we should say so.
+ $head->replace( 'Content-Transfer-Encoding', '8bit' );
+ }
+
+ if ( RT->Config->Get('Crypt')->{'Enable'} ) {
%args = WillSignEncrypt(
%args,
Attachment => $TransactionObj ? $TransactionObj->Attachments->First : undef,
@@ -412,26 +424,15 @@ sub SendEmail {
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', Encode::encode( "UTF-8", $date->RFC2822( Timezone => 'server' ) ) );
- }
-
my $mail_command = RT->Config->Get('MailCommand');
- if ($mail_command eq 'testfile' and not $Mail::Mailer::testfile::config{outfile}) {
- $Mail::Mailer::testfile::config{outfile} = File::Temp->new;
- $RT::Logger->info("Storing outgoing emails in $Mail::Mailer::testfile::config{outfile}");
- }
-
# 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 = shellwords(RT->Config->Get('SendmailArguments'));
+ push @args, "-t" unless grep {$_ eq "-t"} @args;
# SetOutgoingMailFrom and bounces conflict, since they both want -f
if ( $args{'Bounce'} ) {
@@ -441,14 +442,15 @@ sub SendEmail {
my $Overrides = RT->Config->Get('OverrideOutgoingMailFrom') || {};
if ($TicketObj) {
- my $QueueName = $TicketObj->QueueObj->Name;
- my $QueueAddressOverride = $Overrides->{$QueueName};
+ my $Queue = $TicketObj->QueueObj;
+ my $QueueAddressOverride = $Overrides->{$Queue->id}
+ || $Overrides->{$Queue->Name};
if ($QueueAddressOverride) {
$OutgoingMailAddress = $QueueAddressOverride;
} else {
- $OutgoingMailAddress ||= $TicketObj->QueueObj->CorrespondAddress
- || RT->Config->Get('CorrespondAddress');
+ $OutgoingMailAddress ||= $Queue->CorrespondAddress
+ || RT->Config->Get('CorrespondAddress');
}
}
elsif ($Overrides->{'Default'}) {
@@ -502,62 +504,42 @@ sub SendEmail {
}
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.");
- if ($TicketObj) {
- _RecordSendEmailFailure( $TicketObj );
- }
- return 0;
+ } elsif ( $mail_command eq 'mbox' ) {
+ my $now = RT::Date->new(RT->SystemUser);
+ $now->SetToNow;
+
+ state $logfile;
+ unless ($logfile) {
+ my $when = $now->ISO( Timezone => "server" );
+ $when =~ s/\s+/-/g;
+ $logfile = "$RT::VarPath/$when.mbox";
+ $RT::Logger->info("Storing outgoing emails in $logfile");
}
- # duplicate head as we want drop Bcc field
- my $head = $args{'Entity'}->head->dup;
- my @recipients = map $_->address, map
- Email::Address->parse(Encode::decode("UTF-8", $head->get($_))),
- qw(To Cc Bcc);
- $head->delete('Bcc');
-
- my $sender = RT->Config->Get('SMTPFrom')
- || Encode::decode( "UTF-8", $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." );
- if ( $TicketObj ) {
- _RecordSendEmailFailure( $TicketObj );
- }
+ my $fh;
+ unless (open($fh, ">>", $logfile)) {
+ $RT::Logger->crit( "Can't open mbox file $logfile: $!" );
return 0;
}
- }
- else {
+ my $content = $args{Entity}->stringify;
+ $content =~ s/^(>*From )/>$1/mg;
+ print $fh "From $ENV{USER}\@localhost ".localtime."\n";
+ print $fh $content, "\n";
+ close $fh;
+ } 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, grep {$_ ne "-t"}
+ split(/\s+/, RT->Config->Get('SendmailArguments'));
+ } elsif ( $mail_command eq 'testfile' ) {
+ unless ($Mail::Mailer::testfile::config{outfile}) {
+ $Mail::Mailer::testfile::config{outfile} = File::Temp->new;
+ $RT::Logger->info("Storing outgoing emails in $Mail::Mailer::testfile::config{outfile}");
+ }
+ } else {
push @mailer_args, RT->Config->Get('MailParams');
}
@@ -630,10 +612,10 @@ sub SendEmailUsingTemplate {
return -1;
}
- $mail->head->set( $_ => Encode::encode( "UTF-8", $args{ $_ } ) )
+ $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ $_ } ) )
foreach grep defined $args{$_}, qw(To Cc Bcc From);
- $mail->head->set( $_ => Encode::encode( "UTF-8", $args{ExtraHeaders}{$_} ) )
+ $mail->head->replace( $_ => Encode::encode( "UTF-8", $args{ExtraHeaders}{$_} ) )
foreach keys %{ $args{ExtraHeaders} };
SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
@@ -641,195 +623,58 @@ sub SendEmailUsingTemplate {
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;
-
- my ( $ret, $msg ) = SendForward( %args, Entity => $entity, Transaction => $txn );
- if ($ret) {
- my $ticket = $txn->TicketObj;
- my ( $ret, $msg ) = $ticket->_NewTransaction(
- Type => 'Forward Transaction',
- Field => $txn->id,
- Data => join ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc},
- );
- unless ($ret) {
- $RT::Logger->error("Failed to create transaction: $msg");
- }
- }
- return ( $ret, $msg );
-}
-
-=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',
- Description => 'forwarded ticket',
- );
- $entity->add_part( $_ ) foreach
- map $_->ContentAsMIME,
- @{ $txns->ItemsArrayRef };
-
- my ( $ret, $msg ) = SendForward(
- %args,
- Entity => $entity,
- Ticket => $ticket,
- Template => 'Forward Ticket',
- );
-
- if ($ret) {
- my ( $ret, $msg ) = $ticket->_NewTransaction(
- Type => 'Forward Ticket',
- Field => $ticket->id,
- Data => join ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc},
- );
- unless ($ret) {
- $RT::Logger->error("Failed to create transaction: $msg");
- }
- }
-
- return ( $ret, $msg );
-
-}
-
-=head2 SendForward Entity => undef, Ticket => undef, Transaction => undef, Template => undef, To => '', Cc => '', Bcc => ''
+=head2 GetForwardFrom Ticket => undef, Transaction => undef
-Forwards an Entity representing Ticket or Transaction as 'message/rfc822'. Entity is wrapped into Template.
+Resolve the From field to use in forward mail
=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 {
- $RT::Logger->warning($msg);
- }
- unless ( $mail ) {
- $RT::Logger->warning("Couldn't generate email using template '$args{Template}'");
+sub GetForwardFrom {
+ my %args = ( Ticket => undef, Transaction => undef, @_ );
+ my $txn = $args{Transaction};
+ my $ticket = $args{Ticket} || $txn->Object;
- 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',
- Charset => "UTF-8",
- Data => Encode::encode( "UTF-8", $description ),
- );
+ if ( RT->Config->Get('ForwardFromUser') ) {
+ return ( $txn || $ticket )->CurrentUser->EmailAddress;
}
-
- $mail->head->set( $_ => EncodeToMIME( String => $args{$_} ) )
- foreach grep defined $args{$_}, qw(To Cc Bcc);
-
- $mail->make_multipart unless $mail->is_multipart;
- $mail->add_part( $entity );
-
- my $from;
- unless (defined $mail->head->get('Subject')) {
- my $subject = '';
- $subject = $txn->Subject if $txn;
- $subject ||= $ticket->Subject if $ticket;
-
- unless ( RT->Config->Get('ForwardFromUser') ) {
- # XXX: what if want to forward txn of other object than ticket?
- $subject = AddSubjectTag( $subject, $ticket );
- }
-
- $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) );
+ else {
+ return $ticket->QueueObj->CorrespondAddress
+ || RT->Config->Get('CorrespondAddress');
}
-
- $mail->head->set(
- From => EncodeToMIME(
- String => GetForwardFrom( Transaction => $txn, Ticket => $ticket )
- )
- );
-
- 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("Sent email successfully"));
}
-=head2 GetForwardFrom Ticket => undef, Transaction => undef
+=head2 GetForwardAttachments Ticket => undef, Transaction => undef
-Resolve the From field to use in forward mail
+Resolve the Attachments to forward
=cut
-sub GetForwardFrom {
+sub GetForwardAttachments {
my %args = ( Ticket => undef, Transaction => undef, @_ );
my $txn = $args{Transaction};
my $ticket = $args{Ticket} || $txn->Object;
- if ( RT->Config->Get('ForwardFromUser') ) {
- return ( $txn || $ticket )->CurrentUser->EmailAddress;
+ my $attachments = RT::Attachments->new( $ticket->CurrentUser );
+ if ($txn) {
+ $attachments->Limit( FIELD => 'TransactionId', VALUE => $txn->id );
}
else {
- return $ticket->QueueObj->CorrespondAddress
- || RT->Config->Get('CorrespondAddress');
+ $attachments->LimitByTicket( $ticket->id );
+ $attachments->Limit(
+ ALIAS => $attachments->TransactionAlias,
+ FIELD => 'Type',
+ OPERATOR => 'IN',
+ VALUE => [ qw(Create Correspond) ],
+ );
}
+ return $attachments;
}
+
=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.
+Signs and encrypts message using L<RT::Crypt>, 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.
@@ -857,11 +702,12 @@ sub SignEncrypt {
$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 );
+ my %res = RT::Crypt->SignEncrypt( %args );
return 1 unless $res{'exit_code'};
- my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} );
+ my @status = RT::Crypt->ParseStatus(
+ Protocol => $res{'Protocol'}, Status => $res{'status'},
+ );
my @bad_recipients;
foreach my $line ( @status ) {
@@ -925,7 +771,7 @@ sub SignEncrypt {
}
# redo without broken recipients
- %res = RT::Crypt::GnuPG::SignEncrypt( %args );
+ %res = RT::Crypt->SignEncrypt( %args );
return 0 if $res{'exit_code'};
return 1;
@@ -1203,7 +1049,7 @@ sub DeleteRecipientsFromHead {
my %skip = map { lc $_ => 1 } @_;
foreach my $field ( qw(To Cc Bcc) ) {
- $head->set( $field => Encode::encode( "UTF-8",
+ $head->replace( $field => Encode::encode( "UTF-8",
join ', ', map $_->format, grep !$skip{ lc $_->address },
Email::Address->parse( Encode::decode( "UTF-8", $head->get( $field ) ) ) )
);
@@ -1254,17 +1100,35 @@ sub SetInReplyTo {
}
push @references, @id, @rtid;
if ( $args{'Ticket'} ) {
- my $pseudo_ref = '<RT-Ticket-'. $args{'Ticket'}->id .'@'. RT->Config->Get('Organization') .'>';
+ my $pseudo_ref = PseudoReference( $args{'Ticket'} );
push @references, $pseudo_ref unless grep $_ eq $pseudo_ref, @references;
}
- @references = splice @references, 4, -6
+ splice @references, 4, -6
if @references > 10;
my $mail = $args{'Message'};
- $mail->head->set( 'In-Reply-To' => Encode::encode( "UTF-8", join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
- $mail->head->set( 'References' => Encode::encode( "UTF-8", join ' ', @references) );
+ $mail->head->replace( 'In-Reply-To' => Encode::encode( "UTF-8", join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
+ $mail->head->replace( 'References' => Encode::encode( "UTF-8", join ' ', @references) );
}
+sub PseudoReference {
+ my $ticket = shift;
+ return '<RT-Ticket-'. $ticket->id .'@'. RT->Config->Get('Organization') .'>';
+}
+
+=head2 ExtractTicketId
+
+Passed a MIME::Entity. Returns a ticket id or undef to signal 'new ticket'.
+
+This is a great entry point if you need to customize how ticket ids are
+handled for your site. RT-Extension-RepliesToResolved demonstrates one
+possible use for this extension.
+
+If the Subject of this ticket is modified, it will be reloaded by the
+mail gateway code before Ticket creation.
+
+=cut
+
sub ExtractTicketId {
my $entity = shift;
@@ -1273,19 +1137,29 @@ sub ExtractTicketId {
return ParseTicketId( $subject );
}
+=head2 ParseTicketId
+
+Takes a string and searches for [subjecttag #id]
+
+Returns the id if a match is found. Otherwise returns undef.
+
+=cut
+
sub ParseTicketId {
my $Subject = shift;
my $rtname = RT->Config->Get('rtname');
my $test_name = RT->Config->Get('EmailSubjectTagRegex') || qr/\Q$rtname\E/i;
+ # We use @captures and pull out the last capture value to guard against
+ # someone using (...) instead of (?:...) in $EmailSubjectTagRegex.
my $id;
- if ( $Subject =~ s/\[$test_name\s+\#(\d+)\s*\]//i ) {
- $id = $1;
+ if ( my @captures = $Subject =~ /\[$test_name\s+\#(\d+)\s*\]/i ) {
+ $id = $captures[-1];
} else {
foreach my $tag ( RT->System->SubjectTag ) {
- next unless $Subject =~ s/\[\Q$tag\E\s+\#(\d+)\s*\]//i;
- $id = $1;
+ next unless my @captures = $Subject =~ /\[\Q$tag\E\s+\#(\d+)\s*\]/i;
+ $id = $captures[-1];
last;
}
}
@@ -1434,6 +1308,10 @@ sub Gateway {
push @mail_plugins, "Auth::MailFrom" unless @mail_plugins;
@mail_plugins = _LoadPlugins( @mail_plugins );
+ #Set up a queue object
+ my $SystemQueueObj = RT::Queue->new( RT->SystemUser );
+ $SystemQueueObj->Load( $args{'queue'} );
+
my %skip_plugin;
foreach my $class( grep !ref, @mail_plugins ) {
# check if we should apply filter before decoding
@@ -1445,6 +1323,8 @@ sub Gateway {
next unless $check_cb->(
Message => $Message,
RawMessageRef => \$args{'message'},
+ Queue => $SystemQueueObj,
+ Actions => \@actions,
);
$skip_plugin{ $class }++;
@@ -1456,6 +1336,8 @@ sub Gateway {
my ($status, $msg) = $Code->(
Message => $Message,
RawMessageRef => \$args{'message'},
+ Queue => $SystemQueueObj,
+ Actions => \@actions,
);
next if $status > 0;
@@ -1514,10 +1396,6 @@ sub Gateway {
$Right = 'CreateTicket';
}
- #Set up a queue object
- my $SystemQueueObj = RT::Queue->new( RT->SystemUser );
- $SystemQueueObj->Load( $args{'queue'} );
-
# We can safely have no queue of we have a known-good ticket
unless ( $SystemTicket->id || $SystemQueueObj->id ) {
return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef );
@@ -1572,6 +1450,8 @@ sub Gateway {
return ( 0, $result, undef );
}
+ $head->replace('X-RT-Interface' => 'Email');
+
# if plugin's updated SystemTicket then update arguments
$args{'ticket'} = $SystemTicket->Id if $SystemTicket && $SystemTicket->Id;
@@ -1591,8 +1471,6 @@ sub Gateway {
);
}
- $head->replace('X-RT-Interface' => 'Email');
-
my ( $id, $Transaction, $ErrStr ) = $Ticket->Create(
Queue => $SystemQueueObj->Id,
Subject => $NewSubject,
@@ -1932,9 +1810,10 @@ sub IsCorrectAction {
sub _RecordSendEmailFailure {
my $ticket = shift;
if ($ticket) {
- $ticket->_RecordNote(
- NoteType => 'SystemError',
- Content => "Sending the previous mail has failed. Please contact your admin, they can find more details in the logs.",
+ $ticket->_NewTransaction(
+ Type => "SystemError",
+ Data => "Sending the previous mail has failed. Please contact your admin, they can find more details in the logs.", #loc
+ ActivateScrips => 0,
);
return 1;
}
@@ -1944,6 +1823,118 @@ sub _RecordSendEmailFailure {
}
}
+=head2 ConvertHTMLToText HTML
+
+Takes HTML characters and converts it to plain text characters.
+Appropriate for generating a plain text part from an HTML part of an
+email. Returns undef if conversion fails.
+
+=cut
+
+sub ConvertHTMLToText {
+ return _HTMLFormatter()->(@_);
+}
+
+sub _HTMLFormatter {
+ state $formatter;
+ return $formatter if defined $formatter;
+
+ my $wanted = RT->Config->Get("HTMLFormatter");
+
+ my @order;
+ if ($wanted) {
+ @order = ($wanted, "core");
+ } else {
+ @order = ("w3m", "elinks", "links", "html2text", "lynx", "core");
+ }
+ # Always fall back to core, even if it is not listed
+ for my $prog (@order) {
+ if ($prog eq "core") {
+ RT->Logger->debug("Using internal Perl HTML -> text conversion");
+ require HTML::FormatText::WithLinks::AndTables;
+ $formatter = \&_HTMLFormatText;
+ } else {
+ unless (HTML::FormatExternal->require) {
+ RT->Logger->warn("HTML::FormatExternal is not installed; falling back to internal perl formatter")
+ if $wanted;
+ next;
+ }
+
+ my $path = $prog =~ s{(.*/)}{} ? $1 : undef;
+ my $package = "HTML::FormatText::" . ucfirst($prog);
+ unless ($package->require) {
+ RT->Logger->warn("$prog is not a valid formatter provided by HTML::FormatExternal")
+ if $wanted;
+ next;
+ }
+
+ if ($path) {
+ local $ENV{PATH} = $path;
+ local $ENV{HOME} = File::Spec->tmpdir();
+ if (not defined $package->program_version) {
+ RT->Logger->warn("Could not find or run external '$prog' HTML formatter in $path$prog")
+ if $wanted;
+ next;
+ }
+ } else {
+ local $ENV{PATH} = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+ unless defined $ENV{PATH};
+ local $ENV{HOME} = File::Spec->tmpdir();
+ if (not defined $package->program_version) {
+ RT->Logger->warn("Could not find or run external '$prog' HTML formatter in \$PATH ($ENV{PATH}) -- you may need to install it or provide the full path")
+ if $wanted;
+ next;
+ }
+ }
+
+ RT->Logger->debug("Using $prog for HTML -> text conversion");
+ $formatter = sub {
+ my $html = shift;
+ my $text = RT::Util::safe_run_child {
+ local $ENV{PATH} = $path || $ENV{PATH}
+ || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
+ local $ENV{HOME} = File::Spec->tmpdir();
+ $package->format_string(
+ Encode::encode( "UTF-8", $html ),
+ input_charset => "UTF-8",
+ output_charset => "UTF-8",
+ leftmargin => 0, rightmargin => 78
+ );
+ };
+ $text = Encode::decode( "UTF-8", $text );
+ return $text;
+ };
+ }
+ RT->Config->Set( HTMLFormatter => $prog );
+ last;
+ }
+ return $formatter;
+}
+
+sub _HTMLFormatText {
+ my $html = shift;
+
+ my $text;
+ eval {
+ $text = HTML::FormatText::WithLinks::AndTables->convert(
+ $html => {
+ leftmargin => 0,
+ rightmargin => 78,
+ no_rowspacing => 1,
+ before_link => '',
+ after_link => ' (%l)',
+ footnote => '',
+ skip_linked_urls => 1,
+ with_emphasis => 0,
+ }
+ );
+ $text //= '';
+ };
+ $RT::Logger->error("Failed to downgrade HTML to plain text: $@") if $@;
+ return $text;
+}
+
+
RT::Base->_ImportOverlays();
1;
diff --git a/rt/lib/RT/Interface/Email.pm.orig b/rt/lib/RT/Interface/Email.pm.orig
index 74120ba..f860461 100755
--- a/rt/lib/RT/Interface/Email.pm.orig
+++ b/rt/lib/RT/Interface/Email.pm.orig
@@ -2,7 +2,7 @@
#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
# <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
@@ -114,7 +114,7 @@ sub CheckForLoops {
my $head = shift;
# If this instance of RT sent it our, we don't want to take it in
- my $RTLoop = $head->get("X-RT-Loop-Prevention") || "";
+ my $RTLoop = Encode::decode( "UTF-8", $head->get("X-RT-Loop-Prevention") || "" );
chomp ($RTLoop); # remove that newline
if ( $RTLoop eq RT->Config->Get('rtname') ) {
return 1;
@@ -253,22 +253,27 @@ sub MailError {
# the colons are necessary to make ->build include non-standard headers
my %entity_args = (
Type => "multipart/mixed",
- From => $args{'From'},
- Bcc => $args{'Bcc'},
- To => $args{'To'},
- Subject => $args{'Subject'},
- 'X-RT-Loop-Prevention:' => RT->Config->Get('rtname'),
+ From => Encode::encode( "UTF-8", $args{'From'} ),
+ Bcc => Encode::encode( "UTF-8", $args{'Bcc'} ),
+ To => Encode::encode( "UTF-8", $args{'To'} ),
+ Subject => EncodeToMIME( String => $args{'Subject'} ),
+ 'X-RT-Loop-Prevention:' => Encode::encode( "UTF-8", RT->Config->Get('rtname') ),
);
# only set precedence if the sysadmin wants us to
if (defined(RT->Config->Get('DefaultErrorMailPrecedence'))) {
- $entity_args{'Precedence:'} = RT->Config->Get('DefaultErrorMailPrecedence');
+ $entity_args{'Precedence:'} =
+ Encode::encode( "UTF-8", RT->Config->Get('DefaultErrorMailPrecedence') );
}
my $entity = MIME::Entity->build(%entity_args);
SetInReplyTo( Message => $entity, InReplyTo => $args{'MIMEObj'} );
- $entity->attach( Data => $args{'Explanation'} . "\n" );
+ $entity->attach(
+ Type => "text/plain",
+ Charset => "UTF-8",
+ Data => Encode::encode( "UTF-8", $args{'Explanation'} . "\n" ),
+ );
if ( $args{'MIMEObj'} ) {
$args{'MIMEObj'}->sync_headers;
@@ -276,7 +281,7 @@ sub MailError {
}
if ( $args{'Attach'} ) {
- $entity->attach( Data => $args{'Attach'}, Type => 'message/rfc822' );
+ $entity->attach( Data => Encode::encode( "UTF-8", $args{'Attach'} ), Type => 'message/rfc822' );
}
@@ -374,7 +379,7 @@ sub SendEmail {
return 0;
}
- my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
+ my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' );
chomp $msgid;
# If we don't have any recipients to send to, don't send a message;
@@ -411,7 +416,7 @@ sub SendEmail {
require RT::Date;
my $date = RT::Date->new( RT->SystemUser );
$date->SetToNow;
- $args{'Entity'}->head->set( 'Date', $date->RFC2822( Timezone => 'server' ) );
+ $args{'Entity'}->head->set( 'Date', Encode::encode( "UTF-8", $date->RFC2822( Timezone => 'server' ) ) );
}
my $mail_command = RT->Config->Get('MailCommand');
@@ -514,12 +519,13 @@ sub SendEmail {
# 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);
+ my @recipients = map $_->address, map
+ Email::Address->parse(Encode::decode("UTF-8", $head->get($_))),
+ qw(To Cc Bcc);
$head->delete('Bcc');
my $sender = RT->Config->Get('SMTPFrom')
- || $args{'Entity'}->head->get('From');
+ || Encode::decode( "UTF-8", $args{'Entity'}->head->get('From') );
chomp $sender;
my $status = $smtp->mail( $sender )
@@ -624,10 +630,10 @@ sub SendEmailUsingTemplate {
return -1;
}
- $mail->head->set( $_ => Encode::encode_utf8( $args{ $_ } ) )
+ $mail->head->set( $_ => Encode::encode( "UTF-8", $args{ $_ } ) )
foreach grep defined $args{$_}, qw(To Cc Bcc From);
- $mail->head->set( $_ => $args{ExtraHeaders}{$_} )
+ $mail->head->set( $_ => Encode::encode( "UTF-8", $args{ExtraHeaders}{$_} ) )
foreach keys %{ $args{ExtraHeaders} };
SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} );
@@ -760,8 +766,9 @@ sub SendForward {
. $txn->id ." of a ticket #". $txn->ObjectId;
}
$mail = MIME::Entity->build(
- Type => 'text/plain',
- Data => $description,
+ Type => 'text/plain',
+ Charset => "UTF-8",
+ Data => Encode::encode( "UTF-8", $description ),
);
}
@@ -844,7 +851,7 @@ sub SignEncrypt {
);
return 1 unless $args{'Sign'} || $args{'Encrypt'};
- my $msgid = $args{'Entity'}->head->get('Message-ID') || '';
+ my $msgid = Encode::decode( "UTF-8", $args{'Entity'}->head->get('Message-ID') || '' );
chomp $msgid;
$RT::Logger->debug("$msgid Signing message") if $args{'Sign'};
@@ -980,9 +987,6 @@ sub EncodeToMIME {
$value =~ s/\s+$//;
- # 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, '' );
@@ -1087,7 +1091,8 @@ sub ParseCcAddressesFromHead {
&& !IgnoreCcAddress( $_ )
}
map lc $user->CanonicalizeEmailAddress( $_->address ),
- map RT::EmailParser->CleanupAddresses( Email::Address->parse( $args{'Head'}->get( $_ ) ) ),
+ map RT::EmailParser->CleanupAddresses( Email::Address->parse(
+ Encode::decode( "UTF-8", $args{'Head'}->get( $_ ) ) ) ),
qw(To Cc);
}
@@ -1125,7 +1130,7 @@ sub ParseSenderAddressFromHead {
#Figure out who's sending this message.
foreach my $header ( @sender_headers ) {
- my $addr_line = $head->get($header) || next;
+ my $addr_line = Encode::decode( "UTF-8", $head->get($header) ) || next;
my ($addr, $name) = ParseAddressFromHeader( $addr_line );
# only return if the address is not empty
return ($addr, $name, @errors) if $addr;
@@ -1153,7 +1158,7 @@ sub ParseErrorsToAddressFromHead {
foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) {
# If there's a header of that name
- my $headerobj = $head->get($header);
+ my $headerobj = Encode::decode( "UTF-8", $head->get($header) );
if ($headerobj) {
my ( $addr, $name ) = ParseAddressFromHeader($headerobj);
@@ -1198,9 +1203,9 @@ sub DeleteRecipientsFromHead {
my %skip = map { lc $_ => 1 } @_;
foreach my $field ( qw(To Cc Bcc) ) {
- $head->set( $field =>
+ $head->set( $field => Encode::encode( "UTF-8",
join ', ', map $_->format, grep !$skip{ lc $_->address },
- Email::Address->parse( $head->get( $field ) )
+ Email::Address->parse( Encode::decode( "UTF-8", $head->get( $field ) ) ) )
);
}
}
@@ -1233,7 +1238,7 @@ sub SetInReplyTo {
my $get_header = sub {
my @res;
if ( $args{'InReplyTo'}->isa('MIME::Entity') ) {
- @res = $args{'InReplyTo'}->head->get( shift );
+ @res = map {Encode::decode("UTF-8", $_)} $args{'InReplyTo'}->head->get( shift );
} else {
@res = $args{'InReplyTo'}->GetHeader( shift ) || '';
}
@@ -1256,14 +1261,14 @@ sub SetInReplyTo {
if @references > 10;
my $mail = $args{'Message'};
- $mail->head->set( 'In-Reply-To' => Encode::encode_utf8(join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
- $mail->head->set( 'References' => Encode::encode_utf8(join ' ', @references) );
+ $mail->head->set( 'In-Reply-To' => Encode::encode( "UTF-8", join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid;
+ $mail->head->set( 'References' => Encode::encode( "UTF-8", join ' ', @references) );
}
sub ExtractTicketId {
my $entity = shift;
- my $subject = $entity->head->get('Subject') || '';
+ my $subject = Encode::decode( "UTF-8", $entity->head->get('Subject') || '' );
chomp $subject;
return ParseTicketId( $subject );
}
@@ -1468,14 +1473,14 @@ sub Gateway {
my $head = $Message->head;
my $ErrorsTo = ParseErrorsToAddressFromHead( $head );
my $Sender = (ParseSenderAddressFromHead( $head ))[0];
- my $From = $head->get("From");
+ my $From = Encode::decode( "UTF-8", $head->get("From") );
chomp $From if defined $From;
- my $MessageId = $head->get('Message-ID')
+ my $MessageId = Encode::decode( "UTF-8", $head->get('Message-ID') )
|| "<no-message-id-". time . rand(2000) .'@'. RT->Config->Get('Organization') .'>';
#Pull apart the subject line
- my $Subject = $head->get('Subject') || '';
+ my $Subject = Encode::decode( "UTF-8", $head->get('Subject') || '');
chomp $Subject;
# Lets check for mail loops of various sorts.
@@ -1498,7 +1503,7 @@ sub Gateway {
$args{'ticket'} ||= ExtractTicketId( $Message );
# ExtractTicketId may have been overridden, and edited the Subject
- my $NewSubject = $Message->head->get('Subject');
+ my $NewSubject = Encode::decode( "UTF-8", $Message->head->get('Subject') );
chomp $NewSubject;
$SystemTicket = RT::Ticket->new( RT->SystemUser );
@@ -1746,7 +1751,7 @@ sub _RunUnsafeAction {
@_
);
- my $From = $args{Message}->head->get("From");
+ my $From = Encode::decode( "UTF-8", $args{Message}->head->get("From") );
if ( $args{'Action'} =~ /^take$/i ) {
my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id );
@@ -1902,7 +1907,7 @@ sub _HandleMachineGeneratedMail {
# to the scrip. We might want to notify nobody. Or just
# the RT Owner. Or maybe all Privileged watchers.
my ( $Sender, $junk ) = ParseSenderAddressFromHead($head);
- $head->replace( 'RT-Squelch-Replies-To', $Sender );
+ $head->replace( 'RT-Squelch-Replies-To', Encode::encode("UTF-8", $Sender ) );
$head->replace( 'RT-DetectedAutoGenerated', 'true' );
}
return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop );
diff --git a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm b/rt/lib/RT/Interface/Email/Auth/Crypt.pm
index ec409a4..8f8e636 100755..100644
--- a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm
+++ b/rt/lib/RT/Interface/Email/Auth/Crypt.pm
@@ -46,12 +46,30 @@
#
# END BPS TAGGED BLOCK }}}
-package RT::Interface::Email::Auth::GnuPG;
+package RT::Interface::Email::Auth::Crypt;
use strict;
use warnings;
-=head2 GetCurrentUser
+=head1 NAME
+
+RT::Interface::Email::Auth::Crypt - decrypting and verifying protected emails
+
+=head2 DESCRIPTION
+
+This mail plugin decrypts and verifies incoming emails. Supported
+encryption protocols are GnuPG and SMIME.
+
+This code is independant from code that encrypts/sign outgoing emails, so
+it's possible to decrypt data without bringing in encryption. To enable
+it put the module in the mail plugins list:
+
+ Set(@MailPlugins, 'Auth::MailFrom', 'Auth::Crypt', ...other filters...);
+
+C<Auth::Crypt> will not function without C<Auth::MailFrom> listed before
+it.
+
+=head3 GnuPG
To use the gnupg-secured mail gateway, you need to do the following:
@@ -59,48 +77,85 @@ 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(%GnuPGOptions, homedir => '/opt/rt4/var/data/GnuPG');
- Set(@MailPlugins, 'Auth::MailFrom', 'Auth::GnuPG', ...other filter...);
+
+Read also: L<RT::Crypt> and L<RT::Crypt::GnuPG>.
+
+=head3 SMIME
+
+To use the SMIME-secured mail gateway, you need to do the following:
+
+Set up a SMIME key directory with files containing keys for queues'
+addresses and specify the following in your SiteConfig.pm
+
+ Set(%SMIME,
+ Enable => 1,
+ OpenSSL => '/usr/bin/openssl',
+ Keyring => '/opt/rt4/var/data/smime',
+ CAPath => '/opt/rt4/var/data/smime/signing-ca.pem',
+ Passphrase => {
+ 'queue.address@example.com' => 'passphrase',
+ '' => 'fallback',
+ },
+ );
+
+Read also: L<RT::Crypt> and L<RT::Crypt::SMIME>.
=cut
sub ApplyBeforeDecode { return 1 }
-use RT::Crypt::GnuPG;
+use RT::Crypt;
use RT::EmailParser ();
sub GetCurrentUser {
my %args = (
Message => undef,
RawMessageRef => undef,
+ Queue => undef,
+ Actions => undef,
@_
);
- foreach my $p ( $args{'Message'}->parts_DFS ) {
- $p->head->delete($_) for qw(
- X-RT-GnuPG-Status X-RT-Incoming-Encryption
+ # we clean all possible headers
+ my @headers =
+ qw(
+ X-RT-Incoming-Encryption
X-RT-Incoming-Signature X-RT-Privacy
X-RT-Sign X-RT-Encrypt
- );
+ ),
+ map "X-RT-$_-Status", RT::Crypt->Protocols;
+ foreach my $p ( $args{'Message'}->parts_DFS ) {
+ $p->head->delete($_) for @headers;
}
- my $msg = $args{'Message'}->dup;
-
- my ($status, @res) = VerifyDecrypt(
- Entity => $args{'Message'}, AddStatus => 1,
+ my (@res) = RT::Crypt->VerifyDecrypt(
+ %args,
+ Entity => $args{'Message'},
);
- if ( $status && !@res ) {
- $args{'Message'}->head->replace(
- 'X-RT-Incoming-Encryption' => 'Not encrypted'
- );
-
+ if ( !@res ) {
+ if (RT->Config->Get('Crypt')->{'RejectOnUnencrypted'}) {
+ EmailErrorToSender(
+ %args,
+ Template => 'Error: unencrypted message',
+ Arguments => { Message => $args{'Message'} },
+ );
+ return (-1, 'rejected because the message is unencrypted with RejectOnUnencrypted enabled');
+ }
+ else {
+ $args{'Message'}->head->replace(
+ '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");
+ if ( grep {$_->{'exit_code'}} @res ) {
+ my @fail = grep {$_->{status}{Status} ne "DONE"}
+ map { my %ret = %{$_}; map {+{%ret, status => $_}} RT::Crypt->ParseStatus( Protocol => $_->{Protocol}, Status => $_->{status})}
+ @res;
+ for my $fail ( @fail ) {
+ $RT::Logger->warning("Failure during ".$fail->{Protocol}." ". lc($fail->{status}{Operation}) . ": ". $fail->{status}{Message});
+ }
my $reject = HandleErrors( Message => $args{'Message'}, Result => \@res );
return (0, 'rejected because of problems during decrypting and verifying')
if $reject;
@@ -113,14 +168,19 @@ sub GetCurrentUser {
Data => ${ $args{'RawMessageRef'} },
);
- $args{'Message'}->head->replace( 'X-RT-Privacy' => 'PGP' );
-
+ my @found;
+ my @check_protocols = RT::Crypt->EnabledOnIncoming;
foreach my $part ( $args{'Message'}->parts_DFS ) {
my $decrypted;
- my $status = Encode::decode( "UTF-8", $part->head->get( 'X-RT-GnuPG-Status' ) );
- if ( $status ) {
- for ( RT::Crypt::GnuPG::ParseStatus( $status ) ) {
+ foreach my $protocol ( @check_protocols ) {
+ my @status = grep defined && length,
+ map Encode::decode( "UTF-8", $_), $part->head->get( "X-RT-$protocol-Status" );
+ next unless @status;
+
+ push @found, $protocol;
+
+ for ( map RT::Crypt->ParseStatus( Protocol => $protocol, Status => "$_" ), @status ) {
if ( $_->{Operation} eq 'Decrypt' && $_->{Status} eq 'DONE' ) {
$decrypted = 1;
}
@@ -133,11 +193,15 @@ sub GetCurrentUser {
}
$part->head->replace(
- 'X-RT-Incoming-Encryption' =>
+ 'X-RT-Incoming-Encryption' =>
$decrypted ? 'Success' : 'Not encrypted'
);
}
+ my %seen;
+ $args{'Message'}->head->replace( 'X-RT-Privacy' => Encode::encode( "UTF-8", $_ ) )
+ foreach grep !$seen{$_}++, @found;
+
return 1;
}
@@ -152,17 +216,17 @@ sub HandleErrors {
my %sent_once = ();
foreach my $run ( @{ $args{'Result'} } ) {
- my @status = RT::Crypt::GnuPG::ParseStatus( $run->{'status'} );
+ my @status = RT::Crypt->ParseStatus( Protocol => $run->{'Protocol'}, Status => $run->{'status'} );
unless ( $sent_once{'NoPrivateKey'} ) {
unless ( CheckNoPrivateKey( Message => $args{'Message'}, Status => \@status ) ) {
$sent_once{'NoPrivateKey'}++;
- $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnMissingPrivateKey'};
+ $reject = 1 if RT->Config->Get('Crypt')->{'RejectOnMissingPrivateKey'};
}
}
unless ( $sent_once{'BadData'} ) {
unless ( CheckBadData( Message => $args{'Message'}, Status => \@status ) ) {
$sent_once{'BadData'}++;
- $reject = 1 if RT->Config->Get('GnuPG')->{'RejectOnBadData'};
+ $reject = 1 if RT->Config->Get('Crypt')->{'RejectOnBadData'};
}
}
}
@@ -184,20 +248,11 @@ sub CheckNoPrivateKey {
$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,
+ return EmailErrorToSender(
+ %args,
Template => 'Error: no private key',
- Arguments => {
- Message => $args{'Message'},
- TicketObj => $args{'Ticket'},
- },
- InReplyTo => $args{'Message'},
+ Arguments => { Message => $args{'Message'} },
);
- unless ( $status ) {
- $RT::Logger->error("Couldn't send 'Error: no private key'");
- }
- return 0;
}
sub CheckBadData {
@@ -208,50 +263,32 @@ sub CheckBadData {
@{ $args{'Status'} };
return 1 unless @bad_data_messages;
- $RT::Logger->error("Couldn't process a message: ". join ', ', @bad_data_messages );
+ return EmailErrorToSender(
+ %args,
+ Template => 'Error: bad encrypted data',
+ Arguments => { Messages => [ @bad_data_messages ] },
+ );
+}
+
+sub EmailErrorToSender {
+ my %args = (@_);
+
+ $args{'Arguments'} ||= {};
+ $args{'Arguments'}{'TicketObj'} ||= $args{'Ticket'};
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'},
- },
+ Template => $args{'Template'},
+ Arguments => $args{'Arguments'},
InReplyTo => $args{'Message'},
);
unless ( $status ) {
- $RT::Logger->error("Couldn't send 'Error: bad GnuPG data'");
+ $RT::Logger->error("Couldn't send '$args{'Template'}''");
}
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;
-}
-
RT::Base->_ImportOverlays();
1;
-
diff --git a/rt/lib/RT/Interface/REST.pm b/rt/lib/RT/Interface/REST.pm
index edfc5d3..b6f9fd3 100644
--- a/rt/lib/RT/Interface/REST.pm
+++ b/rt/lib/RT/Interface/REST.pm
@@ -283,17 +283,52 @@ sub vpush {
# "Normalise" a hash key that's known to be multi-valued.
sub vsplit {
- my ($val) = @_;
+ my ($val, $strip) = @_;
my @words;
-
- foreach my $line (map {split /\n/} (ref $val eq 'ARRAY') ? @$val : ($val||''))
- {
- # XXX: This should become a real parser, ? la Text::ParseWords.
- $line =~ s/^\s+//;
- $line =~ s/\s+$//;
- push @words, split /\s*,\s*/, $line;
+ my @values = map {split /\n/} (ref $val eq 'ARRAY' ? @$val : $val);
+
+ foreach my $line (@values) {
+ while ($line =~ /\S/) {
+ $line =~ s/^
+ \s* # Trim leading whitespace
+ (?:
+ (") # Quoted string
+ ((?>[^\\"]*(?:\\.[^\\"]*)*))"
+ |
+ (') # Single-quoted string
+ ((?>[^\\']*(?:\\.[^\\']*)*))'
+ |
+ q\{(.*?)\} # A perl-ish q{} string; this does
+ # no paren balancing, however, and
+ # only exists for back-compat
+ |
+ (.*?) # Anything else, until the next comma
+ )
+ \s* # Trim trailing whitespace
+ (?:
+ \Z # Finish at end-of-line
+ |
+ , # Or a comma
+ )
+ //xs or last; # There should be no way this match
+ # fails, but add a failsafe to
+ # prevent infinite-looping if it
+ # somehow does.
+ my ($quote, $quoted) = ($1 ? ($1, $2) : $3 ? ($3, $4) : ('', $5 || $6));
+ # Only unquote the quote character, or the backslash -- and
+ # only if we were originally quoted..
+ if ($5) {
+ $quoted =~ s/([\\'])/\\$1/g;
+ $quote = "'";
+ }
+ if ($strip) {
+ $quoted =~ s/\\([\\$quote])/$1/g if $quote;
+ push @words, $quoted;
+ } else {
+ push @words, "$quote$quoted$quote";
+ }
+ }
}
-
return \@words;
}
@@ -324,11 +359,17 @@ sub process_attachments {
}
my $info = $cgi->uploadInfo($fh);
+ # If Content-ID exists for attachment then we need multipart/related
+ # to be able to refer to this Content-Id in core of mime message
+ if($info->{'Content-ID'}) {
+ $entity->head->set('Content-Type', 'multipart/related');
+ }
my $new_entity = $entity->attach(
Path => $tmp_fn,
Type => $info->{'Content-Type'} || guess_media_type($tmp_fn),
Filename => $file,
Disposition => $info->{'Content-Disposition'} || "attachment",
+ 'Content-ID' => $info->{'Content-ID'},
);
$new_entity->bodyhandle->{'_dirty_hack_to_save_a_ref_tmp_fh'} = $tmp_fh;
$i++;
diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm
index 4cf3a07..dad6a8e 100644
--- a/rt/lib/RT/Interface/Web.pm
+++ b/rt/lib/RT/Interface/Web.pm
@@ -70,6 +70,7 @@ use RT::Interface::Web::Session;
use Digest::MD5 ();
use List::MoreUtils qw();
use JSON qw();
+use Plack::Util;
=head2 SquishedCSS $style
@@ -99,6 +100,37 @@ sub SquishedJS {
return $js;
}
+=head2 JSFiles
+
+=cut
+
+sub JSFiles {
+ return qw{
+ jquery-1.9.1.min.js
+ jquery_noconflict.js
+ jquery-ui-1.10.0.custom.min.js
+ jquery-ui-timepicker-addon.js
+ jquery-ui-patch-datepicker.js
+ jquery.modal.min.js
+ jquery.modal-defaults.js
+ jquery.cookie.js
+ titlebox-state.js
+ i18n.js
+ util.js
+ autocomplete.js
+ jquery.event.hover-1.0.js
+ superfish.js
+ supersubs.js
+ jquery.supposition.js
+ history-folding.js
+ cascaded.js
+ forms.js
+ event-registration.js
+ late.js
+ /static/RichText/ckeditor.js
+ }, RT->Config->Get('JSFiles');
+}
+
=head2 ClearSquished
Removes the cached CSS and JS entries, forcing them to be regenerated
@@ -111,13 +143,13 @@ sub ClearSquished {
%SQUISHED_CSS = ();
}
-=head2 EscapeUTF8 SCALARREF
+=head2 EscapeHTML SCALARREF
does a css-busting but minimalist escaping of whatever html you're passing in.
=cut
-sub EscapeUTF8 {
+sub EscapeHTML {
my $ref = shift;
return unless defined $$ref;
@@ -130,7 +162,15 @@ sub EscapeUTF8 {
$$ref =~ s/'/&#39;/g;
}
-
+# Back-compat
+# XXX: Remove in 4.4
+sub EscapeUTF8 {
+ RT->Deprecated(
+ Instead => "EscapeHTML",
+ Remove => "4.4",
+ );
+ EscapeHTML(@_);
+}
=head2 EscapeURI SCALARREF
@@ -148,13 +188,15 @@ sub EscapeURI {
=head2 EncodeJSON SCALAR
-Encodes the SCALAR to JSON and returns a JSON string. SCALAR may be a simple
-value or a reference.
+Encodes the SCALAR to JSON and returns a JSON Unicode (B<not> UTF-8) string.
+SCALAR may be a simple value or a reference.
=cut
sub EncodeJSON {
- JSON::to_json(shift, { utf8 => 1, allow_nonref => 1 });
+ my $s = JSON::to_json(shift, { allow_nonref => 1 });
+ $s =~ s{/}{\\/}g;
+ return $s;
}
sub _encode_surrogates {
@@ -190,36 +232,29 @@ sub WebCanonicalizeInfo {
-=head2 WebExternalAutoInfo($user);
+=head2 WebRemoteUserAutocreateInfo($user);
-Returns a hash of user attributes, used when WebExternalAuto is set.
+Returns a hash of user attributes, used when WebRemoteUserAutocreate is set.
=cut
-sub WebExternalAutoInfo {
+sub WebRemoteUserAutocreateInfo {
my $user = shift;
my %user_info;
# 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::UserAutocreateDefaultsOnLogin
+ || ( ref($RT::UserAutocreateDefaultsOnLogin) && not exists $RT::UserAutocreateDefaultsOnLogin->{Privileged} ) )
{
$user_info{'Privileged'} = 1;
}
- if ( $^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/ ) {
-
- # Populate fields with information from Unix /etc/passwd
-
- 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' ) {
-
- # Populate fields with information from NT domain controller
- }
+ # Populate fields with information from Unix /etc/passwd
+ my ( $comments, $realname ) = ( getpwnam($user) )[ 5, 6 ];
+ $user_info{'Comments'} = $comments if defined $comments;
+ $user_info{'RealName'} = $realname if defined $realname;
# and return the wad of stuff
return {%user_info};
@@ -278,7 +313,7 @@ sub HandleRequest {
MaybeShowNoAuthPage($ARGS);
- AttemptExternalAuth($ARGS) if RT->Config->Get('WebExternalAuthContinuous') or not _UserLoggedIn();
+ AttemptExternalAuth($ARGS) if RT->Config->Get('WebRemoteUserContinuous') or not _UserLoggedIn();
_ForceLogout() unless _UserLoggedIn();
@@ -300,7 +335,7 @@ sub HandleRequest {
# REST urls get a special 401 response
if ($m->request_comp->path =~ m{^/REST/\d+\.\d+/}) {
- $HTML::Mason::Commands::r->content_type("text/plain");
+ $HTML::Mason::Commands::r->content_type("text/plain; charset=utf-8");
$m->error_format("text");
$m->out("RT/$RT::VERSION 401 Credentials required\n");
$m->out("\n$msg\n") if $msg;
@@ -441,6 +476,18 @@ params.
=cut
sub TangentForLogin {
+ my $login = TangentForLoginURL(@_);
+ Redirect( RT->Config->Get('WebBaseURL') . $login );
+}
+
+=head2 TangentForLoginURL [HASH]
+
+Returns a URL suitable for tangenting for login. Optionally takes a hash which
+is dumped into query params.
+
+=cut
+
+sub TangentForLoginURL {
my $ARGS = shift;
my $hash = SetNextPage($ARGS);
my %query = (@_, next => $hash);
@@ -448,9 +495,9 @@ sub TangentForLogin {
$query{mobile} = 1
if $HTML::Mason::Commands::m->request_comp->path =~ m{^/m(/|$)};
- my $login = RT->Config->Get('WebURL') . 'NoAuth/Login.html?';
+ my $login = RT->Config->Get('WebPath') . '/NoAuth/Login.html?';
$login .= $HTML::Mason::Commands::m->comp('/Elements/QueryString', %query);
- Redirect($login);
+ return $login;
}
=head2 TangentForLoginWithError ERROR
@@ -645,24 +692,24 @@ sub ShowRequestedPage {
sub AttemptExternalAuth {
my $ARGS = shift;
- return unless ( RT->Config->Get('WebExternalAuth') );
+ return unless ( RT->Config->Get('WebRemoteUserAuth') );
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
+ my $logged_in_external_user = _UserLoggedIn() && $HTML::Mason::Commands::session{'WebExternallyAuthed'};
- # do we actually have a REMOTE_USER equivlent?
- if ( RT::Interface::Web::WebCanonicalizeInfo() ) {
- my $orig_user = $user;
+ # If RT is configured for external auth, let's go through and get REMOTE_USER
+ # Do we actually have a REMOTE_USER or equivalent? We only check auth if
+ # 1) we have no logged in user, or 2) we have a user who is externally
+ # authed. If we have a logged in user who is internally authed, don't
+ # check remote user otherwise we may log them out.
+ if (RT::Interface::Web::WebCanonicalizeInfo()
+ and (not _UserLoggedIn() or $logged_in_external_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;
- }
+ my $load_method = RT->Config->Get('WebRemoteUserGecos') ? 'LoadByGecos' : 'Load';
my $next = RemoveNextPage($ARGS->{'next'});
$next = $next->{'url'} if ref $next;
@@ -670,12 +717,12 @@ sub AttemptExternalAuth {
$HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
$HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user);
- if ( RT->Config->Get('WebExternalAuto') and not _UserLoggedIn() ) {
+ if ( RT->Config->Get('WebRemoteUserAutocreate') 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') : {} },
+ %{ ref RT->Config->Get('UserAutocreateDefaultsOnLogin') ? RT->Config->Get('UserAutocreateDefaultsOnLogin') : {} },
Name => $user,
Gecos => $user,
);
@@ -683,10 +730,10 @@ sub AttemptExternalAuth {
if ($val) {
# now get user specific information, to better create our user.
- my $new_user_info = RT::Interface::Web::WebExternalAutoInfo($user);
+ my $new_user_info = RT::Interface::Web::WebRemoteUserAutocreateInfo($user);
# set the attributes that have been defined.
- foreach my $attribute ( $UserObj->WritableAttributes ) {
+ foreach my $attribute ( $UserObj->WritableAttributes, qw(Privileged Disabled) ) {
$m->callback(
Attribute => $attribute,
User => $user,
@@ -699,19 +746,13 @@ sub AttemptExternalAuth {
}
$HTML::Mason::Commands::session{'CurrentUser'}->Load($user);
} else {
-
- # we failed to successfully create the user. abort abort abort.
- delete $HTML::Mason::Commands::session{'CurrentUser'};
-
- if (RT->Config->Get('WebFallbackToInternalAuth')) {
- TangentForLoginWithError($ARGS, 'Cannot create user: [_1]', $msg);
- } else {
- $m->abort();
- }
+ RT->Logger->error("Couldn't auto-create user '$user' when attempting WebRemoteUser: $msg");
+ AbortExternalAuth( Error => "UserAutocreateDefaultsOnLogin" );
}
}
if ( _UserLoggedIn() ) {
+ $HTML::Mason::Commands::session{'WebExternallyAuthed'} = 1;
$m->callback( %$ARGS, CallbackName => 'ExternalAuthSuccessfulLogin', CallbackPage => '/autohandler' );
# It is possible that we did a redirect to the login page,
# if the external auth allows lack of auth through with no
@@ -723,28 +764,45 @@ sub AttemptExternalAuth {
# straight-up external auth would always redirect to /
# when you first hit it.
} else {
- delete $HTML::Mason::Commands::session{'CurrentUser'};
- $user = $orig_user;
-
- unless ( RT->Config->Get('WebFallbackToInternalAuth') ) {
- TangentForLoginWithError($ARGS, 'You are not an authorized user');
- }
- }
- } elsif ( RT->Config->Get('WebFallbackToInternalAuth') ) {
- unless ( defined $HTML::Mason::Commands::session{'CurrentUser'} ) {
- # XXX unreachable due to prior defaulting in HandleRequest (check c34d108)
- TangentForLoginWithError($ARGS, 'You are not an authorized user');
+ # Couldn't auth with the REMOTE_USER provided because an RT
+ # user doesn't exist and we're configured not to create one.
+ RT->Logger->error("Couldn't find internal user for '$user' when attempting WebRemoteUser and RT is not configured for auto-creation. Refer to `perldoc $RT::BasePath/docs/authentication.pod` if you want to allow auto-creation.");
+ AbortExternalAuth(
+ Error => "NoInternalUser",
+ User => $user,
+ );
}
- } 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'};
+ }
+ elsif ($logged_in_external_user) {
+ # The logged in external user was deauthed by the auth system and we
+ # should kick them out.
+ AbortExternalAuth( Error => "Deauthorized" );
+ }
+ elsif (not RT->Config->Get('WebFallbackToRTLogin')) {
+ # Abort if we don't want to fallback internally
+ AbortExternalAuth( Error => "NoRemoteUser" );
}
}
+sub AbortExternalAuth {
+ my %args = @_;
+ my $error = $args{Error} ? "/Errors/WebRemoteUser/$args{Error}" : undef;
+ my $m = $HTML::Mason::Commands::m;
+ my $r = $HTML::Mason::Commands::r;
+
+ _ForceLogout();
+
+ # Clear the decks, not that we should have partial content.
+ $m->clear_buffer;
+
+ $r->status(403);
+ $m->comp($error, %args)
+ if $error and $m->comp_exists($error);
+
+ # Return a 403 Forbidden or we may fallback to a login page with no form
+ $m->abort(403);
+}
+
sub AttemptPasswordAuthentication {
my $ARGS = shift;
return unless defined $ARGS->{user} && defined $ARGS->{pass};
@@ -770,7 +828,7 @@ sub AttemptPasswordAuthentication {
InstantiateNewSession();
$HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
- $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
+ $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler', RedirectTo => \$next );
# Really the only time we don't want to redirect here is if we were
# passed user and pass as query params in the URL.
@@ -838,6 +896,30 @@ sub SendSessionCookie {
$HTML::Mason::Commands::r->err_headers_out->{'Set-Cookie'} = $cookie->as_string;
}
+=head2 GetWebURLFromRequest
+
+People may use different web urls instead of C<$WebURL> in config.
+Return the web url current user is using.
+
+=cut
+
+sub GetWebURLFromRequest {
+
+ my $uri = URI->new( RT->Config->Get('WebURL') );
+
+ if ( defined $ENV{HTTPS} and $ENV{'HTTPS'} eq 'on' ) {
+ $uri->scheme('https');
+ }
+ else {
+ $uri->scheme('http');
+ }
+
+ # [rt3.fsck.com #12716] Apache recommends use of $SERVER_HOST
+ $uri->host( $ENV{'SERVER_HOST'} || $ENV{'HTTP_HOST'} || $ENV{'SERVER_NAME'} );
+ $uri->port( $ENV{'SERVER_PORT'} );
+ return "$uri"; # stringify to be consistent with WebURL in config
+}
+
=head2 Redirect URL
This routine ells the current user's browser to redirect to URL.
@@ -868,15 +950,10 @@ sub Redirect {
&& $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');
- }
-
- # [rt3.fsck.com #12716] Apache recommends use of $SERVER_HOST
- $uri->host( $ENV{'SERVER_HOST'} || $ENV{'HTTP_HOST'} || $ENV{'SERVER_NAME'});
- $uri->port( $ENV{'SERVER_PORT'} );
+ my $env_uri = URI->new(GetWebURLFromRequest());
+ $uri->scheme($env_uri->scheme);
+ $uri->host($env_uri->host);
+ $uri->port($env_uri->port);
}
# not sure why, but on some systems without this call mason doesn't
@@ -890,13 +967,13 @@ sub Redirect {
$HTML::Mason::Commands::m->abort;
}
-=head2 CacheControlExpiresHeaders
+=head2 GetStaticHeaders
-set both Cache-Control and Expires http headers
+return an arrayref of Headers (currently, Cache-Control and Expires).
=cut
-sub CacheControlExpiresHeaders {
+sub GetStaticHeaders {
my %args = @_;
my $Visibility = 'private';
@@ -913,13 +990,28 @@ sub CacheControlExpiresHeaders {
? sprintf "max-age=%d, %s", $args{Time}, $Visibility
: 'no-cache'
;
- $HTML::Mason::Commands::r->headers_out->{'Cache-Control'} = $CacheControl;
my $expires = RT::Date->new(RT->SystemUser);
$expires->SetToNow;
$expires->AddSeconds( $args{Time} ) if $args{Time};
- $HTML::Mason::Commands::r->headers_out->{'Expires'} = $expires->RFC2616;
+ return [
+ Expires => $expires->RFC2616,
+ 'Cache-Control' => $CacheControl,
+ ];
+}
+
+=head2 CacheControlExpiresHeaders
+
+set both Cache-Control and Expires http headers
+
+=cut
+
+sub CacheControlExpiresHeaders {
+ Plack::Util::header_iter( GetStaticHeaders(@_), sub {
+ my ( $key, $val ) = @_;
+ $HTML::Mason::Commands::r->headers_out->{$key} = $val;
+ } );
}
=head2 StaticFileHeaders
@@ -932,20 +1024,12 @@ This routine could really use _accurate_ heuristics. (XXX TODO)
=cut
sub StaticFileHeaders {
- my $date = RT::Date->new(RT->SystemUser);
-
# remove any cookie headers -- if it is cached publicly, it
# shouldn't include anyone's cookie!
delete $HTML::Mason::Commands::r->err_headers_out->{'Set-Cookie'};
# Expire things in a month.
CacheControlExpiresHeaders( Time => 'forever' );
-
- # 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
- # $date->Set( Value => $^T );
- # $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = $date->RFC2616;
}
=head2 ComponentPathIsSafe PATH
@@ -1109,7 +1193,7 @@ sub StripContent {
# Check for plaintext sig
return '' if not $html and $content =~ /^(--)?\Q$sig\E$/;
- # Check for html-formatted sig; we don't use EscapeUTF8 here
+ # Check for html-formatted sig; we don't use EscapeHTML here
# because we want to precisely match the escapting that FCKEditor
# uses.
$sig =~ s/&/&amp;/g;
@@ -1270,6 +1354,16 @@ sub ComponentRoots {
return @roots;
}
+sub StaticRoots {
+ my $self = shift;
+ my @static = (
+ $RT::LocalStaticPath,
+ (map { $_->StaticDir } @{RT->Plugins}),
+ $RT::StaticPath,
+ );
+ return grep { $_ and -d $_ } @static;
+}
+
our %is_whitelisted_component = (
# The RSS feed embeds an auth token in the path, but query
# information for the search. Because it's a straight-up read, in
@@ -1284,6 +1378,7 @@ our %is_whitelisted_component = (
'/Search/Simple.html' => 1,
'/m/tickets/search' => 1,
'/Search/Chart.html' => 1,
+ '/User/Search.html' => 1,
# This page takes Attachment and Transaction argument to figure
# out what to show, but it's read only and will deny information if you
@@ -1466,7 +1561,7 @@ sub ExpandCSRFToken {
if ($data->{attach}) {
my $filename = $data->{attach}{filename};
my $mime = $data->{attach}{mime};
- $HTML::Mason::Commands::session{'Attachments'}{$filename}
+ $HTML::Mason::Commands::session{'Attachments'}{$ARGS->{'Token'}||''}{$filename}
= $mime;
}
@@ -1550,10 +1645,178 @@ sub PotentialPageAction {
return "";
}
+=head2 RewriteInlineImages PARAMHASH
+
+Turns C<< <img src="cid:..."> >> elements in HTML into working images pointing
+back to RT's stored copy.
+
+Takes the following parameters:
+
+=over 4
+
+=item Content
+
+Scalar ref of the HTML content to rewrite. Modified in place to support the
+most common use-case.
+
+=item Attachment
+
+The L<RT::Attachment> object from which the Content originates.
+
+=item Related (optional)
+
+Array ref of related L<RT::Attachment> objects to use for C<Content-ID> matching.
+
+Defaults to the result of the C<Siblings> method on the passed Attachment.
+
+=item AttachmentPath (optional)
+
+The base path to use when rewriting C<src> attributes.
+
+Defaults to C< $WebPath/Ticket/Attachment >
+
+=back
+
+In scalar context, returns the number of elements rewritten.
+
+In list content, returns the attachments IDs referred to by the rewritten <img>
+elements, in the order found. There may be duplicates.
+
+=cut
+
+sub RewriteInlineImages {
+ my %args = (
+ Content => undef,
+ Attachment => undef,
+ Related => undef,
+ AttachmentPath => RT->Config->Get('WebPath')."/Ticket/Attachment",
+ @_
+ );
+
+ return unless defined $args{Content}
+ and ref $args{Content} eq 'SCALAR'
+ and defined $args{Attachment};
+
+ my $related_part = $args{Attachment}->Closest("multipart/related")
+ or return;
+
+ $args{Related} ||= $related_part->Children->ItemsArrayRef;
+ return unless @{$args{Related}};
+
+ my $content = $args{'Content'};
+ my @rewritten;
+
+ require HTML::RewriteAttributes::Resources;
+ $$content = HTML::RewriteAttributes::Resources->rewrite($$content, sub {
+ my $cid = shift;
+ my %meta = @_;
+ return $cid unless lc $meta{tag} eq 'img'
+ and lc $meta{attr} eq 'src'
+ and $cid =~ s/^cid://i;
+
+ for my $attach (@{$args{Related}}) {
+ if (($attach->GetHeader('Content-ID') || '') =~ /^(<)?\Q$cid\E(?(1)>)$/) {
+ push @rewritten, $attach->Id;
+ return "$args{AttachmentPath}/" . $attach->TransactionId . '/' . $attach->Id;
+ }
+ }
+
+ # No attachments means this is a bogus CID. Just pass it through.
+ RT->Logger->debug(qq[Found bogus inline image src="cid:$cid"]);
+ return "cid:$cid";
+ });
+ return @rewritten;
+}
+
+=head2 GetCustomFieldInputName(CustomField => $cf_object, Object => $object, Grouping => $grouping_name)
+
+Returns the standard custom field input name; this is complementary to
+L</_ParseObjectCustomFieldArgs>. Takes the following arguments:
+
+=over
+
+=item CustomField => I<L<RT::CustomField> object>
+
+Required.
+
+=item Object => I<object>
+
+The object that the custom field is applied to; optional. If omitted,
+defaults to a new object of the appropriate class for the custom field.
+
+=item Grouping => I<CF grouping>
+
+The grouping that the custom field is being rendered in. Groupings
+allow a custom field to appear in more than one location per form.
+
+=back
+
+=cut
+
+sub GetCustomFieldInputName {
+ my %args = (
+ CustomField => undef,
+ Object => undef,
+ Grouping => undef,
+ @_,
+ );
+
+ my $name = GetCustomFieldInputNamePrefix(%args);
+
+ if ( $args{CustomField}->Type eq 'Select' ) {
+ if ( $args{CustomField}->RenderType eq 'List' and $args{CustomField}->SingleValue ) {
+ $name .= 'Value';
+ }
+ else {
+ $name .= 'Values';
+ }
+ }
+ elsif ( $args{CustomField}->Type =~ /^(?:Binary|Image)$/ ) {
+ $name .= 'Upload';
+ }
+ elsif ( $args{CustomField}->Type =~ /^(?:Date|DateTime|Text|Wikitext)$/ ) {
+ $name .= 'Values';
+ }
+ else {
+ if ( $args{CustomField}->SingleValue ) {
+ $name .= 'Value';
+ }
+ else {
+ $name .= 'Values';
+ }
+ }
+
+ return $name;
+}
+
+=head2 GetCustomFieldInputNamePrefix(CustomField => $cf_object, Object => $object, Grouping => $grouping_name)
+
+Returns the standard custom field input name prefix(without "Value" or alike suffix)
+
+=cut
+
+sub GetCustomFieldInputNamePrefix {
+ my %args = (
+ CustomField => undef,
+ Object => undef,
+ Grouping => undef,
+ @_,
+ );
+
+ my $prefix = join '-', 'Object', ref( $args{Object} ) || $args{CustomField}->ObjectTypeFromLookupType,
+ ( $args{Object} && $args{Object}->id ? $args{Object}->id : '' ),
+ 'CustomField' . ( $args{Grouping} ? ":$args{Grouping}" : '' ),
+ $args{CustomField}->id, '';
+
+ return $prefix;
+}
+
package HTML::Mason::Commands;
use vars qw/$r $m %session/;
+use Scalar::Util qw(blessed);
+
sub Menu {
return $HTML::Mason::Commands::m->notes('menu');
}
@@ -1566,7 +1829,96 @@ sub PageWidgets {
return $HTML::Mason::Commands::m->notes('page-widgets');
}
+sub RenderMenu {
+ my %args = (toplevel => 1, parent_id => '', depth => 0, @_);
+ return unless $args{'menu'};
+
+ my ($menu, $depth, $toplevel, $id, $parent_id)
+ = @args{qw(menu depth toplevel id parent_id)};
+
+ my $interp = $m->interp;
+ my $web_path = RT->Config->Get('WebPath');
+
+ my $res = '';
+ $res .= ' ' x $depth;
+ $res .= '<ul';
+ $res .= ' id="'. $interp->apply_escapes($id, 'h') .'"'
+ if $id;
+ $res .= ' class="toplevel"' if $toplevel;
+ $res .= ">\n";
+
+ for my $child ($menu->children) {
+ $res .= ' 'x ($depth+1);
+
+ my $item_id = lc(($parent_id? "$parent_id-" : "") .$child->key);
+ $item_id =~ s/\s/-/g;
+ my $eitem_id = $interp->apply_escapes($item_id, 'h');
+ $res .= qq{<li id="li-$eitem_id"};
+
+ my @classes;
+ push @classes, 'has-children' if $child->has_children;
+ push @classes, 'active' if $child->active;
+ $res .= ' class="'. join( ' ', @classes ) .'"'
+ if @classes;
+
+ $res .= '>';
+
+ if ( my $tmp = $child->raw_html ) {
+ $res .= $tmp;
+ } else {
+ $res .= qq{<a id="$eitem_id" class="menu-item};
+ if ( $tmp = $child->class ) {
+ $res .= ' '. $interp->apply_escapes($tmp, 'h');
+ }
+ $res .= '"';
+
+ my $path = $child->path;
+ my $url = (not $path or $path =~ m{^\w+:/}) ? $path : $web_path . $path;
+ $url ||= "#";
+ $res .= ' href="'. $interp->apply_escapes($url, 'h') .'"';
+
+ if ( $tmp = $child->target ) {
+ $res .= ' target="'. $interp->apply_escapes($tmp, 'h') .'"'
+ }
+
+ if ($child->attributes) {
+ for my $key (keys %{$child->attributes}) {
+ my ($name, $value) = map { $interp->apply_escapes($_, 'h') }
+ $key, $child->attributes->{$key};
+ $res .= " $name=\"$value\"";
+ }
+ }
+ $res .= '>';
+
+ if ( $child->escape_title ) {
+ $res .= $interp->apply_escapes($child->title, 'h');
+ } else {
+ $res .= $child->title;
+ }
+ $res .= '</a>';
+ }
+
+ if ( $child->has_children ) {
+ $res .= "\n";
+ $res .= RenderMenu(
+ menu => $child,
+ toplevel => 0,
+ parent_id => $item_id,
+ depth => $depth+1,
+ return => 1,
+ );
+ $res .= "\n";
+ $res .= ' ' x ($depth+1);
+ }
+ $res .= "</li>\n";
+ }
+ $res .= ' ' x $depth;
+ $res .= '</ul>';
+ return $res if $args{'return'};
+ $m->print($res);
+ return '';
+}
=head2 loc ARRAY
@@ -1725,9 +2077,10 @@ sub CreateTicket {
my (@Actions);
- my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
+ my $current_user = $session{'CurrentUser'};
+ my $Ticket = RT::Ticket->new( $current_user );
- my $Queue = RT::Queue->new( $session{'CurrentUser'} );
+ my $Queue = RT::Queue->new( $current_user );
unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
Abort('Queue not found');
}
@@ -1738,12 +2091,12 @@ sub CreateTicket {
my $due;
if ( defined $ARGS{'Due'} and $ARGS{'Due'} =~ /\S/ ) {
- $due = RT::Date->new( $session{'CurrentUser'} );
+ $due = RT::Date->new( $current_user );
$due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
}
my $starts;
if ( defined $ARGS{'Starts'} and $ARGS{'Starts'} =~ /\S/ ) {
- $starts = RT::Date->new( $session{'CurrentUser'} );
+ $starts = RT::Date->new( $current_user );
$starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
}
@@ -1751,34 +2104,44 @@ sub CreateTicket {
Content => $ARGS{Content},
ContentType => $ARGS{ContentType},
StripSignature => 1,
- CurrentUser => $session{'CurrentUser'},
+ CurrentUser => $current_user,
);
+ my $date_now = RT::Date->new( $current_user );
+ $date_now->SetToNow;
my $MIMEObj = MakeMIMEEntity(
Subject => $ARGS{'Subject'},
- From => $ARGS{'From'},
+ From => $ARGS{'From'} || $current_user->EmailAddress,
+ To => $ARGS{'To'} || $Queue->CorrespondAddress
+ || RT->Config->Get('CorrespondAddress'),
Cc => $ARGS{'Cc'},
+ Date => $date_now->RFC2822(Timezone => 'user'),
Body => $sigless,
Type => $ARGS{'ContentType'},
Interface => RT::Interface::Web::MobileClient() ? 'Mobile' : 'Web',
);
- if ( $ARGS{'Attachments'} ) {
- my $rv = $MIMEObj->make_multipart;
- $RT::Logger->error("Couldn't make multipart message")
- if !$rv || $rv !~ /^(?:DONE|ALREADY)$/;
+ my @attachments;
+ if ( my $tmp = $session{'Attachments'}{ $ARGS{'Token'} || '' } ) {
+ push @attachments, grep $_, map $tmp->{$_}, sort keys %$tmp;
- foreach ( map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} } ) {
- unless ($_) {
- $RT::Logger->error("Couldn't add empty attachemnt");
- next;
- }
- $MIMEObj->add_part($_);
- }
+ delete $session{'Attachments'}{ $ARGS{'Token'} || '' }
+ unless $ARGS{'KeepAttachments'};
+ $session{'Attachments'} = $session{'Attachments'}
+ if @attachments;
+ }
+ if ( $ARGS{'Attachments'} ) {
+ push @attachments, grep $_, map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} };
+ }
+ if ( @attachments ) {
+ $MIMEObj->make_multipart;
+ $MIMEObj->add_part( $_ ) foreach @attachments;
}
for my $argument (qw(Encrypt Sign)) {
- $MIMEObj->head->replace( "X-RT-$argument" => $ARGS{$argument} ? 1 : 0 );
+ if ( defined $ARGS{ $argument } ) {
+ $MIMEObj->head->replace( "X-RT-$argument" => $ARGS{$argument} ? 1 : 0 );
+ }
}
my %create_args = (
@@ -1799,16 +2162,25 @@ sub CreateTicket {
Status => $ARGS{'Status'},
Due => $due ? $due->ISO : undef,
Starts => $starts ? $starts->ISO : undef,
- MIMEObj => $MIMEObj
+ MIMEObj => $MIMEObj,
+ SquelchMailTo => $ARGS{'SquelchMailTo'},
+ TransSquelchMailTo => $ARGS{'TransSquelchMailTo'},
);
- my @txn_squelch;
- foreach my $type (qw(Requestor Cc AdminCc)) {
- push @txn_squelch, map $_->address, Email::Address->parse( $create_args{$type} )
- if grep $_ eq $type || $_ eq ( $type . 's' ), @{ $ARGS{'SkipNotification'} || [] };
+ if ($ARGS{'DryRun'}) {
+ $create_args{DryRun} = 1;
+ $create_args{Owner} ||= $RT::Nobody->Id;
+ $create_args{Requestor} ||= $session{CurrentUser}->EmailAddress;
+ $create_args{Subject} ||= '';
+ $create_args{Status} ||= $Queue->Lifecycle->DefaultOnCreate,
+ } else {
+ my @txn_squelch;
+ foreach my $type (qw(Requestor Cc AdminCc)) {
+ push @txn_squelch, map $_->address, Email::Address->parse( $create_args{$type} )
+ if grep $_ eq $type || $_ eq ( $type . 's' ), @{ $ARGS{'SkipNotification'} || [] };
+ }
+ push @{$create_args{TransSquelchMailTo}}, @txn_squelch;
}
- $create_args{TransSquelchMailTo} = \@txn_squelch
- if @txn_squelch;
if ( $ARGS{'AttachTickets'} ) {
require RT::Action::SendEmail;
@@ -1818,69 +2190,16 @@ sub CreateTicket {
: ( $ARGS{'AttachTickets'} ) );
}
- foreach my $arg ( keys %ARGS ) {
- next if $arg =~ /-(?:Magic|Category)$/;
-
- if ( $arg =~ /^Object-RT::Transaction--CustomField-/ ) {
- $create_args{$arg} = $ARGS{$arg};
- }
-
- # Object-RT::Ticket--CustomField-3-Values
- elsif ( $arg =~ /^Object-RT::Ticket--CustomField-(\d+)/ ) {
- my $cfid = $1;
-
- my $cf = RT::CustomField->new( $session{'CurrentUser'} );
- $cf->SetContextObject( $Queue );
- $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);
- next;
- }
-
- my $type = $cf->Type;
-
- 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;
-
- $create_args{"CustomField-$cfid"} = \@values;
- }
- }
-
- # 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',
+ my %cfs = ProcessObjectCustomFieldUpdatesForCreate(
+ ARGSRef => \%ARGS,
+ ContextObject => $Queue,
);
- foreach my $key ( keys %map ) {
- next unless $ARGS{$key};
- $create_args{ $map{$key} } = [ grep $_, split ' ', $ARGS{$key} ];
- }
+ my %links = ProcessLinksForCreate( ARGSRef => \%ARGS );
+
+ my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args, %links, %cfs);
+ return $Trans if $ARGS{DryRun};
- my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
unless ($id) {
Abort($ErrMsg);
}
@@ -1948,10 +2267,18 @@ sub ProcessUpdateMessage {
@_
);
- if ( $args{ARGSRef}->{'UpdateAttachments'}
- && !keys %{ $args{ARGSRef}->{'UpdateAttachments'} } )
- {
- delete $args{ARGSRef}->{'UpdateAttachments'};
+ my @attachments;
+ if ( my $tmp = $session{'Attachments'}{ $args{'ARGSRef'}{'Token'} || '' } ) {
+ push @attachments, grep $_, map $tmp->{$_}, sort keys %$tmp;
+
+ delete $session{'Attachments'}{ $args{'ARGSRef'}{'Token'} || '' }
+ unless $args{'KeepAttachments'};
+ $session{'Attachments'} = $session{'Attachments'}
+ if @attachments;
+ }
+ if ( $args{ARGSRef}{'UpdateAttachments'} ) {
+ push @attachments, grep $_, map $args{ARGSRef}->{UpdateAttachments}{$_},
+ sort keys %{ $args{ARGSRef}->{'UpdateAttachments'} };
}
# Strip the signature
@@ -1973,7 +2300,7 @@ sub ProcessUpdateMessage {
# If, after stripping the signature, we have no message, create a
# Touch transaction if necessary
- if ( not $args{ARGSRef}->{'UpdateAttachments'}
+ if ( not @attachments
and not length $args{ARGSRef}->{'UpdateContent'} )
{
#if ( $args{ARGSRef}->{'UpdateTimeWorked'} ) {
@@ -1993,7 +2320,7 @@ sub ProcessUpdateMessage {
return;
}
- if ( $args{ARGSRef}->{'UpdateSubject'} eq ($args{'TicketObj'}->Subject || '') ) {
+ if ( ($args{ARGSRef}->{'UpdateSubject'}||'') eq ($args{'TicketObj'}->Subject || '') ) {
$args{ARGSRef}->{'UpdateSubject'} = undef;
}
@@ -2017,14 +2344,14 @@ sub ProcessUpdateMessage {
if ( my $msg = $old_txn->Message->First ) {
RT::Interface::Email::SetInReplyTo(
Message => $Message,
- InReplyTo => $msg
+ InReplyTo => $msg,
+ Ticket => $args{'TicketObj'},
);
}
- if ( $args{ARGSRef}->{'UpdateAttachments'} ) {
+ if ( @attachments ) {
$Message->make_multipart;
- $Message->add_part($_) foreach map $args{ARGSRef}->{UpdateAttachments}{$_},
- sort keys %{ $args{ARGSRef}->{'UpdateAttachments'} };
+ $Message->add_part( $_ ) foreach @attachments;
}
if ( $args{ARGSRef}->{'AttachTickets'} ) {
@@ -2036,8 +2363,8 @@ sub ProcessUpdateMessage {
}
my %message_args = (
- Sign => ( $args{ARGSRef}->{'Sign'} ? 1 : 0 ),
- Encrypt => ( $args{ARGSRef}->{'Encrypt'} ? 1 : 0 ),
+ Sign => $args{ARGSRef}->{'Sign'},
+ Encrypt => $args{ARGSRef}->{'Encrypt'},
MIMEObj => $Message,
TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'},
CustomFields => \%txn_customfields,
@@ -2052,11 +2379,11 @@ sub ProcessUpdateMessage {
if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(%message_args);
push( @results, $Description );
- $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
+ $Object->UpdateCustomFields( %{ $args{ARGSRef} } ) if $Object;
} 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;
+ $Object->UpdateCustomFields( %{ $args{ARGSRef} } ) if $Object;
} else {
push( @results,
loc("Update type was neither correspondence nor comment.") . " " . loc("Update not recorded.") );
@@ -2115,36 +2442,39 @@ sub _ProcessUpdateMessageRecipients {
sub ProcessAttachments {
my %args = (
ARGSRef => {},
+ Token => '',
@_
);
- my $ARGSRef = $args{ARGSRef} || {};
+ my $token = $args{'ARGSRef'}{'Token'}
+ ||= $args{'Token'} ||= Digest::MD5::md5_hex( rand(1024) );
+
+ my $update_session = 0;
+
# deal with deleting uploaded attachments
- foreach my $key ( keys %$ARGSRef ) {
- if ( $key =~ m/^DeleteAttach-(.+)$/ ) {
- delete $session{'Attachments'}{$1};
- }
- $session{'Attachments'} = { %{ $session{'Attachments'} || {} } };
+ if ( my $del = $args{'ARGSRef'}{'DeleteAttach'} ) {
+ delete $session{'Attachments'}{ $token }{ $_ }
+ foreach ref $del? @$del : ($del);
+
+ $update_session = 1;
}
# store the uploaded attachment in session
- if ( defined $ARGSRef->{'Attach'} && length $ARGSRef->{'Attach'} )
- { # attachment?
- my $attachment = MakeMIMEEntity( AttachmentFieldName => 'Attach' );
+ my $new = $args{'ARGSRef'}{'Attach'};
+ if ( defined $new && length $new ) {
+ my $attachment = MakeMIMEEntity(
+ AttachmentFieldName => 'Attach'
+ );
# This needs to be decoded because the value is a reference;
# hence it was not decoded along with all of the standard
# arguments in DecodeARGS
- my $file_path = Encode::decode("UTF-8", "$ARGSRef->{'Attach'}");
- $session{'Attachments'} =
- { %{ $session{'Attachments'} || {} }, $file_path => $attachment, };
- }
+ my $file_path = Encode::decode( "UTF-8", "$new");
+ $session{'Attachments'}{ $token }{ $file_path } = $attachment;
- # delete temporary storage entry to make WebUI clean
- unless ( keys %{ $session{'Attachments'} } and $ARGSRef->{'UpdateAttach'} )
- {
- delete $session{'Attachments'};
+ $update_session = 1;
}
+ $session{'Attachments'} = $session{'Attachments'} if $update_session;
}
@@ -2176,7 +2506,7 @@ sub MakeMIMEEntity {
"Message-Id" => Encode::encode( "UTF-8", RT::Interface::Email::GenMessageId ),
"X-RT-Interface" => $args{Interface},
map { $_ => Encode::encode( "UTF-8", $args{ $_} ) }
- grep defined $args{$_}, qw(Subject From Cc)
+ grep defined $args{$_}, qw(Subject From Cc To Date)
);
if ( defined $args{'Body'} && length $args{'Body'} ) {
@@ -2213,7 +2543,7 @@ sub MakeMIMEEntity {
Data => \@content, # Bytes, as read directly from the file, above
);
if ( !$args{'Subject'} && !( defined $args{'Body'} && length $args{'Body'} ) ) {
- $Message->head->set( 'Subject' => Encode::encode( "UTF-8", $filename ) );
+ $Message->head->replace( 'Subject' => Encode::encode( "UTF-8", $filename ) );
}
# Attachment parts really shouldn't get a Message-ID or "interface"
@@ -2277,7 +2607,7 @@ sub ProcessACLChanges {
my $obj;
if ( $object_type eq 'RT::System' ) {
$obj = $RT::System;
- } elsif ( $RT::ACE::OBJECT_TYPES{$object_type} ) {
+ } elsif ( $object_type->DOES('RT::Record::Role::Rights') ) {
$obj = $object_type->new( $session{'CurrentUser'} );
$obj->Load($object_id);
unless ( $obj->id ) {
@@ -2377,7 +2707,7 @@ sub ProcessACLs {
my $obj;
if ( $object_type eq 'RT::System' ) {
$obj = $RT::System;
- } elsif ( $RT::ACE::OBJECT_TYPES{$object_type} ) {
+ } elsif ( $object_type->DOES('RT::Record::Role::Rights') ) {
$obj = $object_type->new( $session{'CurrentUser'} );
$obj->Load($object_id);
unless ( $obj->id ) {
@@ -2627,38 +2957,69 @@ sub ProcessTicketReminders {
if ( $args->{'update-reminders'} ) {
while ( my $reminder = $reminder_collection->Next ) {
- my $resolve_status = $reminder->QueueObj->Lifecycle->ReminderStatusOnResolve;
- if ( $reminder->Status ne $resolve_status && $args->{ 'Complete-Reminder-' . $reminder->id } ) {
- my ($status, $msg) = $Ticket->Reminders->Resolve($reminder);
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
-
+ my $resolve_status = $reminder->LifecycleObj->ReminderStatusOnResolve;
+ my ( $status, $msg, $old_subject, @subresults );
+ if ( $reminder->Status ne $resolve_status
+ && $args->{ 'Complete-Reminder-' . $reminder->id } )
+ {
+ ( $status, $msg ) = $Ticket->Reminders->Resolve($reminder);
+ push @subresults, $msg;
}
- elsif ( $reminder->Status eq $resolve_status && !$args->{ 'Complete-Reminder-' . $reminder->id } ) {
- my ($status, $msg) = $Ticket->Reminders->Open($reminder);
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
+ elsif ( $reminder->Status eq $resolve_status
+ && !$args->{ 'Complete-Reminder-' . $reminder->id } )
+ {
+ ( $status, $msg ) = $Ticket->Reminders->Open($reminder);
+ push @subresults, $msg;
}
- if ( exists( $args->{ 'Reminder-Subject-' . $reminder->id } ) && ( $reminder->Subject ne $args->{ 'Reminder-Subject-' . $reminder->id } )) {
- my ($status, $msg) = $reminder->SetSubject( $args->{ 'Reminder-Subject-' . $reminder->id } ) ;
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
+ if (
+ exists( $args->{ 'Reminder-Subject-' . $reminder->id } )
+ && ( $reminder->Subject ne
+ $args->{ 'Reminder-Subject-' . $reminder->id } )
+ )
+ {
+ $old_subject = $reminder->Subject;
+ ( $status, $msg ) =
+ $reminder->SetSubject(
+ $args->{ 'Reminder-Subject-' . $reminder->id } );
+ push @subresults, $msg;
}
- if ( exists( $args->{ 'Reminder-Owner-' . $reminder->id } ) && ( $reminder->Owner != $args->{ 'Reminder-Owner-' . $reminder->id } )) {
- my ($status, $msg) = $reminder->SetOwner( $args->{ 'Reminder-Owner-' . $reminder->id } , "Force" ) ;
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
+ if (
+ exists( $args->{ 'Reminder-Owner-' . $reminder->id } )
+ && ( $reminder->Owner !=
+ $args->{ 'Reminder-Owner-' . $reminder->id } )
+ )
+ {
+ ( $status, $msg ) =
+ $reminder->SetOwner(
+ $args->{ 'Reminder-Owner-' . $reminder->id }, "Force" );
+ push @subresults, $msg;
}
- if ( exists( $args->{ 'Reminder-Due-' . $reminder->id } ) && $args->{ 'Reminder-Due-' . $reminder->id } ne '' ) {
+ if ( exists( $args->{ 'Reminder-Due-' . $reminder->id } )
+ && $args->{ 'Reminder-Due-' . $reminder->id } ne '' )
+ {
my $DateObj = RT::Date->new( $session{'CurrentUser'} );
+ my $due = $args->{ 'Reminder-Due-' . $reminder->id };
+
$DateObj->Set(
Format => 'unknown',
- Value => $args->{ 'Reminder-Due-' . $reminder->id }
+ Value => $due,
);
- if ( defined $DateObj->Unix && $DateObj->Unix != $reminder->DueObj->Unix ) {
- my ($status, $msg) = $reminder->SetDue( $DateObj->ISO );
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
+ if ( $DateObj->Unix != $reminder->DueObj->Unix ) {
+ ( $status, $msg ) = $reminder->SetDue( $DateObj->ISO );
}
+ else {
+ $msg = loc( "invalid due date: [_1]", $due );
+ }
+
+ push @subresults, $msg;
}
+
+ push @results, map {
+ loc( "Reminder '[_1]': [_2]", $old_subject || $reminder->Subject, $_ )
+ } @subresults;
}
}
@@ -2668,13 +3029,14 @@ sub ProcessTicketReminders {
Format => 'unknown',
Value => $args->{'NewReminder-Due'}
);
- my ( $add_id, $msg ) = $Ticket->Reminders->Add(
+ my ( $status, $msg ) = $Ticket->Reminders->Add(
Subject => $args->{'NewReminder-Subject'},
Owner => $args->{'NewReminder-Owner'},
Due => $due_obj->ISO
);
- if ( $add_id ) {
- push @results, loc("Reminder '[_1]' added", $args->{'NewReminder-Subject'});
+ if ( $status ) {
+ push @results,
+ loc( "Reminder '[_1]': [_2]", $args->{'NewReminder-Subject'}, loc("Created") )
}
else {
push @results, $msg;
@@ -2683,41 +3045,13 @@ sub ProcessTicketReminders {
return @results;
}
-sub ProcessTicketCustomFieldUpdates {
- my %args = @_;
- $args{'Object'} = delete $args{'TicketObj'};
- my $ARGSRef = { %{ $args{'ARGSRef'} } };
-
- # 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+-.*)/ ) {
- $ARGSRef->{"Object-RT::Ticket-$1"} = delete $ARGSRef->{$arg};
- } elsif ( $arg =~ /^CustomField-(\d+-.*)/ ) {
- $ARGSRef->{"Object-RT::Ticket--$1"} = delete $ARGSRef->{$arg};
- } elsif ( $arg =~ /^Object-RT::Transaction-(\d*)-CustomField/ ) {
- delete $ARGSRef->{$arg}; # don't try to update transaction fields
- }
- }
-
- return ProcessObjectCustomFieldUpdates( %args, ARGSRef => $ARGSRef );
-}
-
sub ProcessObjectCustomFieldUpdates {
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};
- }
+ my %custom_fields_to_mod = _ParseObjectCustomFieldArgs($ARGSRef);
# For each of those objects
foreach my $class ( keys %custom_fields_to_mod ) {
@@ -2740,12 +3074,34 @@ sub ProcessObjectCustomFieldUpdates {
$RT::Logger->warning("Couldn't load custom field #$cf");
next;
}
+ my @groupings = sort keys %{ $custom_fields_to_mod{$class}{$id}{$cf} };
+ if (@groupings > 1) {
+ # Check for consistency, in case of JS fail
+ for my $key (qw/AddValue Value Values DeleteValues DeleteValueIds/) {
+ my $base = $custom_fields_to_mod{$class}{$id}{$cf}{$groupings[0]}{$key};
+ $base = [ $base ] unless ref $base;
+ for my $grouping (@groupings[1..$#groupings]) {
+ my $other = $custom_fields_to_mod{$class}{$id}{$cf}{$grouping}{$key};
+ $other = [ $other ] unless ref $other;
+ warn "CF $cf submitted with multiple differing values"
+ if grep {$_} List::MoreUtils::pairwise {
+ no warnings qw(uninitialized);
+ $a ne $b
+ } @{$base}, @{$other};
+ }
+ }
+ # We'll just be picking the 1st grouping in the hash, alphabetically
+ }
push @results,
_ProcessObjectCustomFieldUpdates(
- Prefix => "Object-$class-$id-CustomField-$cf-",
- Object => $Object,
- CustomField => $CustomFieldObj,
- ARGS => $custom_fields_to_mod{$class}{$id}{$cf},
+ Prefix => GetCustomFieldInputNamePrefix(
+ Object => $Object,
+ CustomField => $CustomFieldObj,
+ Grouping => $groupings[0],
+ ),
+ Object => $Object,
+ CustomField => $CustomFieldObj,
+ ARGS => $custom_fields_to_mod{$class}{$id}{$cf}{ $groupings[0] },
);
}
}
@@ -2753,6 +3109,26 @@ sub ProcessObjectCustomFieldUpdates {
return @results;
}
+sub _ParseObjectCustomFieldArgs {
+ my $ARGSRef = shift || {};
+ my %custom_fields_to_mod;
+
+ foreach my $arg ( keys %$ARGSRef ) {
+
+ # format: Object-<object class>-<object id>-CustomField[:<grouping>]-<CF id>-<commands>
+ # you can use GetCustomFieldInputName to generate the complement input name
+ next unless $arg =~ /^Object-([\w:]+)-(\d*)-CustomField(?::(\w+))?-(\d+)-(.*)$/;
+
+ next if $1 eq 'RT::Transaction';# don't try to update transaction fields
+
+ # For each of those objects, find out what custom fields we want to work with.
+ # Class ID CF grouping command
+ $custom_fields_to_mod{$1}{ $2 || 0 }{$4}{$3 || ''}{$5} = $ARGSRef->{$arg};
+ }
+
+ return wantarray ? %custom_fields_to_mod : \%custom_fields_to_mod;
+}
+
sub _ProcessObjectCustomFieldUpdates {
my %args = @_;
my $cf = $args{'CustomField'};
@@ -2762,7 +3138,7 @@ sub _ProcessObjectCustomFieldUpdates {
# 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'} )
+ && ($args{'ARGS'}->{'Values-Magic'}) )
{
delete $args{'ARGS'}->{'Values'};
}
@@ -2771,14 +3147,14 @@ sub _ProcessObjectCustomFieldUpdates {
foreach my $arg ( keys %{ $args{'ARGS'} } ) {
# skip category argument
- next if $arg eq 'Category';
+ next if $arg =~ /-Category$/;
# and TimeUnits
next if $arg eq 'Value-TimeUnits';
# since http won't pass in a form element with a null value, we need
# to fake it
- if ( $arg eq 'Values-Magic' ) {
+ if ( $arg =~ /-Magic$/ ) {
# We don't care about the magic, if there's really a values element;
next if defined $args{'ARGS'}->{'Value'} && length $args{'ARGS'}->{'Value'};
@@ -2791,22 +3167,14 @@ sub _ProcessObjectCustomFieldUpdates {
$args{'ARGS'}->{'Values'} = undef;
}
- my @values = ();
- if ( ref $args{'ARGS'}->{$arg} eq 'ARRAY' ) {
- @values = @{ $args{'ARGS'}->{$arg} };
- } 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 length, map {
- s/\r+\n/\n/g;
- s/^\s+//;
- s/\s+$//;
- $_;
- }
- grep defined, @values;
+ my @values = _NormalizeObjectCustomFieldValue(
+ CustomField => $cf,
+ Param => $args{'Prefix'} . $arg,
+ Value => $args{'ARGS'}->{$arg}
+ );
+
+ # "Empty" values still don't mean anything for Image and Binary fields
+ next if $cf_type =~ /^(?:Image|Binary)$/ and not @values;
if ( $arg eq 'AddValue' || $arg eq 'Value' ) {
foreach my $value (@values) {
@@ -2817,8 +3185,7 @@ sub _ProcessObjectCustomFieldUpdates {
push( @results, $msg );
}
} elsif ( $arg eq 'Upload' ) {
- my $value_hash = _UploadedFile( $args{'Prefix'} . $arg ) or next;
- my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( %$value_hash, Field => $cf, );
+ my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( %{$values[0]}, Field => $cf, );
push( @results, $msg );
} elsif ( $arg eq 'DeleteValues' ) {
foreach my $value (@values) {
@@ -2836,7 +3203,7 @@ sub _ProcessObjectCustomFieldUpdates {
);
push( @results, $msg );
}
- } elsif ( $arg eq 'Values' && !$cf->Repeated ) {
+ } elsif ( $arg eq 'Values' ) {
my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
my %values_hash;
@@ -2870,29 +3237,6 @@ sub _ProcessObjectCustomFieldUpdates {
);
push( @results, $msg );
}
- } 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] ) {
- shift @values;
- next;
- }
-
- $delete_flag ||= 1;
- $old_cf->Delete;
- }
-
- # now add/replace extra things, if any
- foreach my $value (@values) {
- my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
- Field => $cf,
- Value => $value
- );
- push( @results, $msg );
- }
} else {
push(
@results,
@@ -2906,6 +3250,107 @@ sub _ProcessObjectCustomFieldUpdates {
return @results;
}
+sub ProcessObjectCustomFieldUpdatesForCreate {
+ my %args = (
+ ARGSRef => {},
+ ContextObject => undef,
+ @_
+ );
+ my $context = $args{'ContextObject'};
+ my %parsed;
+ my %custom_fields = _ParseObjectCustomFieldArgs( $args{'ARGSRef'} );
+
+ for my $class (keys %custom_fields) {
+ # we're only interested in new objects, so only look at $id == 0
+ for my $cfid (keys %{ $custom_fields{$class}{0} || {} }) {
+ my $cf = RT::CustomField->new( $session{'CurrentUser'} );
+ if ($context) {
+ my $system_cf = RT::CustomField->new( RT->SystemUser );
+ $system_cf->LoadById($cfid);
+ if ($system_cf->ValidateContextObject($context)) {
+ $cf->SetContextObject($context);
+ } else {
+ RT->Logger->error(
+ sprintf "Invalid context object %s (%d) for CF %d; skipping CF",
+ ref $context, $context->id, $system_cf->id
+ );
+ next;
+ }
+ }
+ $cf->LoadById($cfid);
+
+ unless ($cf->id) {
+ RT->Logger->warning("Couldn't load custom field #$cfid");
+ next;
+ }
+
+ my @groupings = sort keys %{ $custom_fields{$class}{0}{$cfid} };
+ if (@groupings > 1) {
+ # Check for consistency, in case of JS fail
+ for my $key (qw/AddValue Value Values DeleteValues DeleteValueIds/) {
+ warn "CF $cfid submitted with multiple differing $key"
+ if grep {($custom_fields{$class}{0}{$cfid}{$_}{$key} || '')
+ ne ($custom_fields{$class}{0}{$cfid}{$groupings[0]}{$key} || '')}
+ @groupings;
+ }
+ # We'll just be picking the 1st grouping in the hash, alphabetically
+ }
+
+ my @values;
+ my $name_prefix = GetCustomFieldInputNamePrefix(
+ CustomField => $cf,
+ Grouping => $groupings[0],
+ );
+ while (my ($arg, $value) = each %{ $custom_fields{$class}{0}{$cfid}{$groupings[0]} }) {
+ # Values-Magic doesn't matter on create; no previous values are being removed
+ # Category is irrelevant for the actual value
+ next if $arg =~ /-Magic$/ or $arg =~ /-Category$/;
+
+ push @values,
+ _NormalizeObjectCustomFieldValue(
+ CustomField => $cf,
+ Param => $name_prefix . $arg,
+ Value => $value,
+ );
+ }
+
+ $parsed{"CustomField-$cfid"} = \@values if @values;
+ }
+ }
+
+ return wantarray ? %parsed : \%parsed;
+}
+
+sub _NormalizeObjectCustomFieldValue {
+ my %args = (
+ Param => "",
+ @_
+ );
+ my $cf_type = $args{CustomField}->Type;
+ my @values = ();
+
+ if ( ref $args{'Value'} eq 'ARRAY' ) {
+ @values = @{ $args{'Value'} };
+ } elsif ( $cf_type =~ /text/i ) { # Both Text and Wikitext
+ @values = ( $args{'Value'} );
+ } else {
+ @values = split /\r*\n/, $args{'Value'}
+ if defined $args{'Value'};
+ }
+ @values = grep length, map {
+ s/\r+\n/\n/g;
+ s/^\s+//;
+ s/\s+$//;
+ $_;
+ }
+ grep defined, @values;
+
+ if ($args{'Param'} =~ /-Upload$/ and $cf_type =~ /^(Image|Binary)$/) {
+ @values = _UploadedFile( $args{'Param'} ) || ();
+ }
+
+ return @values;
+}
=head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS );
@@ -3010,7 +3455,6 @@ sub ProcessTicketDates {
# Set date fields
my @date_fields = qw(
Told
- Resolved
Starts
Started
Due
@@ -3031,9 +3475,7 @@ sub ProcessTicketDates {
);
my $obj = $field . "Obj";
- if ( ( defined $DateObj->Unix )
- and ( $DateObj->Unix != $Ticket->$obj()->Unix() ) )
- {
+ if ( $DateObj->Unix != $Ticket->$obj()->Unix() ) {
my $method = "Set$field";
my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
push @results, "$msg";
@@ -3055,19 +3497,24 @@ Returns an array of results messages.
sub ProcessTicketLinks {
my %args = (
TicketObj => undef,
+ TicketId => undef,
ARGSRef => undef,
@_
);
my $Ticket = $args{'TicketObj'};
+ my $TicketId = $args{'TicketId'} || $Ticket->Id;
my $ARGSRef = $args{'ARGSRef'};
- my (@results) = ProcessRecordLinks( RecordObj => $Ticket, ARGSRef => $ARGSRef );
+ my (@results) = ProcessRecordLinks(
+ %args, RecordObj => $Ticket, RecordId => $TicketId, ARGSRef => $ARGSRef,
+ );
#Merge if we need to
- if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
- $ARGSRef->{ $Ticket->Id . "-MergeInto" } =~ s/\s+//g;
- my ( $val, $msg ) = $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
+ my $input = $TicketId .'-MergeInto';
+ if ( $ARGSRef->{ $input } ) {
+ $ARGSRef->{ $input } =~ s/\s+//g;
+ my ( $val, $msg ) = $Ticket->MergeInto( $ARGSRef->{ $input } );
push @results, $msg;
}
@@ -3078,11 +3525,13 @@ sub ProcessTicketLinks {
sub ProcessRecordLinks {
my %args = (
RecordObj => undef,
+ RecordId => undef,
ARGSRef => undef,
@_
);
my $Record = $args{'RecordObj'};
+ my $RecordId = $args{'RecordId'} || $Record->Id;
my $ARGSRef = $args{'ARGSRef'};
my (@results);
@@ -3109,11 +3558,12 @@ sub ProcessRecordLinks {
my @linktypes = qw( DependsOn MemberOf RefersTo );
foreach my $linktype (@linktypes) {
- if ( $ARGSRef->{ $Record->Id . "-$linktype" } ) {
- $ARGSRef->{ $Record->Id . "-$linktype" } = join( ' ', @{ $ARGSRef->{ $Record->Id . "-$linktype" } } )
- if ref( $ARGSRef->{ $Record->Id . "-$linktype" } );
+ my $input = $RecordId .'-'. $linktype;
+ if ( $ARGSRef->{ $input } ) {
+ $ARGSRef->{ $input } = join( ' ', @{ $ARGSRef->{ $input } } )
+ if ref $ARGSRef->{ $input };
- for my $luri ( split( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) {
+ for my $luri ( split( / /, $ARGSRef->{ $input } ) ) {
next unless $luri;
$luri =~ s/\s+$//; # Strip trailing whitespace
my ( $val, $msg ) = $Record->AddLink(
@@ -3123,11 +3573,12 @@ sub ProcessRecordLinks {
push @results, $msg;
}
}
- if ( $ARGSRef->{ "$linktype-" . $Record->Id } ) {
- $ARGSRef->{ "$linktype-" . $Record->Id } = join( ' ', @{ $ARGSRef->{ "$linktype-" . $Record->Id } } )
- if ref( $ARGSRef->{ "$linktype-" . $Record->Id } );
+ $input = $linktype .'-'. $RecordId;
+ if ( $ARGSRef->{ $input } ) {
+ $ARGSRef->{ $input } = join( ' ', @{ $ARGSRef->{ $input } } )
+ if ref $ARGSRef->{ $input };
- for my $luri ( split( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) {
+ for my $luri ( split( / /, $ARGSRef->{ $input } ) ) {
next unless $luri;
my ( $val, $msg ) = $Record->AddLink(
Base => $luri,
@@ -3142,6 +3593,41 @@ sub ProcessRecordLinks {
return (@results);
}
+=head2 ProcessLinksForCreate
+
+Takes a hash with a single key, C<ARGSRef>, the value of which is a hashref to
+C<%ARGS>.
+
+Converts and returns submitted args in the form of C<new-LINKTYPE> and
+C<LINKTYPE-new> into their appropriate directional link types. For example,
+C<new-DependsOn> becomes C<DependsOn> and C<DependsOn-new> becomes
+C<DependedOnBy>. The incoming arg values are split on whitespace and
+normalized into arrayrefs before being returned.
+
+Primarily used by object creation pages for transforming incoming form inputs
+from F</Elements/EditLinks> into arguments appropriate for individual record
+Create methods.
+
+Returns a hashref in scalar context and a hash in list context.
+
+=cut
+
+sub ProcessLinksForCreate {
+ my %args = @_;
+ my %links;
+
+ foreach my $type ( keys %RT::Link::DIRMAP ) {
+ for ([Base => "new-$type"], [Target => "$type-new"]) {
+ my ($direction, $key) = @$_;
+ next unless $args{ARGSRef}->{$key};
+ $links{ $RT::Link::DIRMAP{$type}->{$direction} } = [
+ grep $_, split ' ', $args{ARGSRef}->{$key}
+ ];
+ }
+ }
+ return wantarray ? %links : \%links;
+}
+
=head2 ProcessTransactionSquelching
Takes a hashref of the submitted form arguments, C<%ARGS>.
@@ -3160,6 +3646,89 @@ sub ProcessTransactionSquelching {
return %squelched;
}
+sub ProcessRecordBulkCustomFields {
+ my %args = (RecordObj => undef, ARGSRef => {}, @_);
+
+ my $ARGSRef = $args{'ARGSRef'};
+
+ my %data;
+
+ my @results;
+ foreach my $key ( keys %$ARGSRef ) {
+ next unless $key =~ /^Bulk-(Add|Delete)-CustomField-(\d+)-(.*)$/;
+ my ($op, $cfid, $rest) = ($1, $2, $3);
+ next if $rest =~ /-Category$/;
+
+ my $res = $data{$cfid} ||= {};
+ unless (keys %$res) {
+ my $cf = RT::CustomField->new( $session{'CurrentUser'} );
+ $cf->Load( $cfid );
+ next unless $cf->Id;
+
+ $res->{'cf'} = $cf;
+ }
+
+ if ( $op eq 'Delete' && $rest eq 'AllValues' ) {
+ $res->{'DeleteAll'} = $ARGSRef->{$key};
+ next;
+ }
+
+ my @values = _NormalizeObjectCustomFieldValue(
+ CustomField => $res->{'cf'},
+ Value => $ARGSRef->{$key},
+ Param => $key,
+ );
+ next unless @values;
+ $res->{$op} = \@values;
+ }
+
+ while ( my ($cfid, $data) = each %data ) {
+ my $current_values = $args{'RecordObj'}->CustomFieldValues( $cfid );
+
+ # just add one value for fields with single value
+ if ( $data->{'Add'} && $data->{'cf'}->MaxValues == 1 ) {
+ next if $current_values->HasEntry($data->{Add}[-1]);
+
+ my ( $id, $msg ) = $args{'RecordObj'}->AddCustomFieldValue(
+ Field => $cfid,
+ Value => $data->{'Add'}[-1],
+ );
+ push @results, $msg;
+ next;
+ }
+
+ if ( $data->{'DeleteAll'} ) {
+ while ( my $value = $current_values->Next ) {
+ my ( $id, $msg ) = $args{'RecordObj'}->DeleteCustomFieldValue(
+ Field => $cfid,
+ ValueId => $value->id,
+ );
+ push @results, $msg;
+ }
+ }
+ foreach my $value ( @{ $data->{'Delete'} || [] } ) {
+ my $entry = $current_values->HasEntry($value);
+ next unless $entry;
+
+ my ( $id, $msg ) = $args{'RecordObj'}->DeleteCustomFieldValue(
+ Field => $cfid,
+ ValueId => $entry->id,
+ );
+ push @results, $msg;
+ }
+ foreach my $value ( @{ $data->{'Add'} || [] } ) {
+ next if $current_values->HasEntry($value);
+
+ my ( $id, $msg ) = $args{'RecordObj'}->AddCustomFieldValue(
+ Field => $cfid,
+ Value => $value
+ );
+ push @results, $msg;
+ }
+ }
+ return @results;
+}
+
=head2 _UploadedFile ( $arg );
Takes a CGI parameter name; if a file is uploaded under that name,
@@ -3220,10 +3789,13 @@ sub ProcessColumnMapValue {
} elsif ( UNIVERSAL::isa( $value, 'SCALAR' ) ) {
return $$value;
}
+ } else {
+ if ($args{'Escape'}) {
+ $value = $m->interp->apply_escapes( $value, 'h' );
+ $value =~ s/\n/<br>/g if defined $value;
+ }
+ return $value;
}
-
- return $m->interp->apply_escapes( $value, 'h' ) if $args{'Escape'};
- return $value;
}
=head2 GetPrincipalsMap OBJECT, CATEGORIES
@@ -3240,10 +3812,10 @@ sub GetPrincipalsMap {
if (/System/) {
my $system = RT::Groups->new($session{'CurrentUser'});
$system->LimitToSystemInternalGroups();
- $system->OrderBy( FIELD => 'Type', ORDER => 'ASC' );
+ $system->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
push @map, [
'System' => $system, # loc_left_pair
- 'Type' => 1,
+ 'Name' => 1,
];
}
elsif (/Groups/) {
@@ -3267,21 +3839,33 @@ sub GetPrincipalsMap {
elsif (/Roles/) {
my $roles = RT::Groups->new($session{'CurrentUser'});
- if ($object->isa('RT::System')) {
- $roles->LimitToRolesForSystem();
- }
- elsif ($object->isa('RT::Queue')) {
- $roles->LimitToRolesForQueue($object->Id);
+ if ($object->isa("RT::CustomField")) {
+ # If we're a custom field, show the global roles for our LookupType.
+ my $class = $object->RecordClassFromLookupType;
+ if ($class and $class->DOES("RT::Record::Role::Roles")) {
+ $roles->LimitToRolesForObject(RT->System);
+ $roles->Limit(
+ FIELD => "Name",
+ FUNCTION => 'LOWER(?)',
+ OPERATOR => "IN",
+ VALUE => [ map {lc $_} $class->Roles ],
+ CASESENSITIVE => 1,
+ );
+ } else {
+ # No roles to show; so show nothing
+ undef $roles;
+ }
+ } else {
+ $roles->LimitToRolesForObject($object);
}
- else {
- $RT::Logger->warn("Skipping unknown object type ($object) for Role principals");
- next;
+
+ if ($roles) {
+ $roles->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
+ push @map, [
+ 'Roles' => $roles, # loc_left_pair
+ 'Name' => 1
+ ];
}
- $roles->OrderBy( FIELD => 'Type', ORDER => 'ASC' );
- push @map, [
- 'Roles' => $roles, # loc_left_pair
- 'Type' => 1
- ];
}
elsif (/Users/) {
my $Users = RT->PrivilegedUsers->UserMembersObj();
@@ -3296,23 +3880,18 @@ sub GetPrincipalsMap {
);
# Limit to UserEquiv groups
- my $groups = $Users->NewAlias('Groups');
- $Users->Join(
- ALIAS1 => $groups,
- FIELD1 => 'id',
- ALIAS2 => $group_members,
- FIELD2 => 'GroupId'
+ my $groups = $Users->Join(
+ ALIAS1 => $group_members,
+ FIELD1 => 'GroupId',
+ TABLE2 => 'Groups',
+ FIELD2 => 'id',
);
- $Users->Limit( ALIAS => $groups, FIELD => 'Domain', VALUE => 'ACLEquivalence' );
- $Users->Limit( ALIAS => $groups, FIELD => 'Type', VALUE => 'UserEquiv' );
+ $Users->Limit( ALIAS => $groups, FIELD => 'Domain', VALUE => 'ACLEquivalence', CASESENSITIVE => 0 );
+ $Users->Limit( ALIAS => $groups, FIELD => 'Name', VALUE => 'UserEquiv', CASESENSITIVE => 0 );
-
- my $display = sub {
- $m->scomp('/Elements/ShowUser', User => $_[0], NoEscape => 1)
- };
push @map, [
'Users' => $Users, # loc_left_pair
- $display => 0
+ 'Format' => 0
];
}
}
@@ -3381,16 +3960,17 @@ following:
=cut
our @SCRUBBER_ALLOWED_TAGS = qw(
- A B U P BR I HR BR SMALL EM FONT SPAN STRONG SUB SUP STRIKE H1 H2 H3 H4 H5
+ A B U P BR I HR BR SMALL EM FONT SPAN STRONG SUB SUP S DEL STRIKE H1 H2 H3 H4 H5
H6 DIV UL OL LI DL DT DD PRE BLOCKQUOTE BDO
);
our %SCRUBBER_ALLOWED_ATTRIBUTES = (
# Match http, https, ftp, mailto and relative urls
# XXX: we also scrub format strings with this module then allow simple config options
- href => qr{^(?:https?:|ftp:|mailto:|/|__Web(?:Path|BaseURL|URL)__)}i,
+ href => qr{^(?:https?:|ftp:|mailto:|/|__Web(?:Path|HomePath|BaseURL|URL)__)}i,
face => 1,
size => 1,
+ color => 1,
target => 1,
style => qr{
^(?:\s*
@@ -3404,6 +3984,12 @@ our %SCRUBBER_ALLOWED_ATTRIBUTES = (
font-family: \s* [\w\s"',.\-]+ |
font-weight: \s* [\w\-]+ |
+ border-style: \s* \w+ |
+ border-color: \s* [#\w]+ |
+ border-width: \s* [\s\w]+ |
+ padding: \s* [\s\w]+ |
+ margin: \s* [\s\w]+ |
+
# MS Office styles, which are probably fine. If we don't, then any
# associated styles in the same attribute get stripped.
mso-[\w\-]+?: \s* [\w\s"',.\-]+
@@ -3416,9 +4002,42 @@ our %SCRUBBER_ALLOWED_ATTRIBUTES = (
our %SCRUBBER_RULES = ();
+# If we're displaying images, let embedded ones through
+if (RT->Config->Get('ShowTransactionImages') or RT->Config->Get('ShowRemoteImages')) {
+ $SCRUBBER_RULES{'img'} = {
+ '*' => 0,
+ alt => 1,
+ };
+
+ my @src;
+ push @src, qr/^cid:/i
+ if RT->Config->Get('ShowTransactionImages');
+
+ push @src, $SCRUBBER_ALLOWED_ATTRIBUTES{'href'}
+ if RT->Config->Get('ShowRemoteImages');
+
+ $SCRUBBER_RULES{'img'}->{'src'} = join "|", @src;
+}
+
sub _NewScrubber {
require HTML::Scrubber;
my $scrubber = HTML::Scrubber->new();
+
+ if (HTML::Gumbo->require) {
+ no warnings 'redefine';
+ my $orig = \&HTML::Scrubber::scrub;
+ *HTML::Scrubber::scrub = sub {
+ my $self = shift;
+
+ eval { $_[0] = HTML::Gumbo->new->parse( $_[0] ); chomp $_[0] };
+ warn "HTML::Gumbo pre-parse failed: $@" if $@;
+ return $orig->($self, @_);
+ };
+ push @SCRUBBER_ALLOWED_TAGS, qw/TABLE THEAD TBODY TFOOT TR TD TH/;
+ $SCRUBBER_ALLOWED_ATTRIBUTES{$_} = 1 for
+ qw/colspan rowspan align valign cellspacing cellpadding border width height/;
+ }
+
$scrubber->default(
0,
{
@@ -3447,6 +4066,21 @@ sub JSON {
RT::Interface::Web::EncodeJSON(@_);
}
+sub CSSClass {
+ my $value = shift;
+ return '' unless defined $value;
+ $value =~ s/[^A-Za-z0-9_-]/_/g;
+ return $value;
+}
+
+sub GetCustomFieldInputName {
+ RT::Interface::Web::GetCustomFieldInputName(@_);
+}
+
+sub GetCustomFieldInputNamePrefix {
+ RT::Interface::Web::GetCustomFieldInputNamePrefix(@_);
+}
+
package RT::Interface::Web;
RT::Base->_ImportOverlays();
diff --git a/rt/lib/RT/Interface/Web.pm.orig b/rt/lib/RT/Interface/Web.pm.orig
deleted file mode 100644
index 59d3154..0000000
--- a/rt/lib/RT/Interface/Web.pm.orig
+++ /dev/null
@@ -1,3454 +0,0 @@
-# BEGIN BPS TAGGED BLOCK {{{
-#
-# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
-# <sales@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 }}}
-
-## 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
-
-
-=cut
-
-use strict;
-use warnings;
-
-package RT::Interface::Web;
-
-use RT::SavedSearches;
-use URI qw();
-use RT::Interface::Web::Menu;
-use RT::Interface::Web::Session;
-use Digest::MD5 ();
-use Encode qw();
-use List::MoreUtils qw();
-use JSON qw();
-
-=head2 SquishedCSS $style
-
-=cut
-
-my %SQUISHED_CSS;
-sub SquishedCSS {
- my $style = shift or die "need name";
- return $SQUISHED_CSS{$style} if $SQUISHED_CSS{$style};
- require RT::Squish::CSS;
- my $css = RT::Squish::CSS->new( Style => $style );
- $SQUISHED_CSS{ $css->Style } = $css;
- return $css;
-}
-
-=head2 SquishedJS
-
-=cut
-
-my $SQUISHED_JS;
-sub SquishedJS {
- return $SQUISHED_JS if $SQUISHED_JS;
-
- require RT::Squish::JS;
- my $js = RT::Squish::JS->new();
- $SQUISHED_JS = $js;
- return $js;
-}
-
-=head2 ClearSquished
-
-Removes the cached CSS and JS entries, forcing them to be regenerated
-on next use.
-
-=cut
-
-sub ClearSquished {
- undef $SQUISHED_JS;
- %SQUISHED_CSS = ();
-}
-
-=head2 EscapeUTF8 SCALARREF
-
-does a css-busting but minimalist escaping of whatever html you're passing in.
-
-=cut
-
-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;
-}
-
-
-
-=head2 EscapeURI SCALARREF
-
-Escapes URI component according to RFC2396
-
-=cut
-
-sub EscapeURI {
- my $ref = shift;
- return unless defined $$ref;
-
- use bytes;
- $$ref =~ s/([^a-zA-Z0-9_.!~*'()-])/uc sprintf("%%%02X", ord($1))/eg;
-}
-
-=head2 EncodeJSON SCALAR
-
-Encodes the SCALAR to JSON and returns a JSON string. SCALAR may be a simple
-value or a reference.
-
-=cut
-
-sub EncodeJSON {
- JSON::to_json(shift, { utf8 => 1, allow_nonref => 1 });
-}
-
-sub _encode_surrogates {
- my $uni = $_[0] - 0x10000;
- return ($uni / 0x400 + 0xD800, $uni % 0x400 + 0xDC00);
-}
-
-sub EscapeJS {
- my $ref = shift;
- return unless defined $$ref;
-
- $$ref = "'" . join('',
- map {
- chr($_) =~ /[a-zA-Z0-9]/ ? chr($_) :
- $_ <= 255 ? sprintf("\\x%02X", $_) :
- $_ <= 65535 ? sprintf("\\u%04X", $_) :
- sprintf("\\u%X\\u%X", _encode_surrogates($_))
- } unpack('U*', $$ref))
- . "'";
-}
-
-=head2 WebCanonicalizeInfo();
-
-Different web servers set different environmental varibles. This
-function must return something suitable for REMOTE_USER. By default,
-just downcase $ENV{'REMOTE_USER'}
-
-=cut
-
-sub WebCanonicalizeInfo {
- return $ENV{'REMOTE_USER'} ? lc $ENV{'REMOTE_USER'} : $ENV{'REMOTE_USER'};
-}
-
-
-
-=head2 WebExternalAutoInfo($user);
-
-Returns a hash of user attributes, used when WebExternalAuto is set.
-
-=cut
-
-sub WebExternalAutoInfo {
- my $user = shift;
-
- my %user_info;
-
- # default to making Privileged users, even if they specify
- # some other default Attributes
- if ( !$RT::AutoCreate
- || ( ref($RT::AutoCreate) && not exists $RT::AutoCreate->{Privileged} ) )
- {
- $user_info{'Privileged'} = 1;
- }
-
- if ( $^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/ ) {
-
- # Populate fields with information from Unix /etc/passwd
-
- 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' ) {
-
- # Populate fields with information from NT domain controller
- }
-
- # and return the wad of stuff
- return {%user_info};
-}
-
-
-sub HandleRequest {
- my $ARGS = shift;
-
- if (RT->Config->Get('DevelMode')) {
- require Module::Refresh;
- Module::Refresh->refresh;
- }
-
- $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 and $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') );
-
- ValidateWebConfig();
-
- DecodeARGS($ARGS);
- local $HTML::Mason::Commands::DECODED_ARGS = $ARGS;
- PreprocessTimeUpdates($ARGS);
-
- InitializeMenu();
- MaybeShowInstallModePage();
-
- $HTML::Mason::Commands::m->comp( '/Elements/SetupSessionCookie', %$ARGS );
- SendSessionCookie();
-
- if ( _UserLoggedIn() ) {
- # make user info up to date
- $HTML::Mason::Commands::session{'CurrentUser'}
- ->Load( $HTML::Mason::Commands::session{'CurrentUser'}->id );
- undef $HTML::Mason::Commands::session{'CurrentUser'}->{'LangHandle'};
- }
- else {
- $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
- }
-
- # Process session-related callbacks before any auth attempts
- $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Session', CallbackPage => '/autohandler' );
-
- MaybeRejectPrivateComponentRequest();
-
- 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' );
-
- if ( $ARGS->{'NotMobile'} ) {
- $HTML::Mason::Commands::session{'NotMobile'} = 1;
- }
-
- unless ( _UserLoggedIn() ) {
- _ForceLogout();
-
- # Authenticate if the user is trying to login via user/pass query args
- my ($authed, $msg) = AttemptPasswordAuthentication($ARGS);
-
- unless ($authed) {
- my $m = $HTML::Mason::Commands::m;
-
- # REST urls get a special 401 response
- if ($m->request_comp->path =~ m{^/REST/\d+\.\d+/}) {
- $HTML::Mason::Commands::r->content_type("text/plain");
- $m->error_format("text");
- $m->out("RT/$RT::VERSION 401 Credentials required\n");
- $m->out("\n$msg\n") if $msg;
- $m->abort;
- }
- # Specially handle /index.html and /m/index.html so that we get a nicer URL
- elsif ( $m->request_comp->path =~ m{^(/m)?/index\.html$} ) {
- my $mobile = $1 ? 1 : 0;
- my $next = SetNextPage($ARGS);
- $m->comp('/NoAuth/Login.html',
- next => $next,
- actions => [$msg],
- mobile => $mobile);
- $m->abort;
- }
- else {
- TangentForLogin($ARGS, results => ($msg ? LoginError($msg) : undef));
- }
- }
- }
-
- MaybeShowInterstitialCSRFPage($ARGS);
-
- # 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(RequestData => {
- Path => $HTML::Mason::Commands::m->request_path,
- });
-
- # Process per-page final cleanup callbacks
- $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Final', CallbackPage => '/autohandler' );
-
- $HTML::Mason::Commands::m->comp( '/Elements/Footer', %$ARGS )
- unless $HTML::Mason::Commands::r->content_type
- =~ qr<^(text|application)/(x-)?(css|javascript)>;
-}
-
-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 LoginError ERROR
-
-Pushes a login error into the Actions session store and returns the hash key.
-
-=cut
-
-sub LoginError {
- my $new = shift;
- my $key = Digest::MD5::md5_hex( rand(1024) );
- push @{ $HTML::Mason::Commands::session{"Actions"}->{$key} ||= [] }, $new;
- $HTML::Mason::Commands::session{'i'}++;
- return $key;
-}
-
-=head2 SetNextPage ARGSRef [PATH]
-
-Intuits and stashes the next page in the sesssion hash. If PATH is
-specified, uses that instead of the value of L<IntuitNextPage()>. Returns
-the hash value.
-
-=cut
-
-sub SetNextPage {
- my $ARGS = shift;
- my $next = $_[0] ? $_[0] : IntuitNextPage();
- my $hash = Digest::MD5::md5_hex($next . $$ . rand(1024));
- my $page = { url => $next };
-
- # If an explicit URL was passed and we didn't IntuitNextPage, then
- # IsPossibleCSRF below is almost certainly unrelated to the actual
- # destination. Currently explicit next pages aren't used in RT, but the
- # API is available.
- if (not $_[0] and RT->Config->Get("RestrictReferrer")) {
- # This isn't really CSRF, but the CSRF heuristics are useful for catching
- # requests which may have unintended side-effects.
- my ($is_csrf, $msg, @loc) = IsPossibleCSRF($ARGS);
- if ($is_csrf) {
- RT->Logger->notice(
- "Marking original destination as having side-effects before redirecting for login.\n"
- ."Request: $next\n"
- ."Reason: " . HTML::Mason::Commands::loc($msg, @loc)
- );
- $page->{'HasSideEffects'} = [$msg, @loc];
- }
- }
-
- $HTML::Mason::Commands::session{'NextPage'}->{$hash} = $page;
- $HTML::Mason::Commands::session{'i'}++;
- return $hash;
-}
-
-=head2 FetchNextPage HASHKEY
-
-Returns the stashed next page hashref for the given hash.
-
-=cut
-
-sub FetchNextPage {
- my $hash = shift || "";
- return $HTML::Mason::Commands::session{'NextPage'}->{$hash};
-}
-
-=head2 RemoveNextPage HASHKEY
-
-Removes the stashed next page for the given hash and returns it.
-
-=cut
-
-sub RemoveNextPage {
- my $hash = shift || "";
- return delete $HTML::Mason::Commands::session{'NextPage'}->{$hash};
-}
-
-=head2 TangentForLogin ARGSRef [HASH]
-
-Redirects to C</NoAuth/Login.html>, setting the value of L<IntuitNextPage> as
-the next page. Takes a hashref of request %ARGS as the first parameter.
-Optionally takes all other parameters as a hash which is dumped into query
-params.
-
-=cut
-
-sub TangentForLogin {
- my $ARGS = shift;
- my $hash = SetNextPage($ARGS);
- my %query = (@_, next => $hash);
-
- $query{mobile} = 1
- if $HTML::Mason::Commands::m->request_comp->path =~ m{^/m(/|$)};
-
- my $login = RT->Config->Get('WebURL') . 'NoAuth/Login.html?';
- $login .= $HTML::Mason::Commands::m->comp('/Elements/QueryString', %query);
- Redirect($login);
-}
-
-=head2 TangentForLoginWithError ERROR
-
-Localizes the passed error message, stashes it with L<LoginError> and then
-calls L<TangentForLogin> with the appropriate results key.
-
-=cut
-
-sub TangentForLoginWithError {
- my $ARGS = shift;
- my $key = LoginError(HTML::Mason::Commands::loc(@_));
- TangentForLogin( $ARGS, results => $key );
-}
-
-=head2 IntuitNextPage
-
-Attempt to figure out the path to which we should return the user after a
-tangent. The current request URL is used, or failing that, the C<WebURL>
-configuration variable.
-
-=cut
-
-sub IntuitNextPage {
- my $req_uri;
-
- # This includes any query parameters. Redirect will take care of making
- # it an absolute URL.
- if ($ENV{'REQUEST_URI'}) {
- $req_uri = $ENV{'REQUEST_URI'};
-
- # collapse multiple leading slashes so the first part doesn't look like
- # a hostname of a schema-less URI
- $req_uri =~ s{^/+}{/};
- }
-
- my $next = defined $req_uri ? $req_uri : RT->Config->Get('WebURL');
-
- # sanitize $next
- my $uri = URI->new($next);
-
- # You get undef scheme with a relative uri like "/Search/Build.html"
- unless (!defined($uri->scheme) || $uri->scheme eq 'http' || $uri->scheme eq 'https') {
- $next = RT->Config->Get('WebURL');
- }
-
- # Make sure we're logging in to the same domain
- # You can get an undef authority with a relative uri like "index.html"
- my $uri_base_url = URI->new(RT->Config->Get('WebBaseURL'));
- unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority) {
- $next = RT->Config->Get('WebURL');
- }
-
- return $next;
-}
-
-=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 !~ m{^(/+)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');
-
- # Don't show the login page to logged in users
- Redirect(RT->Config->Get('WebURL'))
- if $m->base_comp->path eq '/NoAuth/Login.html' and _UserLoggedIn();
-
- # If it's a noauth file, don't ask for auth.
- $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
- $m->abort;
-}
-
-=head2 MaybeRejectPrivateComponentRequest
-
-This function will reject calls to private components, like those under
-C</Elements>. If the requested path is a private component then we will
-abort with a C<403> error.
-
-=cut
-
-sub MaybeRejectPrivateComponentRequest {
- my $m = $HTML::Mason::Commands::m;
- my $path = $m->request_comp->path;
-
- # We do not check for dhandler here, because requesting our dhandlers
- # directly is okay. Mason will invoke the dhandler with a dhandler_arg of
- # 'dhandler'.
-
- if ($path =~ m{
- / # leading slash
- ( Elements |
- _elements | # mobile UI
- Callbacks |
- Widgets |
- autohandler | # requesting this directly is suspicious
- l (_unsafe)? ) # loc component
- ( $ | / ) # trailing slash or end of path
- }xi
- && $path !~ m{ /RTx/Statistics/\w+/Elements/Chart }xi
- )
- {
- warn "rejecting private component $path\n";
- $m->abort(403);
- }
-
- return;
-}
-
-sub InitializeMenu {
- $HTML::Mason::Commands::m->notes('menu', RT::Interface::Web::Menu->new());
- $HTML::Mason::Commands::m->notes('page-menu', RT::Interface::Web::Menu->new());
- $HTML::Mason::Commands::m->notes('page-widgets', RT::Interface::Web::Menu->new());
-
-}
-
-
-=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;
-
- # Ensure that the cookie that we send is up-to-date, in case the
- # session-id has been modified in any way
- SendSessionCookie();
-
- # precache all system level rights for the current user
- $HTML::Mason::Commands::session{CurrentUser}->PrincipalObj->HasRights( Object => RT->System );
-
- # 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 =~ m{^(/+)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;
- }
-
- my $next = RemoveNextPage($ARGS->{'next'});
- $next = $next->{'url'} if ref $next;
- 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'};
-
- if (RT->Config->Get('WebFallbackToInternalAuth')) {
- TangentForLoginWithError($ARGS, 'Cannot create user: [_1]', $msg);
- } else {
- $m->abort();
- }
- }
- }
-
- if ( _UserLoggedIn() ) {
- $m->callback( %$ARGS, CallbackName => 'ExternalAuthSuccessfulLogin', CallbackPage => '/autohandler' );
- # It is possible that we did a redirect to the login page,
- # if the external auth allows lack of auth through with no
- # REMOTE_USER set, instead of forcing a "permission
- # denied" message. Honor the $next.
- Redirect($next) if $next;
- # Unlike AttemptPasswordAuthentication below, we do not
- # force a redirect to / if $next is not set -- otherwise,
- # straight-up external auth would always redirect to /
- # when you first hit it.
- } else {
- delete $HTML::Mason::Commands::session{'CurrentUser'};
- $user = $orig_user;
-
- unless ( RT->Config->Get('WebFallbackToInternalAuth') ) {
- TangentForLoginWithError($ARGS, 'You are not an authorized user');
- }
- }
- } elsif ( RT->Config->Get('WebFallbackToInternalAuth') ) {
- unless ( defined $HTML::Mason::Commands::session{'CurrentUser'} ) {
- # XXX unreachable due to prior defaulting in HandleRequest (check c34d108)
- TangentForLoginWithError($ARGS, 'You are not an authorized user');
- }
- } 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;
- return unless defined $ARGS->{user} && defined $ARGS->{pass};
-
- 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->callback( %$ARGS, CallbackName => 'FailedLogin', CallbackPage => '/autohandler' );
- return (0, HTML::Mason::Commands::loc('Your username or password is incorrect'));
- }
- else {
- $RT::Logger->info("Successful login for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
-
- # It's important to nab the next page from the session before we blow
- # the session away
- my $next = RemoveNextPage($ARGS->{'next'});
- $next = $next->{'url'} if ref $next;
-
- InstantiateNewSession();
- $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
-
- $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
-
- # Really the only time we don't want to redirect here is if we were
- # passed user and pass as query params in the URL.
- if ($next) {
- Redirect($next);
- }
- elsif ($ARGS->{'next'}) {
- # Invalid hash, but still wants to go somewhere, take them to /
- Redirect(RT->Config->Get('WebURL'));
- }
-
- return (1, HTML::Mason::Commands::loc('Logged in'));
- }
-}
-
-=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 ) {
- InstantiateNewSession();
- }
- 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;
- SendSessionCookie();
-}
-
-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 ),
- -httponly => ( RT->Config->Get('WebHttpOnlyCookies') ? 1 : 0 ),
- );
-
- $HTML::Mason::Commands::r->err_headers_out->{'Set-Cookie'} = $cookie->as_string;
-}
-
-=head2 Redirect URL
-
-This routine ells the current user's browser to redirect to URL.
-Additionally, it unties the user's currently active session, helping to avoid
-A bug in Apache::Session 1.81 and earlier which clobbers sessions if we try to use
-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( _NormalizeHost(RT->Config->Get('WebURL')) );
-
- # Make relative URIs absolute from the server host and scheme
- $uri->scheme($server_uri->scheme) if not defined $uri->scheme;
- if (not defined $uri->host) {
- $uri->host($server_uri->host);
- $uri->port($server_uri->port);
- }
-
- # 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)
- # don't do this if $RT::CanonicalizeRedirectURLs 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');
- }
-
- # [rt3.fsck.com #12716] Apache recommends use of $SERVER_HOST
- $uri->host( $ENV{'SERVER_HOST'} || $ENV{'HTTP_HOST'} || $ENV{'SERVER_NAME'});
- $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 CacheControlExpiresHeaders
-
-set both Cache-Control and Expires http headers
-
-=cut
-
-sub CacheControlExpiresHeaders {
- my %args = @_;
-
- my $Visibility = 'private';
- if ( ! defined $args{Time} ) {
- $args{Time} = 0;
- } elsif ( $args{Time} eq 'no-cache' ) {
- $args{Time} = 0;
- } elsif ( $args{Time} eq 'forever' ) {
- $args{Time} = 30 * 24 * 60 * 60;
- $Visibility = 'public';
- }
-
- my $CacheControl = $args{Time}
- ? sprintf "max-age=%d, %s", $args{Time}, $Visibility
- : 'no-cache'
- ;
- $HTML::Mason::Commands::r->headers_out->{'Cache-Control'} = $CacheControl;
-
- my $expires = RT::Date->new(RT->SystemUser);
- $expires->SetToNow;
- $expires->AddSeconds( $args{Time} ) if $args{Time};
-
- $HTML::Mason::Commands::r->headers_out->{'Expires'} = $expires->RFC2616;
-}
-
-=head2 StaticFileHeaders
-
-Send the browser a few headers to try to get it to (somewhat agressively)
-cache RT's static Javascript and CSS files.
-
-This routine could really use _accurate_ heuristics. (XXX TODO)
-
-=cut
-
-sub StaticFileHeaders {
- my $date = RT::Date->new(RT->SystemUser);
-
- # remove any cookie headers -- if it is cached publicly, it
- # shouldn't include anyone's cookie!
- delete $HTML::Mason::Commands::r->err_headers_out->{'Set-Cookie'};
-
- # Expire things in a month.
- CacheControlExpiresHeaders( Time => 'forever' );
-
- # 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
- # $date->Set( Value => $^T );
- # $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = $date->RFC2616;
-}
-
-=head2 ComponentPathIsSafe PATH
-
-Takes C<PATH> and returns a boolean indicating that the user-specified partial
-component path is safe.
-
-Currently "safe" means that the path does not start with a dot (C<.>), does
-not contain a slash-dot C</.>, and does not contain any nulls.
-
-=cut
-
-sub ComponentPathIsSafe {
- my $self = shift;
- my $path = shift;
- return($path !~ m{(?:^|/)\.} and $path !~ m{\0});
-}
-
-=head2 PathIsSafe
-
-Takes a C<< Path => path >> and returns a boolean indicating that
-the path is safely within RT's control or not. The path I<must> be
-relative.
-
-This function does not consult the filesystem at all; it is merely
-a logical sanity checking of the path. This explicitly does not handle
-symlinks; if you have symlinks in RT's webroot pointing outside of it,
-then we assume you know what you are doing.
-
-=cut
-
-sub PathIsSafe {
- my $self = shift;
- my %args = @_;
- my $path = $args{Path};
-
- # Get File::Spec to clean up extra /s, ./, etc
- my $cleaned_up = File::Spec->canonpath($path);
-
- if (!defined($cleaned_up)) {
- $RT::Logger->info("Rejecting path that canonpath doesn't understand: $path");
- return 0;
- }
-
- # Forbid too many ..s. We can't just sum then check because
- # "../foo/bar/baz" should be illegal even though it has more
- # downdirs than updirs. So as soon as we get a negative score
- # (which means "breaking out" of the top level) we reject the path.
-
- my @components = split '/', $cleaned_up;
- my $score = 0;
- for my $component (@components) {
- if ($component eq '..') {
- $score--;
- if ($score < 0) {
- $RT::Logger->info("Rejecting unsafe path: $path");
- return 0;
- }
- }
- elsif ($component eq '.' || $component eq '') {
- # these two have no effect on $score
- }
- else {
- $score++;
- }
- }
-
- return 1;
-}
-
-=head2 SendStaticFile
-
-Takes a File => path and a Type => Content-type
-
-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};
- my $relfile = $args{RelativeFile};
-
- if (defined($relfile) && !$self->PathIsSafe(Path => $relfile)) {
- $HTML::Mason::Commands::r->status(400);
- $HTML::Mason::Commands::m->abort;
- }
-
- $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 MobileClient {
- my $self = shift;
-
-
-if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60|Mobile)/io && !$HTML::Mason::Commands::session{'NotMobile'}) {
- return 1;
-} else {
- return undef;
-}
-
-}
-
-
-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; we don't use EscapeUTF8 here
- # because we want to precisely match the escapting that FCKEditor
- # uses.
- $sig =~ s/&/&amp;/g;
- $sig =~ s/</&lt;/g;
- $sig =~ s/>/&gt;/g;
- $sig =~ s/"/&quot;/g;
- $sig =~ s/'/&#39;/g;
- 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 %args = @_;
-
- my $log_sql_statements = RT->Config->Get('StatementLog');
-
- return unless ($log_sql_statements);
-
- my @log = $RT::Handle->SQLStatementLog;
- $RT::Handle->ClearSQLStatementLog;
-
- $RT::Handle->AddRequestToHistory({
- %{ $args{RequestData} },
- Queries => \@log,
- });
-
- 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{ defined $_ ? qq|'$_'| : 'undef'} @bind]} ]" : "" )
- );
- }
-
-}
-
-my $_has_validated_web_config = 0;
-sub ValidateWebConfig {
- my $self = shift;
-
- # do this once per server instance, not once per request
- return if $_has_validated_web_config;
- $_has_validated_web_config = 1;
-
- my $port = $ENV{SERVER_PORT};
- my $host = $ENV{HTTP_X_FORWARDED_HOST} || $ENV{HTTP_X_FORWARDED_SERVER}
- || $ENV{HTTP_HOST} || $ENV{SERVER_NAME};
- ($host, $port) = ($1, $2) if $host =~ /^(.*?):(\d+)$/;
-
- if ( $port != RT->Config->Get('WebPort') and not $ENV{'rt.explicit_port'}) {
- $RT::Logger->warn("The requested port ($port) does NOT match the configured WebPort ($RT::WebPort). "
- ."Perhaps you should Set(\$WebPort, $port); in RT_SiteConfig.pm, "
- ."otherwise your internal links may be broken.");
- }
-
- if ( $host ne RT->Config->Get('WebDomain') ) {
- $RT::Logger->warn("The requested host ($host) does NOT match the configured WebDomain ($RT::WebDomain). "
- ."Perhaps you should Set(\$WebDomain, '$host'); in RT_SiteConfig.pm, "
- ."otherwise your internal links may be broken.");
- }
-
- return; #next warning flooding our logs, doesn't seem applicable to our use
- # (SCRIPT_NAME is the full path, WebPath is just the beginning)
- #in vanilla RT does something eat the local part of SCRIPT_NAME 1st?
-
- # Unfortunately, there is no reliable way to get the _path_ that was
- # requested at the proxy level; simply disable this warning if we're
- # proxied and there's a mismatch.
- my $proxied = $ENV{HTTP_X_FORWARDED_HOST} || $ENV{HTTP_X_FORWARDED_SERVER};
- if ($ENV{SCRIPT_NAME} ne RT->Config->Get('WebPath') and not $proxied) {
- $RT::Logger->warn("The requested path ($ENV{SCRIPT_NAME}) does NOT match the configured WebPath ($RT::WebPath). "
- ."Perhaps you should Set(\$WebPath, '$ENV{SCRIPT_NAME}'); in RT_SiteConfig.pm, "
- ."otherwise your internal links may be broken.");
- }
-}
-
-sub ComponentRoots {
- my $self = shift;
- my %args = ( Names => 0, @_ );
- my @roots;
- if (defined $HTML::Mason::Commands::m) {
- @roots = $HTML::Mason::Commands::m->interp->comp_root_array;
- } else {
- @roots = (
- [ local => $RT::MasonLocalComponentRoot ],
- (map {[ "plugin-".$_->Name => $_->ComponentRoot ]} @{RT->Plugins}),
- [ standard => $RT::MasonComponentRoot ]
- );
- }
- @roots = map { $_->[1] } @roots unless $args{Names};
- return @roots;
-}
-
-our %is_whitelisted_component = (
- # The RSS feed embeds an auth token in the path, but query
- # information for the search. Because it's a straight-up read, in
- # addition to embedding its own auth, it's fine.
- '/NoAuth/rss/dhandler' => 1,
-
- # While these can be used for denial-of-service against RT
- # (construct a very inefficient query and trick lots of users into
- # running them against RT) it's incredibly useful to be able to link
- # to a search result (or chart) or bookmark a result page.
- '/Search/Results.html' => 1,
- '/Search/Simple.html' => 1,
- '/m/tickets/search' => 1,
- '/Search/Chart.html' => 1,
-
- # This page takes Attachment and Transaction argument to figure
- # out what to show, but it's read only and will deny information if you
- # don't have ShowOutgoingEmail.
- '/Ticket/ShowEmailRecord.html' => 1,
-);
-
-# Components which are blacklisted from automatic, argument-based whitelisting.
-# These pages are not idempotent when called with just an id.
-our %is_blacklisted_component = (
- # Takes only id and toggles bookmark state
- '/Helpers/Toggle/TicketBookmark' => 1,
-);
-
-sub IsCompCSRFWhitelisted {
- my $comp = shift;
- my $ARGS = shift;
-
- return 1 if $is_whitelisted_component{$comp};
-
- my %args = %{ $ARGS };
-
- # If the user specifies a *correct* user and pass then they are
- # golden. This acts on the presumption that external forms may
- # hardcode a username and password -- if a malicious attacker knew
- # both already, CSRF is the least of your problems.
- my $AllowLoginCSRF = not RT->Config->Get('RestrictReferrerLogin');
- if ($AllowLoginCSRF and defined($args{user}) and defined($args{pass})) {
- my $user_obj = RT::CurrentUser->new();
- $user_obj->Load($args{user});
- return 1 if $user_obj->id && $user_obj->IsPassword($args{pass});
-
- delete $args{user};
- delete $args{pass};
- }
-
- # Some pages aren't idempotent even with safe args like id; blacklist
- # them from the automatic whitelisting below.
- return 0 if $is_blacklisted_component{$comp};
-
- # Eliminate arguments that do not indicate an effectful request.
- # For example, "id" is acceptable because that is how RT retrieves a
- # record.
- delete $args{id};
-
- # If they have a results= from MaybeRedirectForResults, that's also fine.
- delete $args{results};
-
- # The homepage refresh, which uses the Refresh header, doesn't send
- # a referer in most browsers; whitelist the one parameter it reloads
- # with, HomeRefreshInterval, which is safe
- delete $args{HomeRefreshInterval};
-
- # The NotMobile flag is fine for any page; it's only used to toggle a flag
- # in the session related to which interface you get.
- delete $args{NotMobile};
-
- # If there are no arguments, then it's likely to be an idempotent
- # request, which are not susceptible to CSRF
- return 1 if !%args;
-
- return 0;
-}
-
-sub IsRefererCSRFWhitelisted {
- my $referer = _NormalizeHost(shift);
- my $base_url = _NormalizeHost(RT->Config->Get('WebBaseURL'));
- $base_url = $base_url->host_port;
-
- my $configs;
- for my $config ( $base_url, RT->Config->Get('ReferrerWhitelist') ) {
- push @$configs,$config;
-
- my $host_port = $referer->host_port;
- if ($config =~ /\*/) {
- # Turn a literal * into a domain component or partial component match.
- # Refer to http://tools.ietf.org/html/rfc2818#page-5
- my $regex = join "[a-zA-Z0-9\-]*",
- map { quotemeta($_) }
- split /\*/, $config;
-
- return 1 if $host_port =~ /^$regex$/i;
- } else {
- return 1 if $host_port eq $config;
- }
- }
-
- return (0,$referer,$configs);
-}
-
-=head3 _NormalizeHost
-
-Takes a URI and creates a URI object that's been normalized
-to handle common problems such as localhost vs 127.0.0.1
-
-=cut
-
-sub _NormalizeHost {
- my $s = shift;
- $s = "http://$s" unless $s =~ /^http/i;
- my $uri= URI->new($s);
- $uri->host('127.0.0.1') if $uri->host eq 'localhost';
-
- return $uri;
-
-}
-
-sub IsPossibleCSRF {
- my $ARGS = shift;
-
- # If first request on this session is to a REST endpoint, then
- # whitelist the REST endpoints -- and explicitly deny non-REST
- # endpoints. We do this because using a REST cookie in a browser
- # would open the user to CSRF attacks to the REST endpoints.
- my $path = $HTML::Mason::Commands::r->path_info;
- $HTML::Mason::Commands::session{'REST'} = $path =~ m{^/+REST/\d+\.\d+(/|$)}
- unless defined $HTML::Mason::Commands::session{'REST'};
-
- if ($HTML::Mason::Commands::session{'REST'}) {
- return 0 if $path =~ m{^/+REST/\d+\.\d+(/|$)};
- my $why = <<EOT;
-This login session belongs to a REST client, and cannot be used to
-access non-REST interfaces of RT for security reasons.
-EOT
- my $details = <<EOT;
-Please log out and back in to obtain a session for normal browsing. If
-you understand the security implications, disabling RT's CSRF protection
-will remove this restriction.
-EOT
- chomp $details;
- HTML::Mason::Commands::Abort( $why, Details => $details );
- }
-
- return 0 if IsCompCSRFWhitelisted(
- $HTML::Mason::Commands::m->request_comp->path,
- $ARGS
- );
-
- # if there is no Referer header then assume the worst
- return (1,
- "your browser did not supply a Referrer header", # loc
- ) if !$ENV{HTTP_REFERER};
-
- my ($whitelisted, $browser, $configs) = IsRefererCSRFWhitelisted($ENV{HTTP_REFERER});
- return 0 if $whitelisted;
-
- if ( @$configs > 1 ) {
- return (1,
- "the Referrer header supplied by your browser ([_1]) is not allowed by RT's configured hostname ([_2]) or whitelisted hosts ([_3])", # loc
- $browser->host_port,
- shift @$configs,
- join(', ', @$configs) );
- }
-
- return (1,
- "the Referrer header supplied by your browser ([_1]) is not allowed by RT's configured hostname ([_2])", # loc
- $browser->host_port,
- $configs->[0]);
-}
-
-sub ExpandCSRFToken {
- my $ARGS = shift;
-
- my $token = delete $ARGS->{CSRF_Token};
- return unless $token;
-
- my $data = $HTML::Mason::Commands::session{'CSRF'}{$token};
- return unless $data;
- return unless $data->{path} eq $HTML::Mason::Commands::r->path_info;
-
- my $user = $HTML::Mason::Commands::session{'CurrentUser'}->UserObj;
- return unless $user->ValidateAuthString( $data->{auth}, $token );
-
- %{$ARGS} = %{$data->{args}};
- $HTML::Mason::Commands::DECODED_ARGS = $ARGS;
-
- # We explicitly stored file attachments with the request, but not in
- # the session yet, as that would itself be an attack. Put them into
- # the session now, so they'll be visible.
- if ($data->{attach}) {
- my $filename = $data->{attach}{filename};
- my $mime = $data->{attach}{mime};
- $HTML::Mason::Commands::session{'Attachments'}{$filename}
- = $mime;
- }
-
- return 1;
-}
-
-sub StoreRequestToken {
- my $ARGS = shift;
-
- my $token = Digest::MD5::md5_hex(time . {} . $$ . rand(1024));
- my $user = $HTML::Mason::Commands::session{'CurrentUser'}->UserObj;
- my $data = {
- auth => $user->GenerateAuthString( $token ),
- path => $HTML::Mason::Commands::r->path_info,
- args => $ARGS,
- };
- if ($ARGS->{Attach}) {
- my $attachment = HTML::Mason::Commands::MakeMIMEEntity( AttachmentFieldName => 'Attach' );
- my $file_path = delete $ARGS->{'Attach'};
- $data->{attach} = {
- filename => Encode::decode_utf8("$file_path"),
- mime => $attachment,
- };
- }
-
- $HTML::Mason::Commands::session{'CSRF'}->{$token} = $data;
- $HTML::Mason::Commands::session{'i'}++;
- return $token;
-}
-
-sub MaybeShowInterstitialCSRFPage {
- my $ARGS = shift;
-
- return unless RT->Config->Get('RestrictReferrer');
-
- # Deal with the form token provided by the interstitial, which lets
- # browsers which never set referer headers still use RT, if
- # painfully. This blows values into ARGS
- return if ExpandCSRFToken($ARGS);
-
- my ($is_csrf, $msg, @loc) = IsPossibleCSRF($ARGS);
- return if !$is_csrf;
-
- $RT::Logger->notice("Possible CSRF: ".RT::CurrentUser->new->loc($msg, @loc));
-
- my $token = StoreRequestToken($ARGS);
- $HTML::Mason::Commands::m->comp(
- '/Elements/CSRF',
- OriginalURL => RT->Config->Get('WebPath') . $HTML::Mason::Commands::r->path_info,
- Reason => HTML::Mason::Commands::loc( $msg, @loc ),
- Token => $token,
- );
- # Calls abort, never gets here
-}
-
-our @POTENTIAL_PAGE_ACTIONS = (
- qr'/Ticket/Create.html' => "create a ticket", # loc
- qr'/Ticket/' => "update a ticket", # loc
- qr'/Admin/' => "modify RT's configuration", # loc
- qr'/Approval/' => "update an approval", # loc
- qr'/Articles/' => "update an article", # loc
- qr'/Dashboards/' => "modify a dashboard", # loc
- qr'/m/ticket/' => "update a ticket", # loc
- qr'Prefs' => "modify your preferences", # loc
- qr'/Search/' => "modify or access a search", # loc
- qr'/SelfService/Create' => "create a ticket", # loc
- qr'/SelfService/' => "update a ticket", # loc
-);
-
-sub PotentialPageAction {
- my $page = shift;
- my @potentials = @POTENTIAL_PAGE_ACTIONS;
- while (my ($pattern, $result) = splice @potentials, 0, 2) {
- return HTML::Mason::Commands::loc($result)
- if $page =~ $pattern;
- }
- return "";
-}
-
-package HTML::Mason::Commands;
-
-use vars qw/$r $m %session/;
-
-sub Menu {
- return $HTML::Mason::Commands::m->notes('menu');
-}
-
-sub PageMenu {
- return $HTML::Mason::Commands::m->notes('page-menu');
-}
-
-sub PageWidgets {
- return $HTML::Mason::Commands::m->notes('page-widgets');
-}
-
-
-
-=head2 loc ARRAY
-
-loc is a nice clean global routine which calls $session{'CurrentUser'}->loc()
-with whatever it's called with. If there is no $session{'CurrentUser'},
-it creates a temporary user, so we have something to get a localisation handle
-through
-
-=cut
-
-sub loc {
-
- 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];
- }
-}
-
-
-
-=head2 loc_fuzzy STRING
-
-loc_fuzzy is for handling localizations of messages that may already
-contain interpolated variables, typically returned from libraries
-outside RT's control. It takes the message string and extracts the
-variable array automatically by matching against the candidate entries
-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) );
- }
-}
-
-
-# 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 => $why, %args );
- $m->abort;
- } else {
- $m->comp( "/Elements/Error", Why => $why, %args );
- $m->abort;
- }
-}
-
-sub MaybeRedirectForResults {
- my %args = (
- Path => $HTML::Mason::Commands::m->request_comp->path,
- Arguments => {},
- Anchor => undef,
- Actions => undef,
- Force => 0,
- @_
- );
- my $has_actions = $args{'Actions'} && grep( defined, @{ $args{'Actions'} } );
- return unless $has_actions || $args{'Force'};
-
- my %arguments = %{ $args{'Arguments'} };
-
- if ( $has_actions ) {
- my $key = Digest::MD5::md5_hex( rand(1024) );
- push @{ $session{"Actions"}{ $key } ||= [] }, @{ $args{'Actions'} };
- $session{'i'}++;
- $arguments{'results'} = $key;
- }
-
- $args{'Path'} =~ s!^/+!!;
- my $url = RT->Config->Get('WebURL') . $args{Path};
-
- if ( keys %arguments ) {
- $url .= '?'. $m->comp( '/Elements/QueryString', %arguments );
- }
- if ( $args{'Anchor'} ) {
- $url .= "#". $args{'Anchor'};
- }
- return RT::Interface::Web::Redirect($url);
-}
-
-=head2 MaybeRedirectToApproval Path => 'path', Whitelist => REGEX, ARGSRef => HASHREF
-
-If the ticket specified by C<< $ARGSRef->{id} >> is an approval ticket,
-redirect to the approvals display page, preserving any arguments.
-
-C<Path>s matching C<Whitelist> are let through.
-
-This is a no-op if the C<ForceApprovalsView> option isn't enabled.
-
-=cut
-
-sub MaybeRedirectToApproval {
- my %args = (
- Path => $HTML::Mason::Commands::m->request_comp->path,
- ARGSRef => {},
- Whitelist => undef,
- @_
- );
-
- return unless $ENV{REQUEST_METHOD} eq 'GET';
-
- my $id = $args{ARGSRef}->{id};
-
- if ( $id
- and RT->Config->Get('ForceApprovalsView')
- and not $args{Path} =~ /$args{Whitelist}/)
- {
- my $ticket = RT::Ticket->new( $session{'CurrentUser'} );
- $ticket->Load($id);
-
- if ($ticket and $ticket->id and lc($ticket->Type) eq 'approval') {
- MaybeRedirectForResults(
- Path => "/Approvals/Display.html",
- Force => 1,
- Anchor => $args{ARGSRef}->{Anchor},
- Arguments => $args{ARGSRef},
- );
- }
- }
-}
-
-=head2 CreateTicket ARGS
-
-Create a new ticket, using Mason's %ARGS. returns @results.
-
-=cut
-
-sub CreateTicket {
- my %ARGS = (@_);
-
- my (@Actions);
-
- my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
-
- my $Queue = RT::Queue->new( $session{'CurrentUser'} );
- unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
- Abort('Queue not found');
- }
-
- unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
- Abort('You have no permission to create tickets in that queue.');
- }
-
- my $due;
- if ( defined $ARGS{'Due'} and $ARGS{'Due'} =~ /\S/ ) {
- $due = RT::Date->new( $session{'CurrentUser'} );
- $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
- }
- my $starts;
- if ( defined $ARGS{'Starts'} and $ARGS{'Starts'} =~ /\S/ ) {
- $starts = RT::Date->new( $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 => $sigless,
- Type => $ARGS{'ContentType'},
- Interface => RT::Interface::Web::MobileClient() ? 'Mobile' : 'Web',
- );
-
- if ( $ARGS{'Attachments'} ) {
- my $rv = $MIMEObj->make_multipart;
- $RT::Logger->error("Couldn't make multipart message")
- if !$rv || $rv !~ /^(?:DONE|ALREADY)$/;
-
- foreach ( map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} } ) {
- unless ($_) {
- $RT::Logger->error("Couldn't add empty attachemnt");
- next;
- }
- $MIMEObj->add_part($_);
- }
- }
-
- for my $argument (qw(Encrypt Sign)) {
- $MIMEObj->head->replace( "X-RT-$argument" => $ARGS{$argument} ? 1 : 0 );
- }
-
- my %create_args = (
- 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'},
- TimeWorked => $ARGS{'TimeWorked'},
- Subject => $ARGS{'Subject'},
- Status => $ARGS{'Status'},
- Due => $due ? $due->ISO : undef,
- Starts => $starts ? $starts->ISO : undef,
- MIMEObj => $MIMEObj
- );
-
- my @txn_squelch;
- foreach my $type (qw(Requestor Cc AdminCc)) {
- push @txn_squelch, map $_->address, Email::Address->parse( $create_args{$type} )
- if grep $_ eq $type || $_ eq ( $type . 's' ), @{ $ARGS{'SkipNotification'} || [] };
- }
- $create_args{TransSquelchMailTo} = \@txn_squelch
- if @txn_squelch;
-
- if ( $ARGS{'AttachTickets'} ) {
- require RT::Action::SendEmail;
- RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets,
- ref $ARGS{'AttachTickets'}
- ? @{ $ARGS{'AttachTickets'} }
- : ( $ARGS{'AttachTickets'} ) );
- }
-
- foreach my $arg ( keys %ARGS ) {
- next if $arg =~ /-(?:Magic|Category)$/;
-
- if ( $arg =~ /^Object-RT::Transaction--CustomField-/ ) {
- $create_args{$arg} = $ARGS{$arg};
- }
-
- # Object-RT::Ticket--CustomField-3-Values
- elsif ( $arg =~ /^Object-RT::Ticket--CustomField-(\d+)/ ) {
- my $cfid = $1;
-
- my $cf = RT::CustomField->new( $session{'CurrentUser'} );
- $cf->SetContextObject( $Queue );
- $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);
- next;
- }
-
- my $type = $cf->Type;
-
- 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;
-
- $create_args{"CustomField-$cfid"} = \@values;
- }
- }
-
- # 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} ];
-
- }
-
- my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
- unless ($id) {
- Abort($ErrMsg);
- }
-
- push( @Actions, split( "\n", $ErrMsg ) );
- unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
- Abort( "No permission to view newly created ticket #" . $Ticket->id . "." );
- }
- return ( $Ticket, @Actions );
-
-}
-
-
-
-=head2 LoadTicket id
-
-Takes a ticket id as its only variable. if it's handed an array, it takes
-the first value.
-
-Returns an RT::Ticket object as the current user.
-
-=cut
-
-sub LoadTicket {
- my $id = shift;
-
- if ( ref($id) eq "ARRAY" ) {
- $id = $id->[0];
- }
-
- unless ($id) {
- Abort("No ticket specified");
- }
-
- my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
- $Ticket->Load($id);
- unless ( $Ticket->id ) {
- Abort("Could not load ticket $id");
- }
- return $Ticket;
-}
-
-
-
-=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
-
-# change from stock: if txn custom fields are set but there's no content
-# or attachment, create a Touch txn instead of doing nothing
-
-sub ProcessUpdateMessage {
-
- my %args = (
- ARGSRef => undef,
- TicketObj => undef,
- SkipSignatureOnly => 1,
- @_
- );
-
- if ( $args{ARGSRef}->{'UpdateAttachments'}
- && !keys %{ $args{ARGSRef}->{'UpdateAttachments'} } )
- {
- delete $args{ARGSRef}->{'UpdateAttachments'};
- }
-
- # 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,
- );
-
- my %txn_customfields;
-
- foreach my $key ( keys %{ $args{ARGSRef} } ) {
- if ( $key =~ /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ ) {
- next if $key =~ /(TimeUnits|Magic)$/;
- $txn_customfields{$key} = $args{ARGSRef}->{$key};
- }
- }
-
- # If, after stripping the signature, we have no message, create a
- # Touch transaction if necessary
- 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'};
- # }
-
- my $timetaken = $args{ARGSRef}->{'UpdateTimeWorked'};
- if ( $timetaken or grep {length $_} values %txn_customfields ) {
- my ( $Transaction, $Description, $Object ) =
- $args{TicketObj}->Touch(
- CustomFields => \%txn_customfields,
- TimeTaken => $timetaken
- );
- return $Description;
- }
- return;
- }
-
- if ( $args{ARGSRef}->{'UpdateSubject'} eq ($args{'TicketObj'}->Subject || '') ) {
- $args{ARGSRef}->{'UpdateSubject'} = undef;
- }
-
- my $Message = MakeMIMEEntity(
- Subject => $args{ARGSRef}->{'UpdateSubject'},
- Body => $args{ARGSRef}->{'UpdateContent'},
- Type => $args{ARGSRef}->{'UpdateContentType'},
- Interface => RT::Interface::Web::MobileClient() ? 'Mobile' : 'Web',
- );
-
- $Message->head->replace( 'Message-ID' => Encode::encode_utf8(
- RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'} )
- ) );
- my $old_txn = RT::Transaction->new( $session{'CurrentUser'} );
- if ( $args{ARGSRef}->{'QuoteTransaction'} ) {
- $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} );
- } else {
- $old_txn = $args{TicketObj}->Transactions->First();
- }
-
- 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 map $args{ARGSRef}->{UpdateAttachments}{$_},
- sort keys %{ $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 %message_args = (
- Sign => ( $args{ARGSRef}->{'Sign'} ? 1 : 0 ),
- Encrypt => ( $args{ARGSRef}->{'Encrypt'} ? 1 : 0 ),
- MIMEObj => $Message,
- TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'},
- CustomFields => \%txn_customfields,
- );
-
- _ProcessUpdateMessageRecipients(
- MessageArgs => \%message_args,
- %args,
- );
-
- my @results;
- if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
- 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(%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.") );
- }
- return @results;
-}
-
-sub _ProcessUpdateMessageRecipients {
- my %args = (
- ARGSRef => undef,
- TicketObj => undef,
- MessageArgs => undef,
- @_,
- );
-
- my $bcc = $args{ARGSRef}->{'UpdateBcc'};
- my $cc = $args{ARGSRef}->{'UpdateCc'};
-
- my $message_args = $args{MessageArgs};
-
- $message_args->{CcMessageTo} = $cc;
- $message_args->{BccMessageTo} = $bcc;
-
- my @txn_squelch;
- foreach my $type (qw(Cc AdminCc)) {
- if (grep $_ eq $type || $_ eq ( $type . 's' ), @{ $args{ARGSRef}->{'SkipNotification'} || [] }) {
- push @txn_squelch, map $_->address, Email::Address->parse( $message_args->{$type} );
- push @txn_squelch, $args{TicketObj}->$type->MemberEmailAddresses;
- push @txn_squelch, $args{TicketObj}->QueueObj->$type->MemberEmailAddresses;
- }
- }
- if (grep $_ eq 'Requestor' || $_ eq 'Requestors', @{ $args{ARGSRef}->{'SkipNotification'} || [] }) {
- push @txn_squelch, map $_->address, Email::Address->parse( $message_args->{Requestor} );
- push @txn_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses;
- }
-
- push @txn_squelch, @{$args{ARGSRef}{SquelchMailTo}} if $args{ARGSRef}{SquelchMailTo};
- $message_args->{SquelchMailTo} = \@txn_squelch
- if @txn_squelch;
-
- 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;
- }
- }
- }
-}
-
-sub ProcessAttachments {
- my %args = (
- ARGSRef => {},
- @_
- );
-
- my $ARGSRef = $args{ARGSRef} || {};
- # deal with deleting uploaded attachments
- foreach my $key ( keys %$ARGSRef ) {
- if ( $key =~ m/^DeleteAttach-(.+)$/ ) {
- delete $session{'Attachments'}{$1};
- }
- $session{'Attachments'} = { %{ $session{'Attachments'} || {} } };
- }
-
- # store the uploaded attachment in session
- if ( defined $ARGSRef->{'Attach'} && length $ARGSRef->{'Attach'} )
- { # attachment?
- my $attachment = MakeMIMEEntity( AttachmentFieldName => 'Attach' );
-
- my $file_path = Encode::decode_utf8("$ARGSRef->{'Attach'}");
- $session{'Attachments'} =
- { %{ $session{'Attachments'} || {} }, $file_path => $attachment, };
- }
-
- # delete temporary storage entry to make WebUI clean
- unless ( keys %{ $session{'Attachments'} } and $ARGSRef->{'UpdateAttach'} )
- {
- delete $session{'Attachments'};
- }
-}
-
-
-=head2 MakeMIMEEntity PARAMHASH
-
-Takes a paramhash Subject, Body and AttachmentFieldName.
-
-Also takes Form, Cc and Type as optional paramhash keys.
-
- Returns a MIME::Entity.
-
-=cut
-
-sub MakeMIMEEntity {
-
- #TODO document what else this takes.
- my %args = (
- Subject => undef,
- From => undef,
- Cc => undef,
- Body => undef,
- AttachmentFieldName => undef,
- Type => undef,
- Interface => 'API',
- @_,
- );
- my $Message = MIME::Entity->build(
- Type => 'multipart/mixed',
- "Message-Id" => Encode::encode_utf8( RT::Interface::Email::GenMessageId ),
- "X-RT-Interface" => $args{Interface},
- map { $_ => Encode::encode_utf8( $args{ $_} ) }
- grep defined $args{$_}, qw(Subject From Cc)
- );
-
- if ( defined $args{'Body'} && length $args{'Body'} ) {
-
- # Make the update content have no 'weird' newlines in it
- $args{'Body'} =~ s/\r\n/\n/gs;
-
- $Message->attach(
- Type => $args{'Type'} || 'text/plain',
- Charset => 'UTF-8',
- Data => $args{'Body'},
- );
- }
-
- if ( $args{'AttachmentFieldName'} ) {
-
- my $cgi_object = $m->cgi_object;
- my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} );
- if ( defined $filehandle && length $filehandle ) {
-
- my ( @content, $buffer );
- while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
- push @content, $buffer;
- }
-
- my $uploadinfo = $cgi_object->uploadInfo($filehandle);
-
- my $filename = "$filehandle";
- $filename =~ s{^.*[\\/]}{};
-
- $Message->attach(
- Type => $uploadinfo->{'Content-Type'},
- Filename => $filename,
- Data => \@content,
- );
- if ( !$args{'Subject'} && !( defined $args{'Body'} && length $args{'Body'} ) ) {
- $Message->head->set( 'Subject' => $filename );
- }
-
- # Attachment parts really shouldn't get a Message-ID or "interface"
- $Message->head->delete('Message-ID');
- $Message->head->delete('X-RT-Interface');
- }
- }
-
- $Message->make_singlepart;
-
- RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
-
- return ($Message);
-
-}
-
-
-
-=head2 ParseDateToISO
-
-Takes a date in an arbitrary format.
-Returns an ISO date and time in GMT
-
-=cut
-
-sub ParseDateToISO {
- my $date = shift;
-
- my $date_obj = RT::Date->new( $session{'CurrentUser'} );
- $date_obj->Set(
- Format => 'unknown',
- Value => $date
- );
- return ( $date_obj->ISO );
-}
-
-
-
-sub ProcessACLChanges {
- my $ARGSref = shift;
-
- my @results;
-
- foreach my $arg ( keys %$ARGSref ) {
- next unless ( $arg =~ /^(GrantRight|RevokeRight)-(\d+)-(.+?)-(\d+)$/ );
-
- my ( $method, $principal_id, $object_type, $object_id ) = ( $1, $2, $3, $4 );
-
- my @rights;
- if ( UNIVERSAL::isa( $ARGSref->{$arg}, 'ARRAY' ) ) {
- @rights = @{ $ARGSref->{$arg} };
- } else {
- @rights = $ARGSref->{$arg};
- }
- @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;
- }
- } 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);
-}
-
-
-=head2 ProcessACLs
-
-ProcessACLs expects values from a series of checkboxes that describe the full
-set of rights a principal should have on an object.
-
-It expects form inputs with names like SetRights-PrincipalId-ObjType-ObjId
-instead of with the prefixes Grant/RevokeRight. Each input should be an array
-listing the rights the principal should have, and ProcessACLs will modify the
-current rights to match. Additionally, the previously unused CheckACL input
-listing PrincipalId-ObjType-ObjId is now used to catch cases when all the
-rights are removed from a principal and as such no SetRights input is
-submitted.
-
-=cut
-
-sub ProcessACLs {
- my $ARGSref = shift;
- my (%state, @results);
-
- my $CheckACL = $ARGSref->{'CheckACL'};
- my @check = grep { defined } (ref $CheckACL eq 'ARRAY' ? @$CheckACL : $CheckACL);
-
- # Check if we want to grant rights to a previously rights-less user
- for my $type (qw(user group)) {
- my $principal = _ParseACLNewPrincipal($ARGSref, $type)
- or next;
-
- unless ($principal->PrincipalId) {
- push @results, loc("Couldn't load the specified principal");
- next;
- }
-
- my $principal_id = $principal->PrincipalId;
-
- # Turn our addprincipal rights spec into a real one
- for my $arg (keys %$ARGSref) {
- next unless $arg =~ /^SetRights-addprincipal-(.+?-\d+)$/;
-
- my $tuple = "$principal_id-$1";
- my $key = "SetRights-$tuple";
-
- # If we have it already, that's odd, but merge them
- if (grep { $_ eq $tuple } @check) {
- $ARGSref->{$key} = [
- (ref $ARGSref->{$key} eq 'ARRAY' ? @{$ARGSref->{$key}} : $ARGSref->{$key}),
- (ref $ARGSref->{$arg} eq 'ARRAY' ? @{$ARGSref->{$arg}} : $ARGSref->{$arg}),
- ];
- } else {
- $ARGSref->{$key} = $ARGSref->{$arg};
- push @check, $tuple;
- }
- }
- }
-
- # Build our rights state for each Principal-Object tuple
- foreach my $arg ( keys %$ARGSref ) {
- next unless $arg =~ /^SetRights-(\d+-.+?-\d+)$/;
-
- my $tuple = $1;
- my $value = $ARGSref->{$arg};
- my @rights = grep { $_ } (ref $value eq 'ARRAY' ? @$value : $value);
- next unless @rights;
-
- $state{$tuple} = { map { $_ => 1 } @rights };
- }
-
- foreach my $tuple (List::MoreUtils::uniq @check) {
- next unless $tuple =~ /^(\d+)-(.+?)-(\d+)$/;
-
- my ( $principal_id, $object_type, $object_id ) = ( $1, $2, $3 );
-
- 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;
- }
- } 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;
- }
-
- my $acls = RT::ACL->new($session{'CurrentUser'});
- $acls->LimitToObject( $obj );
- $acls->LimitToPrincipal( Id => $principal_id );
-
- while ( my $ace = $acls->Next ) {
- my $right = $ace->RightName;
-
- # Has right and should have right
- next if delete $state{$tuple}->{$right};
-
- # Has right and shouldn't have right
- my ($val, $msg) = $principal->RevokeRight( Object => $obj, Right => $right );
- push @results, $msg;
- }
-
- # For everything left, they don't have the right but they should
- for my $right (keys %{ $state{$tuple} || {} }) {
- delete $state{$tuple}->{$right};
- my ($val, $msg) = $principal->GrantRight( Object => $obj, Right => $right );
- push @results, $msg;
- }
-
- # Check our state for leftovers
- if ( keys %{ $state{$tuple} || {} } ) {
- my $missed = join '|', %{$state{$tuple} || {}};
- $RT::Logger->warn(
- "Uh-oh, it looks like we somehow missed a right in "
- ."ProcessACLs. Here's what was leftover: $missed"
- );
- }
- }
-
- return (@results);
-}
-
-=head2 _ParseACLNewPrincipal
-
-Takes a hashref of C<%ARGS> and a principal type (C<user> or C<group>). Looks
-for the presence of rights being added on a principal of the specified type,
-and returns undef if no new principal is being granted rights. Otherwise loads
-up an L<RT::User> or L<RT::Group> object and returns it. Note that the object
-may not be successfully loaded, and you should check C<->id> yourself.
-
-=cut
-
-sub _ParseACLNewPrincipal {
- my $ARGSref = shift;
- my $type = lc shift;
- my $key = "AddPrincipalForRights-$type";
-
- return unless $ARGSref->{$key};
-
- my $principal;
- if ( $type eq 'user' ) {
- $principal = RT::User->new( $session{'CurrentUser'} );
- $principal->LoadByCol( Name => $ARGSref->{$key} );
- }
- elsif ( $type eq 'group' ) {
- $principal = RT::Group->new( $session{'CurrentUser'} );
- $principal->LoadUserDefinedGroup( $ARGSref->{$key} );
- }
- return $principal;
-}
-
-
-=head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs)
-
-@attribs is a list of ticket fields to check and update if they differ from the B<Object>'s current values. ARGSRef is a ref to HTML::Mason's %ARGS.
-
-Returns an array of success/failure messages
-
-=cut
-
-sub UpdateRecordObject {
- my %args = (
- 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'},
- );
-
- return (@results);
-}
-
-
-
-sub ProcessCustomFieldUpdates {
- my %args = (
- CustomFieldObj => undef,
- ARGSRef => undef,
- @_
- );
-
- my $Object = $args{'CustomFieldObj'};
- my $ARGSRef = $args{'ARGSRef'};
-
- my @attribs = qw(Name Type Description Queue SortOrder);
- my @results = UpdateRecordObject(
- AttributesRef => \@attribs,
- Object => $Object,
- ARGSRef => $ARGSRef
- );
-
- my $prefix = "CustomField-" . $Object->Id;
- if ( $ARGSRef->{"$prefix-AddValue-Name"} ) {
- my ( $addval, $addmsg ) = $Object->AddValue(
- Name => $ARGSRef->{"$prefix-AddValue-Name"},
- Description => $ARGSRef->{"$prefix-AddValue-Description"},
- SortOrder => $ARGSRef->{"$prefix-AddValue-SortOrder"},
- );
- push( @results, $addmsg );
- }
-
- 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 );
- }
-
- my $vals = $Object->Values();
- 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 );
- }
- }
- }
-
- return (@results);
-}
-
-
-
-=head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS );
-
-Returns an array of results messages.
-
-=cut
-
-sub ProcessTicketBasics {
-
- my %args = (
- TicketObj => undef,
- ARGSRef => undef,
- @_
- );
-
- my $TicketObj = $args{'TicketObj'};
- my $ARGSRef = $args{'ARGSRef'};
-
- my $OrigOwner = $TicketObj->Owner;
-
- # Set basic fields
- my @attribs = qw(
- Subject
- FinalPriority
- Priority
- TimeEstimated
- TimeWorked
- TimeLeft
- Type
- Status
- Queue
- );
-
- # Canonicalize Queue and Owner to their IDs if they aren't numeric
- for my $field (qw(Queue Owner)) {
- if ( $ARGSRef->{$field} and ( $ARGSRef->{$field} !~ /^(\d+)$/ ) ) {
- my $class = $field eq 'Owner' ? "RT::User" : "RT::$field";
- my $temp = $class->new(RT->SystemUser);
- $temp->Load( $ARGSRef->{$field} );
- if ( $temp->id ) {
- $ARGSRef->{$field} = $temp->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'};
-
- my @results = UpdateRecordObject(
- AttributesRef => \@attribs,
- Object => $TicketObj,
- ARGSRef => $ARGSRef,
- );
-
- # We special case owner changing, so we can use ForceOwnerChange
- if ( $ARGSRef->{'Owner'}
- && $ARGSRef->{'Owner'} !~ /\D/
- && ( $OrigOwner != $ARGSRef->{'Owner'} ) ) {
- my ($ChownType);
- if ( $ARGSRef->{'ForceOwnerChange'} ) {
- $ChownType = "Force";
- }
- else {
- $ChownType = "Set";
- }
-
- my ( $val, $msg ) = $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
- push( @results, $msg );
- }
-
- # }}}
-
- return (@results);
-}
-
-sub ProcessTicketReminders {
- my %args = (
- TicketObj => undef,
- ARGSRef => undef,
- @_
- );
-
- my $Ticket = $args{'TicketObj'};
- my $args = $args{'ARGSRef'};
- my @results;
-
- my $reminder_collection = $Ticket->Reminders->Collection;
-
- if ( $args->{'update-reminders'} ) {
- while ( my $reminder = $reminder_collection->Next ) {
- my $resolve_status = $reminder->QueueObj->Lifecycle->ReminderStatusOnResolve;
- if ( $reminder->Status ne $resolve_status && $args->{ 'Complete-Reminder-' . $reminder->id } ) {
- my ($status, $msg) = $Ticket->Reminders->Resolve($reminder);
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
-
- }
- elsif ( $reminder->Status eq $resolve_status && !$args->{ 'Complete-Reminder-' . $reminder->id } ) {
- my ($status, $msg) = $Ticket->Reminders->Open($reminder);
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
- }
-
- if ( exists( $args->{ 'Reminder-Subject-' . $reminder->id } ) && ( $reminder->Subject ne $args->{ 'Reminder-Subject-' . $reminder->id } )) {
- my ($status, $msg) = $reminder->SetSubject( $args->{ 'Reminder-Subject-' . $reminder->id } ) ;
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
- }
-
- if ( exists( $args->{ 'Reminder-Owner-' . $reminder->id } ) && ( $reminder->Owner != $args->{ 'Reminder-Owner-' . $reminder->id } )) {
- my ($status, $msg) = $reminder->SetOwner( $args->{ 'Reminder-Owner-' . $reminder->id } , "Force" ) ;
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
- }
-
- if ( exists( $args->{ 'Reminder-Due-' . $reminder->id } ) && $args->{ 'Reminder-Due-' . $reminder->id } ne '' ) {
- my $DateObj = RT::Date->new( $session{'CurrentUser'} );
- $DateObj->Set(
- Format => 'unknown',
- Value => $args->{ 'Reminder-Due-' . $reminder->id }
- );
- if ( defined $DateObj->Unix && $DateObj->Unix != $reminder->DueObj->Unix ) {
- my ($status, $msg) = $reminder->SetDue( $DateObj->ISO );
- push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg);
- }
- }
- }
- }
-
- if ( $args->{'NewReminder-Subject'} ) {
- my $due_obj = RT::Date->new( $session{'CurrentUser'} );
- $due_obj->Set(
- Format => 'unknown',
- Value => $args->{'NewReminder-Due'}
- );
- my ( $add_id, $msg ) = $Ticket->Reminders->Add(
- Subject => $args->{'NewReminder-Subject'},
- Owner => $args->{'NewReminder-Owner'},
- Due => $due_obj->ISO
- );
- if ( $add_id ) {
- push @results, loc("Reminder '[_1]' added", $args->{'NewReminder-Subject'});
- }
- else {
- push @results, $msg;
- }
- }
- return @results;
-}
-
-sub ProcessTicketCustomFieldUpdates {
- my %args = @_;
- $args{'Object'} = delete $args{'TicketObj'};
- my $ARGSRef = { %{ $args{'ARGSRef'} } };
-
- # 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+-.*)/ ) {
- $ARGSRef->{"Object-RT::Ticket-$1"} = delete $ARGSRef->{$arg};
- } elsif ( $arg =~ /^CustomField-(\d+-.*)/ ) {
- $ARGSRef->{"Object-RT::Ticket--$1"} = delete $ARGSRef->{$arg};
- } elsif ( $arg =~ /^Object-RT::Transaction-(\d*)-CustomField/ ) {
- delete $ARGSRef->{$arg}; # don't try to update transaction fields
- }
- }
-
- return ProcessObjectCustomFieldUpdates( %args, ARGSRef => $ARGSRef );
-}
-
-sub ProcessObjectCustomFieldUpdates {
- 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};
- }
-
- # For each of those objects
- foreach my $class ( keys %custom_fields_to_mod ) {
- 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;
- unless ( $Object->id ) {
- $RT::Logger->warning("Couldn't load object $class #$id");
- next;
- }
-
- foreach my $cf ( keys %{ $custom_fields_to_mod{$class}{$id} } ) {
- my $CustomFieldObj = RT::CustomField->new( $session{'CurrentUser'} );
- $CustomFieldObj->SetContextObject($Object);
- $CustomFieldObj->LoadById($cf);
- unless ( $CustomFieldObj->id ) {
- $RT::Logger->warning("Couldn't load custom field #$cf");
- next;
- }
- push @results,
- _ProcessObjectCustomFieldUpdates(
- Prefix => "Object-$class-$id-CustomField-$cf-",
- Object => $Object,
- CustomField => $CustomFieldObj,
- ARGS => $custom_fields_to_mod{$class}{$id}{$cf},
- );
- }
- }
- }
- return @results;
-}
-
-sub _ProcessObjectCustomFieldUpdates {
- 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'} } ) {
-
- # skip category argument
- next if $arg eq 'Category';
-
- # and TimeUnits
- next if $arg eq 'Value-TimeUnits';
-
- # 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 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)$/;
-
- $arg = 'Values';
- $args{'ARGS'}->{'Values'} = undef;
- }
-
- my @values = ();
- if ( ref $args{'ARGS'}->{$arg} eq 'ARRAY' ) {
- @values = @{ $args{'ARGS'}->{$arg} };
- } 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 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) {
- my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
- Field => $cf->id,
- Value => $value
- );
- push( @results, $msg );
- }
- } 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'}->DeleteCustomFieldValue(
- Field => $cf,
- Value => $value,
- );
- push( @results, $msg );
- }
- } elsif ( $arg eq 'DeleteValueIds' ) {
- foreach my $value (@values) {
- my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
- Field => $cf,
- ValueId => $value,
- );
- push( @results, $msg );
- }
- } elsif ( $arg eq 'Values' && !$cf->Repeated ) {
- my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
-
- my %values_hash;
- 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 );
- $values_hash{$val} = 1 if $val;
- }
-
- # For Date Cfs, @values is empty when there is no changes (no datas in form input)
- return @results if ( $cf->Type eq 'Date' && ! @values );
-
- # For Date Cfs, @values is empty when there is no changes (no datas in form input)
- return @results if ( $cf->Type =~ /^Date(?:Time)?$/ && ! @values );
-
- $cf_values->RedoSearch;
- while ( my $cf_value = $cf_values->Next ) {
- next if $values_hash{ $cf_value->id };
-
- my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
- Field => $cf,
- ValueId => $cf_value->id
- );
- push( @results, $msg );
- }
- } 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] ) {
- shift @values;
- next;
- }
-
- $delete_flag ||= 1;
- $old_cf->Delete;
- }
-
- # now add/replace extra things, if any
- foreach my $value (@values) {
- my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
- Field => $cf,
- Value => $value
- );
- push( @results, $msg );
- }
- } 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
- )
- );
- }
- }
- return @results;
-}
-
-
-=head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS );
-
-Returns an array of results messages.
-
-=cut
-
-sub ProcessTicketWatchers {
- my %args = (
- TicketObj => undef,
- ARGSRef => undef,
- @_
- );
- my (@results);
-
- my $Ticket = $args{'TicketObj'};
- my $ARGSRef = $args{'ARGSRef'};
-
- # Munge watchers
-
- foreach my $key ( keys %$ARGSRef ) {
-
- # Delete deletable watchers
- if ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) {
- my ( $code, $msg ) = $Ticket->DeleteWatcher(
- PrincipalId => $2,
- Type => $1
- );
- push @results, $msg;
- }
-
- # Delete watchers in the simple style demanded by the bulk manipulator
- elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) {
- my ( $code, $msg ) = $Ticket->DeleteWatcher(
- Email => $ARGSRef->{$key},
- Type => $1
- );
- push @results, $msg;
- }
-
- # Add new wathchers by email address
- elsif ( ( $ARGSRef->{$key} || '' ) =~ /^(?:AdminCc|Cc|Requestor)$/
- and $key =~ /^WatcherTypeEmail(\d*)$/ )
- {
-
- #They're in this order because otherwise $1 gets clobbered :/
- my ( $code, $msg ) = $Ticket->AddWatcher(
- Type => $ARGSRef->{$key},
- Email => $ARGSRef->{ "WatcherAddressEmail" . $1 }
- );
- push @results, $msg;
- }
-
- #Add requestors in the simple style demanded by the bulk manipulator
- elsif ( $key =~ /^Add(Requestor|Cc|AdminCc)$/ ) {
- my ( $code, $msg ) = $Ticket->AddWatcher(
- Type => $1,
- Email => $ARGSRef->{$key}
- );
- push @results, $msg;
- }
-
- # Add new watchers by owner
- elsif ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) {
- my $principal_id = $1;
- my $form = $ARGSRef->{$key};
- foreach my $value ( ref($form) ? @{$form} : ($form) ) {
- next unless $value =~ /^(?:AdminCc|Cc|Requestor)$/i;
-
- my ( $code, $msg ) = $Ticket->AddWatcher(
- Type => $value,
- PrincipalId => $principal_id
- );
- push @results, $msg;
- }
- }
-
- }
- return (@results);
-}
-
-
-
-=head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS );
-
-Returns an array of results messages.
-
-=cut
-
-sub ProcessTicketDates {
- my %args = (
- TicketObj => undef,
- ARGSRef => undef,
- @_
- );
-
- my $Ticket = $args{'TicketObj'};
- my $ARGSRef = $args{'ARGSRef'};
-
- my (@results);
-
- # Set date fields
- my @date_fields = qw(
- Told
- Resolved
- Starts
- Started
- Due
- WillResolve
- );
-
- #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' }
- );
-
- 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";
- }
- }
-
- # }}}
- return (@results);
-}
-
-
-
-=head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS );
-
-Returns an array of results messages.
-
-=cut
-
-sub ProcessTicketLinks {
- my %args = (
- TicketObj => undef,
- ARGSRef => undef,
- @_
- );
-
- my $Ticket = $args{'TicketObj'};
- my $ARGSRef = $args{'ARGSRef'};
-
- my (@results) = ProcessRecordLinks( RecordObj => $Ticket, ARGSRef => $ARGSRef );
-
- #Merge if we need to
- if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
- $ARGSRef->{ $Ticket->Id . "-MergeInto" } =~ s/\s+//g;
- my ( $val, $msg ) = $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
- push @results, $msg;
- }
-
- return (@results);
-}
-
-
-sub ProcessRecordLinks {
- my %args = (
- RecordObj => undef,
- ARGSRef => undef,
- @_
- );
-
- my $Record = $args{'RecordObj'};
- my $ARGSRef = $args{'ARGSRef'};
-
- my (@results);
-
- # Delete links that are gone gone gone.
- foreach my $arg ( keys %$ARGSRef ) {
- if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
- my $base = $1;
- my $type = $2;
- my $target = $3;
-
- my ( $val, $msg ) = $Record->DeleteLink(
- Base => $base,
- Type => $type,
- Target => $target
- );
-
- push @results, $msg;
-
- }
-
- }
-
- my @linktypes = qw( DependsOn MemberOf RefersTo );
-
- foreach my $linktype (@linktypes) {
- if ( $ARGSRef->{ $Record->Id . "-$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 } ) {
- $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 ProcessTransactionSquelching
-
-Takes a hashref of the submitted form arguments, C<%ARGS>.
-
-Returns a hash of squelched addresses.
-
-=cut
-
-sub ProcessTransactionSquelching {
- my $args = shift;
- my %checked = map { $_ => 1 } grep { defined }
- ( ref $args->{'TxnSendMailTo'} eq "ARRAY" ? @{$args->{'TxnSendMailTo'}} :
- defined $args->{'TxnSendMailTo'} ? ($args->{'TxnSendMailTo'}) :
- () );
- my %squelched = map { $_ => 1 } grep { not $checked{$_} } split /,/, ($args->{'TxnRecipients'}||'');
- return %squelched;
-}
-
-=head2 _UploadedFile ( $arg );
-
-Takes a CGI parameter name; if a file is uploaded under that name,
-return a hash reference suitable for AddCustomFieldValue's use:
-C<( Value => $filename, LargeContent => $content, ContentType => $type )>.
-
-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 $upload_info = $cgi_object->uploadInfo($fh);
-
- my $filename = "$fh";
- $filename =~ s#^.*[\\/]##;
- binmode($fh);
-
- return {
- Value => $filename,
- LargeContent => do { local $/; scalar <$fh> },
- 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'} =~ /^(.*?)\.(.+)$/ ) {
- $subkey =~ s/^\{(.*)\}$/$1/;
- 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 GetPrincipalsMap OBJECT, CATEGORIES
-
-Returns an array suitable for passing to /Admin/Elements/EditRights with the
-principal collections mapped from the categories given.
-
-=cut
-
-sub GetPrincipalsMap {
- my $object = shift;
- my @map;
- for (@_) {
- if (/System/) {
- my $system = RT::Groups->new($session{'CurrentUser'});
- $system->LimitToSystemInternalGroups();
- $system->OrderBy( FIELD => 'Type', ORDER => 'ASC' );
- push @map, [
- 'System' => $system, # loc_left_pair
- 'Type' => 1,
- ];
- }
- elsif (/Groups/) {
- my $groups = RT::Groups->new($session{'CurrentUser'});
- $groups->LimitToUserDefinedGroups();
- $groups->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
-
- # Only show groups who have rights granted on this object
- $groups->WithGroupRight(
- Right => '',
- Object => $object,
- IncludeSystemRights => 0,
- IncludeSubgroupMembers => 0,
- );
-
- push @map, [
- 'User Groups' => $groups, # loc_left_pair
- 'Name' => 0
- ];
- }
- elsif (/Roles/) {
- my $roles = RT::Groups->new($session{'CurrentUser'});
-
- if ($object->isa('RT::System')) {
- $roles->LimitToRolesForSystem();
- }
- elsif ($object->isa('RT::Queue')) {
- $roles->LimitToRolesForQueue($object->Id);
- }
- else {
- $RT::Logger->warn("Skipping unknown object type ($object) for Role principals");
- next;
- }
- $roles->OrderBy( FIELD => 'Type', ORDER => 'ASC' );
- push @map, [
- 'Roles' => $roles, # loc_left_pair
- 'Type' => 1
- ];
- }
- elsif (/Users/) {
- my $Users = RT->PrivilegedUsers->UserMembersObj();
- $Users->OrderBy( FIELD => 'Name', ORDER => 'ASC' );
-
- # Only show users who have rights granted on this object
- my $group_members = $Users->WhoHaveGroupRight(
- Right => '',
- Object => $object,
- IncludeSystemRights => 0,
- IncludeSubgroupMembers => 0,
- );
-
- # Limit to UserEquiv groups
- my $groups = $Users->NewAlias('Groups');
- $Users->Join(
- ALIAS1 => $groups,
- FIELD1 => 'id',
- ALIAS2 => $group_members,
- FIELD2 => 'GroupId'
- );
- $Users->Limit( ALIAS => $groups, FIELD => 'Domain', VALUE => 'ACLEquivalence' );
- $Users->Limit( ALIAS => $groups, FIELD => 'Type', VALUE => 'UserEquiv' );
-
-
- my $display = sub {
- $m->scomp('/Elements/ShowUser', User => $_[0], NoEscape => 1)
- };
- push @map, [
- 'Users' => $Users, # loc_left_pair
- $display => 0
- ];
- }
- }
- return @map;
-}
-
-=head2 _load_container_object ( $type, $id );
-
-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 );
-}
-
-=head2 _parse_saved_search ( $arg );
-
-Given a serialization string for saved search, and returns the
-container object and the search id.
-
-=cut
-
-sub _parse_saved_search {
- my $spec = shift;
- return unless $spec;
- 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 );
-}
-
-=head2 ScrubHTML content
-
-Removes unsafe and undesired HTML from the passed content
-
-=cut
-
-my $SCRUBBER;
-sub ScrubHTML {
- my $Content = shift;
- $SCRUBBER = _NewScrubber() unless $SCRUBBER;
-
- $Content = '' if !defined($Content);
- return $SCRUBBER->scrub($Content);
-}
-
-=head2 _NewScrubber
-
-Returns a new L<HTML::Scrubber> object.
-
-If you need to be more lax about what HTML tags and attributes are allowed,
-create C</opt/rt4/local/lib/RT/Interface/Web_Local.pm> with something like the
-following:
-
- package HTML::Mason::Commands;
- # Let tables through
- push @SCRUBBER_ALLOWED_TAGS, qw(TABLE THEAD TBODY TFOOT TR TD TH);
- 1;
-
-=cut
-
-our @SCRUBBER_ALLOWED_TAGS = qw(
- A B U P BR I HR BR SMALL EM FONT SPAN STRONG SUB SUP STRIKE H1 H2 H3 H4 H5
- H6 DIV UL OL LI DL DT DD PRE BLOCKQUOTE BDO
-);
-
-our %SCRUBBER_ALLOWED_ATTRIBUTES = (
- # Match http, https, ftp, mailto and relative urls
- # XXX: we also scrub format strings with this module then allow simple config options
- href => qr{^(?:https?:|ftp:|mailto:|/|__Web(?:Path|BaseURL|URL)__)}i,
- face => 1,
- size => 1,
- target => 1,
- style => qr{
- ^(?:\s*
- (?:(?:background-)?color: \s*
- (?:rgb\(\s* \d+, \s* \d+, \s* \d+ \s*\) | # rgb(d,d,d)
- \#[a-f0-9]{3,6} | # #fff or #ffffff
- [\w\-]+ # green, light-blue, etc.
- ) |
- text-align: \s* \w+ |
- font-size: \s* [\w.\-]+ |
- font-family: \s* [\w\s"',.\-]+ |
- font-weight: \s* [\w\-]+ |
-
- # MS Office styles, which are probably fine. If we don't, then any
- # associated styles in the same attribute get stripped.
- mso-[\w\-]+?: \s* [\w\s"',.\-]+
- )\s* ;? \s*)
- +$ # one or more of these allowed properties from here 'till sunset
- }ix,
- dir => qr/^(rtl|ltr)$/i,
- lang => qr/^\w+(-\w+)?$/,
-);
-
-our %SCRUBBER_RULES = ();
-
-sub _NewScrubber {
- require HTML::Scrubber;
- my $scrubber = HTML::Scrubber->new();
- $scrubber->default(
- 0,
- {
- %SCRUBBER_ALLOWED_ATTRIBUTES,
- '*' => 0, # require attributes be explicitly allowed
- },
- );
- $scrubber->deny(qw[*]);
- $scrubber->allow(@SCRUBBER_ALLOWED_TAGS);
- $scrubber->rules(%SCRUBBER_RULES);
-
- # Scrubbing comments is vital since IE conditional comments can contain
- # arbitrary HTML and we'd pass it right on through.
- $scrubber->comment(0);
-
- return $scrubber;
-}
-
-=head2 JSON
-
-Redispatches to L<RT::Interface::Web/EncodeJSON>
-
-=cut
-
-sub JSON {
- RT::Interface::Web::EncodeJSON(@_);
-}
-
-package RT::Interface::Web;
-RT::Base->_ImportOverlays();
-
-1;
diff --git a/rt/lib/RT/Interface/Web/Handler.pm b/rt/lib/RT/Interface/Web/Handler.pm
index fc95aa1..8667f01 100644
--- a/rt/lib/RT/Interface/Web/Handler.pm
+++ b/rt/lib/RT/Interface/Web/Handler.pm
@@ -54,7 +54,6 @@ use CGI qw/-private_tempfiles/;
use MIME::Entity;
use Text::Wrapper;
use CGI::Cookie;
-use Time::ParseDate;
use Time::HiRes;
use HTML::Scrubber;
use RT::Interface::Web;
@@ -62,6 +61,9 @@ use RT::Interface::Web::Request;
use File::Path qw( rmtree );
use File::Glob qw( bsd_glob );
use File::Spec::Unix;
+use HTTP::Message::PSGI;
+use HTTP::Request;
+use HTTP::Response;
sub DefaultHandlerArgs { (
comp_root => [
@@ -104,7 +106,6 @@ sub InitSessionDir {
}
-use UNIVERSAL::require;
sub NewHandler {
my $class = shift;
$class->require or die $!;
@@ -114,7 +115,7 @@ sub NewHandler {
@_
);
- $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 );
+ $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeHTML );
$handler->interp->set_escape( u => \&RT::Interface::Web::EscapeURI );
$handler->interp->set_escape( j => \&RT::Interface::Web::EscapeJS );
return($handler);
@@ -154,7 +155,7 @@ and is not recommended to change.
=item Clean up state of RT::Action::SendEmail using 'CleanSlate' method
-=item Flush tmp GnuPG key preferences
+=item Flush tmp crypt key preferences
=back
@@ -181,10 +182,9 @@ sub CleanupRequest {
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 );
+ if (RT->Config->Get('Crypt')->{'Enable'}) {
+ RT::Crypt->UseKeyForEncryption();
+ RT::Crypt->UseKeyForSigning( undef );
}
%RT::Ticket::MERGE_CACHE = ( effective => {}, merged => {} );
@@ -248,6 +248,7 @@ MODPERL
use RT::Interface::Web::Handler;
use CGI::Emulate::PSGI;
+use Plack::Builder;
use Plack::Request;
use Plack::Response;
use Plack::Util;
@@ -262,7 +263,7 @@ sub PSGIApp {
$self->InitSessionDir;
- return sub {
+ my $mason = sub {
my $env = shift;
{
@@ -270,7 +271,14 @@ sub PSGIApp {
return $self->_psgi_response_cb( $res->finalize ) if $res;
}
- RT::ConnectToDatabase() unless RT->InstallMode;
+ unless (RT->InstallMode) {
+ unless (eval { RT::ConnectToDatabase() }) {
+ my $res = Plack::Response->new(503);
+ $res->content_type("text/plain");
+ $res->body("Database inaccessible; contact the RT administrator (".RT->Config->Get("OwnerEmail").")");
+ return $self->_psgi_response_cb( $res->finalize, sub { $self->CleanupRequest } );
+ }
+ }
my $req = Plack::Request->new($env);
@@ -307,7 +315,59 @@ sub PSGIApp {
sub {
$self->CleanupRequest()
});
-};
+ };
+
+ my $app = $self->StaticWrap($mason);
+ for my $plugin (RT->Config->Get("Plugins")) {
+ my $wrap = $plugin->can("PSGIWrap")
+ or next;
+ $app = $wrap->($plugin, $app);
+ }
+ return $app;
+}
+
+sub StaticWrap {
+ my $self = shift;
+ my $app = shift;
+ my $builder = Plack::Builder->new;
+
+ my $headers = RT::Interface::Web::GetStaticHeaders(Time => 'forever');
+
+ for my $static ( RT->Config->Get('StaticRoots') ) {
+ if ( ref $static && ref $static eq 'HASH' ) {
+ $builder->add_middleware(
+ '+RT::Interface::Web::Middleware::StaticHeaders',
+ path => $static->{'path'},
+ headers => $headers,
+ );
+ $builder->add_middleware(
+ 'Plack::Middleware::Static',
+ pass_through => 1,
+ %$static
+ );
+ }
+ else {
+ $RT::Logger->error(
+ "Invalid config StaticRoots: item can only be a hashref" );
+ }
+ }
+
+ my $path = sub { s!^/static/!! };
+ $builder->add_middleware(
+ '+RT::Interface::Web::Middleware::StaticHeaders',
+ path => $path,
+ headers => $headers,
+ );
+ for my $root (RT::Interface::Web->StaticRoots) {
+ $builder->add_middleware(
+ 'Plack::Middleware::Static',
+ path => $path,
+ root => $root,
+ pass_through => 1,
+ );
+ }
+ return $builder->to_app($app);
+}
sub _psgi_response_cb {
my $self = shift;
@@ -334,7 +394,19 @@ sub _psgi_response_cb {
return $_[0];
};
});
- }
+}
+
+sub GetStatic {
+ my $class = shift;
+ my $path = shift;
+ my $static = $class->StaticWrap(
+ # Anything the static wrap doesn't handle gets 404'd.
+ sub { [404, [], []] }
+ );
+ my $response = HTTP::Response->from_psgi(
+ $static->( HTTP::Request->new(GET => $path)->to_psgi )
+ );
+ return $response;
}
1;
diff --git a/rt/lib/RT/Interface/Web/Menu.pm b/rt/lib/RT/Interface/Web/Menu.pm
index 03ce8ac..8670b8a 100644
--- a/rt/lib/RT/Interface/Web/Menu.pm
+++ b/rt/lib/RT/Interface/Web/Menu.pm
@@ -57,7 +57,7 @@ use URI;
use Scalar::Util qw(weaken);
__PACKAGE__->mk_accessors(qw(
- key title description raw_html escape_title sort_order target class
+ key title description raw_html escape_title sort_order target class attributes
));
=head1 NAME
@@ -70,9 +70,9 @@ RT::Interface::Web::Menu - Handle the API for menu navigation
Creates a new L<RT::Interface::Web::Menu> object. Possible keys in the
I<PARAMHASH> are L</parent>, L</title>, L</description>, L</path>,
-L</raw_html>, L<escape_title>, L</sort_order>, L</class>, L</target> and
-L</active>. See the subroutines with the respective name below for
-each option's use.
+L</raw_html>, L<escape_title>, L</sort_order>, L</class>, L</target>,
+L<attributes>, and L</active>. See the subroutines with the respective name
+below for each option's use.
=cut
@@ -139,6 +139,12 @@ Get or set the frame or pseudo-target for this link. something like L<_blank>
Gets or sets the CSS class the menu item should have in addition to the default
classes. This is only used if L</raw_html> isn't specified.
+=head2 attributes [HASHREF]
+
+Gets or sets a hashref of HTML attribute name-value pairs that the menu item
+should have in addition to the attributes which have their own accessor, like
+L</class> and L</target>. This is only used if L</raw_html> isn't specified.
+
=head2 path
Gets or sets the URL that the menu's link goes to. If the link
diff --git a/rt/lib/RT/Interface/Web/Middleware/StaticHeaders.pm b/rt/lib/RT/Interface/Web/Middleware/StaticHeaders.pm
new file mode 100644
index 0000000..6d98d9e
--- /dev/null
+++ b/rt/lib/RT/Interface/Web/Middleware/StaticHeaders.pm
@@ -0,0 +1,80 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+# <sales@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::Middleware::StaticHeaders;
+
+use strict;
+use warnings;
+use base 'Plack::Middleware';
+use Plack::Util;
+
+use Plack::Util::Accessor qw(path headers);
+
+sub call {
+ my ( $self, $env ) = @_;
+ my $res = $self->app->($env);
+ my $path_match = $self->path;
+ my $path = $env->{'PATH_INFO'};
+ for ($path) {
+ my $matched = 'CODE' eq ref $path_match ?
+ $path_match->($_, $env)
+ : $_ =~ $path_match;
+ return $res unless $matched;
+ return $self->response_cb( $res,
+ sub {
+ my $res = shift;
+ my $headers = $res->[1];
+ Plack::Util::header_iter( $self->headers, sub {
+ Plack::Util::header_set($headers, @_);
+ } );
+ }
+ );
+ }
+}
+
+1;
diff --git a/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm b/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm
index 1da160c..d7de61c 100755
--- a/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm
+++ b/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm
@@ -113,9 +113,7 @@ sub GetReferencedQueues {
return unless $clause->{Key} eq 'Queue';
return unless $clause->{Op} eq '=';
- my $value = $clause->{Value};
- $value =~ s/\\(.)/$1/g if $value =~ s/^'(.*)'$/$1/;
- $queues->{ $value } = 1;
+ $queues->{ $clause->{RawValue} } = 1;
}
);
@@ -257,6 +255,7 @@ sub ParseSQL {
$callback{'EntryAggregator'} = sub { $node->setNodeValue( $_[0] ) };
$callback{'Condition'} = sub {
my ($key, $op, $value) = @_;
+ my $rawvalue = $value;
my ($main_key) = split /[.]/, $key;
@@ -281,7 +280,7 @@ sub ParseSQL {
$key = "'$key'";
}
- my $clause = { Key => $key, Op => $op, Value => $value };
+ my $clause = { Key => $key, Op => $op, Value => $value, RawValue => $rawvalue };
$node->addChild( __PACKAGE__->new( $clause ) );
};
$callback{'Error'} = sub { push @results, @_ };
diff --git a/rt/lib/RT/Interface/Web/Request.pm b/rt/lib/RT/Interface/Web/Request.pm
index 7a246a3..61cfa70 100644
--- a/rt/lib/RT/Interface/Web/Request.pm
+++ b/rt/lib/RT/Interface/Web/Request.pm
@@ -51,7 +51,6 @@ package RT::Interface::Web::Request;
use strict;
use warnings;
-our $VERSION = '0.30';
use HTML::Mason::PSGIHandler;
use base qw(HTML::Mason::Request::PSGI);
use Params::Validate qw(:all);
@@ -65,8 +64,6 @@ sub new {
=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.
diff --git a/rt/lib/RT/Interface/Web/Session.pm b/rt/lib/RT/Interface/Web/Session.pm
index d854130..c4cc930 100644
--- a/rt/lib/RT/Interface/Web/Session.pm
+++ b/rt/lib/RT/Interface/Web/Session.pm
@@ -84,8 +84,7 @@ sub Class {
my $class = RT->Config->Get('WebSessionClass')
|| $self->Backends->{RT->Config->Get('DatabaseType')}
|| 'Apache::Session::File';
- eval "require $class";
- die $@ if $@;
+ $class->require or die "Can't load $class: $@";
return $class;
}
@@ -98,8 +97,9 @@ sessions class names as values.
sub Backends {
return {
- mysql => 'Apache::Session::MySQL',
- Pg => 'Apache::Session::Postgres',
+ mysql => 'Apache::Session::MySQL',
+ Pg => 'Apache::Session::Postgres',
+ Oracle => 'Apache::Session::Oracle',
};
}
@@ -112,15 +112,27 @@ new session objects.
sub Attributes {
my $class = $_[0]->Class;
- return !$class->isa('Apache::Session::File') ? {
- Handle => $RT::Handle->dbh,
- LockHandle => $RT::Handle->dbh,
- Transaction => 1,
- } : {
+ my $res;
+ if ( my %props = RT->Config->Get('WebSessionProperties') ) {
+ $res = \%props;
+ }
+ elsif ( $class->isa('Apache::Session::File') ) {
+ $res = {
Directory => $RT::MasonSessionDir,
LockDirectory => $RT::MasonSessionDir,
Transaction => 1,
};
+ }
+ else {
+ $res = {
+ Handle => $RT::Handle->dbh,
+ LockHandle => $RT::Handle->dbh,
+ Transaction => 1,
+ };
+ }
+ $res->{LongReadLen} = RT->Config->Get('MaxAttachmentSize')
+ if $class->isa('Apache::Session::Oracle');
+ return $res;
}
=head3 Ids