import of rt 3.0.9
[freeside.git] / rt / lib / RT / Interface / Web.pm
1 # BEGIN LICENSE BLOCK
2
3 # Copyright (c) 1996-2003 Jesse Vincent <jesse@bestpractical.com>
4
5 # (Except where explictly superceded by other copyright notices)
6
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
10 # from www.gnu.org.
11
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.
16
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.
21
22
23 # END LICENSE BLOCK
24 ## Portions Copyright 2000 Tobias Brox <tobix@fsck.com>
25
26 ## This is a library of static subs to be used by the Mason web
27 ## interface to RT
28
29
30 =head1 NAME
31
32 RT::Interface::Web
33
34 =begin testing
35
36 use_ok(RT::Interface::Web);
37
38 =end testing
39
40 =cut
41
42
43 package RT::Interface::Web;
44 use strict;
45
46
47
48
49
50 # {{{ sub NewApacheHandler 
51
52 =head2 NewApacheHandler
53
54   Takes extra options to pass to HTML::Mason::ApacheHandler->new
55   Returns a new Mason::ApacheHandler object
56
57 =cut
58
59 sub NewApacheHandler {
60     require HTML::Mason::ApacheHandler;
61     my $ah = new HTML::Mason::ApacheHandler( 
62     
63         comp_root                    => [
64             [ local    => $RT::MasonLocalComponentRoot ],
65             [ standard => $RT::MasonComponentRoot ]
66         ],
67         args_method => "CGI",
68         default_escape_flags => 'h',
69         allow_globals        => [qw(%session)],
70         data_dir => "$RT::MasonDataDir",
71         autoflush => 1,
72         @_
73     );
74
75     $ah->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 );
76     
77     return ($ah);
78 }
79
80 # }}}
81
82 # {{{ sub NewCGIHandler 
83
84 =head2 NewCGIHandler
85
86   Returns a new Mason::CGIHandler object
87
88 =cut
89
90 sub NewCGIHandler {
91     my %args = (
92         @_
93     );
94
95     my $handler = HTML::Mason::CGIHandler->new(
96         comp_root                    => [
97             [ local    => $RT::MasonLocalComponentRoot ],
98             [ standard => $RT::MasonComponentRoot ]
99         ],
100         data_dir => "$RT::MasonDataDir",
101         default_escape_flags => 'h',
102         allow_globals        => [qw(%session)],
103         autoflush => 1,
104     );
105   
106
107     $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 );
108
109
110     return ($handler);
111
112 }
113 # }}}
114
115
116 # {{{ EscapeUTF8
117
118 =head2 EscapeUTF8 SCALARREF
119
120 does a css-busting but minimalist escaping of whatever html you're passing in.
121
122 =cut
123
124 sub EscapeUTF8  {
125         my  $ref = shift;
126         my $val = $$ref;
127         use bytes;
128         $val =~ s/&/&#38;/g;
129         $val =~ s/</&lt;/g; 
130         $val =~ s/>/&gt;/g;
131         $val =~ s/\(/&#40;/g;
132         $val =~ s/\)/&#41;/g;
133         $val =~ s/"/&#34;/g;
134         $val =~ s/'/&#39;/g;
135         $$ref = $val;
136         Encode::_utf8_on($$ref);
137
138 }
139
140 # }}}
141
142 # {{{ WebCanonicalizeInfo
143
144 =head2 WebCanonicalizeInfo();
145
146 Different web servers set different environmental varibles. This
147 function must return something suitable for REMOTE_USER. By default,
148 just downcase $ENV{'REMOTE_USER'}
149
150 =cut
151
152 sub WebCanonicalizeInfo {
153     my $user;
154
155     if ( defined $ENV{'REMOTE_USER'} ) {
156         $user = lc ( $ENV{'REMOTE_USER'} ) if( length($ENV{'REMOTE_USER'}) );
157     }
158
159     return $user;
160 }
161
162 # }}}
163
164 # {{{ WebExternalAutoInfo
165
166 =head2 WebExternalAutoInfo($user);
167
168 Returns a hash of user attributes, used when WebExternalAuto is set.
169
170 =cut
171
172 sub WebExternalAutoInfo {
173     my $user = shift;
174
175     my %user_info;
176
177     $user_info{'Privileged'} = 1;
178
179     if ($^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/) {
180         # Populate fields with information from Unix /etc/passwd
181
182         my ($comments, $realname) = (getpwnam($user))[5, 6];
183         $user_info{'Comments'} = $comments if defined $comments;
184         $user_info{'RealName'} = $realname if defined $realname;
185     }
186     elsif ($^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1') {
187         # Populate fields with information from NT domain controller
188     }
189
190     # and return the wad of stuff
191     return {%user_info};
192 }
193
194 # }}}
195
196
197 package HTML::Mason::Commands;
198 use strict;
199 use vars qw/$r $m %session/;
200
201
202 # {{{ loc
203
204 =head2 loc ARRAY
205
206 loc is a nice clean global routine which calls $session{'CurrentUser'}->loc()
207 with whatever it's called with. If there is no $session{'CurrentUser'}, 
208 it creates a temporary user, so we have something to get a localisation handle
209 through
210
211 =cut
212
213 sub loc {
214
215     if ($session{'CurrentUser'} && 
216         UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
217         return($session{'CurrentUser'}->loc(@_));
218     }
219     elsif ( my $u = eval { RT::CurrentUser->new($RT::SystemUser->Id) } ) {
220         return ($u->loc(@_));
221     }
222     else {
223         # pathetic case -- SystemUser is gone.
224         return $_[0];
225     }
226 }
227
228 # }}}
229
230
231 # {{{ loc_fuzzy
232
233 =head2 loc_fuzzy STRING
234
235 loc_fuzzy is for handling localizations of messages that may already
236 contain interpolated variables, typically returned from libraries
237 outside RT's control.  It takes the message string and extracts the
238 variable array automatically by matching against the candidate entries
239 inside the lexicon file.
240
241 =cut
242
243 sub loc_fuzzy {
244     my $msg  = shift;
245     
246     if ($session{'CurrentUser'} && 
247         UNIVERSAL::can($session{'CurrentUser'}, 'loc')){
248         return($session{'CurrentUser'}->loc_fuzzy($msg));
249     }
250     else  {
251         my $u = RT::CurrentUser->new($RT::SystemUser->Id);
252         return ($u->loc_fuzzy($msg));
253     }
254 }
255
256 # }}}
257
258
259 # {{{ sub Abort
260 # Error - calls Error and aborts
261 sub Abort {
262
263     if ($session{'ErrorDocument'} && 
264         $session{'ErrorDocumentType'}) {
265         $r->content_type($session{'ErrorDocumentType'});
266         $m->comp($session{'ErrorDocument'} , Why => shift);
267         $m->abort;
268     } 
269     else  {
270         $m->comp("/Elements/Error" , Why => shift);
271         $m->abort;
272     }
273 }
274
275 # }}}
276
277 # {{{ sub CreateTicket 
278
279 =head2 CreateTicket ARGS
280
281 Create a new ticket, using Mason's %ARGS.  returns @results.
282
283 =cut
284
285 sub CreateTicket {
286     my %ARGS = (@_);
287
288     my (@Actions);
289
290     my $Ticket = new RT::Ticket( $session{'CurrentUser'} );
291
292     my $Queue = new RT::Queue( $session{'CurrentUser'} );
293     unless ( $Queue->Load( $ARGS{'Queue'} ) ) {
294         Abort('Queue not found');
295     }
296
297     unless ( $Queue->CurrentUserHasRight('CreateTicket') ) {
298         Abort('You have no permission to create tickets in that queue.');
299     }
300
301     my $due = new RT::Date( $session{'CurrentUser'} );
302     $due->Set( Format => 'unknown', Value => $ARGS{'Due'} );
303     my $starts = new RT::Date( $session{'CurrentUser'} );
304     $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} );
305
306     my @Requestors = split ( /\s*,\s*/, $ARGS{'Requestors'} );
307     my @Cc         = split ( /\s*,\s*/, $ARGS{'Cc'} );
308     my @AdminCc    = split ( /\s*,\s*/, $ARGS{'AdminCc'} );
309
310     my $MIMEObj = MakeMIMEEntity(
311         Subject             => $ARGS{'Subject'},
312         From                => $ARGS{'From'},
313         Cc                  => $ARGS{'Cc'},
314         Body                => $ARGS{'Content'},
315     );
316
317     if ($ARGS{'Attachments'}) {
318         $MIMEObj->make_multipart;
319         $MIMEObj->add_part($_) foreach values %{$ARGS{'Attachments'}};
320     }
321
322     my %create_args = (
323         Queue           => $ARGS{'Queue'},
324         Owner           => $ARGS{'Owner'},
325         InitialPriority => $ARGS{'InitialPriority'},
326         FinalPriority   => $ARGS{'FinalPriority'},
327         TimeLeft        => $ARGS{'TimeLeft'},
328         TimeEstimated        => $ARGS{'TimeEstimated'},
329         TimeWorked      => $ARGS{'TimeWorked'},
330         Requestor       => \@Requestors,
331         Cc              => \@Cc,
332         AdminCc         => \@AdminCc,
333         Subject         => $ARGS{'Subject'},
334         Status          => $ARGS{'Status'},
335         Due             => $due->ISO,
336         Starts          => $starts->ISO,
337         MIMEObj         => $MIMEObj
338     );
339   foreach my $arg (%ARGS) {
340         if ($arg =~ /^CustomField-(\d+)(.*?)$/) {
341             next if ($arg =~ /-Magic$/);
342             $create_args{"CustomField-".$1} = $ARGS{"$arg"};
343         }
344     }
345     my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args);
346     unless ( $id && $Trans ) {
347         Abort($ErrMsg);
348     }
349     my @linktypes = qw( DependsOn MemberOf RefersTo );
350
351     foreach my $linktype (@linktypes) {
352         foreach my $luri ( split ( / /, $ARGS{"new-$linktype"} ) ) {
353             $luri =~ s/\s*$//;    # Strip trailing whitespace
354             my ( $val, $msg ) = $Ticket->AddLink(
355                 Target => $luri,
356                 Type   => $linktype
357             );
358             push ( @Actions, $msg ) unless ($val);
359         }
360
361         foreach my $luri ( split ( / /, $ARGS{"$linktype-new"} ) ) {
362             my ( $val, $msg ) = $Ticket->AddLink(
363                 Base => $luri,
364                 Type => $linktype
365             );
366
367             push ( @Actions, $msg ) unless ($val);
368         }
369     }
370
371     push ( @Actions, split("\n", $ErrMsg) );
372     unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) {
373         Abort( "No permission to view newly created ticket #"
374             . $Ticket->id . "." );
375     }
376     return ( $Ticket, @Actions );
377
378 }
379
380 # }}}
381
382 # {{{ sub LoadTicket - loads a ticket
383
384 =head2  LoadTicket id
385
386 Takes a ticket id as its only variable. if it's handed an array, it takes
387 the first value.
388
389 Returns an RT::Ticket object as the current user.
390
391 =cut
392
393 sub LoadTicket {
394     my $id = shift;
395
396     if ( ref($id) eq "ARRAY" ) {
397         $id = $id->[0];
398     }
399
400     unless ($id) {
401         Abort("No ticket specified");
402     }
403
404     my $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
405     $Ticket->Load($id);
406     unless ( $Ticket->id ) {
407         Abort("Could not load ticket $id");
408     }
409     return $Ticket;
410 }
411
412 # }}}
413
414 # {{{ sub ProcessUpdateMessage
415
416 sub ProcessUpdateMessage {
417
418     #TODO document what else this takes.
419     my %args = (
420         ARGSRef   => undef,
421         Actions   => undef,
422         TicketObj => undef,
423         @_
424     );
425
426     #Make the update content have no 'weird' newlines in it
427     if ( $args{ARGSRef}->{'UpdateContent'} ||
428          $args{ARGSRef}->{'UpdateAttachments'}) {
429
430         if (
431             $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject() )
432         {
433             $args{ARGSRef}->{'UpdateSubject'} = undef;
434         }
435
436         my $Message = MakeMIMEEntity(
437             Subject             => $args{ARGSRef}->{'UpdateSubject'},
438             Body                => $args{ARGSRef}->{'UpdateContent'},
439         );
440
441         if ($args{ARGSRef}->{'UpdateAttachments'}) {
442             $Message->make_multipart;
443             $Message->add_part($_) foreach values %{$args{ARGSRef}->{'UpdateAttachments'}};
444         }
445
446         ## TODO: Implement public comments
447         if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) {
448             my ( $Transaction, $Description ) = $args{TicketObj}->Comment(
449                 CcMessageTo  => $args{ARGSRef}->{'UpdateCc'},
450                 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
451                 MIMEObj      => $Message,
452                 TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'}
453             );
454             push ( @{ $args{Actions} }, $Description );
455         }
456         elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) {
457             my ( $Transaction, $Description ) = $args{TicketObj}->Correspond(
458                 CcMessageTo  => $args{ARGSRef}->{'UpdateCc'},
459                 BccMessageTo => $args{ARGSRef}->{'UpdateBcc'},
460                 MIMEObj      => $Message,
461                 TimeTaken    => $args{ARGSRef}->{'UpdateTimeWorked'}
462             );
463             push ( @{ $args{Actions} }, $Description );
464         }
465         else {
466             push ( @{ $args{'Actions'} },
467                 loc("Update type was neither correspondence nor comment.").
468                 " ".
469                 loc("Update not recorded.")
470             );
471         }
472     }
473 }
474
475 # }}}
476
477 # {{{ sub MakeMIMEEntity
478
479 =head2 MakeMIMEEntity PARAMHASH
480
481 Takes a paramhash Subject, Body and AttachmentFieldName.
482
483   Returns a MIME::Entity.
484
485 =cut
486
487 sub MakeMIMEEntity {
488
489     #TODO document what else this takes.
490     my %args = (
491         Subject             => undef,
492         From                => undef,
493         Cc                  => undef,
494         Body                => undef,
495         AttachmentFieldName => undef,
496 #        map Encode::encode_utf8($_), @_,
497         @_,
498     );
499
500     #Make the update content have no 'weird' newlines in it
501
502     $args{'Body'} =~ s/\r\n/\n/gs;
503     my $Message;
504     {
505         # MIME::Head is not happy in utf-8 domain.  This only happens
506         # when processing an incoming email (so far observed).
507         no utf8;
508         use bytes;
509         $Message = MIME::Entity->build(
510             Subject => $args{'Subject'} || "",
511             From    => $args{'From'},
512             Cc      => $args{'Cc'},
513             Charset => 'utf8',
514             Data    => [ $args{'Body'} ]
515         );
516     }
517
518     my $cgi_object = $m->cgi_object;
519
520     if (my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) {
521
522
523
524     use File::Temp qw(tempfile tempdir);
525
526     #foreach my $filehandle (@filenames) {
527
528     my ( $fh, $temp_file );
529     for ( 1 .. 10 ) {
530         # on NFS and NTFS, it is possible that tempfile() conflicts
531         # with other processes, causing a race condition. we try to
532         # accommodate this by pausing and retrying.
533         last if ($fh, $temp_file) = eval { tempfile() };
534         sleep 1;
535     }
536
537     binmode $fh;    #thank you, windows
538     my ($buffer);
539     while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) {
540         print $fh $buffer;
541     }
542
543     my $uploadinfo = $cgi_object->uploadInfo($filehandle);
544
545     # Prefer the cached name first over CGI.pm stringification.
546     my $filename = $RT::Mason::CGI::Filename;
547     $filename = "$filehandle" unless defined($filename);
548                    
549     $filename =~ s#^.*[\\/]##;
550
551     $Message->attach(
552         Path     => $temp_file,
553         Filename => Encode::decode_utf8($filename),
554         Type     => $uploadinfo->{'Content-Type'},
555     );
556     close($fh);
557
558     #   }
559
560     }
561
562     $Message->make_singlepart();
563     RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
564
565     return ($Message);
566
567 }
568
569 # }}}
570
571 # {{{ sub ProcessSearchQuery
572
573 =head2 ProcessSearchQuery
574
575   Takes a form such as the one filled out in webrt/Search/Elements/PickRestriction and turns it into something that RT::Tickets can understand.
576
577 TODO Doc exactly what comes in the paramhash
578
579
580 =cut
581
582 sub ProcessSearchQuery {
583     my %args = @_;
584
585     ## TODO: The only parameter here is %ARGS.  Maybe it would be
586     ## cleaner to load this parameter as $ARGS, and use $ARGS->{...}
587     ## instead of $args{ARGS}->{...} ? :)
588
589     #Searches are sticky.
590     if ( defined $session{'tickets'} ) {
591
592         # Reset the old search
593         $session{'tickets'}->GotoFirstItem;
594     }
595     else {
596
597         # Init a new search
598         $session{'tickets'} = RT::Tickets->new( $session{'CurrentUser'} );
599     }
600
601     #Import a bookmarked search if we have one
602     if ( defined $args{ARGS}->{'Bookmark'} ) {
603         $session{'tickets'}->ThawLimits( $args{ARGS}->{'Bookmark'} );
604     }
605
606     # {{{ Goto next/prev page
607     if ( $args{ARGS}->{'GotoPage'} eq 'Next' ) {
608         $session{'tickets'}->NextPage;
609     }
610     elsif ( $args{ARGS}->{'GotoPage'} eq 'Prev' ) {
611         $session{'tickets'}->PrevPage;
612     }
613     elsif ( $args{ARGS}->{'GotoPage'} > 0 ) {
614         $session{'tickets'}->GotoPage( $args{ARGS}->{GotoPage} - 1 );
615     }
616
617     # }}}
618
619     # {{{ Deal with limiting the search
620
621     if ( $args{ARGS}->{'RefreshSearchInterval'} ) {
622         $session{'tickets_refresh_interval'} =
623           $args{ARGS}->{'RefreshSearchInterval'};
624     }
625
626     if ( $args{ARGS}->{'TicketsSortBy'} ) {
627         $session{'tickets_sort_by'}    = $args{ARGS}->{'TicketsSortBy'};
628         $session{'tickets_sort_order'} = $args{ARGS}->{'TicketsSortOrder'};
629         $session{'tickets'}->OrderBy(
630             FIELD => $args{ARGS}->{'TicketsSortBy'},
631             ORDER => $args{ARGS}->{'TicketsSortOrder'}
632         );
633     }
634
635     # }}}
636
637     # {{{ Set the query limit
638     if ( defined $args{ARGS}->{'RowsPerPage'} ) {
639         $RT::Logger->debug(
640             "limiting to " . $args{ARGS}->{'RowsPerPage'} . " rows" );
641
642         $session{'tickets_rows_per_page'} = $args{ARGS}->{'RowsPerPage'};
643         $session{'tickets'}->RowsPerPage( $args{ARGS}->{'RowsPerPage'} );
644     }
645
646     # }}}
647     # {{{ Limit priority
648     if ( $args{ARGS}->{'ValueOfPriority'} ne '' ) {
649         $session{'tickets'}->LimitPriority(
650             VALUE    => $args{ARGS}->{'ValueOfPriority'},
651             OPERATOR => $args{ARGS}->{'PriorityOp'}
652         );
653     }
654
655     # }}}
656     # {{{ Limit owner
657     if ( $args{ARGS}->{'ValueOfOwner'} ne '' ) {
658         $session{'tickets'}->LimitOwner(
659             VALUE    => $args{ARGS}->{'ValueOfOwner'},
660             OPERATOR => $args{ARGS}->{'OwnerOp'}
661         );
662     }
663
664     # }}}
665     # {{{ Limit requestor email
666      if ( $args{ARGS}->{'ValueOfWatcherRole'} ne '' ) {
667          $session{'tickets'}->LimitWatcher(
668              TYPE     => $args{ARGS}->{'WatcherRole'},
669              VALUE    => $args{ARGS}->{'ValueOfWatcherRole'},
670              OPERATOR => $args{ARGS}->{'WatcherRoleOp'},
671
672         );
673     }
674
675     # }}}
676     # {{{ Limit Queue
677     if ( $args{ARGS}->{'ValueOfQueue'} ne '' ) {
678         $session{'tickets'}->LimitQueue(
679             VALUE    => $args{ARGS}->{'ValueOfQueue'},
680             OPERATOR => $args{ARGS}->{'QueueOp'}
681         );
682     }
683
684     # }}}
685     # {{{ Limit Status
686     if ( $args{ARGS}->{'ValueOfStatus'} ne '' ) {
687         if ( ref( $args{ARGS}->{'ValueOfStatus'} ) ) {
688             foreach my $value ( @{ $args{ARGS}->{'ValueOfStatus'} } ) {
689                 $session{'tickets'}->LimitStatus(
690                     VALUE    => $value,
691                     OPERATOR => $args{ARGS}->{'StatusOp'},
692                 );
693             }
694         }
695         else {
696             $session{'tickets'}->LimitStatus(
697                 VALUE    => $args{ARGS}->{'ValueOfStatus'},
698                 OPERATOR => $args{ARGS}->{'StatusOp'},
699             );
700         }
701
702     }
703
704     # }}}
705     # {{{ Limit Subject
706     if ( $args{ARGS}->{'ValueOfSubject'} ne '' ) {
707             my $val = $args{ARGS}->{'ValueOfSubject'};
708         if ($args{ARGS}->{'SubjectOp'} =~ /like/) {
709             $val = "%".$val."%";
710         }
711         $session{'tickets'}->LimitSubject(
712             VALUE    => $val,
713             OPERATOR => $args{ARGS}->{'SubjectOp'},
714         );
715     }
716
717     # }}}    
718     # {{{ Limit Dates
719     if ( $args{ARGS}->{'ValueOfDate'} ne '' ) {
720         my $date = ParseDateToISO( $args{ARGS}->{'ValueOfDate'} );
721         $args{ARGS}->{'DateType'} =~ s/_Date$//;
722
723         if ( $args{ARGS}->{'DateType'} eq 'Updated' ) {
724             $session{'tickets'}->LimitTransactionDate(
725                                             VALUE    => $date,
726                                             OPERATOR => $args{ARGS}->{'DateOp'},
727             );
728         }
729         else {
730             $session{'tickets'}->LimitDate( FIELD => $args{ARGS}->{'DateType'},
731                                             VALUE => $date,
732                                             OPERATOR => $args{ARGS}->{'DateOp'},
733             );
734         }
735     }
736
737     # }}}    
738     # {{{ Limit Content
739     if ( $args{ARGS}->{'ValueOfAttachmentField'} ne '' ) {
740         my $val = $args{ARGS}->{'ValueOfAttachmentField'};
741         if ($args{ARGS}->{'AttachmentFieldOp'} =~ /like/) {
742             $val = "%".$val."%";
743         }
744         $session{'tickets'}->Limit(
745             FIELD   => $args{ARGS}->{'AttachmentField'},
746             VALUE    => $val,
747             OPERATOR => $args{ARGS}->{'AttachmentFieldOp'},
748         );
749     }
750
751     # }}}   
752
753  # {{{ Limit CustomFields
754
755     foreach my $arg ( keys %{ $args{ARGS} } ) {
756         my $id;
757         if ( $arg =~ /^CustomField(\d+)$/ ) {
758             $id = $1;
759         }
760         else {
761             next;
762         }
763         next unless ( $args{ARGS}->{$arg} );
764
765         my $form = $args{ARGS}->{$arg};
766         my $oper = $args{ARGS}->{ "CustomFieldOp" . $id };
767         foreach my $value ( ref($form) ? @{$form} : ($form) ) {
768             my $quote = 1;
769             if ($oper =~ /like/i) {
770                 $value = "%".$value."%";
771             }
772             if ( $value =~ /^null$/i ) {
773
774                 #Don't quote the string 'null'
775                 $quote = 0;
776
777                 # Convert the operator to something apropriate for nulls
778                 $oper = 'IS'     if ( $oper eq '=' );
779                 $oper = 'IS NOT' if ( $oper eq '!=' );
780             }
781             $session{'tickets'}->LimitCustomField( CUSTOMFIELD => $id,
782                                                    OPERATOR    => $oper,
783                                                    QUOTEVALUE  => $quote,
784                                                    VALUE       => $value );
785         }
786     }
787
788     # }}}
789
790
791 }
792
793 # }}}
794
795 # {{{ sub ParseDateToISO
796
797 =head2 ParseDateToISO
798
799 Takes a date in an arbitrary format.
800 Returns an ISO date and time in GMT
801
802 =cut
803
804 sub ParseDateToISO {
805     my $date = shift;
806
807     my $date_obj = RT::Date->new($session{'CurrentUser'});
808     $date_obj->Set(
809         Format => 'unknown',
810         Value  => $date
811     );
812     return ( $date_obj->ISO );
813 }
814
815 # }}}
816
817 # {{{ sub Config 
818 # TODO: This might eventually read the cookies, user configuration
819 # information from the DB, queue configuration information from the
820 # DB, etc.
821
822 sub Config {
823     my $args = shift;
824     my $key  = shift;
825     return $args->{$key} || $RT::WebOptions{$key};
826 }
827
828 # }}}
829
830 # {{{ sub ProcessACLChanges
831
832 sub ProcessACLChanges {
833     my $ARGSref = shift;
834
835     my %ARGS     = %$ARGSref;
836
837     my ( $ACL, @results );
838
839
840     foreach my $arg (keys %ARGS) {
841         if ($arg =~ /GrantRight-(\d+)-(.*?)-(\d+)$/) {
842             my $principal_id = $1;
843             my $object_type = $2;
844             my $object_id = $3;
845             my $rights = $ARGS{$arg};
846
847             my $principal = RT::Principal->new($session{'CurrentUser'});
848             $principal->Load($principal_id);
849
850             my $obj;
851
852              if ($object_type eq 'RT::System') {
853                 $obj = $RT::System;
854             } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
855                 $obj = $object_type->new($session{'CurrentUser'});
856                 $obj->Load($object_id);      
857             } else {
858                 push (@results, loc("System Error"). ': '.
859                                 loc("Rights could not be granted for [_1]", $object_type));
860                 next;
861             }
862
863             my @rights = ref($ARGS{$arg}) eq 'ARRAY' ? @{$ARGS{$arg}} : ($ARGS{$arg});
864             foreach my $right (@rights) {
865                 next unless ($right);
866                 my ($val, $msg) = $principal->GrantRight(Object => $obj, Right => $right);
867                 push (@results, $msg);
868             }
869         }
870        elsif ($arg =~ /RevokeRight-(\d+)-(.*?)-(\d+)-(.*?)$/) {
871             my $principal_id = $1;
872             my $object_type = $2;
873             my $object_id = $3;
874             my $right = $4;
875
876             my $principal = RT::Principal->new($session{'CurrentUser'});
877             $principal->Load($principal_id);
878             next unless ($right);
879             my $obj;
880
881              if ($object_type eq 'RT::System') {
882                 $obj = $RT::System;
883             } elsif ($RT::ACE::OBJECT_TYPES{$object_type}) {
884                 $obj = $object_type->new($session{'CurrentUser'});
885                 $obj->Load($object_id);      
886             } else {
887                 die;
888                 push (@results, loc("System Error"). ': '.
889                                 loc("Rights could not be revoked for [_1]", $object_type));
890                 next;
891             }
892             my ($val, $msg) = $principal->RevokeRight(Object => $obj, Right => $right);
893             push (@results, $msg);
894         }
895
896
897     }
898
899     return (@results);
900
901     }
902
903 # }}}
904
905 # {{{ sub UpdateRecordObj
906
907 =head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs)
908
909 @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.
910
911 Returns an array of success/failure messages
912
913 =cut
914
915 sub UpdateRecordObject {
916     my %args = (
917         ARGSRef       => undef,
918         AttributesRef => undef,
919         Object        => undef,
920         AttributePrefix => undef,
921         @_
922     );
923
924     my (@results);
925
926     my $object     = $args{'Object'};
927     my $attributes = $args{'AttributesRef'};
928     my $ARGSRef    = $args{'ARGSRef'};
929     foreach my $attribute (@$attributes) {
930         my $value;
931         if ( defined $ARGSRef->{$attribute} ) {
932             $value = $ARGSRef->{$attribute};
933         }
934         elsif (
935               defined( $args{'AttributePrefix'} )
936               && defined(
937                   $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute }
938               )
939           ) {
940             $value = $ARGSRef->{ $args{'AttributePrefix'} . "-" . $attribute };
941
942         } else {
943                 next;
944         }
945
946             $value =~ s/\r\n/\n/gs;
947
948         if ($value ne $object->$attribute()){
949
950               my $method = "Set$attribute";
951               my ( $code, $msg ) = $object->$method($value);
952
953               push @results, loc($attribute) . ': ' . loc_fuzzy($msg);
954 =for loc
955                                    "[_1] could not be set to [_2].",       # loc
956                                    "That is already the current value",    # loc
957                                    "No value sent to _Set!\n",             # loc
958                                    "Illegal value for [_1]",               # loc
959                                    "The new value has been set.",          # loc
960                                    "No column specified",                  # loc
961                                    "Immutable field",                      # loc
962                                    "Nonexistant field?",                   # loc
963                                    "Invalid data",                         # loc
964                                    "Couldn't find row",                    # loc
965                                    "Missing a primary key?: [_1]",         # loc
966                                    "Found Object",                         # loc
967 =cut
968           };
969     }
970     return (@results);
971 }
972
973 # }}}
974
975 # {{{ Sub ProcessCustomFieldUpdates
976
977 sub ProcessCustomFieldUpdates {
978     my %args = (
979         CustomFieldObj => undef,
980         ARGSRef        => undef,
981         @_
982     );
983
984     my $Object  = $args{'CustomFieldObj'};
985     my $ARGSRef = $args{'ARGSRef'};
986
987     my @attribs = qw( Name Type Description Queue SortOrder);
988     my @results = UpdateRecordObject(
989         AttributesRef => \@attribs,
990         Object        => $Object,
991         ARGSRef       => $ARGSRef
992     );
993
994     if ( $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" } ) {
995
996         my ( $addval, $addmsg ) = $Object->AddValue(
997             Name =>
998               $ARGSRef->{ "CustomField-" . $Object->Id . "-AddValue-Name" },
999             Description => $ARGSRef->{ "CustomField-"
1000                   . $Object->Id
1001                   . "-AddValue-Description" },
1002             SortOrder => $ARGSRef->{ "CustomField-"
1003                   . $Object->Id
1004                   . "-AddValue-SortOrder" },
1005         );
1006         push ( @results, $addmsg );
1007     }
1008     my @delete_values = (
1009         ref $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } eq
1010           'ARRAY' )
1011       ? @{ $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } }
1012       : ( $ARGSRef->{ 'CustomField-' . $Object->Id . '-DeleteValue' } );
1013     foreach my $id (@delete_values) {
1014         next unless defined $id;
1015         my ( $err, $msg ) = $Object->DeleteValue($id);
1016         push ( @results, $msg );
1017     }
1018
1019     my $vals = $Object->Values();
1020     while (my $cfv = $vals->Next()) {
1021         if (my $so = $ARGSRef->{ 'CustomField-' . $Object->Id . '-SortOrder' . $cfv->Id }) {
1022             if ($cfv->SortOrder != $so) {
1023                 my ( $err, $msg ) = $cfv->SetSortOrder($so);
1024                 push ( @results, $msg );
1025             }
1026         }
1027     }
1028
1029     return (@results);
1030 }
1031
1032 # }}}
1033
1034 # {{{ sub ProcessTicketBasics
1035
1036 =head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1037
1038 Returns an array of results messages.
1039
1040 =cut
1041
1042 sub ProcessTicketBasics {
1043
1044     my %args = (
1045         TicketObj => undef,
1046         ARGSRef   => undef,
1047         @_
1048     );
1049
1050     my $TicketObj = $args{'TicketObj'};
1051     my $ARGSRef   = $args{'ARGSRef'};
1052
1053     # {{{ Set basic fields 
1054     my @attribs = qw(
1055       Subject
1056       FinalPriority
1057       Priority
1058       TimeEstimated
1059       TimeWorked
1060       TimeLeft
1061       Status
1062       Queue
1063     );
1064
1065     if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) {
1066         my $tempqueue = RT::Queue->new($RT::SystemUser);
1067         $tempqueue->Load( $ARGSRef->{'Queue'} );
1068         if ( $tempqueue->id ) {
1069             $ARGSRef->{'Queue'} = $tempqueue->Id();
1070         }
1071     }
1072
1073     my @results = UpdateRecordObject(
1074         AttributesRef => \@attribs,
1075         Object        => $TicketObj,
1076         ARGSRef       => $ARGSRef
1077     );
1078
1079     # We special case owner changing, so we can use ForceOwnerChange
1080     if ( $ARGSRef->{'Owner'} && ( $TicketObj->Owner != $ARGSRef->{'Owner'} ) ) {
1081         my ($ChownType);
1082         if ( $ARGSRef->{'ForceOwnerChange'} ) {
1083             $ChownType = "Force";
1084         }
1085         else {
1086             $ChownType = "Give";
1087         }
1088
1089         my ( $val, $msg ) =
1090           $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType );
1091         push ( @results, $msg );
1092     }
1093
1094     # }}}
1095
1096     return (@results);
1097 }
1098
1099 # }}}
1100
1101 # {{{ Sub ProcessTicketCustomFieldUpdates
1102
1103 sub ProcessTicketCustomFieldUpdates {
1104     my %args = (
1105         ARGSRef => undef,
1106         @_
1107     );
1108
1109     my @results;
1110
1111     my $ARGSRef = $args{'ARGSRef'};
1112
1113     # Build up a list of tickets that we want to work with
1114     my %tickets_to_mod;
1115     my %custom_fields_to_mod;
1116     foreach my $arg ( keys %{$ARGSRef} ) {
1117         if ( $arg =~ /^Ticket-(\d+)-CustomField-(\d+)-/ ) {
1118
1119             # For each of those tickets, find out what custom fields we want to work with.
1120             $custom_fields_to_mod{$1}{$2} = 1;
1121         }
1122     }
1123
1124     # For each of those tickets
1125     foreach my $tick ( keys %custom_fields_to_mod ) {
1126         my $Ticket = $args{'TicketObj'};
1127         if (!$Ticket or $Ticket->id != $tick) {
1128             $Ticket = RT::Ticket->new( $session{'CurrentUser'} );
1129             $Ticket->Load($tick);
1130         }
1131
1132         # For each custom field  
1133         foreach my $cf ( keys %{ $custom_fields_to_mod{$tick} } ) {
1134
1135             my $CustomFieldObj = RT::CustomField->new($session{'CurrentUser'});
1136             $CustomFieldObj->LoadById($cf);
1137
1138             foreach my $arg ( keys %{$ARGSRef} ) {
1139                 # since http won't pass in a form element with a null value, we need
1140                 # to fake it
1141                 if ($arg =~ /^(.*?)-Values-Magic$/ ) {
1142                     # We don't care about the magic, if there's really a values element;
1143                     next if (exists $ARGSRef->{$1.'-Values'}) ;
1144
1145                     $arg = $1."-Values";
1146                     $ARGSRef->{$1."-Values"} = undef;
1147                 
1148                 }
1149                 next unless ( $arg =~ /^Ticket-$tick-CustomField-$cf-/ );
1150                 my @values =
1151                   ( ref( $ARGSRef->{$arg} ) eq 'ARRAY' ) 
1152                   ? @{ $ARGSRef->{$arg} }
1153                   : split /\n/, $ARGSRef->{$arg} ;
1154                 if ( ( $arg =~ /-AddValue$/ ) || ( $arg =~ /-Value$/ ) ) {
1155                     foreach my $value (@values) {
1156                         next unless length($value);
1157                         my ( $val, $msg ) = $Ticket->AddCustomFieldValue(
1158                             Field => $cf,
1159                             Value => $value
1160                         );
1161                         push ( @results, $msg );
1162                     }
1163                 }
1164                 elsif ( $arg =~ /-DeleteValues$/ ) {
1165                     foreach my $value (@values) {
1166                         next unless length($value);
1167                         my ( $val, $msg ) = $Ticket->DeleteCustomFieldValue(
1168                             Field => $cf,
1169                             Value => $value
1170                         );
1171                         push ( @results, $msg );
1172                     }
1173                 }
1174                 elsif ( $arg =~ /-Values$/ and $CustomFieldObj->Type !~ /Entry/) {
1175                     my $cf_values = $Ticket->CustomFieldValues($cf);
1176
1177                     my %values_hash;
1178                     foreach my $value (@values) {
1179                         next unless length($value);
1180
1181                         # build up a hash of values that the new set has
1182                         $values_hash{$value} = 1;
1183
1184                         unless ( $cf_values->HasEntry($value) ) {
1185                             my ( $val, $msg ) = $Ticket->AddCustomFieldValue(
1186                                 Field => $cf,
1187                                 Value => $value
1188                             );
1189                             push ( @results, $msg );
1190                         }
1191
1192                     }
1193                     while ( my $cf_value = $cf_values->Next ) {
1194                         unless ( $values_hash{ $cf_value->Content } == 1 ) {
1195                             my ( $val, $msg ) = $Ticket->DeleteCustomFieldValue(
1196                                 Field => $cf,
1197                                 Value => $cf_value->Content
1198                             );
1199                             push ( @results, $msg);
1200
1201                         }
1202
1203                     }
1204                 }
1205                 elsif ( $arg =~ /-Values$/ ) {
1206                     my $cf_values = $Ticket->CustomFieldValues($cf);
1207
1208                     # keep everything up to the point of difference, delete the rest
1209                     my $delete_flag;
1210                     foreach my $old_cf (@{$cf_values->ItemsArrayRef}) {
1211                         if (!$delete_flag and @values and $old_cf->Content eq $values[0]) {
1212                             shift @values;
1213                             next;
1214                         }
1215
1216                         $delete_flag ||= 1;
1217                         $old_cf->Delete;
1218                     }
1219
1220                     # now add/replace extra things, if any
1221                     foreach my $value (@values) {
1222                         my ( $val, $msg ) = $Ticket->AddCustomFieldValue(
1223                             Field => $cf,
1224                             Value => $value
1225                         );
1226                         push ( @results, $msg );
1227                     }
1228                 }
1229                 else {
1230                     push ( @results, "User asked for an unknown update type for custom field " . $cf->Name . " for ticket " . $Ticket->id );
1231                 }
1232             }
1233         }
1234         return (@results);
1235     }
1236 }
1237
1238 # }}}
1239
1240 # {{{ sub ProcessTicketWatchers
1241
1242 =head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1243
1244 Returns an array of results messages.
1245
1246 =cut
1247
1248 sub ProcessTicketWatchers {
1249     my %args = (
1250         TicketObj => undef,
1251         ARGSRef   => undef,
1252         @_
1253     );
1254     my (@results);
1255
1256     my $Ticket  = $args{'TicketObj'};
1257     my $ARGSRef = $args{'ARGSRef'};
1258
1259     # {{{ Munge watchers
1260
1261     foreach my $key ( keys %$ARGSRef ) {
1262
1263         # {{{ Delete deletable watchers
1264         if ( ( $key =~ /^Ticket-DelWatcher-Type-(.*)-Principal-(\d+)$/ )  ) {
1265             my ( $code, $msg ) = 
1266                 $Ticket->DeleteWatcher(PrincipalId => $2,
1267                                        Type => $1);
1268             push @results, $msg;
1269         }
1270
1271         # Delete watchers in the simple style demanded by the bulk manipulator
1272         elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) {
1273             my ( $code, $msg ) = $Ticket->DeleteWatcher( Type => $ARGSRef->{$key}, PrincipalId => $1 );
1274             push @results, $msg;
1275         }
1276
1277         # }}}
1278
1279         # Add new wathchers by email address      
1280         elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
1281             and ( $key =~ /^WatcherTypeEmail(\d*)$/ ) )
1282         {
1283
1284             #They're in this order because otherwise $1 gets clobbered :/
1285             my ( $code, $msg ) = $Ticket->AddWatcher(
1286                 Type  => $ARGSRef->{$key},
1287                 Email => $ARGSRef->{ "WatcherAddressEmail" . $1 }
1288             );
1289             push @results, $msg;
1290         }
1291
1292         #Add requestors in the simple style demanded by the bulk manipulator
1293         elsif ( $key =~ /^Add(Requestor|Cc|AdminCc)$/ ) {
1294             my ( $code, $msg ) = $Ticket->AddWatcher(
1295                 Type  => $1,
1296                 Email => $ARGSRef->{$key}
1297             );
1298             push @results, $msg;
1299         }
1300
1301         # Add new  watchers by owner
1302         elsif ( ( $ARGSRef->{$key} =~ /^(AdminCc|Cc|Requestor)$/ )
1303             and ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) ) {
1304
1305             #They're in this order because otherwise $1 gets clobbered :/
1306             my ( $code, $msg ) =
1307               $Ticket->AddWatcher( Type => $ARGSRef->{$key}, PrincipalId => $1 );
1308             push @results, $msg;
1309         }
1310     }
1311
1312     # }}}
1313
1314     return (@results);
1315 }
1316
1317 # }}}
1318
1319 # {{{ sub ProcessTicketDates
1320
1321 =head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1322
1323 Returns an array of results messages.
1324
1325 =cut
1326
1327 sub ProcessTicketDates {
1328     my %args = (
1329         TicketObj => undef,
1330         ARGSRef   => undef,
1331         @_
1332     );
1333
1334     my $Ticket  = $args{'TicketObj'};
1335     my $ARGSRef = $args{'ARGSRef'};
1336
1337     my (@results);
1338
1339     # {{{ Set date fields
1340     my @date_fields = qw(
1341       Told
1342       Resolved
1343       Starts
1344       Started
1345       Due
1346     );
1347
1348     #Run through each field in this list. update the value if apropriate
1349     foreach my $field (@date_fields) {
1350         my ( $code, $msg );
1351
1352         my $DateObj = RT::Date->new( $session{'CurrentUser'} );
1353
1354         #If it's something other than just whitespace
1355         if ( $ARGSRef->{ $field . '_Date' } ne '' ) {
1356             $DateObj->Set(
1357                 Format => 'unknown',
1358                 Value  => $ARGSRef->{ $field . '_Date' }
1359             );
1360             my $obj = $field . "Obj";
1361             if ( ( defined $DateObj->Unix )
1362                 and ( $DateObj->Unix ne $Ticket->$obj()->Unix() ) )
1363             {
1364                 my $method = "Set$field";
1365                 my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO );
1366                 push @results, "$msg";
1367             }
1368         }
1369     }
1370
1371     # }}}
1372     return (@results);
1373 }
1374
1375 # }}}
1376
1377 # {{{ sub ProcessTicketLinks
1378
1379 =head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS );
1380
1381 Returns an array of results messages.
1382
1383 =cut
1384
1385 sub ProcessTicketLinks {
1386     my %args = ( TicketObj => undef,
1387                  ARGSRef   => undef,
1388                  @_ );
1389
1390     my $Ticket  = $args{'TicketObj'};
1391     my $ARGSRef = $args{'ARGSRef'};
1392
1393     my (@results);
1394
1395     # Delete links that are gone gone gone.
1396     foreach my $arg ( keys %$ARGSRef ) {
1397         if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) {
1398             my $base   = $1;
1399             my $type   = $2;
1400             my $target = $3;
1401
1402             push @results,
1403               "Trying to delete: Base: $base Target: $target  Type $type";
1404             my ( $val, $msg ) = $Ticket->DeleteLink( Base   => $base,
1405                                                      Type   => $type,
1406                                                      Target => $target );
1407
1408             push @results, $msg;
1409
1410         }
1411
1412     }
1413
1414     my @linktypes = qw( DependsOn MemberOf RefersTo );
1415
1416     foreach my $linktype (@linktypes) {
1417         if ( $ARGSRef->{ $Ticket->Id . "-$linktype" } ) {
1418             for my $luri ( split ( / /, $ARGSRef->{ $Ticket->Id . "-$linktype" } ) ) {
1419                 $luri =~ s/\s*$//;    # Strip trailing whitespace
1420                 my ( $val, $msg ) = $Ticket->AddLink( Target => $luri,
1421                                                       Type   => $linktype );
1422                 push @results, $msg;
1423             }
1424         }
1425         if ( $ARGSRef->{ "$linktype-" . $Ticket->Id } ) {
1426
1427             for my $luri ( split ( / /, $ARGSRef->{ "$linktype-" . $Ticket->Id } ) ) {
1428                 my ( $val, $msg ) = $Ticket->AddLink( Base => $luri,
1429                                                       Type => $linktype );
1430
1431                 push @results, $msg;
1432             }
1433         } 
1434     }
1435
1436     #Merge if we need to
1437     if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) {
1438         my ( $val, $msg ) =
1439           $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } );
1440         push @results, $msg;
1441     }
1442
1443     return (@results);
1444 }
1445
1446 # }}}
1447
1448 eval "require RT::Interface::Web_Vendor";
1449 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm});
1450 eval "require RT::Interface::Web_Local";
1451 die $@ if ($@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm});
1452
1453 1;