3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
5 # (Except where explictly superceded by other copyright notices)
7 # This work is made available to you under the terms of Version 2 of
8 # the GNU General Public License. A copy of that license should have
9 # been provided with this software, but in any event can be snarfed
12 # This work is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 # General Public License for more details.
17 # Unless otherwise specified, all modifications, corrections or
18 # extensions to this work which alter its source code become the
19 # property of Best Practical Solutions, LLC when submitted for
20 # inclusion in the work.
24 ## Portions Copyright 2000 Tobias Brox <tobix@fsck.com>
26 ## This is a library of static subs to be used by the Mason web
36 use_ok(RT::Interface::Web);
43 package RT::Interface::Web;
50 # {{{ sub NewApacheHandler
52 =head2 NewApacheHandler
54 Takes extra options to pass to HTML::Mason::ApacheHandler->new
55 Returns a new Mason::ApacheHandler object
59 sub NewApacheHandler {
60 require HTML::Mason::ApacheHandler;
61 my $ah = new HTML::Mason::ApacheHandler(
64 [ local => $RT::MasonLocalComponentRoot ],
65 [ standard => $RT::MasonComponentRoot ]
68 default_escape_flags => 'h',
69 allow_globals => [qw(%session)],
70 data_dir => "$RT::MasonDataDir",
75 $ah->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 );
82 # {{{ sub NewCGIHandler
86 Returns a new Mason::CGIHandler object
95 my $handler = HTML::Mason::CGIHandler->new(
97 [ local => $RT::MasonLocalComponentRoot ],
98 [ standard => $RT::MasonComponentRoot ]
100 data_dir => "$RT::MasonDataDir",
101 default_escape_flags => 'h',
102 allow_globals => [qw(%session)],
108 $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 );
119 =head2 EscapeUTF8 SCALARREF
121 does a css-busting but minimalist escaping of whatever html you're passing in.
132 $val =~ s/\(/(/g;
133 $val =~ s/\)/)/g;
137 Encode::_utf8_on($$ref);
143 # {{{ WebCanonicalizeInfo
145 =head2 WebCanonicalizeInfo();
147 Different web servers set different environmental varibles. This
148 function must return something suitable for REMOTE_USER. By default,
149 just downcase $ENV{'REMOTE_USER'}
153 sub WebCanonicalizeInfo {
156 if ( defined $ENV{'REMOTE_USER'} ) {
157 $user = lc ( $ENV{'REMOTE_USER'} ) if( length($ENV{'REMOTE_USER'}) );
165 # {{{ WebExternalAutoInfo
167 =head2 WebExternalAutoInfo($user);
169 Returns a hash of user attributes, used when WebExternalAuto is set.
173 sub WebExternalAutoInfo {
178 $user_info{'Privileged'} = 1;
180 if ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
181 # Populate fields with information from Unix /etc/passwd
183 my ($comments, $realname) = (getpwnam($user))[5, 6];
184 $user_info{'Comments'} = $comments if defined $comments;
185 $user_info{'RealName'} = $realname if defined $realname;
187 elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
188 # Populate fields with information from NT domain controller
191 # and return the wad of stuff
198 package HTML::Mason::Commands;
200 use vars qw/$r $m %session/;
207 loc is a nice clean global routine which calls $session{'CurrentUser'}->loc()
208 with whatever it's called with. If there is no $session{'CurrentUser'},
209 it creates a temporary user, so we have something to get a localisation handle
216 if ($session{'CurrentUser'} &&
217 UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
218 return($session{'CurrentUser'}->loc(@_));
220 elsif ( my $u = eval { RT::CurrentUser->new($RT::SystemUser->Id) } ) {
221 return ($u->loc(@_));
224 # pathetic case -- SystemUser is gone.
234 =head2 loc_fuzzy STRING
236 loc_fuzzy is for handling localizations of messages that may already
237 contain interpolated variables, typically returned from libraries
238 outside RT's control. It takes the message string and extracts the
239 variable array automatically by matching against the candidate entries
240 inside the lexicon file.
247 if ($session{'CurrentUser'} &&
248 UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
249 return($session{'CurrentUser'}->loc_fuzzy($msg));
252 my $u = RT::CurrentUser->new($RT::SystemUser->Id);
253 return ($u->loc_fuzzy($msg));
261 # Error - calls Error and aborts
264 if ($session{'ErrorDocument'} &&
265 $session{'ErrorDocumentType'}) {
266 $r->content_type($session{'ErrorDocumentType'});
267 $m->comp($session{'ErrorDocument'} , Why => shift);
271 $m->comp("/Elements/Error" , Why => shift);
278 # {{{ sub CreateTicket
280 =head2 CreateTicket ARGS
282 Create a new ticket, using Mason's %ARGS. returns @results.
291 my $Ticket = new RT::Ticket( $session{'CurrentUser'} );
293 my $Queue = new RT::Queue( $session{'CurrentUser'} );
294 unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
295 Abort('Queue not found');
298 unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
299 Abort('You have no permission to create tickets in that queue.');
302 my $due = new RT::Date( $session{'CurrentUser'} );
303 $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
304 my $starts = new RT::Date( $session{'CurrentUser'} );
305 $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
307 my @Requestors = split ( /\s*,\s*/, $ARGS{'Requestors'} );
308 my @Cc = split ( /\s*,\s*/, $ARGS{'Cc'} );
309 my @AdminCc = split ( /\s*,\s*/, $ARGS{'AdminCc'} );
311 my $MIMEObj = MakeMIMEEntity(
312 Subject => $ARGS{'Subject'},
313 From => $ARGS{'From'},
315 Body => $ARGS{'Content'},
318 if ($ARGS{'Attachments'}) {
319 $MIMEObj->make_multipart;
320 $MIMEObj->add_part($_) foreach values %{$ARGS{'Attachments'}};
324 Queue => $ARGS{'Queue'},
325 Owner => $ARGS{'Owner'},
326 InitialPriority => $ARGS{'InitialPriority'},
327 FinalPriority => $ARGS{'FinalPriority'},
328 TimeLeft => $ARGS{'TimeLeft'},
329 TimeEstimated => $ARGS{'TimeEstimated'},
330 TimeWorked => $ARGS{'TimeWorked'},
331 Requestor => \@Requestors,
333 AdminCc => \@AdminCc,
334 Subject => $ARGS{'Subject'},
335 Status => $ARGS{'Status'},
337 Starts => $starts->ISO,
340 foreach my $arg (%ARGS) {
341 if ($arg =~ /^CustomField-(\d+)(.*?)$/) {
342 next if ($arg =~ /-Magic$/);
343 $create_args{"CustomField-".$1} = $ARGS{"$arg"};
346 my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
347 unless ( $id && $Trans ) {
350 my @linktypes = qw( DependsOn MemberOf RefersTo );
352 foreach my $linktype (@linktypes) {
353 foreach my $luri ( split ( / /, $ARGS{"new-$linktype"} ) ) {
354 $luri =~ s/\s*$//; # Strip trailing whitespace
355 my ( $val, $msg ) = $Ticket->AddLink(
359 push ( @Actions, $msg ) unless ($val);
362 foreach my $luri ( split ( / /, $ARGS{"$linktype-new"} ) ) {
363 my ( $val, $msg ) = $Ticket->AddLink(
368 push ( @Actions, $msg ) unless ($val);
372 push ( @Actions, split("\n", $ErrMsg) );
373 unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
374 Abort( "No permission to view newly created ticket #"
375 . $Ticket->id . "." );
377 return ( $Ticket, @Actions );
383 # {{{ sub LoadTicket - loads a ticket
387 Takes a ticket id as its only variable. if it's handed an array, it takes
390 Returns an RT::Ticket object as the current user.
397 if ( ref($id) eq "ARRAY" ) {
402 Abort("No ticket specified");
405 my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
407 unless ( $Ticket->id ) {
408 Abort("Could not load ticket $id");
415 # {{{ sub ProcessUpdateMessage
417 sub ProcessUpdateMessage {
419 #TODO document what else this takes.
427 #Make the update content have no 'weird' newlines in it
428 if ( $args{ARGSRef}->{'UpdateTimeWorked'} ||
429 $args{ARGSRef}->{'UpdateContent'} ||
430 $args{ARGSRef}->{'UpdateAttachments'}) {
433 $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() )
435 $args{ARGSRef}->{'UpdateSubject'} = undef;
438 my $Message = MakeMIMEEntity(
439 Subject => $args{ARGSRef}->{'UpdateSubject'},
440 Body => $args{ARGSRef}->{'UpdateContent'},
443 if ($args{ARGSRef}->{'UpdateAttachments'}) {
444 $Message->make_multipart;
445 $Message->add_part($_) foreach values %{$args{ARGSRef}->{'UpdateAttachments'}};
448 ## TODO: Implement public comments
449 if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
450 my ( $Transaction, $Description ) = $args{TicketObj}->Comment(
451 CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
452 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
454 TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
456 push ( @{ $args{Actions} }, $Description );
458 elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
459 my ( $Transaction, $Description ) = $args{TicketObj}->Correspond(
460 CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
461 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
463 TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
465 push ( @{ $args{Actions} }, $Description );
468 push ( @{ $args{'Actions'} },
469 loc("Update type was neither correspondence nor comment.").
471 loc("Update not recorded.")
479 # {{{ sub MakeMIMEEntity
481 =head2 MakeMIMEEntity PARAMHASH
483 Takes a paramhash Subject, Body and AttachmentFieldName.
485 Returns a MIME::Entity.
491 #TODO document what else this takes.
497 AttachmentFieldName => undef,
498 # map Encode::encode_utf8($_), @_,
502 #Make the update content have no 'weird' newlines in it
504 $args{'Body'} =~ s/\r\n/\n/gs;
507 # MIME::Head is not happy in utf-8 domain. This only happens
508 # when processing an incoming email (so far observed).
511 $Message = MIME::Entity->build(
512 Subject => $args{'Subject'} || "",
513 From => $args{'From'},
516 Data => [ $args{'Body'} ]
520 my $cgi_object = $m->cgi_object;
522 if (my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) {
526 use File::Temp qw(tempfile tempdir);
528 #foreach my $filehandle (@filenames) {
530 my ( $fh, $temp_file );
532 # on NFS and NTFS, it is possible that tempfile() conflicts
533 # with other processes, causing a race condition. we try to
534 # accommodate this by pausing and retrying.
535 last if ($fh, $temp_file) = eval { tempfile( UNLINK => 1) };
539 binmode $fh; #thank you, windows
541 while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
545 my $uploadinfo = $cgi_object->uploadInfo($filehandle);
547 # Prefer the cached name first over CGI.pm stringification.
548 my $filename = $RT::Mason::CGI::Filename;
549 $filename = "$filehandle" unless defined($filename);
551 $filename =~ s#^.*[\\/]##;
555 Filename => Encode::decode_utf8($filename),
556 Type => $uploadinfo->{'Content-Type'},
564 $Message->make_singlepart();
565 RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
573 # {{{ sub ProcessSearchQuery
575 =head2 ProcessSearchQuery
577 Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand.
579 TODO Doc exactly what comes in the paramhash
584 sub ProcessSearchQuery {
587 ## TODO: The only parameter here is %ARGS. Maybe it would be
588 ## cleaner to load this parameter as $ARGS, and use $ARGS->{...}
589 ## instead of $args{ARGS}->{...} ? :)
591 #Searches are sticky.
592 if ( defined $session{'tickets'} ) {
594 # Reset the old search
595 $session{'tickets'}->GotoFirstItem;
600 $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} );
603 #Import a bookmarked search if we have one
604 if ( defined $args{ARGS}->{'Bookmark'} ) {
605 $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} );
608 # {{{ Goto next/prev page
609 if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) {
610 $session{'tickets'}->NextPage;
612 elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) {
613 $session{'tickets'}->PrevPage;
615 elsif ( $args{ARGS}->{'GotoPage'} > 0 ) {
616 $session{'tickets'}->GotoPage( $args{ARGS}->{GotoPage} - 1 );
621 # {{{ Deal with limiting the search
623 if ( $args{ARGS}->{'RefreshSearchInterval'} ) {
624 $session{'tickets_refresh_interval'} =
625 $args{ARGS}->{'RefreshSearchInterval'};
628 if ( $args{ARGS}->{'TicketsSortBy'} ) {
629 $session{'tickets_sort_by'} = $args{ARGS}->{'TicketsSortBy'};
630 $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'};
631 $session{'tickets'}->OrderBy(
632 FIELD => $args{ARGS}->{'TicketsSortBy'},
633 ORDER => $args{ARGS}->{'TicketsSortOrder'}
639 # {{{ Set the query limit
640 if ( defined $args{ARGS}->{'RowsPerPage'} ) {
642 "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" );
644 $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'};
645 $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} );
650 if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) {
651 $session{'tickets'}->LimitPriority(
652 VALUE => $args{ARGS}->{'ValueOfPriority'},
653 OPERATOR => $args{ARGS}->{'PriorityOp'}
659 if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) {
660 $session{'tickets'}->LimitOwner(
661 VALUE => $args{ARGS}->{'ValueOfOwner'},
662 OPERATOR => $args{ARGS}->{'OwnerOp'}
667 # {{{ Limit requestor email
668 if ( $args{ARGS}->{'ValueOfWatcherRole'} ne '' ) {
669 $session{'tickets'}->LimitWatcher(
670 TYPE => $args{ARGS}->{'WatcherRole'},
671 VALUE => $args{ARGS}->{'ValueOfWatcherRole'},
672 OPERATOR => $args{ARGS}->{'WatcherRoleOp'},
679 if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) {
680 $session{'tickets'}->LimitQueue(
681 VALUE => $args{ARGS}->{'ValueOfQueue'},
682 OPERATOR => $args{ARGS}->{'QueueOp'}
688 if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) {
689 if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) {
690 foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) {
691 $session{'tickets'}->LimitStatus(
693 OPERATOR => $args{ARGS}->{'StatusOp'},
698 $session{'tickets'}->LimitStatus(
699 VALUE => $args{ARGS}->{'ValueOfStatus'},
700 OPERATOR => $args{ARGS}->{'StatusOp'},
708 if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) {
709 my $val = $args{ARGS}->{'ValueOfSubject'};
710 if ($args{ARGS}->{'SubjectOp'} =~ /like/) {
713 $session{'tickets'}->LimitSubject(
715 OPERATOR => $args{ARGS}->{'SubjectOp'},
721 if ( $args{ARGS}->{'ValueOfDate'} ne '' ) {
722 my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} );
723 $args{ARGS}->{'DateType'} =~ s/_Date$//;
725 if ( $args{ARGS}->{'DateType'} eq 'Updated' ) {
726 $session{'tickets'}->LimitTransactionDate(
728 OPERATOR => $args{ARGS}->{'DateOp'},
732 $session{'tickets'}->LimitDate( FIELD => $args{ARGS}->{'DateType'},
734 OPERATOR => $args{ARGS}->{'DateOp'},
741 if ( $args{ARGS}->{'ValueOfAttachmentField'} ne '' ) {
742 my $val = $args{ARGS}->{'ValueOfAttachmentField'};
743 if ($args{ARGS}->{'AttachmentFieldOp'} =~ /like/) {
746 $session{'tickets'}->Limit(
747 FIELD => $args{ARGS}->{'AttachmentField'},
749 OPERATOR => $args{ARGS}->{'AttachmentFieldOp'},
755 # {{{ Limit CustomFields
757 foreach my $arg ( keys %{ $args{ARGS} } ) {
759 if ( $arg =~ /^CustomField(\d+)$/ ) {
765 next unless ( $args{ARGS}->{$arg} );
767 my $form = $args{ARGS}->{$arg};
768 my $oper = $args{ARGS}->{ "CustomFieldOp" . $id };
769 foreach my $value ( ref($form) ? @{$form} : ($form) ) {
771 if ($oper =~ /like/i) {
772 $value = "%".$value."%";
774 if ( $value =~ /^null$/i ) {
776 #Don't quote the string 'null'
779 # Convert the operator to something apropriate for nulls
780 $oper = 'IS' if ( $oper eq '=' );
781 $oper = 'IS NOT' if ( $oper eq '!=' );
783 $session{'tickets'}->LimitCustomField( CUSTOMFIELD => $id,
785 QUOTEVALUE => $quote,
797 # {{{ sub ParseDateToISO
799 =head2 ParseDateToISO
801 Takes a date in an arbitrary format.
802 Returns an ISO date and time in GMT
809 my $date_obj = RT::Date->new($session{'CurrentUser'});
814 return ( $date_obj->ISO );
820 # TODO: This might eventually read the cookies, user configuration
821 # information from the DB, queue configuration information from the
827 return $args->{$key} || $RT::WebOptions{$key};
832 # {{{ sub ProcessACLChanges
834 sub ProcessACLChanges {
837 my %ARGS = %$ARGSref;
839 my ( $ACL, @results );
842 foreach my $arg (keys %ARGS) {
843 if ($arg =~ /GrantRight-(\d+)-(.*?)-(\d+)$/) {
844 my $principal_id = $1;
845 my $object_type = $2;
847 my $rights = $ARGS{$arg};
849 my $principal = RT::Principal->new($session{'CurrentUser'});
850 $principal->Load($principal_id);
854 if ($object_type eq 'RT::System') {
856 } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
857 $obj = $object_type->new($session{'CurrentUser'});
858 $obj->Load($object_id);
860 push (@results, loc("System Error"). ': '.
861 loc("Rights could not be granted for [_1]", $object_type));
865 my @rights = ref($ARGS{$arg}) eq 'ARRAY' ? @{$ARGS{$arg}} : ($ARGS{$arg});
866 foreach my $right (@rights) {
867 next unless ($right);
868 my ($val, $msg) = $principal->GrantRight(Object => $obj, Right => $right);
869 push (@results, $msg);
872 elsif ($arg =~ /RevokeRight-(\d+)-(.*?)-(\d+)-(.*?)$/) {
873 my $principal_id = $1;
874 my $object_type = $2;
878 my $principal = RT::Principal->new($session{'CurrentUser'});
879 $principal->Load($principal_id);
880 next unless ($right);
883 if ($object_type eq 'RT::System') {
885 } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
886 $obj = $object_type->new($session{'CurrentUser'});
887 $obj->Load($object_id);
890 push (@results, loc("System Error"). ': '.
891 loc("Rights could not be revoked for [_1]", $object_type));
894 my ($val, $msg) = $principal->RevokeRight(Object => $obj, Right => $right);
895 push (@results, $msg);
907 # {{{ sub UpdateRecordObj
909 =head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs)
911 @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.
913 Returns an array of success/failure messages
917 sub UpdateRecordObject {
920 AttributesRef => undef,
922 AttributePrefix => undef,
928 my $object = $args{'Object'};
929 my $attributes = $args{'AttributesRef'};
930 my $ARGSRef = $args{'ARGSRef'};
931 foreach my $attribute (@$attributes) {
933 if ( defined $ARGSRef->{$attribute} ) {
934 $value = $ARGSRef->{$attribute};
937 defined( $args{'AttributePrefix'} )
939 $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }
942 $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute };
948 $value =~ s/\r\n/\n/gs;
950 if ($value ne $object->$attribute()){
952 my $method = "Set$attribute";
953 my ( $code, $msg ) = $object->$method($value);
955 push @results, loc($attribute) . ': ' . loc_fuzzy($msg);
957 "[_1] could not be set to [_2].", # loc
958 "That is already the current value", # loc
959 "No value sent to _Set!\n", # loc
960 "Illegal value for [_1]", # loc
961 "The new value has been set.", # loc
962 "No column specified", # loc
963 "Immutable field", # loc
964 "Nonexistant field?", # loc
965 "Invalid data", # loc
966 "Couldn't find row", # loc
967 "Missing a primary key?: [_1]", # loc
968 "Found Object", # loc
977 # {{{ Sub ProcessCustomFieldUpdates
979 sub ProcessCustomFieldUpdates {
981 CustomFieldObj => undef,
986 my $Object = $args{'CustomFieldObj'};
987 my $ARGSRef = $args{'ARGSRef'};
989 my @attribs = qw( Name Type Description Queue SortOrder);
990 my @results = UpdateRecordObject(
991 AttributesRef => \@attribs,
996 if ( $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" } ) {
998 my ( $addval, $addmsg ) = $Object->AddValue(
1000 $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" },
1001 Description => $ARGSRef->{ "CustomField-"
1003 . "-AddValue-Description" },
1004 SortOrder => $ARGSRef->{ "CustomField-"
1006 . "-AddValue-SortOrder" },
1008 push ( @results, $addmsg );
1010 my @delete_values = (
1011 ref $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } eq
1013 ? @{ $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } }
1014 : ( $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } );
1015 foreach my $id (@delete_values) {
1016 next unless defined $id;
1017 my ( $err, $msg ) = $Object->DeleteValue($id);
1018 push ( @results, $msg );
1021 my $vals = $Object->Values();
1022 while (my $cfv = $vals->Next()) {
1023 if (my $so = $ARGSRef->{ 'CustomField-' . $Object->Id . '-SortOrder' . $cfv->Id }) {
1024 if ($cfv->SortOrder != $so) {
1025 my ( $err, $msg ) = $cfv->SetSortOrder($so);
1026 push ( @results, $msg );
1036 # {{{ sub ProcessTicketBasics
1038 =head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1040 Returns an array of results messages.
1044 sub ProcessTicketBasics {
1052 my $TicketObj = $args{'TicketObj'};
1053 my $ARGSRef = $args{'ARGSRef'};
1055 # {{{ Set basic fields
1067 if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
1068 my $tempqueue = RT::Queue->new($RT::SystemUser);
1069 $tempqueue->Load( $ARGSRef->{'Queue'} );
1070 if ( $tempqueue->id ) {
1071 $ARGSRef->{'Queue'} = $tempqueue->Id();
1075 my @results = UpdateRecordObject(
1076 AttributesRef => \@attribs,
1077 Object => $TicketObj,
1081 # We special case owner changing, so we can use ForceOwnerChange
1082 if ( $ARGSRef->{'Owner'} && ( $TicketObj->Owner != $ARGSRef->{'Owner'} ) ) {
1084 if ( $ARGSRef->{'ForceOwnerChange'} ) {
1085 $ChownType = "Force";
1088 $ChownType = "Give";
1092 $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
1093 push ( @results, $msg );
1103 # {{{ Sub ProcessTicketCustomFieldUpdates
1105 sub ProcessTicketCustomFieldUpdates {
1113 my $ARGSRef = $args{'ARGSRef'};
1115 # Build up a list of tickets that we want to work with
1117 my %custom_fields_to_mod;
1118 foreach my $arg ( keys %{$ARGSRef} ) {
1119 if ( $arg =~ /^Ticket-(\d+)-CustomField-(\d+)-/ ) {
1121 # For each of those tickets, find out what custom fields we want to work with.
1122 $custom_fields_to_mod{$1}{$2} = 1;
1126 # For each of those tickets
1127 foreach my $tick ( keys %custom_fields_to_mod ) {
1128 my $Ticket = $args{'TicketObj'};
1129 if (!$Ticket or $Ticket->id != $tick) {
1130 $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
1131 $Ticket->Load($tick);
1134 # For each custom field
1135 foreach my $cf ( keys %{ $custom_fields_to_mod{$tick} } ) {
1137 my $CustomFieldObj = RT::CustomField->new($session{'CurrentUser'});
1138 $CustomFieldObj->LoadById($cf);
1140 foreach my $arg ( keys %{$ARGSRef} ) {
1141 # since http won't pass in a form element with a null value, we need
1143 if ($arg =~ /^(.*?)-Values-Magic$/ ) {
1144 # We don't care about the magic, if there's really a values element;
1145 next if (exists $ARGSRef->{$1.'-Values'}) ;
1147 $arg = $1."-Values";
1148 $ARGSRef->{$1."-Values"} = undef;
1151 next unless ( $arg =~ /^Ticket-$tick-CustomField-$cf-/ );
1153 ( ref( $ARGSRef->{$arg} ) eq 'ARRAY' )
1154 ? @{ $ARGSRef->{$arg} }
1155 : split /\n/, $ARGSRef->{$arg} ;
1156 if ( ( $arg =~ /-AddValue$/ ) || ( $arg =~ /-Value$/ ) ) {
1157 foreach my $value (@values) {
1158 next unless length($value);
1159 my ( $val, $msg ) = $Ticket->AddCustomFieldValue(
1163 push ( @results, $msg );
1166 elsif ( $arg =~ /-DeleteValues$/ ) {
1167 foreach my $value (@values) {
1168 next unless length($value);
1169 my ( $val, $msg ) = $Ticket->DeleteCustomFieldValue(
1173 push ( @results, $msg );
1176 elsif ( $arg =~ /-Values$/ and $CustomFieldObj->Type !~ /Entry/) {
1177 my $cf_values = $Ticket->CustomFieldValues($cf);
1180 foreach my $value (@values) {
1181 next unless length($value);
1183 # build up a hash of values that the new set has
1184 $values_hash{$value} = 1;
1186 unless ( $cf_values->HasEntry($value) ) {
1187 my ( $val, $msg ) = $Ticket->AddCustomFieldValue(
1191 push ( @results, $msg );
1195 while ( my $cf_value = $cf_values->Next ) {
1196 unless ( $values_hash{ $cf_value->Content } == 1 ) {
1197 my ( $val, $msg ) = $Ticket->DeleteCustomFieldValue(
1199 Value => $cf_value->Content
1201 push ( @results, $msg);
1207 elsif ( $arg =~ /-Values$/ ) {
1208 my $cf_values = $Ticket->CustomFieldValues($cf);
1210 # keep everything up to the point of difference, delete the rest
1212 foreach my $old_cf (@{$cf_values->ItemsArrayRef}) {
1213 if (!$delete_flag and @values and $old_cf->Content eq $values[0]) {
1222 # now add/replace extra things, if any
1223 foreach my $value (@values) {
1224 my ( $val, $msg ) = $Ticket->AddCustomFieldValue(
1228 push ( @results, $msg );
1232 push ( @results, "User asked for an unknown update type for custom field " . $cf->Name . " for ticket " . $Ticket->id );
1242 # {{{ sub ProcessTicketWatchers
1244 =head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1246 Returns an array of results messages.
1250 sub ProcessTicketWatchers {
1258 my $Ticket = $args{'TicketObj'};
1259 my $ARGSRef = $args{'ARGSRef'};
1261 # {{{ Munge watchers
1263 foreach my $key ( keys %$ARGSRef ) {
1265 # {{{ Delete deletable watchers
1266 if ( ( $key =~ /^Ticket-DelWatcher-Type-(.*)-Principal-(\d+)$/ ) ) {
1267 my ( $code, $msg ) =
1268 $Ticket->DeleteWatcher(PrincipalId => $2,
1270 push @results, $msg;
1273 # Delete watchers in the simple style demanded by the bulk manipulator
1274 elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) {
1275 my ( $code, $msg ) = $Ticket->DeleteWatcher( Type => $ARGSRef->{$key}, PrincipalId => $1 );
1276 push @results, $msg;
1281 # Add new wathchers by email address
1282 elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
1283 and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) )
1286 #They're in this order because otherwise $1 gets clobbered :/
1287 my ( $code, $msg ) = $Ticket->AddWatcher(
1288 Type => $ARGSRef->{$key},
1289 Email => $ARGSRef->{ "WatcherAddressEmail" . $1 }
1291 push @results, $msg;
1294 #Add requestors in the simple style demanded by the bulk manipulator
1295 elsif ( $key =~ /^Add(Requestor|Cc|AdminCc)$/ ) {
1296 my ( $code, $msg ) = $Ticket->AddWatcher(
1298 Email => $ARGSRef->{$key}
1300 push @results, $msg;
1303 # Add new watchers by owner
1304 elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
1305 and ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) ) {
1307 #They're in this order because otherwise $1 gets clobbered :/
1308 my ( $code, $msg ) =
1309 $Ticket->AddWatcher( Type => $ARGSRef->{$key}, PrincipalId => $1 );
1310 push @results, $msg;
1321 # {{{ sub ProcessTicketDates
1323 =head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1325 Returns an array of results messages.
1329 sub ProcessTicketDates {
1336 my $Ticket = $args{'TicketObj'};
1337 my $ARGSRef = $args{'ARGSRef'};
1341 # {{{ Set date fields
1342 my @date_fields = qw(
1350 #Run through each field in this list. update the value if apropriate
1351 foreach my $field (@date_fields) {
1354 my $DateObj = RT::Date->new( $session{'CurrentUser'} );
1356 #If it's something other than just whitespace
1357 if ( $ARGSRef->{ $field . '_Date' } ne '' ) {
1359 Format => 'unknown',
1360 Value => $ARGSRef->{ $field . '_Date' }
1362 my $obj = $field . "Obj";
1363 if ( ( defined $DateObj->Unix )
1364 and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) )
1366 my $method = "Set$field";
1367 my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
1368 push @results, "$msg";
1379 # {{{ sub ProcessTicketLinks
1381 =head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1383 Returns an array of results messages.
1387 sub ProcessTicketLinks {
1388 my %args = ( TicketObj => undef,
1392 my $Ticket = $args{'TicketObj'};
1393 my $ARGSRef = $args{'ARGSRef'};
1397 # Delete links that are gone gone gone.
1398 foreach my $arg ( keys %$ARGSRef ) {
1399 if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
1405 "Trying to delete: Base: $base Target: $target Type $type";
1406 my ( $val, $msg ) = $Ticket->DeleteLink( Base => $base,
1408 Target => $target );
1410 push @results, $msg;
1416 my @linktypes = qw( DependsOn MemberOf RefersTo );
1418 foreach my $linktype (@linktypes) {
1419 if ( $ARGSRef->{ $Ticket->Id . "-$linktype" } ) {
1420 for my $luri ( split ( / /, $ARGSRef->{ $Ticket->Id . "-$linktype" } ) ) {
1421 $luri =~ s/\s*$//; # Strip trailing whitespace
1422 my ( $val, $msg ) = $Ticket->AddLink( Target => $luri,
1423 Type => $linktype );
1424 push @results, $msg;
1427 if ( $ARGSRef->{ "$linktype-" . $Ticket->Id } ) {
1429 for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Ticket->Id } ) ) {
1430 my ( $val, $msg ) = $Ticket->AddLink( Base => $luri,
1431 Type => $linktype );
1433 push @results, $msg;
1438 #Merge if we need to
1439 if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
1441 $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
1442 push @results, $msg;
1450 eval "require RT::Interface::Web_Vendor";
1451 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm});
1452 eval "require RT::Interface::Web_Local";
1453 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm});