1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
21 # General Public License for more details.
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/copyleft/gpl.html.
30 # CONTRIBUTION SUBMISSION POLICY:
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
47 # END BPS TAGGED BLOCK }}}
48 ## Portions Copyright 2000 Tobias Brox <tobix@fsck.com>
50 ## This is a library of static subs to be used by the Mason web
60 use_ok(RT::Interface::Web);
70 package RT::Interface::Web;
72 use RT::SavedSearches;
77 =head2 EscapeUTF8 SCALARREF
79 does a css-busting but minimalist escaping of whatever html you're passing in.
85 return unless defined $$ref;
96 Encode::_utf8_on($$ref);
105 =head2 EscapeURI SCALARREF
107 Escapes URI component according to RFC2396
114 $$ref = Encode::encode_utf8( $$ref );
115 $$ref =~ s/([^a-zA-Z0-9_.!~*'()-])/uc sprintf("%%%02X", ord($1))/eg;
116 Encode::_utf8_on( $$ref );
121 # {{{ WebCanonicalizeInfo
123 =head2 WebCanonicalizeInfo();
125 Different web servers set different environmental varibles. This
126 function must return something suitable for REMOTE_USER. By default,
127 just downcase $ENV{'REMOTE_USER'}
131 sub WebCanonicalizeInfo {
134 if ( defined $ENV{'REMOTE_USER'} ) {
135 $user = lc ( $ENV{'REMOTE_USER'} ) if( length($ENV{'REMOTE_USER'}) );
143 # {{{ WebExternalAutoInfo
145 =head2 WebExternalAutoInfo($user);
147 Returns a hash of user attributes, used when WebExternalAuto is set.
151 sub WebExternalAutoInfo {
156 $user_info{'Privileged'} = 1;
158 if ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
159 # Populate fields with information from Unix /etc/passwd
161 my ($comments, $realname) = (getpwnam($user))[5, 6];
162 $user_info{'Comments'} = $comments if defined $comments;
163 $user_info{'RealName'} = $realname if defined $realname;
165 elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
166 # Populate fields with information from NT domain controller
169 # and return the wad of stuff
179 This routine ells the current user's browser to redirect to URL.
180 Additionally, it unties the user's currently active session, helping to avoid
181 A bug in Apache::Session 1.81 and earlier which clobbers sessions if we try to use
182 a cached DBI statement handle twice at the same time.
188 my $redir_to = shift;
189 untie $HTML::Mason::Commands::session;
190 my $uri = URI->new($redir_to);
191 my $server_uri = URI->new($RT::WebURL);
193 # If the user is coming in via a non-canonical
194 # hostname, don't redirect them to the canonical host,
195 # it will just upset them (and invalidate their credentials)
196 if ($uri->host eq $server_uri->host &&
197 $uri->port eq $server_uri->port) {
198 $uri->host($ENV{'HTTP_HOST'});
199 $uri->port($ENV{'SERVER_PORT'});
202 $HTML::Mason::Commands::m->redirect($uri->canonical);
203 $HTML::Mason::Commands::m->abort;
207 =head2 StaticFileHeaders
209 Send the browser a few headers to try to get it to (somewhat agressively)
210 cache RT's static Javascript and CSS files.
212 This routine could really use _accurate_ heuristics. (XXX TODO)
216 sub StaticFileHeaders {
217 # Expire things in a month.
218 $HTML::Mason::Commands::r->headers_out->{'Expires'} = HTTP::Date::time2str( time() + 2592000 );
220 # Last modified at server start time
221 $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = HTTP::Date::time2str($^T);
226 package HTML::Mason::Commands;
227 use vars qw/$r $m %session/;
234 loc is a nice clean global routine which calls $session{'CurrentUser'}->loc()
235 with whatever it's called with. If there is no $session{'CurrentUser'},
236 it creates a temporary user, so we have something to get a localisation handle
243 if ($session{'CurrentUser'} &&
244 UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
245 return($session{'CurrentUser'}->loc(@_));
247 elsif ( my $u = eval { RT::CurrentUser->new($RT::SystemUser->Id) } ) {
248 return ($u->loc(@_));
251 # pathetic case -- SystemUser is gone.
261 =head2 loc_fuzzy STRING
263 loc_fuzzy is for handling localizations of messages that may already
264 contain interpolated variables, typically returned from libraries
265 outside RT's control. It takes the message string and extracts the
266 variable array automatically by matching against the candidate entries
267 inside the lexicon file.
274 if ($session{'CurrentUser'} &&
275 UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
276 return($session{'CurrentUser'}->loc_fuzzy($msg));
279 my $u = RT::CurrentUser->new($RT::SystemUser->Id);
280 return ($u->loc_fuzzy($msg));
288 # Error - calls Error and aborts
291 if ($session{'ErrorDocument'} &&
292 $session{'ErrorDocumentType'}) {
293 $r->content_type($session{'ErrorDocumentType'});
294 $m->comp($session{'ErrorDocument'} , Why => shift);
298 $m->comp("/Elements/Error" , Why => shift);
305 # {{{ sub CreateTicket
307 =head2 CreateTicket ARGS
309 Create a new ticket, using Mason's %ARGS. returns @results.
318 my $Ticket = new RT::Ticket( $session{'CurrentUser'} );
320 my $Queue = new RT::Queue( $session{'CurrentUser'} );
321 unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
322 Abort('Queue not found');
325 unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
326 Abort('You have no permission to create tickets in that queue.');
329 my $due = new RT::Date( $session{'CurrentUser'} );
330 $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
331 my $starts = new RT::Date( $session{'CurrentUser'} );
332 $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
334 my @Requestors = split ( /\s*,\s*/, $ARGS{'Requestors'} );
335 my @Cc = split ( /\s*,\s*/, $ARGS{'Cc'} );
336 my @AdminCc = split ( /\s*,\s*/, $ARGS{'AdminCc'} );
338 my $MIMEObj = MakeMIMEEntity(
339 Subject => $ARGS{'Subject'},
340 From => $ARGS{'From'},
342 Body => $ARGS{'Content'},
345 if ( $ARGS{'Attachments'} ) {
346 my $rv = $MIMEObj->make_multipart;
347 $RT::Logger->error("Couldn't make multipart message")
348 if !$rv || $rv !~ /^(?:DONE|ALREADY)$/;
350 foreach ( values %{$ARGS{'Attachments'}} ) {
352 $RT::Logger->error("Couldn't add empty attachemnt");
355 $MIMEObj->add_part($_);
360 Type => $ARGS{'Type'} || 'ticket',
361 Queue => $ARGS{'Queue'},
362 Owner => $ARGS{'Owner'},
363 InitialPriority => $ARGS{'InitialPriority'},
364 FinalPriority => $ARGS{'FinalPriority'},
365 TimeLeft => $ARGS{'TimeLeft'},
366 TimeEstimated => $ARGS{'TimeEstimated'},
367 TimeWorked => $ARGS{'TimeWorked'},
368 Requestor => \@Requestors,
370 AdminCc => \@AdminCc,
371 Subject => $ARGS{'Subject'},
372 Status => $ARGS{'Status'},
374 Starts => $starts->ISO,
377 foreach my $arg (keys %ARGS) {
378 next if $arg =~ /-(?:Magic|Category)$/;
380 if ($arg =~ /^Object-RT::Transaction--CustomField-/) {
381 $create_args{$arg} = $ARGS{$arg};
383 # Object-RT::Ticket--CustomField-3-Values
384 elsif ($arg =~ /^Object-RT::Ticket--CustomField-(\d+)(.*?)$/) {
386 my $cf = RT::CustomField->new( $session{'CurrentUser'});
389 if ( $cf->Type eq 'Freeform' && ! $cf->SingleValue) {
390 $ARGS{$arg} =~ s/\r\n/\n/g;
391 $ARGS{$arg} = [split('\n', $ARGS{$arg})];
394 if ( $cf->Type =~ /text/i) { # Catch both Text and Wikitext
395 $ARGS{$arg} =~ s/\r//g;
398 if ( $arg =~ /-Upload$/ ) {
399 $create_args{"CustomField-".$cfid} = _UploadedFile($arg);
402 $create_args{"CustomField-".$cfid} = $ARGS{"$arg"};
408 # XXX TODO This code should be about six lines. and badly needs refactoring.
410 # {{{ turn new link lists into arrays, and pass in the proper arguments
411 my (@dependson, @dependedonby, @parents, @children, @refersto, @referredtoby);
413 foreach my $luri ( split ( / /, $ARGS{"new-DependsOn"} ) ) {
414 $luri =~ s/\s*$//; # Strip trailing whitespace
415 push @dependson, $luri;
417 $create_args{'DependsOn'} = \@dependson;
419 foreach my $luri ( split ( / /, $ARGS{"DependsOn-new"} ) ) {
420 push @dependedonby, $luri;
422 $create_args{'DependedOnBy'} = \@dependedonby;
424 foreach my $luri ( split ( / /, $ARGS{"new-MemberOf"} ) ) {
425 $luri =~ s/\s*$//; # Strip trailing whitespace
426 push @parents, $luri;
428 $create_args{'Parents'} = \@parents;
430 foreach my $luri ( split ( / /, $ARGS{"MemberOf-new"} ) ) {
431 push @children, $luri;
433 $create_args{'Children'} = \@children;
435 foreach my $luri ( split ( / /, $ARGS{"new-RefersTo"} ) ) {
436 $luri =~ s/\s*$//; # Strip trailing whitespace
437 push @refersto, $luri;
439 $create_args{'RefersTo'} = \@refersto;
441 foreach my $luri ( split ( / /, $ARGS{"RefersTo-new"} ) ) {
442 push @referredtoby, $luri;
444 $create_args{'ReferredToBy'} = \@referredtoby;
448 my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
453 push ( @Actions, split("\n", $ErrMsg) );
454 unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
455 Abort( "No permission to view newly created ticket #"
456 . $Ticket->id . "." );
458 return ( $Ticket, @Actions );
464 # {{{ sub LoadTicket - loads a ticket
468 Takes a ticket id as its only variable. if it's handed an array, it takes
471 Returns an RT::Ticket object as the current user.
478 if ( ref($id) eq "ARRAY" ) {
483 Abort("No ticket specified");
486 my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
488 unless ( $Ticket->id ) {
489 Abort("Could not load ticket $id");
496 # {{{ sub ProcessUpdateMessage
498 sub ProcessUpdateMessage {
500 #TODO document what else this takes.
508 #Make the update content have no 'weird' newlines in it
509 if ( $args{ARGSRef}->{'UpdateTimeWorked'}
510 || $args{ARGSRef}->{'UpdateContent'}
511 || $args{ARGSRef}->{'UpdateAttachments'} )
515 $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() )
517 $args{ARGSRef}->{'UpdateSubject'} = undef;
520 my $Message = MakeMIMEEntity(
521 Subject => $args{ARGSRef}->{'UpdateSubject'},
522 Body => $args{ARGSRef}->{'UpdateContent'},
525 $Message->head->add( 'Message-ID' =>
530 . int(rand(2000)) . "."
531 . $args{'TicketObj'}->id . "-"
533 . "0" . "@" # Email sent
536 my $old_txn = RT::Transaction->new( $session{'CurrentUser'} );
537 if ( $args{ARGSRef}->{'QuoteTransaction'} ) {
538 $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} );
541 $old_txn = $args{TicketObj}->Transactions->First();
544 if ( $old_txn->Message && $old_txn->Message->First ) {
545 my @in_reply_to = split(/\s+/m, $old_txn->Message->First->GetHeader('In-Reply-To') || '');
546 my @references = split(/\s+/m, $old_txn->Message->First->GetHeader('References') || '' );
547 my @msgid = split(/\s+/m,$old_txn->Message->First->GetHeader('Message-ID') || '');
548 my @rtmsgid = split(/\s+/m,$old_txn->Message->First->GetHeader('RT-Message-ID') || '');
550 $Message->head->replace( 'In-Reply-To', join (' ', @rtmsgid ? @rtmsgid : @msgid));
551 $Message->head->replace( 'References', join(' ', @references, @msgid, @rtmsgid));
554 if ( $args{ARGSRef}->{'UpdateAttachments'} ) {
555 $Message->make_multipart;
556 $Message->add_part($_)
557 foreach values %{ $args{ARGSRef}->{'UpdateAttachments'} };
560 ## TODO: Implement public comments
561 if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
562 my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(
563 CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
564 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
566 TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
568 push( @{ $args{Actions} }, $Description );
569 $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
571 elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
572 my ( $Transaction, $Description, $Object ) =
573 $args{TicketObj}->Correspond(
574 CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
575 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
577 TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
579 push( @{ $args{Actions} }, $Description );
580 $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
584 @{ $args{'Actions'} },
585 loc("Update type was neither correspondence nor comment.") . " "
586 . loc("Update not recorded.")
594 # {{{ sub MakeMIMEEntity
596 =head2 MakeMIMEEntity PARAMHASH
598 Takes a paramhash Subject, Body and AttachmentFieldName.
600 Returns a MIME::Entity.
606 #TODO document what else this takes.
612 AttachmentFieldName => undef,
613 # map Encode::encode_utf8($_), @_,
617 #Make the update content have no 'weird' newlines in it
619 $args{'Body'} =~ s/\r\n/\n/gs;
622 # MIME::Head is not happy in utf-8 domain. This only happens
623 # when processing an incoming email (so far observed).
626 $Message = MIME::Entity->build(
627 Subject => $args{'Subject'} || "",
628 From => $args{'From'},
630 'Charset:' => 'utf8',
631 Data => [ $args{'Body'} ]
635 my $cgi_object = $m->cgi_object;
637 if (my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) {
641 use File::Temp qw(tempfile tempdir);
643 #foreach my $filehandle (@filenames) {
645 my ( $fh, $temp_file );
647 # on NFS and NTFS, it is possible that tempfile() conflicts
648 # with other processes, causing a race condition. we try to
649 # accommodate this by pausing and retrying.
650 last if ($fh, $temp_file) = eval { tempfile( UNLINK => 1) };
654 binmode $fh; #thank you, windows
656 while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
660 my $uploadinfo = $cgi_object->uploadInfo($filehandle);
662 # Prefer the cached name first over CGI.pm stringification.
663 my $filename = $RT::Mason::CGI::Filename;
664 $filename = "$filehandle" unless defined($filename);
666 $filename =~ s#^.*[\\/]##;
670 Filename => Encode::decode_utf8($filename),
671 Type => $uploadinfo->{'Content-Type'},
679 $Message->make_singlepart();
680 RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
688 # {{{ sub ProcessSearchQuery
690 =head2 ProcessSearchQuery
692 Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand.
694 TODO Doc exactly what comes in the paramhash
699 sub ProcessSearchQuery {
702 ## TODO: The only parameter here is %ARGS. Maybe it would be
703 ## cleaner to load this parameter as $ARGS, and use $ARGS->{...}
704 ## instead of $args{ARGS}->{...} ? :)
706 #Searches are sticky.
707 if ( defined $session{'tickets'} ) {
709 # Reset the old search
710 $session{'tickets'}->GotoFirstItem;
715 $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} );
718 #Import a bookmarked search if we have one
719 if ( defined $args{ARGS}->{'Bookmark'} ) {
720 $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} );
723 # {{{ Goto next/prev page
724 if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) {
725 $session{'tickets'}->NextPage;
727 elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) {
728 $session{'tickets'}->PrevPage;
730 elsif ( $args{ARGS}->{'GotoPage'} > 0 ) {
731 $session{'tickets'}->GotoPage( $args{ARGS}->{GotoPage} - 1 );
736 # {{{ Deal with limiting the search
738 if ( $args{ARGS}->{'RefreshSearchInterval'} ) {
739 $session{'tickets_refresh_interval'} =
740 $args{ARGS}->{'RefreshSearchInterval'};
743 if ( $args{ARGS}->{'TicketsSortBy'} ) {
744 $session{'tickets_sort_by'} = $args{ARGS}->{'TicketsSortBy'};
745 $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'};
746 $session{'tickets'}->OrderBy(
747 FIELD => $args{ARGS}->{'TicketsSortBy'},
748 ORDER => $args{ARGS}->{'TicketsSortOrder'}
754 # {{{ Set the query limit
755 if ( defined $args{ARGS}->{'RowsPerPage'} ) {
757 "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" );
759 $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'};
760 $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} );
765 if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) {
766 $session{'tickets'}->LimitPriority(
767 VALUE => $args{ARGS}->{'ValueOfPriority'},
768 OPERATOR => $args{ARGS}->{'PriorityOp'}
774 if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) {
775 $session{'tickets'}->LimitOwner(
776 VALUE => $args{ARGS}->{'ValueOfOwner'},
777 OPERATOR => $args{ARGS}->{'OwnerOp'}
782 # {{{ Limit requestor email
783 if ( $args{ARGS}->{'ValueOfWatcherRole'} ne '' ) {
784 $session{'tickets'}->LimitWatcher(
785 TYPE => $args{ARGS}->{'WatcherRole'},
786 VALUE => $args{ARGS}->{'ValueOfWatcherRole'},
787 OPERATOR => $args{ARGS}->{'WatcherRoleOp'},
794 if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) {
795 $session{'tickets'}->LimitQueue(
796 VALUE => $args{ARGS}->{'ValueOfQueue'},
797 OPERATOR => $args{ARGS}->{'QueueOp'}
803 if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) {
804 if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) {
805 foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) {
806 $session{'tickets'}->LimitStatus(
808 OPERATOR => $args{ARGS}->{'StatusOp'},
813 $session{'tickets'}->LimitStatus(
814 VALUE => $args{ARGS}->{'ValueOfStatus'},
815 OPERATOR => $args{ARGS}->{'StatusOp'},
823 if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) {
824 my $val = $args{ARGS}->{'ValueOfSubject'};
825 if ($args{ARGS}->{'SubjectOp'} =~ /like/) {
828 $session{'tickets'}->LimitSubject(
830 OPERATOR => $args{ARGS}->{'SubjectOp'},
836 if ( $args{ARGS}->{'ValueOfDate'} ne '' ) {
837 my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} );
838 $args{ARGS}->{'DateType'} =~ s/_Date$//;
840 if ( $args{ARGS}->{'DateType'} eq 'Updated' ) {
841 $session{'tickets'}->LimitTransactionDate(
843 OPERATOR => $args{ARGS}->{'DateOp'},
847 $session{'tickets'}->LimitDate( FIELD => $args{ARGS}->{'DateType'},
849 OPERATOR => $args{ARGS}->{'DateOp'},
856 if ( $args{ARGS}->{'ValueOfAttachmentField'} ne '' ) {
857 my $val = $args{ARGS}->{'ValueOfAttachmentField'};
858 if ($args{ARGS}->{'AttachmentFieldOp'} =~ /like/) {
861 $session{'tickets'}->Limit(
862 FIELD => $args{ARGS}->{'AttachmentField'},
864 OPERATOR => $args{ARGS}->{'AttachmentFieldOp'},
870 # {{{ Limit CustomFields
872 foreach my $arg ( keys %{ $args{ARGS} } ) {
874 if ( $arg =~ /^CustomField(\d+)$/ ) {
880 next unless ( $args{ARGS}->{$arg} );
882 my $form = $args{ARGS}->{$arg};
883 my $oper = $args{ARGS}->{ "CustomFieldOp" . $id };
884 foreach my $value ( ref($form) ? @{$form} : ($form) ) {
886 if ($oper =~ /like/i) {
887 $value = "%".$value."%";
889 if ( $value =~ /^null$/i ) {
891 #Don't quote the string 'null'
894 # Convert the operator to something apropriate for nulls
895 $oper = 'IS' if ( $oper eq '=' );
896 $oper = 'IS NOT' if ( $oper eq '!=' );
898 $session{'tickets'}->LimitCustomField( CUSTOMFIELD => $id,
900 QUOTEVALUE => $quote,
912 # {{{ sub ParseDateToISO
914 =head2 ParseDateToISO
916 Takes a date in an arbitrary format.
917 Returns an ISO date and time in GMT
924 my $date_obj = RT::Date->new($session{'CurrentUser'});
929 return ( $date_obj->ISO );
934 # {{{ sub ProcessACLChanges
936 sub ProcessACLChanges {
939 my %ARGS = %$ARGSref;
941 my ( $ACL, @results );
944 foreach my $arg (keys %ARGS) {
945 if ($arg =~ /GrantRight-(\d+)-(.*?)-(\d+)$/) {
946 my $principal_id = $1;
947 my $object_type = $2;
949 my $rights = $ARGS{$arg};
951 my $principal = RT::Principal->new($session{'CurrentUser'});
952 $principal->Load($principal_id);
956 if ($object_type eq 'RT::System') {
958 } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
959 $obj = $object_type->new($session{'CurrentUser'});
960 $obj->Load($object_id);
962 push (@results, loc("System Error"). ': '.
963 loc("Rights could not be granted for [_1]", $object_type));
967 my @rights = ref($ARGS{$arg}) eq 'ARRAY' ? @{$ARGS{$arg}} : ($ARGS{$arg});
968 foreach my $right (@rights) {
969 next unless ($right);
970 my ($val, $msg) = $principal->GrantRight(Object => $obj, Right => $right);
971 push (@results, $msg);
974 elsif ($arg =~ /RevokeRight-(\d+)-(.*?)-(\d+)-(.*?)$/) {
975 my $principal_id = $1;
976 my $object_type = $2;
980 my $principal = RT::Principal->new($session{'CurrentUser'});
981 $principal->Load($principal_id);
982 next unless ($right);
985 if ($object_type eq 'RT::System') {
987 } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
988 $obj = $object_type->new($session{'CurrentUser'});
989 $obj->Load($object_id);
991 push (@results, loc("System Error"). ': '.
992 loc("Rights could not be revoked for [_1]", $object_type));
995 my ($val, $msg) = $principal->RevokeRight(Object => $obj, Right => $right);
996 push (@results, $msg);
1008 # {{{ sub UpdateRecordObj
1010 =head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs)
1012 @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.
1014 Returns an array of success/failure messages
1018 sub UpdateRecordObject {
1021 AttributesRef => undef,
1023 AttributePrefix => undef,
1027 my $Object = $args{'Object'};
1028 my @results = $Object->Update(AttributesRef => $args{'AttributesRef'},
1029 ARGSRef => $args{'ARGSRef'},
1030 AttributePrefix => $args{'AttributePrefix'}
1038 # {{{ Sub ProcessCustomFieldUpdates
1040 sub ProcessCustomFieldUpdates {
1042 CustomFieldObj => undef,
1047 my $Object = $args{'CustomFieldObj'};
1048 my $ARGSRef = $args{'ARGSRef'};
1050 my @attribs = qw( Name Type Description Queue SortOrder);
1051 my @results = UpdateRecordObject(
1052 AttributesRef => \@attribs,
1057 if ( $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" } ) {
1059 my ( $addval, $addmsg ) = $Object->AddValue(
1061 $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" },
1062 Description => $ARGSRef->{ "CustomField-"
1064 . "-AddValue-Description" },
1065 SortOrder => $ARGSRef->{ "CustomField-"
1067 . "-AddValue-SortOrder" },
1069 push ( @results, $addmsg );
1071 my @delete_values = (
1072 ref $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } eq
1074 ? @{ $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } }
1075 : ( $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } );
1076 foreach my $id (@delete_values) {
1077 next unless defined $id;
1078 my ( $err, $msg ) = $Object->DeleteValue($id);
1079 push ( @results, $msg );
1082 my $vals = $Object->Values();
1083 while (my $cfv = $vals->Next()) {
1084 if (my $so = $ARGSRef->{ 'CustomField-' . $Object->Id . '-SortOrder' . $cfv->Id }) {
1085 if ($cfv->SortOrder != $so) {
1086 my ( $err, $msg ) = $cfv->SetSortOrder($so);
1087 push ( @results, $msg );
1097 # {{{ sub ProcessTicketBasics
1099 =head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1101 Returns an array of results messages.
1105 sub ProcessTicketBasics {
1113 my $TicketObj = $args{'TicketObj'};
1114 my $ARGSRef = $args{'ARGSRef'};
1116 # {{{ Set basic fields
1130 if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
1131 my $tempqueue = RT::Queue->new($RT::SystemUser);
1132 $tempqueue->Load( $ARGSRef->{'Queue'} );
1133 if ( $tempqueue->id ) {
1134 $ARGSRef->{'Queue'} = $tempqueue->Id();
1139 # Status isn't a field that can be set to a null value.
1140 # RT core complains if you try
1141 delete $ARGSRef->{'Status'} unless ($ARGSRef->{'Status'});
1143 my @results = UpdateRecordObject(
1144 AttributesRef => \@attribs,
1145 Object => $TicketObj,
1149 # We special case owner changing, so we can use ForceOwnerChange
1150 if ( $ARGSRef->{'Owner'} && ( $TicketObj->Owner != $ARGSRef->{'Owner'} ) ) {
1152 if ( $ARGSRef->{'ForceOwnerChange'} ) {
1153 $ChownType = "Force";
1156 $ChownType = "Give";
1160 $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
1161 push ( @results, $msg );
1171 sub ProcessTicketCustomFieldUpdates {
1173 $args{'Object'} = delete $args{'TicketObj'};
1174 my $ARGSRef = { %{ $args{'ARGSRef'} } };
1176 # Build up a list of objects that we want to work with
1177 my %custom_fields_to_mod;
1178 foreach my $arg ( keys %$ARGSRef ) {
1179 if ( $arg =~ /^Ticket-(\d+-.*)/) {
1180 $ARGSRef->{"Object-RT::Ticket-$1"} = delete $ARGSRef->{$arg};
1182 elsif ( $arg =~ /^CustomField-(\d+-.*)/) {
1183 $ARGSRef->{"Object-RT::Ticket--$1"} = delete $ARGSRef->{$arg};
1187 return ProcessObjectCustomFieldUpdates(%args, ARGSRef => $ARGSRef);
1190 sub ProcessObjectCustomFieldUpdates {
1192 my $ARGSRef = $args{'ARGSRef'};
1195 # Build up a list of objects that we want to work with
1196 my %custom_fields_to_mod;
1197 foreach my $arg ( keys %$ARGSRef ) {
1198 # format: Object-<object class>-<object id>-CustomField-<CF id>-<commands>
1199 next unless $arg =~ /^Object-([\w:]+)-(\d*)-CustomField-(\d+)-(.*)$/;
1201 # For each of those objects, find out what custom fields we want to work with.
1202 $custom_fields_to_mod{ $1 }{ $2 || 0 }{ $3 }{ $4 } = $ARGSRef->{ $arg };
1205 # For each of those objects
1206 foreach my $class ( keys %custom_fields_to_mod ) {
1207 foreach my $id ( keys %{$custom_fields_to_mod{$class}} ) {
1208 my $Object = $args{'Object'};
1209 $Object = $class->new( $session{'CurrentUser'} )
1210 unless $Object && ref $Object eq $class;
1212 $Object->Load( $id ) unless ($Object->id || 0) == $id;
1213 unless ( $Object->id ) {
1214 $RT::Logger->warning("Couldn't load object $class #$id");
1218 foreach my $cf ( keys %{ $custom_fields_to_mod{ $class }{ $id } } ) {
1219 my $CustomFieldObj = RT::CustomField->new( $session{'CurrentUser'} );
1220 $CustomFieldObj->LoadById( $cf );
1221 unless ( $CustomFieldObj->id ) {
1222 $RT::Logger->warning("Couldn't load custom field #$id");
1225 push @results, _ProcessObjectCustomFieldUpdates(
1226 Prefix => "Object-$class-$id-CustomField-$cf-",
1228 CustomField => $CustomFieldObj,
1229 ARGS => $custom_fields_to_mod{$class}{$id}{$cf},
1237 sub _ProcessObjectCustomFieldUpdates {
1239 my $cf = $args{'CustomField'};
1240 my $cf_type = $cf->Type;
1243 foreach my $arg ( keys %{ $args{'ARGS'} } ) {
1245 # since http won't pass in a form element with a null value, we need
1247 if ( $arg eq 'Values-Magic' ) {
1248 # We don't care about the magic, if there's really a values element;
1249 next if $args{'ARGS'}->{'Value'} || $args{'ARGS'}->{'Values'};
1251 # "Empty" values does not mean anything for Image and Binary fields
1252 next if $cf_type =~ /^(?:Image|Binary)$/;
1255 $args{'ARGS'}->{'Values'} = undef;
1259 if ( ref $args{'ARGS'}->{ $arg } eq 'ARRAY' ) {
1260 @values = @{ $args{'ARGS'}->{$arg} };
1261 } elsif ( $cf_type =~ /text/i ) { # Both Text and Wikitext
1262 @values = ($args{'ARGS'}->{$arg});
1264 @values = split /\n/, $args{'ARGS'}->{ $arg };
1267 if ( ( $cf_type eq 'Freeform' && !$cf->SingleValue ) || $cf_type =~ /text/i ) {
1268 s/\r//g foreach @values;
1270 @values = grep defined && $_ ne '', @values;
1272 if ( $arg eq 'AddValue' || $arg eq 'Value' ) {
1273 foreach my $value (@values) {
1274 my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
1278 push ( @results, $msg );
1281 elsif ( $arg eq 'Upload' ) {
1282 my $value_hash = _UploadedFile( $args{'Prefix'} . $arg ) or next;
1283 my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
1287 push ( @results, $msg );
1289 elsif ( $arg eq 'DeleteValues' ) {
1290 foreach my $value ( @values ) {
1291 my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
1295 push ( @results, $msg );
1298 elsif ( $arg eq 'DeleteValueIds' ) {
1299 foreach my $value ( @values ) {
1300 my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
1304 push ( @results, $msg );
1307 elsif ( $arg eq 'Values' && !$cf->Repeated ) {
1308 my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
1311 foreach my $value ( @values ) {
1312 # build up a hash of values that the new set has
1313 $values_hash{$value} = 1;
1314 next if $cf_values->HasEntry( $value );
1316 my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
1320 push ( @results, $msg );
1323 $cf_values->RedoSearch;
1324 while ( my $cf_value = $cf_values->Next ) {
1325 next if $values_hash{ $cf_value->Content };
1327 my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
1329 Value => $cf_value->Content
1331 push ( @results, $msg);
1334 elsif ( $arg eq 'Values' ) {
1335 my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
1337 # keep everything up to the point of difference, delete the rest
1339 foreach my $old_cf (@{$cf_values->ItemsArrayRef}) {
1340 if (!$delete_flag and @values and $old_cf->Content eq $values[0]) {
1349 # now add/replace extra things, if any
1350 foreach my $value ( @values ) {
1351 my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
1355 push ( @results, $msg );
1360 loc("User asked for an unknown update type for custom field [_1] for [_2] object #[_3]",
1361 $cf->Name, ref $args{'Object'}, $args{'Object'}->id )
1368 # {{{ sub ProcessTicketWatchers
1370 =head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1372 Returns an array of results messages.
1376 sub ProcessTicketWatchers {
1384 my $Ticket = $args{'TicketObj'};
1385 my $ARGSRef = $args{'ARGSRef'};
1389 foreach my $key ( keys %$ARGSRef ) {
1391 # Delete deletable watchers
1392 if ( ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) )
1394 my ( $code, $msg ) = $Ticket->DeleteWatcher(
1398 push @results, $msg;
1401 # Delete watchers in the simple style demanded by the bulk manipulator
1402 elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) {
1403 my ( $code, $msg ) = $Ticket->DeleteWatcher(
1404 Email => $ARGSRef->{$key},
1407 push @results, $msg;
1410 # Add new wathchers by email address
1411 elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
1412 and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) )
1415 #They're in this order because otherwise $1 gets clobbered :/
1416 my ( $code, $msg ) = $Ticket->AddWatcher(
1417 Type => $ARGSRef->{$key},
1418 Email => $ARGSRef->{ "WatcherAddressEmail" . $1 }
1420 push @results, $msg;
1423 #Add requestors in the simple style demanded by the bulk manipulator
1424 elsif ( $key =~ /^Add(Requestor|Cc|AdminCc)$/ ) {
1425 my ( $code, $msg ) = $Ticket->AddWatcher(
1427 Email => $ARGSRef->{$key}
1429 push @results, $msg;
1432 # Add new watchers by owner
1433 elsif ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) {
1434 my $principal_id = $1;
1435 my $form = $ARGSRef->{$key};
1436 foreach my $value ( ref($form) ? @{$form} : ($form) ) {
1437 next unless $value =~ /^(?:AdminCc|Cc|Requestor)$/i;
1439 my ( $code, $msg ) = $Ticket->AddWatcher(
1441 PrincipalId => $principal_id
1443 push @results, $msg;
1453 # {{{ sub ProcessTicketDates
1455 =head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1457 Returns an array of results messages.
1461 sub ProcessTicketDates {
1468 my $Ticket = $args{'TicketObj'};
1469 my $ARGSRef = $args{'ARGSRef'};
1473 # {{{ Set date fields
1474 my @date_fields = qw(
1482 #Run through each field in this list. update the value if apropriate
1483 foreach my $field (@date_fields) {
1486 my $DateObj = RT::Date->new( $session{'CurrentUser'} );
1488 #If it's something other than just whitespace
1489 if ( $ARGSRef->{ $field . '_Date' } ne '' ) {
1491 Format => 'unknown',
1492 Value => $ARGSRef->{ $field . '_Date' }
1494 my $obj = $field . "Obj";
1495 if ( ( defined $DateObj->Unix )
1496 and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) )
1498 my $method = "Set$field";
1499 my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
1500 push @results, "$msg";
1511 # {{{ sub ProcessTicketLinks
1513 =head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1515 Returns an array of results messages.
1519 sub ProcessTicketLinks {
1520 my %args = ( TicketObj => undef,
1524 my $Ticket = $args{'TicketObj'};
1525 my $ARGSRef = $args{'ARGSRef'};
1528 my (@results) = ProcessRecordLinks(RecordObj => $Ticket,
1529 ARGSRef => $ARGSRef);
1531 #Merge if we need to
1532 if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
1534 $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
1535 push @results, $msg;
1543 sub ProcessRecordLinks {
1544 my %args = ( RecordObj => undef,
1548 my $Record = $args{'RecordObj'};
1549 my $ARGSRef = $args{'ARGSRef'};
1553 # Delete links that are gone gone gone.
1554 foreach my $arg ( keys %$ARGSRef ) {
1555 if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
1561 "Trying to delete: Base: $base Target: $target Type $type";
1562 my ( $val, $msg ) = $Record->DeleteLink( Base => $base,
1564 Target => $target );
1566 push @results, $msg;
1572 my @linktypes = qw( DependsOn MemberOf RefersTo );
1574 foreach my $linktype (@linktypes) {
1575 if ( $ARGSRef->{ $Record->Id . "-$linktype" } ) {
1576 for my $luri ( split ( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) {
1577 $luri =~ s/\s*$//; # Strip trailing whitespace
1578 my ( $val, $msg ) = $Record->AddLink( Target => $luri,
1579 Type => $linktype );
1580 push @results, $msg;
1583 if ( $ARGSRef->{ "$linktype-" . $Record->Id } ) {
1585 for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) {
1586 my ( $val, $msg ) = $Record->AddLink( Base => $luri,
1587 Type => $linktype );
1589 push @results, $msg;
1598 =head2 _UploadedFile ( $arg );
1600 Takes a CGI parameter name; if a file is uploaded under that name,
1601 return a hash reference suitable for AddCustomFieldValue's use:
1602 C<( Value => $filename, LargeContent => $content, ContentType => $type )>.
1604 Returns C<undef> if no files were uploaded in the C<$arg> field.
1610 my $cgi_object = $m->cgi_object;
1611 my $fh = $cgi_object->upload($arg) or return undef;
1612 my $upload_info = $cgi_object->uploadInfo($fh);
1614 my $filename = "$fh";
1615 $filename =~ s#^.*[\\/]##;
1620 LargeContent => do { local $/; scalar <$fh> },
1621 ContentType => $upload_info->{'Content-Type'},
1625 =head2 _load_container_object ( $type, $id );
1627 Instantiate container object for saving searches.
1631 sub _load_container_object {
1632 my ($obj_type, $obj_id) = @_;
1633 return RT::SavedSearch->new($session{'CurrentUser'})->_load_privacy_object($obj_type, $obj_id);
1636 =head2 _parse_saved_search ( $arg );
1638 Given a serialization string for saved search, and returns the
1639 container object and the search id.
1643 sub _parse_saved_search {
1645 return unless $spec;
1646 if ($spec !~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/ ) {
1653 return (_load_container_object ($obj_type, $obj_id), $search_id);
1656 eval "require RT::Interface::Web_Vendor";
1657 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm});
1658 eval "require RT::Interface::Web_Local";
1659 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm});