1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
46 ## Portions Copyright 2000 Tobias Brox <tobix@fsck.com>
48 ## This is a library of static subs to be used by the Mason web
58 use_ok(RT::Interface::Web);
65 package RT::Interface::Web;
72 =head2 EscapeUTF8 SCALARREF
74 does a css-busting but minimalist escaping of whatever html you're passing in.
80 return unless defined $$ref;
91 Encode::_utf8_on($$ref);
100 =head2 EscapeURI SCALARREF
102 Escapes URI component according to RFC2396
109 $$ref = Encode::encode_utf8( $$ref );
110 $$ref =~ s/([^a-zA-Z0-9_.!~*'()-])/uc sprintf("%%%02X", ord($1))/eg;
111 Encode::_utf8_on( $$ref );
116 # {{{ WebCanonicalizeInfo
118 =head2 WebCanonicalizeInfo();
120 Different web servers set different environmental varibles. This
121 function must return something suitable for REMOTE_USER. By default,
122 just downcase $ENV{'REMOTE_USER'}
126 sub WebCanonicalizeInfo {
129 if ( defined $ENV{'REMOTE_USER'} ) {
130 $user = lc ( $ENV{'REMOTE_USER'} ) if( length($ENV{'REMOTE_USER'}) );
138 # {{{ WebExternalAutoInfo
140 =head2 WebExternalAutoInfo($user);
142 Returns a hash of user attributes, used when WebExternalAuto is set.
146 sub WebExternalAutoInfo {
151 $user_info{'Privileged'} = 1;
153 if ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
154 # Populate fields with information from Unix /etc/passwd
156 my ($comments, $realname) = (getpwnam($user))[5, 6];
157 $user_info{'Comments'} = $comments if defined $comments;
158 $user_info{'RealName'} = $realname if defined $realname;
160 elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
161 # Populate fields with information from NT domain controller
164 # and return the wad of stuff
171 package HTML::Mason::Commands;
173 use vars qw/$r $m %session/;
180 loc is a nice clean global routine which calls $session{'CurrentUser'}->loc()
181 with whatever it's called with. If there is no $session{'CurrentUser'},
182 it creates a temporary user, so we have something to get a localisation handle
189 if ($session{'CurrentUser'} &&
190 UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
191 return($session{'CurrentUser'}->loc(@_));
193 elsif ( my $u = eval { RT::CurrentUser->new($RT::SystemUser->Id) } ) {
194 return ($u->loc(@_));
197 # pathetic case -- SystemUser is gone.
207 =head2 loc_fuzzy STRING
209 loc_fuzzy is for handling localizations of messages that may already
210 contain interpolated variables, typically returned from libraries
211 outside RT's control. It takes the message string and extracts the
212 variable array automatically by matching against the candidate entries
213 inside the lexicon file.
220 if ($session{'CurrentUser'} &&
221 UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
222 return($session{'CurrentUser'}->loc_fuzzy($msg));
225 my $u = RT::CurrentUser->new($RT::SystemUser->Id);
226 return ($u->loc_fuzzy($msg));
234 # Error - calls Error and aborts
237 if ($session{'ErrorDocument'} &&
238 $session{'ErrorDocumentType'}) {
239 $r->content_type($session{'ErrorDocumentType'});
240 $m->comp($session{'ErrorDocument'} , Why => shift);
244 $m->comp("/Elements/Error" , Why => shift);
251 # {{{ sub CreateTicket
253 =head2 CreateTicket ARGS
255 Create a new ticket, using Mason's %ARGS. returns @results.
264 my $Ticket = new RT::Ticket( $session{'CurrentUser'} );
266 my $Queue = new RT::Queue( $session{'CurrentUser'} );
267 unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
268 Abort('Queue not found');
271 unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
272 Abort('You have no permission to create tickets in that queue.');
275 my $due = new RT::Date( $session{'CurrentUser'} );
276 $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
277 my $starts = new RT::Date( $session{'CurrentUser'} );
278 $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
280 my @Requestors = split ( /\s*,\s*/, $ARGS{'Requestors'} );
281 my @Cc = split ( /\s*,\s*/, $ARGS{'Cc'} );
282 my @AdminCc = split ( /\s*,\s*/, $ARGS{'AdminCc'} );
284 my $MIMEObj = MakeMIMEEntity(
285 Subject => $ARGS{'Subject'},
286 From => $ARGS{'From'},
288 Body => $ARGS{'Content'},
291 if ( $ARGS{'Attachments'} ) {
292 my $rv = $MIMEObj->make_multipart;
293 $RT::Logger->error("Couldn't make multipart message")
294 if !$rv || $rv !~ /^(?:DONE|ALREADY)$/;
296 foreach ( values %{$ARGS{'Attachments'}} ) {
298 $RT::Logger->error("Couldn't add empty attachemnt");
301 $MIMEObj->add_part($_);
306 Type => $ARGS{'Type'} || 'ticket',
307 Queue => $ARGS{'Queue'},
308 Owner => $ARGS{'Owner'},
309 InitialPriority => $ARGS{'InitialPriority'},
310 FinalPriority => $ARGS{'FinalPriority'},
311 TimeLeft => $ARGS{'TimeLeft'},
312 TimeEstimated => $ARGS{'TimeEstimated'},
313 TimeWorked => $ARGS{'TimeWorked'},
314 Requestor => \@Requestors,
316 AdminCc => \@AdminCc,
317 Subject => $ARGS{'Subject'},
318 Status => $ARGS{'Status'},
320 Starts => $starts->ISO,
323 foreach my $arg (keys %ARGS) {
326 next if ($arg =~ /-Magic$/);
327 #Object-RT::Ticket--CustomField-3-Values
328 if ($arg =~ /^Object-RT::Transaction--CustomField-/) {
329 $create_args{$arg} = $ARGS{$arg};
331 elsif ($arg =~ /^Object-RT::Ticket--CustomField-(\d+)(.*?)$/) {
333 my $cf = RT::CustomField->new( $session{'CurrentUser'});
336 if ( $cf->Type eq 'Freeform' && ! $cf->SingleValue) {
337 $ARGS{$arg} =~ s/\r\n/\n/g;
338 $ARGS{$arg} = [split('\n', $ARGS{$arg})];
341 if ( $cf->Type =~ /text/i) { # Catch both Text and Wikitext
342 $ARGS{$arg} =~ s/\r//g;
345 if ( $arg =~ /-Upload$/ ) {
346 $create_args{"CustomField-".$cfid} = _UploadedFile($arg);
349 $create_args{"CustomField-".$cfid} = $ARGS{"$arg"};
355 # XXX TODO This code should be about six lines. and badly needs refactoring.
357 # {{{ turn new link lists into arrays, and pass in the proper arguments
358 my (@dependson, @dependedonby, @parents, @children, @refersto, @referredtoby);
360 foreach my $luri ( split ( / /, $ARGS{"new-DependsOn"} ) ) {
361 $luri =~ s/\s*$//; # Strip trailing whitespace
362 push @dependson, $luri;
364 $create_args{'DependsOn'} = \@dependson;
366 foreach my $luri ( split ( / /, $ARGS{"DependsOn-new"} ) ) {
367 push @dependedonby, $luri;
369 $create_args{'DependedOnBy'} = \@dependedonby;
371 foreach my $luri ( split ( / /, $ARGS{"new-MemberOf"} ) ) {
372 $luri =~ s/\s*$//; # Strip trailing whitespace
373 push @parents, $luri;
375 $create_args{'Parents'} = \@parents;
377 foreach my $luri ( split ( / /, $ARGS{"MemberOf-new"} ) ) {
378 push @children, $luri;
380 $create_args{'Children'} = \@children;
382 foreach my $luri ( split ( / /, $ARGS{"new-RefersTo"} ) ) {
383 $luri =~ s/\s*$//; # Strip trailing whitespace
384 push @refersto, $luri;
386 $create_args{'RefersTo'} = \@refersto;
388 foreach my $luri ( split ( / /, $ARGS{"RefersTo-new"} ) ) {
389 push @referredtoby, $luri;
391 $create_args{'ReferredToBy'} = \@referredtoby;
395 my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
396 unless ( $id && $Trans ) {
400 push ( @Actions, split("\n", $ErrMsg) );
401 unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
402 Abort( "No permission to view newly created ticket #"
403 . $Ticket->id . "." );
405 return ( $Ticket, @Actions );
411 # {{{ sub LoadTicket - loads a ticket
415 Takes a ticket id as its only variable. if it's handed an array, it takes
418 Returns an RT::Ticket object as the current user.
425 if ( ref($id) eq "ARRAY" ) {
430 Abort("No ticket specified");
433 my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
435 unless ( $Ticket->id ) {
436 Abort("Could not load ticket $id");
443 # {{{ sub ProcessUpdateMessage
445 sub ProcessUpdateMessage {
447 #TODO document what else this takes.
455 #Make the update content have no 'weird' newlines in it
456 if ( $args{ARGSRef}->{'UpdateTimeWorked'}
457 || $args{ARGSRef}->{'UpdateContent'}
458 || $args{ARGSRef}->{'UpdateAttachments'} )
462 $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() )
464 $args{ARGSRef}->{'UpdateSubject'} = undef;
467 my $Message = MakeMIMEEntity(
468 Subject => $args{ARGSRef}->{'UpdateSubject'},
469 Body => $args{ARGSRef}->{'UpdateContent'},
472 $Message->head->add( 'Message-ID' =>
477 . int(rand(2000)) . "."
478 . $args{'TicketObj'}->id . "-"
480 . "0" . "@" # Email sent
483 my $old_txn = RT::Transaction->new( $session{'CurrentUser'} );
484 if ( $args{ARGSRef}->{'QuoteTransaction'} ) {
485 $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} );
488 $old_txn = $args{TicketObj}->Transactions->First();
491 if ( $old_txn->Message && $old_txn->Message->First ) {
492 my @in_reply_to = split(/\s+/m, $old_txn->Message->First->GetHeader('In-Reply-To') || '');
493 my @references = split(/\s+/m, $old_txn->Message->First->GetHeader('References') || '' );
494 my @msgid = split(/\s+/m,$old_txn->Message->First->GetHeader('Message-ID') || '');
495 my @rtmsgid = split(/\s+/m,$old_txn->Message->First->GetHeader('RT-Message-ID') || '');
497 $Message->head->replace( 'In-Reply-To', join (' ', @rtmsgid ? @rtmsgid : @msgid));
498 $Message->head->replace( 'References', join(' ', @references, @msgid, @rtmsgid));
501 if ( $args{ARGSRef}->{'UpdateAttachments'} ) {
502 $Message->make_multipart;
503 $Message->add_part($_)
504 foreach values %{ $args{ARGSRef}->{'UpdateAttachments'} };
507 ## TODO: Implement public comments
508 if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
509 my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(
510 CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
511 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
513 TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
515 push( @{ $args{Actions} }, $Description );
516 $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
518 elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
519 my ( $Transaction, $Description, $Object ) =
520 $args{TicketObj}->Correspond(
521 CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
522 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
524 TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
526 push( @{ $args{Actions} }, $Description );
527 $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object;
531 @{ $args{'Actions'} },
532 loc("Update type was neither correspondence nor comment.") . " "
533 . loc("Update not recorded.")
541 # {{{ sub MakeMIMEEntity
543 =head2 MakeMIMEEntity PARAMHASH
545 Takes a paramhash Subject, Body and AttachmentFieldName.
547 Returns a MIME::Entity.
553 #TODO document what else this takes.
559 AttachmentFieldName => undef,
560 # map Encode::encode_utf8($_), @_,
564 #Make the update content have no 'weird' newlines in it
566 $args{'Body'} =~ s/\r\n/\n/gs;
569 # MIME::Head is not happy in utf-8 domain. This only happens
570 # when processing an incoming email (so far observed).
573 $Message = MIME::Entity->build(
574 Subject => $args{'Subject'} || "",
575 From => $args{'From'},
578 Data => [ $args{'Body'} ]
582 my $cgi_object = $m->cgi_object;
584 if (my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) {
588 use File::Temp qw(tempfile tempdir);
590 #foreach my $filehandle (@filenames) {
592 my ( $fh, $temp_file );
594 # on NFS and NTFS, it is possible that tempfile() conflicts
595 # with other processes, causing a race condition. we try to
596 # accommodate this by pausing and retrying.
597 last if ($fh, $temp_file) = eval { tempfile( UNLINK => 1) };
601 binmode $fh; #thank you, windows
603 while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
607 my $uploadinfo = $cgi_object->uploadInfo($filehandle);
609 # Prefer the cached name first over CGI.pm stringification.
610 my $filename = $RT::Mason::CGI::Filename;
611 $filename = "$filehandle" unless defined($filename);
613 $filename =~ s#^.*[\\/]##;
617 Filename => Encode::decode_utf8($filename),
618 Type => $uploadinfo->{'Content-Type'},
626 $Message->make_singlepart();
627 RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
635 # {{{ sub ProcessSearchQuery
637 =head2 ProcessSearchQuery
639 Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand.
641 TODO Doc exactly what comes in the paramhash
646 sub ProcessSearchQuery {
649 ## TODO: The only parameter here is %ARGS. Maybe it would be
650 ## cleaner to load this parameter as $ARGS, and use $ARGS->{...}
651 ## instead of $args{ARGS}->{...} ? :)
653 #Searches are sticky.
654 if ( defined $session{'tickets'} ) {
656 # Reset the old search
657 $session{'tickets'}->GotoFirstItem;
662 $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} );
665 #Import a bookmarked search if we have one
666 if ( defined $args{ARGS}->{'Bookmark'} ) {
667 $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} );
670 # {{{ Goto next/prev page
671 if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) {
672 $session{'tickets'}->NextPage;
674 elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) {
675 $session{'tickets'}->PrevPage;
677 elsif ( $args{ARGS}->{'GotoPage'} > 0 ) {
678 $session{'tickets'}->GotoPage( $args{ARGS}->{GotoPage} - 1 );
683 # {{{ Deal with limiting the search
685 if ( $args{ARGS}->{'RefreshSearchInterval'} ) {
686 $session{'tickets_refresh_interval'} =
687 $args{ARGS}->{'RefreshSearchInterval'};
690 if ( $args{ARGS}->{'TicketsSortBy'} ) {
691 $session{'tickets_sort_by'} = $args{ARGS}->{'TicketsSortBy'};
692 $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'};
693 $session{'tickets'}->OrderBy(
694 FIELD => $args{ARGS}->{'TicketsSortBy'},
695 ORDER => $args{ARGS}->{'TicketsSortOrder'}
701 # {{{ Set the query limit
702 if ( defined $args{ARGS}->{'RowsPerPage'} ) {
704 "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" );
706 $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'};
707 $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} );
712 if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) {
713 $session{'tickets'}->LimitPriority(
714 VALUE => $args{ARGS}->{'ValueOfPriority'},
715 OPERATOR => $args{ARGS}->{'PriorityOp'}
721 if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) {
722 $session{'tickets'}->LimitOwner(
723 VALUE => $args{ARGS}->{'ValueOfOwner'},
724 OPERATOR => $args{ARGS}->{'OwnerOp'}
729 # {{{ Limit requestor email
730 if ( $args{ARGS}->{'ValueOfWatcherRole'} ne '' ) {
731 $session{'tickets'}->LimitWatcher(
732 TYPE => $args{ARGS}->{'WatcherRole'},
733 VALUE => $args{ARGS}->{'ValueOfWatcherRole'},
734 OPERATOR => $args{ARGS}->{'WatcherRoleOp'},
741 if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) {
742 $session{'tickets'}->LimitQueue(
743 VALUE => $args{ARGS}->{'ValueOfQueue'},
744 OPERATOR => $args{ARGS}->{'QueueOp'}
750 if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) {
751 if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) {
752 foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) {
753 $session{'tickets'}->LimitStatus(
755 OPERATOR => $args{ARGS}->{'StatusOp'},
760 $session{'tickets'}->LimitStatus(
761 VALUE => $args{ARGS}->{'ValueOfStatus'},
762 OPERATOR => $args{ARGS}->{'StatusOp'},
770 if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) {
771 my $val = $args{ARGS}->{'ValueOfSubject'};
772 if ($args{ARGS}->{'SubjectOp'} =~ /like/) {
775 $session{'tickets'}->LimitSubject(
777 OPERATOR => $args{ARGS}->{'SubjectOp'},
783 if ( $args{ARGS}->{'ValueOfDate'} ne '' ) {
784 my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} );
785 $args{ARGS}->{'DateType'} =~ s/_Date$//;
787 if ( $args{ARGS}->{'DateType'} eq 'Updated' ) {
788 $session{'tickets'}->LimitTransactionDate(
790 OPERATOR => $args{ARGS}->{'DateOp'},
794 $session{'tickets'}->LimitDate( FIELD => $args{ARGS}->{'DateType'},
796 OPERATOR => $args{ARGS}->{'DateOp'},
803 if ( $args{ARGS}->{'ValueOfAttachmentField'} ne '' ) {
804 my $val = $args{ARGS}->{'ValueOfAttachmentField'};
805 if ($args{ARGS}->{'AttachmentFieldOp'} =~ /like/) {
808 $session{'tickets'}->Limit(
809 FIELD => $args{ARGS}->{'AttachmentField'},
811 OPERATOR => $args{ARGS}->{'AttachmentFieldOp'},
817 # {{{ Limit CustomFields
819 foreach my $arg ( keys %{ $args{ARGS} } ) {
821 if ( $arg =~ /^CustomField(\d+)$/ ) {
827 next unless ( $args{ARGS}->{$arg} );
829 my $form = $args{ARGS}->{$arg};
830 my $oper = $args{ARGS}->{ "CustomFieldOp" . $id };
831 foreach my $value ( ref($form) ? @{$form} : ($form) ) {
833 if ($oper =~ /like/i) {
834 $value = "%".$value."%";
836 if ( $value =~ /^null$/i ) {
838 #Don't quote the string 'null'
841 # Convert the operator to something apropriate for nulls
842 $oper = 'IS' if ( $oper eq '=' );
843 $oper = 'IS NOT' if ( $oper eq '!=' );
845 $session{'tickets'}->LimitCustomField( CUSTOMFIELD => $id,
847 QUOTEVALUE => $quote,
859 # {{{ sub ParseDateToISO
861 =head2 ParseDateToISO
863 Takes a date in an arbitrary format.
864 Returns an ISO date and time in GMT
871 my $date_obj = RT::Date->new($session{'CurrentUser'});
876 return ( $date_obj->ISO );
881 # {{{ sub ProcessACLChanges
883 sub ProcessACLChanges {
886 my %ARGS = %$ARGSref;
888 my ( $ACL, @results );
891 foreach my $arg (keys %ARGS) {
892 if ($arg =~ /GrantRight-(\d+)-(.*?)-(\d+)$/) {
893 my $principal_id = $1;
894 my $object_type = $2;
896 my $rights = $ARGS{$arg};
898 my $principal = RT::Principal->new($session{'CurrentUser'});
899 $principal->Load($principal_id);
903 if ($object_type eq 'RT::System') {
905 } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
906 $obj = $object_type->new($session{'CurrentUser'});
907 $obj->Load($object_id);
909 push (@results, loc("System Error"). ': '.
910 loc("Rights could not be granted for [_1]", $object_type));
914 my @rights = ref($ARGS{$arg}) eq 'ARRAY' ? @{$ARGS{$arg}} : ($ARGS{$arg});
915 foreach my $right (@rights) {
916 next unless ($right);
917 my ($val, $msg) = $principal->GrantRight(Object => $obj, Right => $right);
918 push (@results, $msg);
921 elsif ($arg =~ /RevokeRight-(\d+)-(.*?)-(\d+)-(.*?)$/) {
922 my $principal_id = $1;
923 my $object_type = $2;
927 my $principal = RT::Principal->new($session{'CurrentUser'});
928 $principal->Load($principal_id);
929 next unless ($right);
932 if ($object_type eq 'RT::System') {
934 } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
935 $obj = $object_type->new($session{'CurrentUser'});
936 $obj->Load($object_id);
938 push (@results, loc("System Error"). ': '.
939 loc("Rights could not be revoked for [_1]", $object_type));
942 my ($val, $msg) = $principal->RevokeRight(Object => $obj, Right => $right);
943 push (@results, $msg);
955 # {{{ sub UpdateRecordObj
957 =head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs)
959 @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.
961 Returns an array of success/failure messages
965 sub UpdateRecordObject {
968 AttributesRef => undef,
970 AttributePrefix => undef,
974 my $Object = $args{'Object'};
975 my @results = $Object->Update(AttributesRef => $args{'AttributesRef'},
976 ARGSRef => $args{'ARGSRef'},
977 AttributePrefix => $args{'AttributePrefix'}
985 # {{{ Sub ProcessCustomFieldUpdates
987 sub ProcessCustomFieldUpdates {
989 CustomFieldObj => undef,
994 my $Object = $args{'CustomFieldObj'};
995 my $ARGSRef = $args{'ARGSRef'};
997 my @attribs = qw( Name Type Description Queue SortOrder);
998 my @results = UpdateRecordObject(
999 AttributesRef => \@attribs,
1004 if ( $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" } ) {
1006 my ( $addval, $addmsg ) = $Object->AddValue(
1008 $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" },
1009 Description => $ARGSRef->{ "CustomField-"
1011 . "-AddValue-Description" },
1012 SortOrder => $ARGSRef->{ "CustomField-"
1014 . "-AddValue-SortOrder" },
1016 push ( @results, $addmsg );
1018 my @delete_values = (
1019 ref $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } eq
1021 ? @{ $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } }
1022 : ( $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } );
1023 foreach my $id (@delete_values) {
1024 next unless defined $id;
1025 my ( $err, $msg ) = $Object->DeleteValue($id);
1026 push ( @results, $msg );
1029 my $vals = $Object->Values();
1030 while (my $cfv = $vals->Next()) {
1031 if (my $so = $ARGSRef->{ 'CustomField-' . $Object->Id . '-SortOrder' . $cfv->Id }) {
1032 if ($cfv->SortOrder != $so) {
1033 my ( $err, $msg ) = $cfv->SetSortOrder($so);
1034 push ( @results, $msg );
1044 # {{{ sub ProcessTicketBasics
1046 =head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1048 Returns an array of results messages.
1052 sub ProcessTicketBasics {
1060 my $TicketObj = $args{'TicketObj'};
1061 my $ARGSRef = $args{'ARGSRef'};
1063 # {{{ Set basic fields
1076 if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
1077 my $tempqueue = RT::Queue->new($RT::SystemUser);
1078 $tempqueue->Load( $ARGSRef->{'Queue'} );
1079 if ( $tempqueue->id ) {
1080 $ARGSRef->{'Queue'} = $tempqueue->Id();
1085 # Status isn't a field that can be set to a null value.
1086 # RT core complains if you try
1087 delete $ARGSRef->{'Status'} unless ($ARGSRef->{'Status'});
1089 my @results = UpdateRecordObject(
1090 AttributesRef => \@attribs,
1091 Object => $TicketObj,
1095 # We special case owner changing, so we can use ForceOwnerChange
1096 if ( $ARGSRef->{'Owner'} && ( $TicketObj->Owner != $ARGSRef->{'Owner'} ) ) {
1098 if ( $ARGSRef->{'ForceOwnerChange'} ) {
1099 $ChownType = "Force";
1102 $ChownType = "Give";
1106 $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
1107 push ( @results, $msg );
1117 sub ProcessTicketCustomFieldUpdates {
1119 $args{'Object'} = delete $args{'TicketObj'};
1120 my $ARGSRef = { %{ $args{'ARGSRef'} } };
1122 # Build up a list of objects that we want to work with
1123 my %custom_fields_to_mod;
1124 foreach my $arg ( keys %$ARGSRef ) {
1125 if ( $arg =~ /^Ticket-(\d+-.*)/) {
1126 $ARGSRef->{"Object-RT::Ticket-$1"} = delete $ARGSRef->{$arg};
1128 elsif ( $arg =~ /^CustomField-(\d+-.*)/) {
1129 $ARGSRef->{"Object-RT::Ticket--$1"} = delete $ARGSRef->{$arg};
1133 return ProcessObjectCustomFieldUpdates(%args, ARGSRef => $ARGSRef);
1136 sub ProcessObjectCustomFieldUpdates {
1138 my $ARGSRef = $args{'ARGSRef'};
1141 # Build up a list of objects that we want to work with
1142 my %custom_fields_to_mod;
1143 foreach my $arg ( keys %$ARGSRef ) {
1144 # format: Object-<object class>-<object id>-CustomField-<CF id>-<commands>
1145 next unless $arg =~ /^Object-([\w:]+)-(\d*)-CustomField-(\d+)-(.*)$/;
1147 # For each of those objects, find out what custom fields we want to work with.
1148 $custom_fields_to_mod{ $1 }{ $2 || 0 }{ $3 }{ $4 } = $ARGSRef->{ $arg };
1151 # For each of those objects
1152 foreach my $class ( keys %custom_fields_to_mod ) {
1153 foreach my $id ( keys %{$custom_fields_to_mod{$class}} ) {
1154 my $Object = $args{'Object'};
1155 $Object = $class->new( $session{'CurrentUser'} )
1156 unless $Object && ref $Object eq $class;
1158 $Object->Load( $id ) unless ($Object->id || 0) == $id;
1159 unless ( $Object->id ) {
1160 $RT::Logger->warning("Couldn't load object $class #$id");
1164 foreach my $cf ( keys %{ $custom_fields_to_mod{ $class }{ $id } } ) {
1165 my $CustomFieldObj = RT::CustomField->new( $session{'CurrentUser'} );
1166 $CustomFieldObj->LoadById( $cf );
1167 unless ( $CustomFieldObj->id ) {
1168 $RT::Logger->warning("Couldn't load custom field #$id");
1171 push @results, _ProcessObjectCustomFieldUpdates(
1172 Prefix => "Object-$class-$id-CustomField-$cf-",
1174 CustomField => $CustomFieldObj,
1175 ARGS => $custom_fields_to_mod{$class}{$id}{$cf},
1183 sub _ProcessObjectCustomFieldUpdates {
1185 my $cf = $args{'CustomField'};
1186 my $cf_type = $cf->Type;
1189 foreach my $arg ( keys %{ $args{'ARGS'} } ) {
1191 # since http won't pass in a form element with a null value, we need
1193 if ( $arg eq 'Values-Magic' ) {
1194 # We don't care about the magic, if there's really a values element;
1195 next if $args{'ARGS'}->{'Value'} || $args{'ARGS'}->{'Values'};
1197 # "Empty" values does not mean anything for Image and Binary fields
1198 next if $cf_type =~ /^(?:Image|Binary)$/;
1201 $args{'ARGS'}->{'Values'} = undef;
1205 if ( ref $args{'ARGS'}->{ $arg } eq 'ARRAY' ) {
1206 @values = @{ $args{'ARGS'}->{$arg} };
1207 } elsif ( $cf_type =~ /text/i ) { # Both Text and Wikitext
1208 @values = ($args{'ARGS'}->{$arg});
1210 @values = split /\n/, $args{'ARGS'}->{ $arg };
1213 if ( ( $cf_type eq 'Freeform' && !$cf->SingleValue ) || $cf_type =~ /text/i ) {
1214 s/\r//g foreach @values;
1216 @values = grep defined && $_ ne '', @values;
1218 if ( $arg eq 'AddValue' || $arg eq 'Value' ) {
1219 foreach my $value (@values) {
1220 my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
1224 push ( @results, $msg );
1227 elsif ( $arg eq 'Upload' ) {
1228 my $value_hash = _UploadedFile( $args{'Prefix'} . $arg ) or next;
1229 my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
1233 push ( @results, $msg );
1235 elsif ( $arg eq 'DeleteValues' ) {
1236 foreach my $value ( @values ) {
1237 my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
1241 push ( @results, $msg );
1244 elsif ( $arg eq 'DeleteValueIds' ) {
1245 foreach my $value ( @values ) {
1246 my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
1250 push ( @results, $msg );
1253 elsif ( $arg eq 'Values' && !$cf->Repeated ) {
1254 my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
1257 foreach my $value ( @values ) {
1258 # build up a hash of values that the new set has
1259 $values_hash{$value} = 1;
1260 next if $cf_values->HasEntry( $value );
1262 my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
1266 push ( @results, $msg );
1269 $cf_values->RedoSearch;
1270 while ( my $cf_value = $cf_values->Next ) {
1271 next if $values_hash{ $cf_value->Content };
1273 my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue(
1275 Value => $cf_value->Content
1277 push ( @results, $msg);
1280 elsif ( $arg eq 'Values' ) {
1281 my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id );
1283 # keep everything up to the point of difference, delete the rest
1285 foreach my $old_cf (@{$cf_values->ItemsArrayRef}) {
1286 if (!$delete_flag and @values and $old_cf->Content eq $values[0]) {
1295 # now add/replace extra things, if any
1296 foreach my $value ( @values ) {
1297 my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue(
1301 push ( @results, $msg );
1305 push ( @results, loc("User asked for an unknown update type"
1306 ." for custom field [_1] for [_2] object #[_3]",
1307 $cf->Name, ref $args{'Object'}, $args{'Object'}->id )
1314 # {{{ sub ProcessTicketWatchers
1316 =head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1318 Returns an array of results messages.
1322 sub ProcessTicketWatchers {
1330 my $Ticket = $args{'TicketObj'};
1331 my $ARGSRef = $args{'ARGSRef'};
1333 # {{{ Munge watchers
1335 foreach my $key ( keys %$ARGSRef ) {
1337 # {{{ Delete deletable watchers
1338 if ( ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) ) {
1339 my ( $code, $msg ) =
1340 $Ticket->DeleteWatcher(PrincipalId => $2,
1342 push @results, $msg;
1345 # Delete watchers in the simple style demanded by the bulk manipulator
1346 elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) {
1347 my ( $code, $msg ) = $Ticket->DeleteWatcher( Email => $ARGSRef->{$key}, Type => $1 );
1348 push @results, $msg;
1353 # Add new wathchers by email address
1354 elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
1355 and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) )
1358 #They're in this order because otherwise $1 gets clobbered :/
1359 my ( $code, $msg ) = $Ticket->AddWatcher(
1360 Type => $ARGSRef->{$key},
1361 Email => $ARGSRef->{ "WatcherAddressEmail" . $1 }
1363 push @results, $msg;
1366 #Add requestors in the simple style demanded by the bulk manipulator
1367 elsif ( $key =~ /^Add(Requestor|Cc|AdminCc)$/ ) {
1368 my ( $code, $msg ) = $Ticket->AddWatcher(
1370 Email => $ARGSRef->{$key}
1372 push @results, $msg;
1375 # Add new watchers by owner
1376 elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
1377 and ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) ) {
1379 #They're in this order because otherwise $1 gets clobbered :/
1380 my ( $code, $msg ) =
1381 $Ticket->AddWatcher( Type => $ARGSRef->{$key}, PrincipalId => $1 );
1382 push @results, $msg;
1393 # {{{ sub ProcessTicketDates
1395 =head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1397 Returns an array of results messages.
1401 sub ProcessTicketDates {
1408 my $Ticket = $args{'TicketObj'};
1409 my $ARGSRef = $args{'ARGSRef'};
1413 # {{{ Set date fields
1414 my @date_fields = qw(
1422 #Run through each field in this list. update the value if apropriate
1423 foreach my $field (@date_fields) {
1426 my $DateObj = RT::Date->new( $session{'CurrentUser'} );
1428 #If it's something other than just whitespace
1429 if ( $ARGSRef->{ $field . '_Date' } ne '' ) {
1431 Format => 'unknown',
1432 Value => $ARGSRef->{ $field . '_Date' }
1434 my $obj = $field . "Obj";
1435 if ( ( defined $DateObj->Unix )
1436 and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) )
1438 my $method = "Set$field";
1439 my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
1440 push @results, "$msg";
1451 # {{{ sub ProcessTicketLinks
1453 =head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1455 Returns an array of results messages.
1459 sub ProcessTicketLinks {
1460 my %args = ( TicketObj => undef,
1464 my $Ticket = $args{'TicketObj'};
1465 my $ARGSRef = $args{'ARGSRef'};
1468 my (@results) = ProcessRecordLinks(RecordObj => $Ticket,
1469 ARGSRef => $ARGSRef);
1471 #Merge if we need to
1472 if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
1474 $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
1475 push @results, $msg;
1483 sub ProcessRecordLinks {
1484 my %args = ( RecordObj => undef,
1488 my $Record = $args{'RecordObj'};
1489 my $ARGSRef = $args{'ARGSRef'};
1493 # Delete links that are gone gone gone.
1494 foreach my $arg ( keys %$ARGSRef ) {
1495 if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
1501 "Trying to delete: Base: $base Target: $target Type $type";
1502 my ( $val, $msg ) = $Record->DeleteLink( Base => $base,
1504 Target => $target );
1506 push @results, $msg;
1512 my @linktypes = qw( DependsOn MemberOf RefersTo );
1514 foreach my $linktype (@linktypes) {
1515 if ( $ARGSRef->{ $Record->Id . "-$linktype" } ) {
1516 for my $luri ( split ( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) {
1517 $luri =~ s/\s*$//; # Strip trailing whitespace
1518 my ( $val, $msg ) = $Record->AddLink( Target => $luri,
1519 Type => $linktype );
1520 push @results, $msg;
1523 if ( $ARGSRef->{ "$linktype-" . $Record->Id } ) {
1525 for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) {
1526 my ( $val, $msg ) = $Record->AddLink( Base => $luri,
1527 Type => $linktype );
1529 push @results, $msg;
1538 =head2 _UploadedFile ( $arg );
1540 Takes a CGI parameter name; if a file is uploaded under that name,
1541 return a hash reference suitable for AddCustomFieldValue's use:
1542 C<( Value => $filename, LargeContent => $content, ContentType => $type )>.
1544 Returns C<undef> if no files were uploaded in the C<$arg> field.
1550 my $cgi_object = $m->cgi_object;
1551 my $fh = $cgi_object->upload($arg) or return undef;
1552 my $upload_info = $cgi_object->uploadInfo($fh);
1554 my $filename = "$fh";
1555 $filename =~ s#^.*[\\/]##;
1560 LargeContent => do { local $/; scalar <$fh> },
1561 ContentType => $upload_info->{'Content-Type'},
1565 eval "require RT::Interface::Web_Vendor";
1566 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm});
1567 eval "require RT::Interface::Web_Local";
1568 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm});