summaryrefslogtreecommitdiff
path: root/rt/lib/RT/Interface/Web.pm
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT/Interface/Web.pm')
-rw-r--r--rt/lib/RT/Interface/Web.pm1849
1 files changed, 1093 insertions, 756 deletions
diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm
index 4e5fca1..b4279fb 100644
--- a/rt/lib/RT/Interface/Web.pm
+++ b/rt/lib/RT/Interface/Web.pm
@@ -1,40 +1,40 @@
# BEGIN BPS TAGGED BLOCK {{{
-#
+#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
+#
+# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
# <jesse@bestpractical.com>
-#
+#
# (Except where explicitly superseded by other copyright notices)
-#
-#
+#
+#
# LICENSE:
-#
+#
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
-#
+#
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
-#
+#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
-#
-#
+#
+#
# CONTRIBUTION SUBMISSION POLICY:
-#
+#
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
-#
+#
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
@@ -43,34 +43,31 @@
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
-#
+#
# END BPS TAGGED BLOCK }}}
+
## Portions Copyright 2000 Tobias Brox <tobix@fsck.com>
## This is a library of static subs to be used by the Mason web
## interface to RT
-
=head1 NAME
RT::Interface::Web
-=begin testing
-
-use_ok(RT::Interface::Web);
-
-=end testing
=cut
-
use strict;
use warnings;
package RT::Interface::Web;
-use HTTP::Date;
+
use RT::SavedSearches;
-use URI;
+use URI qw();
+use RT::Interface::Web::Session;
+use Digest::MD5 ();
+use Encode qw();
# {{{ EscapeUTF8
@@ -80,22 +77,17 @@ does a css-busting but minimalist escaping of whatever html you're passing in.
=cut
-sub EscapeUTF8 {
- my $ref = shift;
- return unless defined $$ref;
- my $val = $$ref;
- use bytes;
- $val =~ s/&/&#38;/g;
- $val =~ s/</&lt;/g;
- $val =~ s/>/&gt;/g;
- $val =~ s/\(/&#40;/g;
- $val =~ s/\)/&#41;/g;
- $val =~ s/"/&#34;/g;
- $val =~ s/'/&#39;/g;
- $$ref = $val;
- Encode::_utf8_on($$ref);
-
-
+sub EscapeUTF8 {
+ my $ref = shift;
+ return unless defined $$ref;
+
+ $$ref =~ s/&/&#38;/g;
+ $$ref =~ s/</&lt;/g;
+ $$ref =~ s/>/&gt;/g;
+ $$ref =~ s/\(/&#40;/g;
+ $$ref =~ s/\)/&#41;/g;
+ $$ref =~ s/"/&#34;/g;
+ $$ref =~ s/'/&#39;/g;
}
# }}}
@@ -108,12 +100,12 @@ Escapes URI component according to RFC2396
=cut
-use Encode qw();
sub EscapeURI {
my $ref = shift;
- $$ref = Encode::encode_utf8( $$ref );
+ return unless defined $$ref;
+
+ use bytes;
$$ref =~ s/([^a-zA-Z0-9_.!~*'()-])/uc sprintf("%%%02X", ord($1))/eg;
- Encode::_utf8_on( $$ref );
}
# }}}
@@ -129,13 +121,7 @@ just downcase $ENV{'REMOTE_USER'}
=cut
sub WebCanonicalizeInfo {
- my $user;
-
- if ( defined $ENV{'REMOTE_USER'} ) {
- $user = lc ( $ENV{'REMOTE_USER'} ) if( length($ENV{'REMOTE_USER'}) );
- }
-
- return $user;
+ return $ENV{'REMOTE_USER'} ? lc $ENV{'REMOTE_USER'} : $ENV{'REMOTE_USER'};
}
# }}}
@@ -155,19 +141,21 @@ sub WebExternalAutoInfo {
# default to making Privileged users, even if they specify
# some other default Attributes
- if (!$RT::AutoCreate ||
- ( ref($RT::AutoCreate) && not exists $RT::AutoCreate->{Privileged})) {
+ if ( !$RT::AutoCreate
+ || ( ref($RT::AutoCreate) && not exists $RT::AutoCreate->{Privileged} ) )
+ {
$user_info{'Privileged'} = 1;
}
- if ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
+ if ( $^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/ ) {
+
# Populate fields with information from Unix /etc/passwd
- my ($comments, $realname) = (getpwnam($user))[5, 6];
+ my ( $comments, $realname ) = ( getpwnam($user) )[ 5, 6 ];
$user_info{'Comments'} = $comments if defined $comments;
$user_info{'RealName'} = $realname if defined $realname;
- }
- elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
+ } elsif ( $^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1' ) {
+
# Populate fields with information from NT domain controller
}
@@ -177,7 +165,323 @@ sub WebExternalAutoInfo {
# }}}
+sub HandleRequest {
+ my $ARGS = shift;
+
+ $HTML::Mason::Commands::r->content_type("text/html; charset=utf-8");
+
+ $HTML::Mason::Commands::m->{'rt_base_time'} = [ Time::HiRes::gettimeofday() ];
+
+ # Roll back any dangling transactions from a previous failed connection
+ $RT::Handle->ForceRollback() if $RT::Handle->TransactionDepth;
+
+ MaybeEnableSQLStatementLog();
+
+ # avoid reentrancy, as suggested by masonbook
+ local *HTML::Mason::Commands::session unless $HTML::Mason::Commands::m->is_subrequest;
+
+ $HTML::Mason::Commands::m->autoflush( $HTML::Mason::Commands::m->request_comp->attr('AutoFlush') )
+ if ( $HTML::Mason::Commands::m->request_comp->attr_exists('AutoFlush') );
+
+ DecodeARGS($ARGS);
+ PreprocessTimeUpdates($ARGS);
+
+ MaybeShowInstallModePage();
+
+ $HTML::Mason::Commands::m->comp( '/Elements/SetupSessionCookie', %$ARGS );
+ SendSessionCookie();
+ $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new() unless _UserLoggedIn();
+
+ MaybeShowNoAuthPage($ARGS);
+
+ AttemptExternalAuth($ARGS) if RT->Config->Get('WebExternalAuthContinuous') or not _UserLoggedIn();
+
+ _ForceLogout() unless _UserLoggedIn();
+
+ # Process per-page authentication callbacks
+ $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Auth', CallbackPage => '/autohandler' );
+
+ unless ( _UserLoggedIn() ) {
+ _ForceLogout();
+
+ # If the user is logging in, let's authenticate
+ if ( defined $ARGS->{user} && defined $ARGS->{pass} ) {
+ AttemptPasswordAuthentication($ARGS);
+ } else {
+ # if no credentials then show him login page
+ $HTML::Mason::Commands::m->comp( '/Elements/Login', %$ARGS );
+ $HTML::Mason::Commands::m->abort;
+ }
+ }
+
+ # now it applies not only to home page, but any dashboard that can be used as a workspace
+ $HTML::Mason::Commands::session{'home_refresh_interval'} = $ARGS->{'HomeRefreshInterval'}
+ if ( $ARGS->{'HomeRefreshInterval'} );
+
+ # Process per-page global callbacks
+ $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Default', CallbackPage => '/autohandler' );
+
+ ShowRequestedPage($ARGS);
+ LogRecordedSQLStatements();
+}
+
+sub _ForceLogout {
+
+ delete $HTML::Mason::Commands::session{'CurrentUser'};
+}
+
+sub _UserLoggedIn {
+ if ( $HTML::Mason::Commands::session{CurrentUser} && $HTML::Mason::Commands::session{'CurrentUser'}->id ) {
+ return 1;
+ } else {
+ return undef;
+ }
+
+}
+
+=head2 MaybeShowInstallModePage
+
+This function, called exclusively by RT's autohandler, dispatches
+a request to RT's Installation workflow, only if Install Mode is enabled in the configuration file.
+
+If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler
+
+=cut
+
+sub MaybeShowInstallModePage {
+ return unless RT->InstallMode;
+
+ my $m = $HTML::Mason::Commands::m;
+ if ( $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex') ) {
+ $m->call_next();
+ } elsif ( $m->request_comp->path !~ '^(/+)Install/' ) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "Install/index.html" );
+ } else {
+ $m->call_next();
+ }
+ $m->abort();
+}
+
+=head2 MaybeShowNoAuthPage \%ARGS
+
+This function, called exclusively by RT's autohandler, dispatches
+a request to the page a user requested (but only if it matches the "noauth" regex.
+
+If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler
+
+=cut
+sub MaybeShowNoAuthPage {
+ my $ARGS = shift;
+
+ my $m = $HTML::Mason::Commands::m;
+
+ return unless $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex');
+
+ # If it's a noauth file, don't ask for auth.
+ SendSessionCookie();
+ $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
+ $m->abort;
+}
+
+=head2 ShowRequestedPage \%ARGS
+
+This function, called exclusively by RT's autohandler, dispatches
+a request to the page a user requested (making sure that unpriviled users
+can only see self-service pages.
+
+=cut
+
+sub ShowRequestedPage {
+ my $ARGS = shift;
+
+ my $m = $HTML::Mason::Commands::m;
+
+ SendSessionCookie();
+
+ # If the user isn't privileged, they can only see SelfService
+ unless ( $HTML::Mason::Commands::session{'CurrentUser'}->Privileged ) {
+
+ # if the user is trying to access a ticket, redirect them
+ if ( $m->request_comp->path =~ '^(/+)Ticket/Display.html' && $ARGS->{'id'} ) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/Display.html?id=" . $ARGS->{'id'} );
+ }
+
+ # otherwise, drop the user at the SelfService default page
+ elsif ( $m->base_comp->path !~ RT->Config->Get('SelfServiceRegex') ) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/" );
+ }
+
+ # if user is in SelfService dir let him do anything
+ else {
+ $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
+ }
+ } else {
+ $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
+ }
+
+}
+
+sub AttemptExternalAuth {
+ my $ARGS = shift;
+
+ return unless ( RT->Config->Get('WebExternalAuth') );
+
+ my $user = $ARGS->{user};
+ my $m = $HTML::Mason::Commands::m;
+
+ # If RT is configured for external auth, let's go through and get REMOTE_USER
+
+ # do we actually have a REMOTE_USER equivlent?
+ if ( RT::Interface::Web::WebCanonicalizeInfo() ) {
+ my $orig_user = $user;
+
+ $user = RT::Interface::Web::WebCanonicalizeInfo();
+ my $load_method = RT->Config->Get('WebExternalGecos') ? 'LoadByGecos' : 'Load';
+
+ if ( $^O eq 'MSWin32' and RT->Config->Get('WebExternalGecos') ) {
+ my $NodeName = Win32::NodeName();
+ $user =~ s/^\Q$NodeName\E\\//i;
+ }
+
+ InstantiateNewSession() unless _UserLoggedIn;
+ $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
+ $HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user);
+
+ if ( RT->Config->Get('WebExternalAuto') and not _UserLoggedIn() ) {
+
+ # Create users on-the-fly
+ my $UserObj = RT::User->new($RT::SystemUser);
+ my ( $val, $msg ) = $UserObj->Create(
+ %{ ref RT->Config->Get('AutoCreate') ? RT->Config->Get('AutoCreate') : {} },
+ Name => $user,
+ Gecos => $user,
+ );
+
+ if ($val) {
+
+ # now get user specific information, to better create our user.
+ my $new_user_info = RT::Interface::Web::WebExternalAutoInfo($user);
+
+ # set the attributes that have been defined.
+ foreach my $attribute ( $UserObj->WritableAttributes ) {
+ $m->callback(
+ Attribute => $attribute,
+ User => $user,
+ UserInfo => $new_user_info,
+ CallbackName => 'NewUser',
+ CallbackPage => '/autohandler'
+ );
+ my $method = "Set$attribute";
+ $UserObj->$method( $new_user_info->{$attribute} ) if defined $new_user_info->{$attribute};
+ }
+ $HTML::Mason::Commands::session{'CurrentUser'}->Load($user);
+ } else {
+
+ # we failed to successfully create the user. abort abort abort.
+ delete $HTML::Mason::Commands::session{'CurrentUser'};
+ $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc( 'Cannot create user: [_1]', $msg ) )
+ if RT->Config->Get('WebFallbackToInternalAuth');;
+ $m->abort();
+ }
+ }
+
+ if ( _UserLoggedIn() ) {
+ $m->callback( %$ARGS, CallbackName => 'ExternalAuthSuccessfulLogin', CallbackPage => '/autohandler' );
+ } else {
+ delete $HTML::Mason::Commands::session{'CurrentUser'};
+ $user = $orig_user;
+
+ if ( RT->Config->Get('WebExternalOnly') ) {
+ $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') );
+ $m->abort();
+ }
+ }
+ } elsif ( RT->Config->Get('WebFallbackToInternalAuth') ) {
+ unless ( defined $HTML::Mason::Commands::session{'CurrentUser'} ) {
+ # XXX unreachable due to prior defaulting in HandleRequest (check c34d108)
+ $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') );
+ $m->abort();
+ }
+ } else {
+
+ # WebExternalAuth is set, but we don't have a REMOTE_USER. abort
+ # XXX: we must return AUTH_REQUIRED status or we fallback to
+ # internal auth here too.
+ delete $HTML::Mason::Commands::session{'CurrentUser'}
+ if defined $HTML::Mason::Commands::session{'CurrentUser'};
+ }
+}
+
+sub AttemptPasswordAuthentication {
+ my $ARGS = shift;
+ my $user_obj = RT::CurrentUser->new();
+ $user_obj->Load( $ARGS->{user} );
+
+ my $m = $HTML::Mason::Commands::m;
+
+ unless ( $user_obj->id && $user_obj->IsPassword( $ARGS->{pass} ) ) {
+ $RT::Logger->error("FAILED LOGIN for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
+ $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('Your username or password is incorrect'), );
+ $m->callback( %$ARGS, CallbackName => 'FailedLogin', CallbackPage => '/autohandler' );
+ $m->abort;
+ }
+
+ $RT::Logger->info("Successful login for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
+ InstantiateNewSession();
+ $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
+ $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
+}
+
+=head2 LoadSessionFromCookie
+
+Load or setup a session cookie for the current user.
+
+=cut
+
+sub _SessionCookieName {
+ my $cookiename = "RT_SID_" . RT->Config->Get('rtname');
+ $cookiename .= "." . $ENV{'SERVER_PORT'} if $ENV{'SERVER_PORT'};
+ return $cookiename;
+}
+
+sub LoadSessionFromCookie {
+
+ my %cookies = CGI::Cookie->fetch;
+ my $cookiename = _SessionCookieName();
+ my $SessionCookie = ( $cookies{$cookiename} ? $cookies{$cookiename}->value : undef );
+ tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', $SessionCookie;
+ unless ( $SessionCookie && $HTML::Mason::Commands::session{'_session_id'} eq $SessionCookie ) {
+ undef $cookies{$cookiename};
+ }
+ if ( int RT->Config->Get('AutoLogoff') ) {
+ my $now = int( time / 60 );
+ my $last_update = $HTML::Mason::Commands::session{'_session_last_update'} || 0;
+
+ if ( $last_update && ( $now - $last_update - RT->Config->Get('AutoLogoff') ) > 0 ) {
+ InstantiateNewSession();
+ }
+
+ # save session on each request when AutoLogoff is turned on
+ $HTML::Mason::Commands::session{'_session_last_update'} = $now if $now != $last_update;
+ }
+}
+
+sub InstantiateNewSession {
+ tied(%HTML::Mason::Commands::session)->delete if tied(%HTML::Mason::Commands::session);
+ tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', undef;
+}
+
+sub SendSessionCookie {
+ my $cookie = CGI::Cookie->new(
+ -name => _SessionCookieName(),
+ -value => $HTML::Mason::Commands::session{_session_id},
+ -path => RT->Config->Get('WebPath'),
+ -secure => ( RT->Config->Get('WebSecureCookies') ? 1 : 0 )
+ );
+
+ $HTML::Mason::Commands::r->err_headers_out->{'Set-Cookie'} = $cookie->as_string;
+}
=head2 Redirect URL
@@ -188,27 +492,42 @@ a cached DBI statement handle twice at the same time.
=cut
-
sub Redirect {
my $redir_to = shift;
untie $HTML::Mason::Commands::session;
- my $uri = URI->new($redir_to);
- my $server_uri = URI->new($RT::WebURL);
+ my $uri = URI->new($redir_to);
+ my $server_uri = URI->new( RT->Config->Get('WebURL') );
# If the user is coming in via a non-canonical
# hostname, don't redirect them to the canonical host,
# it will just upset them (and invalidate their credentials)
- if ($uri->host eq $server_uri->host &&
- $uri->port eq $server_uri->port) {
- $uri->host($ENV{'HTTP_HOST'});
- $uri->port($ENV{'SERVER_PORT'});
+ # don't do this if $RT::CanoniaclRedirectURLs is true
+ if ( !RT->Config->Get('CanonicalizeRedirectURLs')
+ && $uri->host eq $server_uri->host
+ && $uri->port eq $server_uri->port )
+ {
+ if ( defined $ENV{HTTPS} and $ENV{'HTTPS'} eq 'on' ) {
+ $uri->scheme('https');
+ } else {
+ $uri->scheme('http');
}
- $HTML::Mason::Commands::m->redirect($uri->canonical);
+ # [rt3.fsck.com #12716] Apache recommends use of $SERVER_HOST
+ $uri->host( $ENV{'SERVER_HOST'} || $ENV{'HTTP_HOST'} );
+ $uri->port( $ENV{'SERVER_PORT'} );
+ }
+
+ # not sure why, but on some systems without this call mason doesn't
+ # set status to 302, but 200 instead and people see blank pages
+ $HTML::Mason::Commands::r->status(302);
+
+ # Perlbal expects a status message, but Mason's default redirect status
+ # doesn't provide one. See also rt.cpan.org #36689.
+ $HTML::Mason::Commands::m->redirect( $uri->canonical, "302 Found" );
+
$HTML::Mason::Commands::m->abort;
}
-
=head2 StaticFileHeaders
Send the browser a few headers to try to get it to (somewhat agressively)
@@ -219,23 +538,196 @@ This routine could really use _accurate_ heuristics. (XXX TODO)
=cut
sub StaticFileHeaders {
+ my $date = RT::Date->new($RT::SystemUser);
+
# make cache public
$HTML::Mason::Commands::r->headers_out->{'Cache-Control'} = 'max-age=259200, public';
# Expire things in a month.
- $HTML::Mason::Commands::r->headers_out->{'Expires'} = HTTP::Date::time2str( time() + 2592000 );
+ $date->Set( Value => time + 30 * 24 * 60 * 60 );
+ $HTML::Mason::Commands::r->headers_out->{'Expires'} = $date->RFC2616;
# if we set 'Last-Modified' then browser request a comp using 'If-Modified-Since'
# request, but we don't handle it and generate full reply again
# Last modified at server start time
- #$HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = HTTP::Date::time2str($^T);
+ # $date->Set( Value => $^T );
+ # $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = $date->RFC2616;
+}
+
+=head2 SendStaticFile
+
+Takes a File => path and a Type => Content-type
+
+If Type isn't provided and File is an image, it will
+figure out a sane Content-type, otherwise it will
+send application/octet-stream
+
+Will set caching headers using StaticFileHeaders
+
+=cut
+
+sub SendStaticFile {
+ my $self = shift;
+ my %args = @_;
+ my $file = $args{File};
+ my $type = $args{Type};
+
+ $self->StaticFileHeaders();
+
+ unless ($type) {
+ if ( $file =~ /\.(gif|png|jpe?g)$/i ) {
+ $type = "image/$1";
+ $type =~ s/jpg/jpeg/gi;
+ }
+ $type ||= "application/octet-stream";
+ }
+ $HTML::Mason::Commands::r->content_type($type);
+ open my $fh, "<$file" or die "couldn't open file: $!";
+ binmode($fh);
+ {
+ local $/ = \16384;
+ $HTML::Mason::Commands::m->out($_) while (<$fh>);
+ $HTML::Mason::Commands::m->flush_buffer;
+ }
+ close $fh;
+}
+
+sub StripContent {
+ my %args = @_;
+ my $content = $args{Content};
+ return '' unless $content;
+
+ # Make the content have no 'weird' newlines in it
+ $content =~ s/\r+\n/\n/g;
+
+ my $return_content = $content;
+
+ my $html = $args{ContentType} && $args{ContentType} eq "text/html";
+ my $sigonly = $args{StripSignature};
+
+ # massage content to easily detect if there's any real content
+ $content =~ s/\s+//g; # yes! remove all the spaces
+ if ( $html ) {
+ # remove html version of spaces and newlines
+ $content =~ s!&nbsp;!!g;
+ $content =~ s!<br/?>!!g;
+ }
+ # Filter empty content when type is text/html
+ return '' if $html && $content !~ /\S/;
+
+ # If we aren't supposed to strip the sig, just bail now.
+ return $return_content unless $sigonly;
+
+ # Find the signature
+ my $sig = $args{'CurrentUser'}->UserObj->Signature || '';
+ $sig =~ s/\s+//g;
+
+ # Check for plaintext sig
+ return '' if not $html and $content =~ /^(--)?\Q$sig\E$/;
+
+ # Check for html-formatted sig
+ RT::Interface::Web::EscapeUTF8( \$sig );
+ return ''
+ if $html
+ and $content =~ m{^(?:<p>)?(--)?\Q$sig\E(?:</p>)?$}s;
+
+ # Pass it through
+ return $return_content;
+}
+
+sub DecodeARGS {
+ my $ARGS = shift;
+
+ %{$ARGS} = map {
+
+ # if they've passed multiple values, they'll be an array. if they've
+ # passed just one, a scalar whatever they are, mark them as utf8
+ my $type = ref($_);
+ ( !$type )
+ ? Encode::is_utf8($_)
+ ? $_
+ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ )
+ : ( $type eq 'ARRAY' )
+ ? [ map { ( ref($_) or Encode::is_utf8($_) ) ? $_ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) }
+ @$_ ]
+ : ( $type eq 'HASH' )
+ ? { map { ( ref($_) or Encode::is_utf8($_) ) ? $_ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) }
+ %$_ }
+ : $_
+ } %$ARGS;
}
+sub PreprocessTimeUpdates {
+ my $ARGS = shift;
+
+ # Later in the code we use
+ # $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %ARGS );
+ # instead of $m->call_next to avoid problems with UTF8 keys in arguments.
+ # The call_next method pass through original arguments and if you have
+ # an argument with unicode key then in a next component you'll get two
+ # records in the args hash: one with key without UTF8 flag and another
+ # with the flag, which may result into errors. "{ base_comp => $m->request_comp }"
+ # is copied from mason's source to get the same results as we get from
+ # call_next method, this feature is not documented, so we just leave it
+ # here to avoid possible side effects.
+
+ # This code canonicalizes time inputs in hours into minutes
+ foreach my $field ( keys %$ARGS ) {
+ next unless $field =~ /^(.*)-TimeUnits$/i && $ARGS->{$1};
+ my $local = $1;
+ $ARGS->{$local} =~ s{\b (?: (\d+) \s+ )? (\d+)/(\d+) \b}
+ {($1 || 0) + $3 ? $2 / $3 : 0}xe;
+ if ( $ARGS->{$field} && $ARGS->{$field} =~ /hours/i ) {
+ $ARGS->{$local} *= 60;
+ }
+ delete $ARGS->{$field};
+ }
+
+}
+
+sub MaybeEnableSQLStatementLog {
+
+ my $log_sql_statements = RT->Config->Get('StatementLog');
+
+ if ($log_sql_statements) {
+ $RT::Handle->ClearSQLStatementLog;
+ $RT::Handle->LogSQLStatements(1);
+ }
+
+}
+
+sub LogRecordedSQLStatements {
+ my $log_sql_statements = RT->Config->Get('StatementLog');
+
+ return unless ($log_sql_statements);
+
+ my @log = $RT::Handle->SQLStatementLog;
+ $RT::Handle->ClearSQLStatementLog;
+ for my $stmt (@log) {
+ my ( $time, $sql, $bind, $duration ) = @{$stmt};
+ my @bind;
+ if ( ref $bind ) {
+ @bind = @{$bind};
+ } else {
+
+ # Older DBIx-SB
+ $duration = $bind;
+ }
+ $RT::Logger->log(
+ level => $log_sql_statements,
+ message => "SQL("
+ . sprintf( "%.6f", $duration )
+ . "s): $sql;"
+ . ( @bind ? " [ bound values: @{[map{qq|'$_'|} @bind]} ]" : "" )
+ );
+ }
+
+}
package HTML::Mason::Commands;
-use vars qw/$r $m %session/;
+use vars qw/$r $m %session/;
# {{{ loc
@@ -250,14 +742,19 @@ through
sub loc {
- if ($session{'CurrentUser'} &&
- UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
- return($session{'CurrentUser'}->loc(@_));
- }
- elsif ( my $u = eval { RT::CurrentUser->new($RT::SystemUser->Id) } ) {
- return ($u->loc(@_));
- }
- else {
+ if ( $session{'CurrentUser'}
+ && UNIVERSAL::can( $session{'CurrentUser'}, 'loc' ) )
+ {
+ return ( $session{'CurrentUser'}->loc(@_) );
+ } elsif (
+ my $u = eval {
+ RT::CurrentUser->new();
+ }
+ )
+ {
+ return ( $u->loc(@_) );
+ } else {
+
# pathetic case -- SystemUser is gone.
return $_[0];
}
@@ -265,7 +762,6 @@ sub loc {
# }}}
-
# {{{ loc_fuzzy
=head2 loc_fuzzy STRING
@@ -279,40 +775,41 @@ inside the lexicon file.
=cut
sub loc_fuzzy {
- my $msg = shift;
-
- if ($session{'CurrentUser'} &&
- UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
- return($session{'CurrentUser'}->loc_fuzzy($msg));
- }
- else {
- my $u = RT::CurrentUser->new($RT::SystemUser->Id);
- return ($u->loc_fuzzy($msg));
+ my $msg = shift;
+
+ if ( $session{'CurrentUser'}
+ && UNIVERSAL::can( $session{'CurrentUser'}, 'loc' ) )
+ {
+ return ( $session{'CurrentUser'}->loc_fuzzy($msg) );
+ } else {
+ my $u = RT::CurrentUser->new( $RT::SystemUser->Id );
+ return ( $u->loc_fuzzy($msg) );
}
}
# }}}
-
# {{{ sub Abort
# Error - calls Error and aborts
sub Abort {
+ my $why = shift;
+ my %args = @_;
- if ($session{'ErrorDocument'} &&
- $session{'ErrorDocumentType'}) {
- $r->content_type($session{'ErrorDocumentType'});
- $m->comp($session{'ErrorDocument'} , Why => shift);
+ if ( $session{'ErrorDocument'}
+ && $session{'ErrorDocumentType'} )
+ {
+ $r->content_type( $session{'ErrorDocumentType'} );
+ $m->comp( $session{'ErrorDocument'}, Why => $why, %args );
$m->abort;
- }
- else {
- $m->comp("/Elements/Error" , Why => shift);
+ } else {
+ $m->comp( "/Elements/Error", Why => $why, %args );
$m->abort;
}
}
# }}}
-# {{{ sub CreateTicket
+# {{{ sub CreateTicket
=head2 CreateTicket ARGS
@@ -336,17 +833,30 @@ sub CreateTicket {
Abort('You have no permission to create tickets in that queue.');
}
- my $due = new RT::Date( $session{'CurrentUser'} );
- $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
- my $starts = new RT::Date( $session{'CurrentUser'} );
- $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
+ my $due;
+ if ( defined $ARGS{'Due'} and $ARGS{'Due'} =~ /\S/ ) {
+ $due = new RT::Date( $session{'CurrentUser'} );
+ $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
+ }
+ my $starts;
+ if ( defined $ARGS{'Starts'} and $ARGS{'Starts'} =~ /\S/ ) {
+ $starts = new RT::Date( $session{'CurrentUser'} );
+ $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
+ }
+
+ my $sigless = RT::Interface::Web::StripContent(
+ Content => $ARGS{Content},
+ ContentType => $ARGS{ContentType},
+ StripSignature => 1,
+ CurrentUser => $session{'CurrentUser'},
+ );
my $MIMEObj = MakeMIMEEntity(
- Subject => $ARGS{'Subject'},
- From => $ARGS{'From'},
- Cc => $ARGS{'Cc'},
- Body => $ARGS{'Content'},
- Type => $ARGS{'ContentType'},
+ Subject => $ARGS{'Subject'},
+ From => $ARGS{'From'},
+ Cc => $ARGS{'Cc'},
+ Body => $sigless,
+ Type => $ARGS{'ContentType'},
);
if ( $ARGS{'Attachments'} ) {
@@ -354,8 +864,8 @@ sub CreateTicket {
$RT::Logger->error("Couldn't make multipart message")
if !$rv || $rv !~ /^(?:DONE|ALREADY)$/;
- foreach ( values %{$ARGS{'Attachments'}} ) {
- unless ( $_ ) {
+ foreach ( values %{ $ARGS{'Attachments'} } ) {
+ unless ($_) {
$RT::Logger->error("Couldn't add empty attachemnt");
next;
}
@@ -363,121 +873,120 @@ sub CreateTicket {
}
}
+ foreach my $argument (qw(Encrypt Sign)) {
+ $MIMEObj->head->add( "X-RT-$argument" => $ARGS{$argument} ) if defined $ARGS{$argument};
+ }
+
my %create_args = (
- Type => $ARGS{'Type'} || 'ticket',
- Queue => $ARGS{'Queue'},
- Owner => $ARGS{'Owner'},
+ Type => $ARGS{'Type'} || 'ticket',
+ Queue => $ARGS{'Queue'},
+ Owner => $ARGS{'Owner'},
+
+ # note: name change
+ Requestor => $ARGS{'Requestors'},
+ Cc => $ARGS{'Cc'},
+ AdminCc => $ARGS{'AdminCc'},
InitialPriority => $ARGS{'InitialPriority'},
FinalPriority => $ARGS{'FinalPriority'},
TimeLeft => $ARGS{'TimeLeft'},
- TimeEstimated => $ARGS{'TimeEstimated'},
+ TimeEstimated => $ARGS{'TimeEstimated'},
TimeWorked => $ARGS{'TimeWorked'},
Subject => $ARGS{'Subject'},
Status => $ARGS{'Status'},
- Due => $due->ISO,
- Starts => $starts->ISO,
+ Due => $due ? $due->ISO : undef,
+ Starts => $starts ? $starts->ISO : undef,
MIMEObj => $MIMEObj
);
my @temp_squelch;
- foreach my $type (qw(Requestors Cc AdminCc)) {
- my @tmp = map { $_->format } grep { $_->address} Mail::Address->parse( $ARGS{ $type } );
+ foreach my $type (qw(Requestor Cc AdminCc)) {
+ push @temp_squelch, map $_->address, Email::Address->parse( $create_args{$type} )
+ if grep $_ eq $type || $_ eq ( $type . 's' ), @{ $ARGS{'SkipNotification'} || [] };
+
+ }
- $create_args{ $type } = [
- grep $_, map {
- my $user = RT::User->new( $RT::SystemUser );
- $user->LoadOrCreateByEmail( $_ );
- # convert to ids to avoid work later
- $user->id;
- } @tmp
- ];
- $RT::Logger->debug(
- "$type got ".join(',',@{$create_args{ $type }}) );
+ if (@temp_squelch) {
+ require RT::Action::SendEmail;
+ RT::Action::SendEmail->SquelchMailTo( RT::Action::SendEmail->SquelchMailTo, @temp_squelch );
+ }
+ if ( $ARGS{'AttachTickets'} ) {
+ require RT::Action::SendEmail;
+ RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets,
+ ref $ARGS{'AttachTickets'}
+ ? @{ $ARGS{'AttachTickets'} }
+ : ( $ARGS{'AttachTickets'} ) );
}
- # XXX: workaround for name conflict :(
- $create_args{'Requestor'} = delete $create_args{'Requestors'};
- foreach my $arg (keys %ARGS) {
+ foreach my $arg ( keys %ARGS ) {
next if $arg =~ /-(?:Magic|Category)$/;
- if ($arg =~ /^Object-RT::Transaction--CustomField-/) {
+ if ( $arg =~ /^Object-RT::Transaction--CustomField-/ ) {
$create_args{$arg} = $ARGS{$arg};
}
+
# Object-RT::Ticket--CustomField-3-Values
- elsif ($arg =~ /^Object-RT::Ticket--CustomField-(\d+)(.*?)$/) {
+ elsif ( $arg =~ /^Object-RT::Ticket--CustomField-(\d+)/ ) {
my $cfid = $1;
- my $cf = RT::CustomField->new( $session{'CurrentUser'});
- $cf->Load($cfid);
- if ( $cf->Type eq 'Freeform' && ! $cf->SingleValue) {
- $ARGS{$arg} =~ s/\r\n/\n/g;
- $ARGS{$arg} = [split('\n', $ARGS{$arg})];
- }
-
- if ( $cf->Type =~ /text/i) { # Catch both Text and Wikitext
- $ARGS{$arg} =~ s/\r//g;
+ my $cf = RT::CustomField->new( $session{'CurrentUser'} );
+ $cf->Load($cfid);
+ unless ( $cf->id ) {
+ $RT::Logger->error( "Couldn't load custom field #" . $cfid );
+ next;
}
if ( $arg =~ /-Upload$/ ) {
- $create_args{"CustomField-".$cfid} = _UploadedFile($arg);
- }
- else {
- $create_args{"CustomField-".$cfid} = $ARGS{"$arg"};
+ $create_args{"CustomField-$cfid"} = _UploadedFile($arg);
+ next;
}
- }
- }
+ my $type = $cf->Type;
- # XXX TODO This code should be about six lines. and badly needs refactoring.
-
- # {{{ turn new link lists into arrays, and pass in the proper arguments
- my (@dependson, @dependedonby, @parents, @children, @refersto, @referredtoby);
-
- foreach my $luri ( split ( / /, $ARGS{"new-DependsOn"} ) ) {
- $luri =~ s/\s*$//; # Strip trailing whitespace
- push @dependson, $luri;
- }
- $create_args{'DependsOn'} = \@dependson;
-
- foreach my $luri ( split ( / /, $ARGS{"DependsOn-new"} ) ) {
- push @dependedonby, $luri;
- }
- $create_args{'DependedOnBy'} = \@dependedonby;
+ my @values = ();
+ if ( ref $ARGS{$arg} eq 'ARRAY' ) {
+ @values = @{ $ARGS{$arg} };
+ } elsif ( $type =~ /text/i ) {
+ @values = ( $ARGS{$arg} );
+ } else {
+ no warnings 'uninitialized';
+ @values = split /\r*\n/, $ARGS{$arg};
+ }
+ @values = grep length, map {
+ s/\r+\n/\n/g;
+ s/^\s+//;
+ s/\s+$//;
+ $_;
+ }
+ grep defined, @values;
- foreach my $luri ( split ( / /, $ARGS{"new-MemberOf"} ) ) {
- $luri =~ s/\s*$//; # Strip trailing whitespace
- push @parents, $luri;
+ $create_args{"CustomField-$cfid"} = \@values;
+ }
}
- $create_args{'Parents'} = \@parents;
- foreach my $luri ( split ( / /, $ARGS{"MemberOf-new"} ) ) {
- push @children, $luri;
- }
- $create_args{'Children'} = \@children;
+ # turn new link lists into arrays, and pass in the proper arguments
+ my %map = (
+ 'new-DependsOn' => 'DependsOn',
+ 'DependsOn-new' => 'DependedOnBy',
+ 'new-MemberOf' => 'Parents',
+ 'MemberOf-new' => 'Children',
+ 'new-RefersTo' => 'RefersTo',
+ 'RefersTo-new' => 'ReferredToBy',
+ );
+ foreach my $key ( keys %map ) {
+ next unless $ARGS{$key};
+ $create_args{ $map{$key} } = [ grep $_, split ' ', $ARGS{$key} ];
- foreach my $luri ( split ( / /, $ARGS{"new-RefersTo"} ) ) {
- $luri =~ s/\s*$//; # Strip trailing whitespace
- push @refersto, $luri;
}
- $create_args{'RefersTo'} = \@refersto;
- foreach my $luri ( split ( / /, $ARGS{"RefersTo-new"} ) ) {
- push @referredtoby, $luri;
- }
- $create_args{'ReferredToBy'} = \@referredtoby;
- # }}}
-
-
my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
- unless ( $id ) {
+ unless ($id) {
Abort($ErrMsg);
}
- push ( @Actions, split("\n", $ErrMsg) );
+ push( @Actions, split( "\n", $ErrMsg ) );
unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
- Abort( "No permission to view newly created ticket #"
- . $Ticket->id . "." );
+ Abort( "No permission to view newly created ticket #" . $Ticket->id . "." );
}
return ( $Ticket, @Actions );
@@ -519,99 +1028,130 @@ sub LoadTicket {
# {{{ sub ProcessUpdateMessage
+=head2 ProcessUpdateMessage
+
+Takes paramhash with fields ARGSRef, TicketObj and SkipSignatureOnly.
+
+Don't write message if it only contains current user's signature and
+SkipSignatureOnly argument is true. Function anyway adds attachments
+and updates time worked field even if skips message. The default value
+is true.
+
+=cut
+
sub ProcessUpdateMessage {
- #TODO document what else this takes.
my %args = (
- ARGSRef => undef,
- Actions => undef,
- TicketObj => undef,
+ ARGSRef => undef,
+ TicketObj => undef,
+ SkipSignatureOnly => 1,
@_
);
- #Make the update content have no 'weird' newlines in it
- if ( $args{ARGSRef}->{'UpdateTimeWorked'}
- || $args{ARGSRef}->{'UpdateContent'}
- || $args{ARGSRef}->{'UpdateAttachments'} )
+ if ( $args{ARGSRef}->{'UpdateAttachments'}
+ && !keys %{ $args{ARGSRef}->{'UpdateAttachments'} } )
{
+ delete $args{ARGSRef}->{'UpdateAttachments'};
+ }
- if (
- $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() )
- {
- $args{ARGSRef}->{'UpdateSubject'} = undef;
+ # Strip the signature
+ $args{ARGSRef}->{UpdateContent} = RT::Interface::Web::StripContent(
+ Content => $args{ARGSRef}->{UpdateContent},
+ ContentType => $args{ARGSRef}->{UpdateContentType},
+ StripSignature => $args{SkipSignatureOnly},
+ CurrentUser => $args{'TicketObj'}->CurrentUser,
+ );
+
+ # If, after stripping the signature, we have no message, move the
+ # UpdateTimeWorked into adjusted TimeWorked, so that a later
+ # ProcessBasics can deal -- then bail out.
+ if ( not $args{ARGSRef}->{'UpdateAttachments'}
+ and not length $args{ARGSRef}->{'UpdateContent'} )
+ {
+ if ( $args{ARGSRef}->{'UpdateTimeWorked'} ) {
+ $args{ARGSRef}->{TimeWorked} = $args{TicketObj}->TimeWorked + delete $args{ARGSRef}->{'UpdateTimeWorked'};
}
+ return;
+ }
- my $Message = MakeMIMEEntity(
- Subject => $args{ARGSRef}->{'UpdateSubject'},
- Body => $args{ARGSRef}->{'UpdateContent'},
- Type => $args{ARGSRef}->{'UpdateContentType'},
- );
+ if ( $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject ) {
+ $args{ARGSRef}->{'UpdateSubject'} = undef;
+ }
- $Message->head->add( 'Message-ID' =>
- "<rt-"
- . $RT::VERSION . "-"
- . $$ . "-"
- . CORE::time() . "-"
- . int(rand(2000)) . "."
- . $args{'TicketObj'}->id . "-"
- . "0" . "-" # Scrip
- . "0" . "@" # Email sent
- . $RT::Organization
- . ">" );
- my $old_txn = RT::Transaction->new( $session{'CurrentUser'} );
- if ( $args{ARGSRef}->{'QuoteTransaction'} ) {
- $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} );
- }
- else {
- $old_txn = $args{TicketObj}->Transactions->First();
- }
+ my $Message = MakeMIMEEntity(
+ Subject => $args{ARGSRef}->{'UpdateSubject'},
+ Body => $args{ARGSRef}->{'UpdateContent'},
+ Type => $args{ARGSRef}->{'UpdateContentType'},
+ );
- if ( $old_txn->Message && $old_txn->Message->First ) {
- my @in_reply_to = split(/\s+/m, $old_txn->Message->First->GetHeader('In-Reply-To') || '');
- my @references = split(/\s+/m, $old_txn->Message->First->GetHeader('References') || '' );
- my @msgid = split(/\s+/m,$old_txn->Message->First->GetHeader('Message-ID') || '');
- my @rtmsgid = split(/\s+/m,$old_txn->Message->First->GetHeader('RT-Message-ID') || '');
+ $Message->head->add( 'Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'}, ) );
+ my $old_txn = RT::Transaction->new( $session{'CurrentUser'} );
+ if ( $args{ARGSRef}->{'QuoteTransaction'} ) {
+ $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} );
+ } else {
+ $old_txn = $args{TicketObj}->Transactions->First();
+ }
- $Message->head->replace( 'In-Reply-To', join (' ', @rtmsgid ? @rtmsgid : @msgid));
- $Message->head->replace( 'References', join(' ', @references, @msgid, @rtmsgid));
- }
+ if ( my $msg = $old_txn->Message->First ) {
+ RT::Interface::Email::SetInReplyTo(
+ Message => $Message,
+ InReplyTo => $msg
+ );
+ }
if ( $args{ARGSRef}->{'UpdateAttachments'} ) {
$Message->make_multipart;
- $Message->add_part($_)
- foreach values %{ $args{ARGSRef}->{'UpdateAttachments'} };
+ $Message->add_part($_) foreach values %{ $args{ARGSRef}->{'UpdateAttachments'} };
+ }
+
+ if ( $args{ARGSRef}->{'AttachTickets'} ) {
+ require RT::Action::SendEmail;
+ RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets,
+ ref $args{ARGSRef}->{'AttachTickets'}
+ ? @{ $args{ARGSRef}->{'AttachTickets'} }
+ : ( $args{ARGSRef}->{'AttachTickets'} ) );
+ }
+
+ my $bcc = $args{ARGSRef}->{'UpdateBcc'};
+ my $cc = $args{ARGSRef}->{'UpdateCc'};
+
+ my %message_args = (
+ CcMessageTo => $cc,
+ BccMessageTo => $bcc,
+ Sign => $args{ARGSRef}->{'Sign'},
+ Encrypt => $args{ARGSRef}->{'Encrypt'},
+ MIMEObj => $Message,
+ TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
+ );
+
+ unless ( $args{'ARGSRef'}->{'UpdateIgnoreAddressCheckboxes'} ) {
+ foreach my $key ( keys %{ $args{ARGSRef} } ) {
+ next unless $key =~ /^Update(Cc|Bcc)-(.*)$/;
+
+ my $var = ucfirst($1) . 'MessageTo';
+ my $value = $2;
+ if ( $message_args{$var} ) {
+ $message_args{$var} .= ", $value";
+ } else {
+ $message_args{$var} = $value;
+ }
+ }
}
- ## TODO: Implement public comments
+ my @results;
if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
- my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(
- CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
- BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
- MIMEObj => $Message,
- TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
- );
- push( @{ $args{Actions} }, $Description );
+ my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(%message_args);
+ push( @results, $Description );
$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
- }
- elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
- my ( $Transaction, $Description, $Object ) =
- $args{TicketObj}->Correspond(
- CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
- BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
- MIMEObj => $Message,
- TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
- );
- push( @{ $args{Actions} }, $Description );
+ } elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
+ my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Correspond(%message_args);
+ push( @results, $Description );
$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
+ } else {
+ push( @results,
+ loc("Update type was neither correspondence nor comment.") . " " . loc("Update not recorded.") );
}
- else {
- push(
- @{ $args{'Actions'} },
- loc("Update type was neither correspondence nor comment.") . " "
- . loc("Update not recorded.")
- );
- }
-}
+ return @results;
}
# }}}
@@ -638,301 +1178,65 @@ sub MakeMIMEEntity {
Body => undef,
AttachmentFieldName => undef,
Type => undef,
-# map Encode::encode_utf8($_), @_,
@_,
);
+ my $Message = MIME::Entity->build(
+ Type => 'multipart/mixed',
+ Subject => $args{'Subject'} || "",
+ From => $args{'From'},
+ Cc => $args{'Cc'},
+ );
- #Make the update content have no 'weird' newlines in it
+ if ( defined $args{'Body'} && length $args{'Body'} ) {
+
+ # Make the update content have no 'weird' newlines in it
+ $args{'Body'} =~ s/\r\n/\n/gs;
- $args{'Body'} =~ s/\r\n/\n/gs if $args{'Body'};
- my $Message;
- {
# MIME::Head is not happy in utf-8 domain. This only happens
# when processing an incoming email (so far observed).
no utf8;
use bytes;
- $Message = MIME::Entity->build(
- Subject => $args{'Subject'} || "",
- From => $args{'From'},
- Cc => $args{'Cc'},
- Type => $args{'Type'} || 'text/plain',
- 'Charset:' => 'utf8',
- Data => [ $args{'Body'} ]
+ $Message->attach(
+ Type => $args{'Type'} || 'text/plain',
+ Charset => 'UTF-8',
+ Data => $args{'Body'},
);
}
- my $cgi_object = $m->cgi_object;
+ if ( $args{'AttachmentFieldName'} ) {
- if (my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) {
+ my $cgi_object = $m->cgi_object;
+ if ( my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) {
-
- use File::Temp qw(tempfile tempdir);
-
- #foreach my $filehandle (@filenames) {
-
- my ( $fh, $temp_file );
- for ( 1 .. 10 ) {
- # on NFS and NTFS, it is possible that tempfile() conflicts
- # with other processes, causing a race condition. we try to
- # accommodate this by pausing and retrying.
- last if ($fh, $temp_file) = eval { tempfile( UNLINK => 1) };
- sleep 1;
- }
-
- binmode $fh; #thank you, windows
- my ($buffer);
- while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
- print $fh $buffer;
- }
-
- my $uploadinfo = $cgi_object->uploadInfo($filehandle);
-
- # Prefer the cached name first over CGI.pm stringification.
- my $filename = $RT::Mason::CGI::Filename;
- $filename = "$filehandle" unless defined($filename);
-
- $filename =~ s#^.*[\\/]##;
-
- $Message->attach(
- Path => $temp_file,
- Filename => Encode::decode_utf8($filename),
- Type => $uploadinfo->{'Content-Type'},
- );
- close($fh);
-
- # }
-
- }
-
- $Message->make_singlepart();
- RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
-
- return ($Message);
-
-}
-
-# }}}
-
-# {{{ sub ProcessSearchQuery
-
-=head2 ProcessSearchQuery
-
- Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand.
-
-TODO Doc exactly what comes in the paramhash
-
-
-=cut
-
-sub ProcessSearchQuery {
- my %args = @_;
-
- ## TODO: The only parameter here is %ARGS. Maybe it would be
- ## cleaner to load this parameter as $ARGS, and use $ARGS->{...}
- ## instead of $args{ARGS}->{...} ? :)
-
- #Searches are sticky.
- if ( defined $session{'tickets'} ) {
-
- # Reset the old search
- $session{'tickets'}->GotoFirstItem;
- }
- else {
-
- # Init a new search
- $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} );
- }
-
- #Import a bookmarked search if we have one
- if ( defined $args{ARGS}->{'Bookmark'} ) {
- $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} );
- }
-
- # {{{ Goto next/prev page
- if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) {
- $session{'tickets'}->NextPage;
- }
- elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) {
- $session{'tickets'}->PrevPage;
- }
- elsif ( $args{ARGS}->{'GotoPage'} > 0 ) {
- $session{'tickets'}->GotoPage( $args{ARGS}->{GotoPage} - 1 );
- }
-
- # }}}
-
- # {{{ Deal with limiting the search
-
- if ( $args{ARGS}->{'RefreshSearchInterval'} ) {
- $session{'tickets_refresh_interval'} =
- $args{ARGS}->{'RefreshSearchInterval'};
- }
-
- if ( $args{ARGS}->{'TicketsSortBy'} ) {
- $session{'tickets_sort_by'} = $args{ARGS}->{'TicketsSortBy'};
- $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'};
- $session{'tickets'}->OrderBy(
- FIELD => $args{ARGS}->{'TicketsSortBy'},
- ORDER => $args{ARGS}->{'TicketsSortOrder'}
- );
- }
-
- # }}}
-
- # {{{ Set the query limit
- if ( defined $args{ARGS}->{'RowsPerPage'} ) {
- $RT::Logger->debug(
- "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" );
-
- $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'};
- $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} );
- }
-
- # }}}
- # {{{ Limit priority
- if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) {
- $session{'tickets'}->LimitPriority(
- VALUE => $args{ARGS}->{'ValueOfPriority'},
- OPERATOR => $args{ARGS}->{'PriorityOp'}
- );
- }
-
- # }}}
- # {{{ Limit owner
- if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) {
- $session{'tickets'}->LimitOwner(
- VALUE => $args{ARGS}->{'ValueOfOwner'},
- OPERATOR => $args{ARGS}->{'OwnerOp'}
- );
- }
-
- # }}}
- # {{{ Limit requestor email
- if ( $args{ARGS}->{'ValueOfWatcherRole'} ne '' ) {
- $session{'tickets'}->LimitWatcher(
- TYPE => $args{ARGS}->{'WatcherRole'},
- VALUE => $args{ARGS}->{'ValueOfWatcherRole'},
- OPERATOR => $args{ARGS}->{'WatcherRoleOp'},
-
- );
- }
-
- # }}}
- # {{{ Limit Queue
- if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) {
- $session{'tickets'}->LimitQueue(
- VALUE => $args{ARGS}->{'ValueOfQueue'},
- OPERATOR => $args{ARGS}->{'QueueOp'}
- );
- }
-
- # }}}
- # {{{ Limit Status
- if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) {
- if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) {
- foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) {
- $session{'tickets'}->LimitStatus(
- VALUE => $value,
- OPERATOR => $args{ARGS}->{'StatusOp'},
- );
+ my ( @content, $buffer );
+ while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
+ push @content, $buffer;
}
- }
- else {
- $session{'tickets'}->LimitStatus(
- VALUE => $args{ARGS}->{'ValueOfStatus'},
- OPERATOR => $args{ARGS}->{'StatusOp'},
- );
- }
- }
+ my $uploadinfo = $cgi_object->uploadInfo($filehandle);
- # }}}
- # {{{ Limit Subject
- if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) {
- my $val = $args{ARGS}->{'ValueOfSubject'};
- if ($args{ARGS}->{'SubjectOp'} =~ /like/) {
- $val = "%".$val."%";
- }
- $session{'tickets'}->LimitSubject(
- VALUE => $val,
- OPERATOR => $args{ARGS}->{'SubjectOp'},
- );
- }
+ # Prefer the cached name first over CGI.pm stringification.
+ my $filename = $RT::Mason::CGI::Filename;
+ $filename = "$filehandle" unless defined($filename);
+ $filename = Encode::decode_utf8($filename);
+ $filename =~ s{^.*[\\/]}{};
- # }}}
- # {{{ Limit Dates
- if ( $args{ARGS}->{'ValueOfDate'} ne '' ) {
- my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} );
- $args{ARGS}->{'DateType'} =~ s/_Date$//;
-
- if ( $args{ARGS}->{'DateType'} eq 'Updated' ) {
- $session{'tickets'}->LimitTransactionDate(
- VALUE => $date,
- OPERATOR => $args{ARGS}->{'DateOp'},
+ $Message->attach(
+ Type => $uploadinfo->{'Content-Type'},
+ Filename => $filename,
+ Data => \@content,
);
- }
- else {
- $session{'tickets'}->LimitDate( FIELD => $args{ARGS}->{'DateType'},
- VALUE => $date,
- OPERATOR => $args{ARGS}->{'DateOp'},
- );
- }
- }
-
- # }}}
- # {{{ Limit Content
- if ( $args{ARGS}->{'ValueOfAttachmentField'} ne '' ) {
- my $val = $args{ARGS}->{'ValueOfAttachmentField'};
- if ($args{ARGS}->{'AttachmentFieldOp'} =~ /like/) {
- $val = "%".$val."%";
- }
- $session{'tickets'}->Limit(
- FIELD => $args{ARGS}->{'AttachmentField'},
- VALUE => $val,
- OPERATOR => $args{ARGS}->{'AttachmentFieldOp'},
- );
- }
-
- # }}}
-
- # {{{ Limit CustomFields
-
- foreach my $arg ( keys %{ $args{ARGS} } ) {
- my $id;
- if ( $arg =~ /^CustomField(\d+)$/ ) {
- $id = $1;
- }
- else {
- next;
- }
- next unless ( $args{ARGS}->{$arg} );
-
- my $form = $args{ARGS}->{$arg};
- my $oper = $args{ARGS}->{ "CustomFieldOp" . $id };
- foreach my $value ( ref($form) ? @{$form} : ($form) ) {
- my $quote = 1;
- if ($oper =~ /like/i) {
- $value = "%".$value."%";
- }
- if ( $value =~ /^null$/i ) {
-
- #Don't quote the string 'null'
- $quote = 0;
-
- # Convert the operator to something apropriate for nulls
- $oper = 'IS' if ( $oper eq '=' );
- $oper = 'IS NOT' if ( $oper eq '!=' );
+ if ( !$args{'Subject'} && !( defined $args{'Body'} && length $args{'Body'} ) ) {
+ $Message->head->set( 'Subject' => $filename );
}
- $session{'tickets'}->LimitCustomField( CUSTOMFIELD => $id,
- OPERATOR => $oper,
- QUOTEVALUE => $quote,
- VALUE => $value );
}
}
- # }}}
+ $Message->make_singlepart;
+ RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
+ return ($Message);
}
@@ -950,7 +1254,7 @@ Returns an ISO date and time in GMT
sub ParseDateToISO {
my $date = shift;
- my $date_obj = RT::Date->new($session{'CurrentUser'});
+ my $date_obj = RT::Date->new( $session{'CurrentUser'} );
$date_obj->Set(
Format => 'unknown',
Value => $date
@@ -965,72 +1269,51 @@ sub ParseDateToISO {
sub ProcessACLChanges {
my $ARGSref = shift;
- my %ARGS = %$ARGSref;
-
- my ( $ACL, @results );
-
+ #XXX: why don't we get ARGSref like in other Process* subs?
- foreach my $arg (keys %ARGS) {
- if ($arg =~ /GrantRight-(\d+)-(.*?)-(\d+)$/) {
- my $principal_id = $1;
- my $object_type = $2;
- my $object_id = $3;
- my $rights = $ARGS{$arg};
+ my @results;
- my $principal = RT::Principal->new($session{'CurrentUser'});
- $principal->Load($principal_id);
+ foreach my $arg ( keys %$ARGSref ) {
+ next unless ( $arg =~ /^(GrantRight|RevokeRight)-(\d+)-(.+?)-(\d+)$/ );
- my $obj;
+ my ( $method, $principal_id, $object_type, $object_id ) = ( $1, $2, $3, $4 );
- if ($object_type eq 'RT::System') {
- $obj = $RT::System;
- } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
- $obj = $object_type->new($session{'CurrentUser'});
- $obj->Load($object_id);
- } else {
- push (@results, loc("System Error"). ': '.
- loc("Rights could not be granted for [_1]", $object_type));
- next;
- }
-
- my @rights = ref($ARGS{$arg}) eq 'ARRAY' ? @{$ARGS{$arg}} : ($ARGS{$arg});
- foreach my $right (@rights) {
- next unless ($right);
- my ($val, $msg) = $principal->GrantRight(Object => $obj, Right => $right);
- push (@results, $msg);
- }
+ my @rights;
+ if ( UNIVERSAL::isa( $ARGSref->{$arg}, 'ARRAY' ) ) {
+ @rights = @{ $ARGSref->{$arg} };
+ } else {
+ @rights = $ARGSref->{$arg};
}
- elsif ($arg =~ /RevokeRight-(\d+)-(.*?)-(\d+)-(.*?)$/) {
- my $principal_id = $1;
- my $object_type = $2;
- my $object_id = $3;
- my $right = $4;
-
- my $principal = RT::Principal->new($session{'CurrentUser'});
- $principal->Load($principal_id);
- next unless ($right);
- my $obj;
-
- if ($object_type eq 'RT::System') {
- $obj = $RT::System;
- } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
- $obj = $object_type->new($session{'CurrentUser'});
- $obj->Load($object_id);
- } else {
- push (@results, loc("System Error"). ': '.
- loc("Rights could not be revoked for [_1]", $object_type));
+ @rights = grep $_, @rights;
+ next unless @rights;
+
+ my $principal = RT::Principal->new( $session{'CurrentUser'} );
+ $principal->Load($principal_id);
+
+ my $obj;
+ if ( $object_type eq 'RT::System' ) {
+ $obj = $RT::System;
+ } elsif ( $RT::ACE::OBJECT_TYPES{$object_type} ) {
+ $obj = $object_type->new( $session{'CurrentUser'} );
+ $obj->Load($object_id);
+ unless ( $obj->id ) {
+ $RT::Logger->error("couldn't load $object_type #$object_id");
next;
}
- my ($val, $msg) = $principal->RevokeRight(Object => $obj, Right => $right);
- push (@results, $msg);
+ } else {
+ $RT::Logger->error("object type '$object_type' is incorrect");
+ push( @results, loc("System Error") . ': ' . loc( "Rights could not be granted for [_1]", $object_type ) );
+ next;
}
-
+ foreach my $right (@rights) {
+ my ( $val, $msg ) = $principal->$method( Object => $obj, Right => $right );
+ push( @results, $msg );
+ }
}
return (@results);
-
- }
+}
# }}}
@@ -1046,18 +1329,19 @@ Returns an array of success/failure messages
sub UpdateRecordObject {
my %args = (
- ARGSRef => undef,
- AttributesRef => undef,
- Object => undef,
+ ARGSRef => undef,
+ AttributesRef => undef,
+ Object => undef,
AttributePrefix => undef,
@_
);
- my $Object = $args{'Object'};
- my @results = $Object->Update(AttributesRef => $args{'AttributesRef'},
- ARGSRef => $args{'ARGSRef'},
- AttributePrefix => $args{'AttributePrefix'}
- );
+ my $Object = $args{'Object'};
+ my @results = $Object->Update(
+ AttributesRef => $args{'AttributesRef'},
+ ARGSRef => $args{'ARGSRef'},
+ AttributePrefix => $args{'AttributePrefix'},
+ );
return (@results);
}
@@ -1076,44 +1360,40 @@ sub ProcessCustomFieldUpdates {
my $Object = $args{'CustomFieldObj'};
my $ARGSRef = $args{'ARGSRef'};
- my @attribs = qw( Name Type Description Queue SortOrder);
+ my @attribs = qw(Name Type Description Queue SortOrder);
my @results = UpdateRecordObject(
AttributesRef => \@attribs,
Object => $Object,
ARGSRef => $ARGSRef
);
- if ( $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" } ) {
-
+ my $prefix = "CustomField-" . $Object->Id;
+ if ( $ARGSRef->{"$prefix-AddValue-Name"} ) {
my ( $addval, $addmsg ) = $Object->AddValue(
- Name =>
- $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" },
- Description => $ARGSRef->{ "CustomField-"
- . $Object->Id
- . "-AddValue-Description" },
- SortOrder => $ARGSRef->{ "CustomField-"
- . $Object->Id
- . "-AddValue-SortOrder" },
+ Name => $ARGSRef->{"$prefix-AddValue-Name"},
+ Description => $ARGSRef->{"$prefix-AddValue-Description"},
+ SortOrder => $ARGSRef->{"$prefix-AddValue-SortOrder"},
);
- push ( @results, $addmsg );
+ push( @results, $addmsg );
}
- my @delete_values = (
- ref $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } eq
- 'ARRAY' )
- ? @{ $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } }
- : ( $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } );
+
+ my @delete_values
+ = ( ref $ARGSRef->{"$prefix-DeleteValue"} eq 'ARRAY' )
+ ? @{ $ARGSRef->{"$prefix-DeleteValue"} }
+ : ( $ARGSRef->{"$prefix-DeleteValue"} );
+
foreach my $id (@delete_values) {
next unless defined $id;
my ( $err, $msg ) = $Object->DeleteValue($id);
- push ( @results, $msg );
+ push( @results, $msg );
}
my $vals = $Object->Values();
- while (my $cfv = $vals->Next()) {
- if (my $so = $ARGSRef->{ 'CustomField-' . $Object->Id . '-SortOrder' . $cfv->Id }) {
- if ($cfv->SortOrder != $so) {
+ while ( my $cfv = $vals->Next() ) {
+ if ( my $so = $ARGSRef->{ "$prefix-SortOrder" . $cfv->Id } ) {
+ if ( $cfv->SortOrder != $so ) {
my ( $err, $msg ) = $cfv->SetSortOrder($so);
- push ( @results, $msg );
+ push( @results, $msg );
}
}
}
@@ -1142,37 +1422,35 @@ sub ProcessTicketBasics {
my $TicketObj = $args{'TicketObj'};
my $ARGSRef = $args{'ARGSRef'};
- # {{{ Set basic fields
+ # {{{ Set basic fields
my @attribs = qw(
- Subject
- FinalPriority
- Priority
- TimeEstimated
- TimeWorked
- TimeLeft
- Type
- Status
- Queue
+ Subject
+ FinalPriority
+ Priority
+ TimeEstimated
+ TimeWorked
+ TimeLeft
+ Type
+ Status
+ Queue
);
-
if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
my $tempqueue = RT::Queue->new($RT::SystemUser);
$tempqueue->Load( $ARGSRef->{'Queue'} );
if ( $tempqueue->id ) {
- $ARGSRef->{'Queue'} = $tempqueue->Id();
+ $ARGSRef->{'Queue'} = $tempqueue->id;
}
}
+ # Status isn't a field that can be set to a null value.
+ # RT core complains if you try
+ delete $ARGSRef->{'Status'} unless $ARGSRef->{'Status'};
- # Status isn't a field that can be set to a null value.
- # RT core complains if you try
- delete $ARGSRef->{'Status'} unless ($ARGSRef->{'Status'});
-
my @results = UpdateRecordObject(
AttributesRef => \@attribs,
Object => $TicketObj,
- ARGSRef => $ARGSRef
+ ARGSRef => $ARGSRef,
);
# We special case owner changing, so we can use ForceOwnerChange
@@ -1180,14 +1458,12 @@ sub ProcessTicketBasics {
my ($ChownType);
if ( $ARGSRef->{'ForceOwnerChange'} ) {
$ChownType = "Force";
- }
- else {
+ } else {
$ChownType = "Give";
}
- my ( $val, $msg ) =
- $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
- push ( @results, $msg );
+ my ( $val, $msg ) = $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
+ push( @results, $msg );
}
# }}}
@@ -1205,58 +1481,59 @@ sub ProcessTicketCustomFieldUpdates {
# Build up a list of objects that we want to work with
my %custom_fields_to_mod;
foreach my $arg ( keys %$ARGSRef ) {
- if ( $arg =~ /^Ticket-(\d+-.*)/) {
+ if ( $arg =~ /^Ticket-(\d+-.*)/ ) {
$ARGSRef->{"Object-RT::Ticket-$1"} = delete $ARGSRef->{$arg};
- }
- elsif ( $arg =~ /^CustomField-(\d+-.*)/) {
+ } elsif ( $arg =~ /^CustomField-(\d+-.*)/ ) {
$ARGSRef->{"Object-RT::Ticket--$1"} = delete $ARGSRef->{$arg};
}
}
- return ProcessObjectCustomFieldUpdates(%args, ARGSRef => $ARGSRef);
+ return ProcessObjectCustomFieldUpdates( %args, ARGSRef => $ARGSRef );
}
sub ProcessObjectCustomFieldUpdates {
- my %args = @_;
+ my %args = @_;
my $ARGSRef = $args{'ARGSRef'};
my @results;
# Build up a list of objects that we want to work with
my %custom_fields_to_mod;
foreach my $arg ( keys %$ARGSRef ) {
+
# format: Object-<object class>-<object id>-CustomField-<CF id>-<commands>
next unless $arg =~ /^Object-([\w:]+)-(\d*)-CustomField-(\d+)-(.*)$/;
# For each of those objects, find out what custom fields we want to work with.
- $custom_fields_to_mod{ $1 }{ $2 || 0 }{ $3 }{ $4 } = $ARGSRef->{ $arg };
+ $custom_fields_to_mod{$1}{ $2 || 0 }{$3}{$4} = $ARGSRef->{$arg};
}
# For each of those objects
foreach my $class ( keys %custom_fields_to_mod ) {
- foreach my $id ( keys %{$custom_fields_to_mod{$class}} ) {
+ foreach my $id ( keys %{ $custom_fields_to_mod{$class} } ) {
my $Object = $args{'Object'};
$Object = $class->new( $session{'CurrentUser'} )
unless $Object && ref $Object eq $class;
- $Object->Load( $id ) unless ($Object->id || 0) == $id;
+ $Object->Load($id) unless ( $Object->id || 0 ) == $id;
unless ( $Object->id ) {
$RT::Logger->warning("Couldn't load object $class #$id");
next;
}
- foreach my $cf ( keys %{ $custom_fields_to_mod{ $class }{ $id } } ) {
+ foreach my $cf ( keys %{ $custom_fields_to_mod{$class}{$id} } ) {
my $CustomFieldObj = RT::CustomField->new( $session{'CurrentUser'} );
- $CustomFieldObj->LoadById( $cf );
+ $CustomFieldObj->LoadById($cf);
unless ( $CustomFieldObj->id ) {
- $RT::Logger->warning("Couldn't load custom field #$id");
+ $RT::Logger->warning("Couldn't load custom field #$cf");
next;
}
- push @results, _ProcessObjectCustomFieldUpdates(
+ push @results,
+ _ProcessObjectCustomFieldUpdates(
Prefix => "Object-$class-$id-CustomField-$cf-",
Object => $Object,
CustomField => $CustomFieldObj,
ARGS => $custom_fields_to_mod{$class}{$id}{$cf},
- );
+ );
}
}
}
@@ -1264,20 +1541,32 @@ sub ProcessObjectCustomFieldUpdates {
}
sub _ProcessObjectCustomFieldUpdates {
- my %args = @_;
- my $cf = $args{'CustomField'};
+ my %args = @_;
+ my $cf = $args{'CustomField'};
my $cf_type = $cf->Type;
+ # Remove blank Values since the magic field will take care of this. Sometimes
+ # the browser gives you a blank value which causes CFs to be processed twice
+ if ( defined $args{'ARGS'}->{'Values'}
+ && !length $args{'ARGS'}->{'Values'}
+ && $args{'ARGS'}->{'Values-Magic'} )
+ {
+ delete $args{'ARGS'}->{'Values'};
+ }
+
my @results;
foreach my $arg ( keys %{ $args{'ARGS'} } ) {
-
- next if $arg =~ /Category$/;
+
+ # skip category argument
+ next if $arg eq 'Category';
# since http won't pass in a form element with a null value, we need
# to fake it
if ( $arg eq 'Values-Magic' ) {
+
# We don't care about the magic, if there's really a values element;
- next if $args{'ARGS'}->{'Value'} || $args{'ARGS'}->{'Values'};
+ next if defined $args{'ARGS'}->{'Value'} && length $args{'ARGS'}->{'Value'};
+ next if defined $args{'ARGS'}->{'Values'} && length $args{'ARGS'}->{'Values'};
# "Empty" values does not mean anything for Image and Binary fields
next if $cf_type =~ /^(?:Image|Binary)$/;
@@ -1287,18 +1576,21 @@ sub _ProcessObjectCustomFieldUpdates {
}
my @values = ();
- if ( ref $args{'ARGS'}->{ $arg } eq 'ARRAY' ) {
+ if ( ref $args{'ARGS'}->{$arg} eq 'ARRAY' ) {
@values = @{ $args{'ARGS'}->{$arg} };
- } elsif ( $cf_type =~ /text/i ) { # Both Text and Wikitext
- @values = ($args{'ARGS'}->{$arg});
- } elsif ( defined( $args{'ARGS'}->{ $arg } ) ) {
- @values = split /\n/, $args{'ARGS'}->{ $arg };
- }
-
- if ( ( $cf_type eq 'Freeform' && !$cf->SingleValue ) || $cf_type =~ /text/i ) {
- s/\r//g foreach @values;
+ } elsif ( $cf_type =~ /text/i ) { # Both Text and Wikitext
+ @values = ( $args{'ARGS'}->{$arg} );
+ } else {
+ @values = split /\r*\n/, $args{'ARGS'}->{$arg}
+ if defined $args{'ARGS'}->{$arg};
}
- @values = grep defined && $_ ne '', @values;
+ @values = grep length, map {
+ s/\r+\n/\n/g;
+ s/^\s+//;
+ s/\s+$//;
+ $_;
+ }
+ grep defined, @values;
if ( $arg eq 'AddValue' || $arg eq 'Value' ) {
foreach my $value (@values) {
@@ -1306,69 +1598,63 @@ sub _ProcessObjectCustomFieldUpdates {
Field => $cf->id,
Value => $value
);
- push ( @results, $msg );
+ push( @results, $msg );
}
- }
- elsif ( $arg eq 'Upload' ) {
+ } elsif ( $arg eq 'Upload' ) {
my $value_hash = _UploadedFile( $args{'Prefix'} . $arg ) or next;
- my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
- %$value_hash,
- Field => $cf,
- );
- push ( @results, $msg );
- }
- elsif ( $arg eq 'DeleteValues' ) {
- foreach my $value ( @values ) {
+ my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( %$value_hash, Field => $cf, );
+ push( @results, $msg );
+ } elsif ( $arg eq 'DeleteValues' ) {
+ foreach my $value (@values) {
my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
Field => $cf,
Value => $value,
);
- push ( @results, $msg );
+ push( @results, $msg );
}
- }
- elsif ( $arg eq 'DeleteValueIds' ) {
- foreach my $value ( @values ) {
+ } elsif ( $arg eq 'DeleteValueIds' ) {
+ foreach my $value (@values) {
my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
Field => $cf,
ValueId => $value,
);
- push ( @results, $msg );
+ push( @results, $msg );
}
- }
- elsif ( $arg eq 'Values' && !$cf->Repeated ) {
+ } elsif ( $arg eq 'Values' && !$cf->Repeated ) {
my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
my %values_hash;
- foreach my $value ( @values ) {
- # build up a hash of values that the new set has
- $values_hash{$value} = 1;
- next if $cf_values->HasEntry( $value );
+ foreach my $value (@values) {
+ if ( my $entry = $cf_values->HasEntry($value) ) {
+ $values_hash{ $entry->id } = 1;
+ next;
+ }
my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
Field => $cf,
Value => $value
);
- push ( @results, $msg );
+ push( @results, $msg );
+ $values_hash{$val} = 1 if $val;
}
$cf_values->RedoSearch;
while ( my $cf_value = $cf_values->Next ) {
- next if $values_hash{ $cf_value->Content };
+ next if $values_hash{ $cf_value->id };
my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
- Field => $cf,
- Value => $cf_value->Content
+ Field => $cf,
+ ValueId => $cf_value->id
);
- push ( @results, $msg);
+ push( @results, $msg );
}
- }
- elsif ( $arg eq 'Values' ) {
+ } elsif ( $arg eq 'Values' ) {
my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
# keep everything up to the point of difference, delete the rest
my $delete_flag;
- foreach my $old_cf (@{$cf_values->ItemsArrayRef}) {
- if (!$delete_flag and @values and $old_cf->Content eq $values[0]) {
+ foreach my $old_cf ( @{ $cf_values->ItemsArrayRef } ) {
+ if ( !$delete_flag and @values and $old_cf->Content eq $values[0] ) {
shift @values;
next;
}
@@ -1378,18 +1664,20 @@ sub _ProcessObjectCustomFieldUpdates {
}
# now add/replace extra things, if any
- foreach my $value ( @values ) {
+ foreach my $value (@values) {
my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
Field => $cf,
Value => $value
);
- push ( @results, $msg );
+ push( @results, $msg );
}
- }
- else {
- push ( @results,
+ } else {
+ push(
+ @results,
loc("User asked for an unknown update type for custom field [_1] for [_2] object #[_3]",
- $cf->Name, ref $args{'Object'}, $args{'Object'}->id )
+ $cf->Name, ref $args{'Object'},
+ $args{'Object'}->id
+ )
);
}
}
@@ -1420,8 +1708,7 @@ sub ProcessTicketWatchers {
foreach my $key ( keys %$ARGSRef ) {
# Delete deletable watchers
- if ( ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) )
- {
+ if ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) {
my ( $code, $msg ) = $Ticket->DeleteWatcher(
PrincipalId => $2,
Type => $1
@@ -1439,8 +1726,8 @@ sub ProcessTicketWatchers {
}
# Add new wathchers by email address
- elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
- and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) )
+ elsif ( ( $ARGSRef->{$key} || '' ) =~ /^(?:AdminCc|Cc|Requestor)$/
+ and $key =~ /^WatcherTypeEmail(\d*)$/ )
{
#They're in this order because otherwise $1 gets clobbered :/
@@ -1463,7 +1750,7 @@ sub ProcessTicketWatchers {
# Add new watchers by owner
elsif ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) {
my $principal_id = $1;
- my $form = $ARGSRef->{$key};
+ my $form = $ARGSRef->{$key};
foreach my $value ( ref($form) ? @{$form} : ($form) ) {
next unless $value =~ /^(?:AdminCc|Cc|Requestor)$/i;
@@ -1503,33 +1790,33 @@ sub ProcessTicketDates {
# {{{ Set date fields
my @date_fields = qw(
- Told
- Resolved
- Starts
- Started
- Due
+ Told
+ Resolved
+ Starts
+ Started
+ Due
);
#Run through each field in this list. update the value if apropriate
foreach my $field (@date_fields) {
+ next unless exists $ARGSRef->{ $field . '_Date' };
+ next if $ARGSRef->{ $field . '_Date' } eq '';
+
my ( $code, $msg );
my $DateObj = RT::Date->new( $session{'CurrentUser'} );
+ $DateObj->Set(
+ Format => 'unknown',
+ Value => $ARGSRef->{ $field . '_Date' }
+ );
- #If it's something other than just whitespace
- if ( $ARGSRef->{ $field . '_Date' } && ($ARGSRef->{ $field . '_Date' } ne '') ) {
- $DateObj->Set(
- Format => 'unknown',
- Value => $ARGSRef->{ $field . '_Date' }
- );
- my $obj = $field . "Obj";
- if ( ( defined $DateObj->Unix )
- and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) )
- {
- my $method = "Set$field";
- my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
- push @results, "$msg";
- }
+ my $obj = $field . "Obj";
+ if ( ( defined $DateObj->Unix )
+ and ( $DateObj->Unix != $Ticket->$obj()->Unix() ) )
+ {
+ my $method = "Set$field";
+ my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
+ push @results, "$msg";
}
}
@@ -1548,21 +1835,21 @@ Returns an array of results messages.
=cut
sub ProcessTicketLinks {
- my %args = ( TicketObj => undef,
- ARGSRef => undef,
- @_ );
+ my %args = (
+ TicketObj => undef,
+ ARGSRef => undef,
+ @_
+ );
my $Ticket = $args{'TicketObj'};
my $ARGSRef = $args{'ARGSRef'};
-
- my (@results) = ProcessRecordLinks(RecordObj => $Ticket,
- ARGSRef => $ARGSRef);
+ my (@results) = ProcessRecordLinks( RecordObj => $Ticket, ARGSRef => $ARGSRef );
#Merge if we need to
if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
- my ( $val, $msg ) =
- $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
+ $ARGSRef->{ $Ticket->Id . "-MergeInto" } =~ s/\s+//g;
+ my ( $val, $msg ) = $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
push @results, $msg;
}
@@ -1572,9 +1859,11 @@ sub ProcessTicketLinks {
# }}}
sub ProcessRecordLinks {
- my %args = ( RecordObj => undef,
- ARGSRef => undef,
- @_ );
+ my %args = (
+ RecordObj => undef,
+ ARGSRef => undef,
+ @_
+ );
my $Record = $args{'RecordObj'};
my $ARGSRef = $args{'ARGSRef'};
@@ -1588,11 +1877,11 @@ sub ProcessRecordLinks {
my $type = $2;
my $target = $3;
- push @results,
- "Trying to delete: Base: $base Target: $target Type $type";
- my ( $val, $msg ) = $Record->DeleteLink( Base => $base,
- Type => $type,
- Target => $target );
+ my ( $val, $msg ) = $Record->DeleteLink(
+ Base => $base,
+ Type => $type,
+ Target => $target
+ );
push @results, $msg;
@@ -1604,28 +1893,38 @@ sub ProcessRecordLinks {
foreach my $linktype (@linktypes) {
if ( $ARGSRef->{ $Record->Id . "-$linktype" } ) {
- for my $luri ( split ( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) {
- $luri =~ s/\s*$//; # Strip trailing whitespace
- my ( $val, $msg ) = $Record->AddLink( Target => $luri,
- Type => $linktype );
+ $ARGSRef->{ $Record->Id . "-$linktype" } = join( ' ', @{ $ARGSRef->{ $Record->Id . "-$linktype" } } )
+ if ref( $ARGSRef->{ $Record->Id . "-$linktype" } );
+
+ for my $luri ( split( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) {
+ next unless $luri;
+ $luri =~ s/\s+$//; # Strip trailing whitespace
+ my ( $val, $msg ) = $Record->AddLink(
+ Target => $luri,
+ Type => $linktype
+ );
push @results, $msg;
}
}
if ( $ARGSRef->{ "$linktype-" . $Record->Id } ) {
-
- for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) {
- my ( $val, $msg ) = $Record->AddLink( Base => $luri,
- Type => $linktype );
+ $ARGSRef->{ "$linktype-" . $Record->Id } = join( ' ', @{ $ARGSRef->{ "$linktype-" . $Record->Id } } )
+ if ref( $ARGSRef->{ "$linktype-" . $Record->Id } );
+
+ for my $luri ( split( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) {
+ next unless $luri;
+ my ( $val, $msg ) = $Record->AddLink(
+ Base => $luri,
+ Type => $linktype
+ );
push @results, $msg;
}
- }
+ }
}
return (@results);
}
-
=head2 _UploadedFile ( $arg );
Takes a CGI parameter name; if a file is uploaded under that name,
@@ -1637,9 +1936,9 @@ Returns C<undef> if no files were uploaded in the C<$arg> field.
=cut
sub _UploadedFile {
- my $arg = shift;
- my $cgi_object = $m->cgi_object;
- my $fh = $cgi_object->upload($arg) or return undef;
+ my $arg = shift;
+ my $cgi_object = $m->cgi_object;
+ my $fh = $cgi_object->upload($arg) or return undef;
my $upload_info = $cgi_object->uploadInfo($fh);
my $filename = "$fh";
@@ -1647,12 +1946,50 @@ sub _UploadedFile {
binmode($fh);
return {
- Value => $filename,
+ Value => $filename,
LargeContent => do { local $/; scalar <$fh> },
- ContentType => $upload_info->{'Content-Type'},
+ ContentType => $upload_info->{'Content-Type'},
};
}
+sub GetColumnMapEntry {
+ my %args = ( Map => {}, Name => '', Attribute => undef, @_ );
+
+ # deal with the simplest thing first
+ if ( $args{'Map'}{ $args{'Name'} } ) {
+ return $args{'Map'}{ $args{'Name'} }{ $args{'Attribute'} };
+ }
+
+ # complex things
+ elsif ( my ( $mainkey, $subkey ) = $args{'Name'} =~ /^(.*?)\.{(.+)}$/ ) {
+ return undef unless $args{'Map'}->{$mainkey};
+ return $args{'Map'}{$mainkey}{ $args{'Attribute'} }
+ unless ref $args{'Map'}{$mainkey}{ $args{'Attribute'} } eq 'CODE';
+
+ return sub { $args{'Map'}{$mainkey}{ $args{'Attribute'} }->( @_, $subkey ) };
+ }
+ return undef;
+}
+
+sub ProcessColumnMapValue {
+ my $value = shift;
+ my %args = ( Arguments => [], Escape => 1, @_ );
+
+ if ( ref $value ) {
+ if ( UNIVERSAL::isa( $value, 'CODE' ) ) {
+ my @tmp = $value->( @{ $args{'Arguments'} } );
+ return ProcessColumnMapValue( ( @tmp > 1 ? \@tmp : $tmp[0] ), %args );
+ } elsif ( UNIVERSAL::isa( $value, 'ARRAY' ) ) {
+ return join '', map ProcessColumnMapValue( $_, %args ), @$value;
+ } elsif ( UNIVERSAL::isa( $value, 'SCALAR' ) ) {
+ return $$value;
+ }
+ }
+
+ return $m->interp->apply_escapes( $value, 'h' ) if $args{'Escape'};
+ return $value;
+}
+
=head2 _load_container_object ( $type, $id );
Instantiate container object for saving searches.
@@ -1660,8 +1997,8 @@ Instantiate container object for saving searches.
=cut
sub _load_container_object {
- my ($obj_type, $obj_id) = @_;
- return RT::SavedSearch->new($session{'CurrentUser'})->_load_privacy_object($obj_type, $obj_id);
+ my ( $obj_type, $obj_id ) = @_;
+ return RT::SavedSearch->new( $session{'CurrentUser'} )->_load_privacy_object( $obj_type, $obj_id );
}
=head2 _parse_saved_search ( $arg );
@@ -1674,19 +2011,19 @@ container object and the search id.
sub _parse_saved_search {
my $spec = shift;
return unless $spec;
- if ($spec !~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/ ) {
+ if ( $spec !~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/ ) {
return;
}
my $obj_type = $1;
my $obj_id = $2;
my $search_id = $3;
- return (_load_container_object ($obj_type, $obj_id), $search_id);
+ return ( _load_container_object( $obj_type, $obj_id ), $search_id );
}
eval "require RT::Interface::Web_Vendor";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm});
+die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm} );
eval "require RT::Interface::Web_Local";
-die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm});
+die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm} );
1;