1 # {{{ BEGIN BPS TAGGED BLOCK
5 # This software is Copyright (c) 1996-2004 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.
90 Encode::_utf8_on($$ref);
97 # {{{ WebCanonicalizeInfo
99 =head2 WebCanonicalizeInfo();
101 Different web servers set different environmental varibles. This
102 function must return something suitable for REMOTE_USER. By default,
103 just downcase $ENV{'REMOTE_USER'}
107 sub WebCanonicalizeInfo {
110 if ( defined $ENV{'REMOTE_USER'} ) {
111 $user = lc ( $ENV{'REMOTE_USER'} ) if( length($ENV{'REMOTE_USER'}) );
119 # {{{ WebExternalAutoInfo
121 =head2 WebExternalAutoInfo($user);
123 Returns a hash of user attributes, used when WebExternalAuto is set.
127 sub WebExternalAutoInfo {
132 $user_info{'Privileged'} = 1;
134 if ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
135 # Populate fields with information from Unix /etc/passwd
137 my ($comments, $realname) = (getpwnam($user))[5, 6];
138 $user_info{'Comments'} = $comments if defined $comments;
139 $user_info{'RealName'} = $realname if defined $realname;
141 elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
142 # Populate fields with information from NT domain controller
145 # and return the wad of stuff
152 package HTML::Mason::Commands;
154 use vars qw/$r $m %session/;
161 loc is a nice clean global routine which calls $session{'CurrentUser'}->loc()
162 with whatever it's called with. If there is no $session{'CurrentUser'},
163 it creates a temporary user, so we have something to get a localisation handle
170 if ($session{'CurrentUser'} &&
171 UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
172 return($session{'CurrentUser'}->loc(@_));
174 elsif ( my $u = eval { RT::CurrentUser->new($RT::SystemUser->Id) } ) {
175 return ($u->loc(@_));
178 # pathetic case -- SystemUser is gone.
188 =head2 loc_fuzzy STRING
190 loc_fuzzy is for handling localizations of messages that may already
191 contain interpolated variables, typically returned from libraries
192 outside RT's control. It takes the message string and extracts the
193 variable array automatically by matching against the candidate entries
194 inside the lexicon file.
201 if ($session{'CurrentUser'} &&
202 UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
203 return($session{'CurrentUser'}->loc_fuzzy($msg));
206 my $u = RT::CurrentUser->new($RT::SystemUser->Id);
207 return ($u->loc_fuzzy($msg));
215 # Error - calls Error and aborts
218 if ($session{'ErrorDocument'} &&
219 $session{'ErrorDocumentType'}) {
220 $r->content_type($session{'ErrorDocumentType'});
221 $m->comp($session{'ErrorDocument'} , Why => shift);
225 $m->comp("/Elements/Error" , Why => shift);
232 # {{{ sub CreateTicket
234 =head2 CreateTicket ARGS
236 Create a new ticket, using Mason's %ARGS. returns @results.
245 my $Ticket = new RT::Ticket( $session{'CurrentUser'} );
247 my $Queue = new RT::Queue( $session{'CurrentUser'} );
248 unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
249 Abort('Queue not found');
252 unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
253 Abort('You have no permission to create tickets in that queue.');
256 my $due = new RT::Date( $session{'CurrentUser'} );
257 $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
258 my $starts = new RT::Date( $session{'CurrentUser'} );
259 $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
261 my @Requestors = split ( /\s*,\s*/, $ARGS{'Requestors'} );
262 my @Cc = split ( /\s*,\s*/, $ARGS{'Cc'} );
263 my @AdminCc = split ( /\s*,\s*/, $ARGS{'AdminCc'} );
265 my $MIMEObj = MakeMIMEEntity(
266 Subject => $ARGS{'Subject'},
267 From => $ARGS{'From'},
269 Body => $ARGS{'Content'},
272 if ($ARGS{'Attachments'}) {
273 $MIMEObj->make_multipart;
274 $MIMEObj->add_part($_) foreach values %{$ARGS{'Attachments'}};
278 Type => $ARGS{'Type'} || 'ticket',
279 Queue => $ARGS{'Queue'},
280 Owner => $ARGS{'Owner'},
281 InitialPriority => $ARGS{'InitialPriority'},
282 FinalPriority => $ARGS{'FinalPriority'},
283 TimeLeft => $ARGS{'TimeLeft'},
284 TimeEstimated => $ARGS{'TimeEstimated'},
285 TimeWorked => $ARGS{'TimeWorked'},
286 Requestor => \@Requestors,
288 AdminCc => \@AdminCc,
289 Subject => $ARGS{'Subject'},
290 Status => $ARGS{'Status'},
292 Starts => $starts->ISO,
295 foreach my $arg (%ARGS) {
296 if ($arg =~ /^CustomField-(\d+)(.*?)$/) {
297 next if ($arg =~ /-Magic$/);
298 $create_args{"CustomField-".$1} = $ARGS{"$arg"};
302 # turn new link lists into arrays, and pass in the proper arguments
303 my (@dependson, @dependedonby,
305 @refersto, @referredtoby);
307 foreach my $luri ( split ( / /, $ARGS{"new-DependsOn"} ) ) {
308 $luri =~ s/\s*$//; # Strip trailing whitespace
309 push @dependson, $luri;
311 $create_args{'DependsOn'} = \@dependson;
313 foreach my $luri ( split ( / /, $ARGS{"DependsOn-new"} ) ) {
314 push @dependedonby, $luri;
316 $create_args{'DependedOnBy'} = \@dependedonby;
318 foreach my $luri ( split ( / /, $ARGS{"new-MemberOf"} ) ) {
319 $luri =~ s/\s*$//; # Strip trailing whitespace
320 push @parents, $luri;
322 $create_args{'Parents'} = \@parents;
324 foreach my $luri ( split ( / /, $ARGS{"MemberOf-new"} ) ) {
325 push @children, $luri;
327 $create_args{'Children'} = \@children;
329 foreach my $luri ( split ( / /, $ARGS{"new-RefersTo"} ) ) {
330 $luri =~ s/\s*$//; # Strip trailing whitespace
331 push @refersto, $luri;
333 $create_args{'RefersTo'} = \@refersto;
335 foreach my $luri ( split ( / /, $ARGS{"RefersTo-new"} ) ) {
336 push @referredtoby, $luri;
338 $create_args{'ReferredToBy'} = \@referredtoby;
340 my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
341 unless ( $id && $Trans ) {
345 push ( @Actions, split("\n", $ErrMsg) );
346 unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
347 Abort( "No permission to view newly created ticket #"
348 . $Ticket->id . "." );
350 return ( $Ticket, @Actions );
356 # {{{ sub LoadTicket - loads a ticket
360 Takes a ticket id as its only variable. if it's handed an array, it takes
363 Returns an RT::Ticket object as the current user.
370 if ( ref($id) eq "ARRAY" ) {
375 Abort("No ticket specified");
378 my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
380 unless ( $Ticket->id ) {
381 Abort("Could not load ticket $id");
388 # {{{ sub ProcessUpdateMessage
390 sub ProcessUpdateMessage {
392 #TODO document what else this takes.
400 #Make the update content have no 'weird' newlines in it
401 if ( $args{ARGSRef}->{'UpdateTimeWorked'} ||
402 $args{ARGSRef}->{'UpdateContent'} ||
403 $args{ARGSRef}->{'UpdateAttachments'}) {
406 $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() )
408 $args{ARGSRef}->{'UpdateSubject'} = undef;
411 my $Message = MakeMIMEEntity(
412 Subject => $args{ARGSRef}->{'UpdateSubject'},
413 Body => $args{ARGSRef}->{'UpdateContent'},
416 if ($args{ARGSRef}->{'UpdateAttachments'}) {
417 $Message->make_multipart;
418 $Message->add_part($_) foreach values %{$args{ARGSRef}->{'UpdateAttachments'}};
421 ## TODO: Implement public comments
422 if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
423 my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(
424 CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
425 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
427 TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
429 push ( @{ $args{Actions} }, $Description );
431 elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
432 my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Correspond(
433 CcMessageTo => $args{ARGSRef}->{'UpdateCc'},
434 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
436 TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
438 push ( @{ $args{Actions} }, $Description );
441 push ( @{ $args{'Actions'} },
442 loc("Update type was neither correspondence nor comment.").
444 loc("Update not recorded.")
452 # {{{ sub MakeMIMEEntity
454 =head2 MakeMIMEEntity PARAMHASH
456 Takes a paramhash Subject, Body and AttachmentFieldName.
458 Returns a MIME::Entity.
464 #TODO document what else this takes.
470 AttachmentFieldName => undef,
471 # map Encode::encode_utf8($_), @_,
475 #Make the update content have no 'weird' newlines in it
477 $args{'Body'} =~ s/\r\n/\n/gs;
480 # MIME::Head is not happy in utf-8 domain. This only happens
481 # when processing an incoming email (so far observed).
484 $Message = MIME::Entity->build(
485 Subject => $args{'Subject'} || "",
486 From => $args{'From'},
489 Data => [ $args{'Body'} ]
493 my $cgi_object = $m->cgi_object;
495 if (my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) {
499 use File::Temp qw(tempfile tempdir);
501 #foreach my $filehandle (@filenames) {
503 my ( $fh, $temp_file );
505 # on NFS and NTFS, it is possible that tempfile() conflicts
506 # with other processes, causing a race condition. we try to
507 # accommodate this by pausing and retrying.
508 last if ($fh, $temp_file) = eval { tempfile( UNLINK => 1) };
512 binmode $fh; #thank you, windows
514 while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
518 my $uploadinfo = $cgi_object->uploadInfo($filehandle);
520 # Prefer the cached name first over CGI.pm stringification.
521 my $filename = $RT::Mason::CGI::Filename;
522 $filename = "$filehandle" unless defined($filename);
524 $filename =~ s#^.*[\\/]##;
528 Filename => Encode::decode_utf8($filename),
529 Type => $uploadinfo->{'Content-Type'},
537 $Message->make_singlepart();
538 RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
546 # {{{ sub ProcessSearchQuery
548 =head2 ProcessSearchQuery
550 Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand.
552 TODO Doc exactly what comes in the paramhash
557 sub ProcessSearchQuery {
560 ## TODO: The only parameter here is %ARGS. Maybe it would be
561 ## cleaner to load this parameter as $ARGS, and use $ARGS->{...}
562 ## instead of $args{ARGS}->{...} ? :)
564 #Searches are sticky.
565 if ( defined $session{'tickets'} ) {
567 # Reset the old search
568 $session{'tickets'}->GotoFirstItem;
573 $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} );
576 #Import a bookmarked search if we have one
577 if ( defined $args{ARGS}->{'Bookmark'} ) {
578 $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} );
581 # {{{ Goto next/prev page
582 if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) {
583 $session{'tickets'}->NextPage;
585 elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) {
586 $session{'tickets'}->PrevPage;
588 elsif ( $args{ARGS}->{'GotoPage'} > 0 ) {
589 $session{'tickets'}->GotoPage( $args{ARGS}->{GotoPage} - 1 );
594 # {{{ Deal with limiting the search
596 if ( $args{ARGS}->{'RefreshSearchInterval'} ) {
597 $session{'tickets_refresh_interval'} =
598 $args{ARGS}->{'RefreshSearchInterval'};
601 if ( $args{ARGS}->{'TicketsSortBy'} ) {
602 $session{'tickets_sort_by'} = $args{ARGS}->{'TicketsSortBy'};
603 $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'};
604 $session{'tickets'}->OrderBy(
605 FIELD => $args{ARGS}->{'TicketsSortBy'},
606 ORDER => $args{ARGS}->{'TicketsSortOrder'}
612 # {{{ Set the query limit
613 if ( defined $args{ARGS}->{'RowsPerPage'} ) {
615 "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" );
617 $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'};
618 $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} );
623 if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) {
624 $session{'tickets'}->LimitPriority(
625 VALUE => $args{ARGS}->{'ValueOfPriority'},
626 OPERATOR => $args{ARGS}->{'PriorityOp'}
632 if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) {
633 $session{'tickets'}->LimitOwner(
634 VALUE => $args{ARGS}->{'ValueOfOwner'},
635 OPERATOR => $args{ARGS}->{'OwnerOp'}
640 # {{{ Limit requestor email
641 if ( $args{ARGS}->{'ValueOfWatcherRole'} ne '' ) {
642 $session{'tickets'}->LimitWatcher(
643 TYPE => $args{ARGS}->{'WatcherRole'},
644 VALUE => $args{ARGS}->{'ValueOfWatcherRole'},
645 OPERATOR => $args{ARGS}->{'WatcherRoleOp'},
652 if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) {
653 $session{'tickets'}->LimitQueue(
654 VALUE => $args{ARGS}->{'ValueOfQueue'},
655 OPERATOR => $args{ARGS}->{'QueueOp'}
661 if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) {
662 if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) {
663 foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) {
664 $session{'tickets'}->LimitStatus(
666 OPERATOR => $args{ARGS}->{'StatusOp'},
671 $session{'tickets'}->LimitStatus(
672 VALUE => $args{ARGS}->{'ValueOfStatus'},
673 OPERATOR => $args{ARGS}->{'StatusOp'},
681 if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) {
682 my $val = $args{ARGS}->{'ValueOfSubject'};
683 if ($args{ARGS}->{'SubjectOp'} =~ /like/) {
686 $session{'tickets'}->LimitSubject(
688 OPERATOR => $args{ARGS}->{'SubjectOp'},
694 if ( $args{ARGS}->{'ValueOfDate'} ne '' ) {
695 my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} );
696 $args{ARGS}->{'DateType'} =~ s/_Date$//;
698 if ( $args{ARGS}->{'DateType'} eq 'Updated' ) {
699 $session{'tickets'}->LimitTransactionDate(
701 OPERATOR => $args{ARGS}->{'DateOp'},
705 $session{'tickets'}->LimitDate( FIELD => $args{ARGS}->{'DateType'},
707 OPERATOR => $args{ARGS}->{'DateOp'},
714 if ( $args{ARGS}->{'ValueOfAttachmentField'} ne '' ) {
715 my $val = $args{ARGS}->{'ValueOfAttachmentField'};
716 if ($args{ARGS}->{'AttachmentFieldOp'} =~ /like/) {
719 $session{'tickets'}->Limit(
720 FIELD => $args{ARGS}->{'AttachmentField'},
722 OPERATOR => $args{ARGS}->{'AttachmentFieldOp'},
728 # {{{ Limit CustomFields
730 foreach my $arg ( keys %{ $args{ARGS} } ) {
732 if ( $arg =~ /^CustomField(\d+)$/ ) {
738 next unless ( $args{ARGS}->{$arg} );
740 my $form = $args{ARGS}->{$arg};
741 my $oper = $args{ARGS}->{ "CustomFieldOp" . $id };
742 foreach my $value ( ref($form) ? @{$form} : ($form) ) {
744 if ($oper =~ /like/i) {
745 $value = "%".$value."%";
747 if ( $value =~ /^null$/i ) {
749 #Don't quote the string 'null'
752 # Convert the operator to something apropriate for nulls
753 $oper = 'IS' if ( $oper eq '=' );
754 $oper = 'IS NOT' if ( $oper eq '!=' );
756 $session{'tickets'}->LimitCustomField( CUSTOMFIELD => $id,
758 QUOTEVALUE => $quote,
770 # {{{ sub ParseDateToISO
772 =head2 ParseDateToISO
774 Takes a date in an arbitrary format.
775 Returns an ISO date and time in GMT
782 my $date_obj = RT::Date->new($session{'CurrentUser'});
787 return ( $date_obj->ISO );
793 # TODO: This might eventually read the cookies, user configuration
794 # information from the DB, queue configuration information from the
800 return $args->{$key} || $RT::WebOptions{$key};
805 # {{{ sub ProcessACLChanges
807 sub ProcessACLChanges {
810 my %ARGS = %$ARGSref;
812 my ( $ACL, @results );
815 foreach my $arg (keys %ARGS) {
816 if ($arg =~ /GrantRight-(\d+)-(.*?)-(\d+)$/) {
817 my $principal_id = $1;
818 my $object_type = $2;
820 my $rights = $ARGS{$arg};
822 my $principal = RT::Principal->new($session{'CurrentUser'});
823 $principal->Load($principal_id);
827 if ($object_type eq 'RT::System') {
829 } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
830 $obj = $object_type->new($session{'CurrentUser'});
831 $obj->Load($object_id);
833 push (@results, loc("System Error"). ': '.
834 loc("Rights could not be granted for [_1]", $object_type));
838 my @rights = ref($ARGS{$arg}) eq 'ARRAY' ? @{$ARGS{$arg}} : ($ARGS{$arg});
839 foreach my $right (@rights) {
840 next unless ($right);
841 my ($val, $msg) = $principal->GrantRight(Object => $obj, Right => $right);
842 push (@results, $msg);
845 elsif ($arg =~ /RevokeRight-(\d+)-(.*?)-(\d+)-(.*?)$/) {
846 my $principal_id = $1;
847 my $object_type = $2;
851 my $principal = RT::Principal->new($session{'CurrentUser'});
852 $principal->Load($principal_id);
853 next unless ($right);
856 if ($object_type eq 'RT::System') {
858 } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
859 $obj = $object_type->new($session{'CurrentUser'});
860 $obj->Load($object_id);
863 push (@results, loc("System Error"). ': '.
864 loc("Rights could not be revoked for [_1]", $object_type));
867 my ($val, $msg) = $principal->RevokeRight(Object => $obj, Right => $right);
868 push (@results, $msg);
880 # {{{ sub UpdateRecordObj
882 =head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs)
884 @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.
886 Returns an array of success/failure messages
890 sub UpdateRecordObject {
893 AttributesRef => undef,
895 AttributePrefix => undef,
899 my $Object = $args{'Object'};
900 my @results = $Object->Update(AttributesRef => $args{'AttributesRef'},
901 ARGSRef => $args{'ARGSRef'},
902 AttributePrefix => $args{'AttributePrefix'}
910 # {{{ Sub ProcessCustomFieldUpdates
912 sub ProcessCustomFieldUpdates {
914 CustomFieldObj => undef,
919 my $Object = $args{'CustomFieldObj'};
920 my $ARGSRef = $args{'ARGSRef'};
922 my @attribs = qw( Name Type Description Queue SortOrder);
923 my @results = UpdateRecordObject(
924 AttributesRef => \@attribs,
929 if ( $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" } ) {
931 my ( $addval, $addmsg ) = $Object->AddValue(
933 $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" },
934 Description => $ARGSRef->{ "CustomField-"
936 . "-AddValue-Description" },
937 SortOrder => $ARGSRef->{ "CustomField-"
939 . "-AddValue-SortOrder" },
941 push ( @results, $addmsg );
943 my @delete_values = (
944 ref $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } eq
946 ? @{ $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } }
947 : ( $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } );
948 foreach my $id (@delete_values) {
949 next unless defined $id;
950 my ( $err, $msg ) = $Object->DeleteValue($id);
951 push ( @results, $msg );
954 my $vals = $Object->Values();
955 while (my $cfv = $vals->Next()) {
956 if (my $so = $ARGSRef->{ 'CustomField-' . $Object->Id . '-SortOrder' . $cfv->Id }) {
957 if ($cfv->SortOrder != $so) {
958 my ( $err, $msg ) = $cfv->SetSortOrder($so);
959 push ( @results, $msg );
969 # {{{ sub ProcessTicketBasics
971 =head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS );
973 Returns an array of results messages.
977 sub ProcessTicketBasics {
985 my $TicketObj = $args{'TicketObj'};
986 my $ARGSRef = $args{'ARGSRef'};
988 # {{{ Set basic fields
1001 if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
1002 my $tempqueue = RT::Queue->new($RT::SystemUser);
1003 $tempqueue->Load( $ARGSRef->{'Queue'} );
1004 if ( $tempqueue->id ) {
1005 $ARGSRef->{'Queue'} = $tempqueue->Id();
1009 $ARGSRef->{'Status'} ||= $TicketObj->Status;
1011 my @results = UpdateRecordObject(
1012 AttributesRef => \@attribs,
1013 Object => $TicketObj,
1017 # We special case owner changing, so we can use ForceOwnerChange
1018 if ( $ARGSRef->{'Owner'} && ( $TicketObj->Owner != $ARGSRef->{'Owner'} ) ) {
1020 if ( $ARGSRef->{'ForceOwnerChange'} ) {
1021 $ChownType = "Force";
1024 $ChownType = "Give";
1028 $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
1029 push ( @results, $msg );
1039 # {{{ Sub ProcessTicketCustomFieldUpdates
1041 sub ProcessTicketCustomFieldUpdates {
1049 my $ARGSRef = $args{'ARGSRef'};
1051 # Build up a list of tickets that we want to work with
1053 my %custom_fields_to_mod;
1054 foreach my $arg ( keys %{$ARGSRef} ) {
1055 if ( $arg =~ /^Ticket-(\d+)-CustomField-(\d+)-/ ) {
1057 # For each of those tickets, find out what custom fields we want to work with.
1058 $custom_fields_to_mod{$1}{$2} = 1;
1062 # For each of those tickets
1063 foreach my $tick ( keys %custom_fields_to_mod ) {
1064 my $Ticket = $args{'TicketObj'};
1065 if (!$Ticket or $Ticket->id != $tick) {
1066 $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
1067 $Ticket->Load($tick);
1070 # For each custom field
1071 foreach my $cf ( keys %{ $custom_fields_to_mod{$tick} } ) {
1073 my $CustomFieldObj = RT::CustomField->new($session{'CurrentUser'});
1074 $CustomFieldObj->LoadById($cf);
1076 foreach my $arg ( keys %{$ARGSRef} ) {
1077 # since http won't pass in a form element with a null value, we need
1079 if ($arg =~ /^(.*?)-Values-Magic$/ ) {
1080 # We don't care about the magic, if there's really a values element;
1081 next if (exists $ARGSRef->{$1.'-Values'}) ;
1083 $arg = $1."-Values";
1084 $ARGSRef->{$1."-Values"} = undef;
1087 next unless ( $arg =~ /^Ticket-$tick-CustomField-$cf-/ );
1089 ( ref( $ARGSRef->{$arg} ) eq 'ARRAY' )
1090 ? @{ $ARGSRef->{$arg} }
1091 : split /\n/, $ARGSRef->{$arg} ;
1093 #for poor windows boxen that pass in "\r\n"
1097 if ( ( $arg =~ /-AddValue$/ ) || ( $arg =~ /-Value$/ ) ) {
1098 foreach my $value (@values) {
1099 next unless length($value);
1100 my ( $val, $msg ) = $Ticket->AddCustomFieldValue(
1104 push ( @results, $msg );
1107 elsif ( $arg =~ /-DeleteValues$/ ) {
1108 foreach my $value (@values) {
1109 next unless length($value);
1110 my ( $val, $msg ) = $Ticket->DeleteCustomFieldValue(
1114 push ( @results, $msg );
1117 elsif ( $arg =~ /-Values$/ and $CustomFieldObj->Type !~ /Entry/) {
1118 my $cf_values = $Ticket->CustomFieldValues($cf);
1121 foreach my $value (@values) {
1122 next unless length($value);
1124 # build up a hash of values that the new set has
1125 $values_hash{$value} = 1;
1127 unless ( $cf_values->HasEntry($value) ) {
1128 my ( $val, $msg ) = $Ticket->AddCustomFieldValue(
1132 push ( @results, $msg );
1136 while ( my $cf_value = $cf_values->Next ) {
1137 unless ( $values_hash{ $cf_value->Content } == 1 ) {
1138 my ( $val, $msg ) = $Ticket->DeleteCustomFieldValue(
1140 Value => $cf_value->Content
1142 push ( @results, $msg);
1148 elsif ( $arg =~ /-Values$/ ) {
1149 my $cf_values = $Ticket->CustomFieldValues($cf);
1151 # keep everything up to the point of difference, delete the rest
1153 foreach my $old_cf (@{$cf_values->ItemsArrayRef}) {
1154 if (!$delete_flag and @values and $old_cf->Content eq $values[0]) {
1163 # now add/replace extra things, if any
1164 foreach my $value (@values) {
1165 my ( $val, $msg ) = $Ticket->AddCustomFieldValue(
1169 push ( @results, $msg );
1173 push ( @results, "User asked for an unknown update type for custom field " . $cf->Name . " for ticket " . $Ticket->id );
1183 # {{{ sub ProcessTicketWatchers
1185 =head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1187 Returns an array of results messages.
1191 sub ProcessTicketWatchers {
1199 my $Ticket = $args{'TicketObj'};
1200 my $ARGSRef = $args{'ARGSRef'};
1202 # {{{ Munge watchers
1204 foreach my $key ( keys %$ARGSRef ) {
1206 # {{{ Delete deletable watchers
1207 if ( ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) ) {
1208 my ( $code, $msg ) =
1209 $Ticket->DeleteWatcher(PrincipalId => $2,
1211 push @results, $msg;
1214 # Delete watchers in the simple style demanded by the bulk manipulator
1215 elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) {
1216 my ( $code, $msg ) = $Ticket->DeleteWatcher( Email => $ARGSRef->{$key}, Type => $1 );
1217 push @results, $msg;
1222 # Add new wathchers by email address
1223 elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
1224 and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) )
1227 #They're in this order because otherwise $1 gets clobbered :/
1228 my ( $code, $msg ) = $Ticket->AddWatcher(
1229 Type => $ARGSRef->{$key},
1230 Email => $ARGSRef->{ "WatcherAddressEmail" . $1 }
1232 push @results, $msg;
1235 #Add requestors in the simple style demanded by the bulk manipulator
1236 elsif ( $key =~ /^Add(Requestor|Cc|AdminCc)$/ ) {
1237 my ( $code, $msg ) = $Ticket->AddWatcher(
1239 Email => $ARGSRef->{$key}
1241 push @results, $msg;
1244 # Add new watchers by owner
1245 elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
1246 and ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) ) {
1248 #They're in this order because otherwise $1 gets clobbered :/
1249 my ( $code, $msg ) =
1250 $Ticket->AddWatcher( Type => $ARGSRef->{$key}, PrincipalId => $1 );
1251 push @results, $msg;
1262 # {{{ sub ProcessTicketDates
1264 =head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1266 Returns an array of results messages.
1270 sub ProcessTicketDates {
1277 my $Ticket = $args{'TicketObj'};
1278 my $ARGSRef = $args{'ARGSRef'};
1282 # {{{ Set date fields
1283 my @date_fields = qw(
1291 #Run through each field in this list. update the value if apropriate
1292 foreach my $field (@date_fields) {
1295 my $DateObj = RT::Date->new( $session{'CurrentUser'} );
1297 #If it's something other than just whitespace
1298 if ( $ARGSRef->{ $field . '_Date' } ne '' ) {
1300 Format => 'unknown',
1301 Value => $ARGSRef->{ $field . '_Date' }
1303 my $obj = $field . "Obj";
1304 if ( ( defined $DateObj->Unix )
1305 and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) )
1307 my $method = "Set$field";
1308 my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
1309 push @results, "$msg";
1320 # {{{ sub ProcessTicketLinks
1322 =head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1324 Returns an array of results messages.
1328 sub ProcessTicketLinks {
1329 my %args = ( TicketObj => undef,
1333 my $Ticket = $args{'TicketObj'};
1334 my $ARGSRef = $args{'ARGSRef'};
1336 my (@results) = ProcessRecordLinks(RecordObj => $Ticket,
1337 ARGSRef => $ARGSRef);
1339 #Merge if we need to
1340 if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
1342 $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
1343 push @results, $msg;
1351 sub ProcessRecordLinks {
1352 my %args = ( RecordObj => undef,
1356 my $Record = $args{'RecordObj'};
1357 my $ARGSRef = $args{'ARGSRef'};
1361 # Delete links that are gone gone gone.
1362 foreach my $arg ( keys %$ARGSRef ) {
1363 if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
1369 "Trying to delete: Base: $base Target: $target Type $type";
1370 my ( $val, $msg ) = $Record->DeleteLink( Base => $base,
1372 Target => $target );
1374 push @results, $msg;
1380 my @linktypes = qw( DependsOn MemberOf RefersTo );
1382 foreach my $linktype (@linktypes) {
1383 if ( $ARGSRef->{ $Record->Id . "-$linktype" } ) {
1384 for my $luri ( split ( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) {
1385 $luri =~ s/\s*$//; # Strip trailing whitespace
1386 my ( $val, $msg ) = $Record->AddLink( Target => $luri,
1387 Type => $linktype );
1388 push @results, $msg;
1391 if ( $ARGSRef->{ "$linktype-" . $Record->Id } ) {
1393 for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) {
1394 my ( $val, $msg ) = $Record->AddLink( Base => $luri,
1395 Type => $linktype );
1397 push @results, $msg;
1405 eval "require RT::Interface::Web_Vendor";
1406 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm});
1407 eval "require RT::Interface::Web_Local";
1408 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm});