import rt 2.0.14
[freeside.git] / rt / lib / RT / Ticket.pm
1 # $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Ticket.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
2 # (c) 1996-2001 Jesse Vincent <jesse@fsck.com>
3 # This software is redistributable under the terms of the GNU GPL
4 #
5
6 =head1 NAME
7
8   RT::Ticket - RT ticket object
9
10 =head1 SYNOPSIS
11
12   use RT::Ticket;
13   my $ticket = new RT::Ticket($CurrentUser);
14   $ticket->Load($ticket_id);
15
16 =head1 DESCRIPTION
17
18 This module lets you manipulate RT\'s ticket object.
19
20
21 =head1 METHODS
22
23 =cut
24
25
26
27 package RT::Ticket;
28 use RT::Queue;
29 use RT::User;
30 use RT::Record;
31 use RT::Link;
32 use RT::Links;
33 use RT::Date;
34 use RT::Watcher;
35
36
37 @ISA= qw(RT::Record);
38
39
40 =begin testing
41
42 use RT::TestHarness;
43
44 ok(require RT::Ticket, "Loading the RT::Ticket library");
45
46 =end testing
47
48 =cut
49
50 # {{{ sub _Init
51
52 sub _Init {
53     my $self = shift;
54     $self->{'table'} = "Tickets";
55     return ($self->SUPER::_Init(@_));
56 }
57
58 # }}}
59
60 # {{{ sub Load
61
62 =head2 Load
63
64 Takes a single argument. This can be a ticket id, ticket alias or 
65 local ticket uri.  If the ticket can't be loaded, returns undef.
66 Otherwise, returns the ticket id.
67
68 =cut
69
70 sub Load {
71    my $self = shift;
72    my $id = shift;
73
74    #TODO modify this routine to look at EffectiveId and do the recursive load
75    # thing. be careful to cache all the interim tickets we try so we don't loop forever.
76    
77    #If it's a local URI, turn it into a ticket id
78    if ($id =~ /^$RT::TicketBaseURI(\d+)$/)  {
79        $id = $1;
80    }
81    #If it's a remote URI, we're going to punt for now
82    elsif ($id =~ '://' ) {
83        return (undef);
84    }
85    
86    #If we have an integer URI, load the ticket
87    if ( $id =~ /^\d+$/ ) {
88        my $ticketid = $self->LoadById($id);
89    
90        unless ($ticketid) {
91            $RT::Logger->debug("$self tried to load a bogus ticket: $id\n");
92            return(undef);
93        }
94    }
95    
96    #It's not a URI. It's not a numerical ticket ID. Punt!
97    else {
98        return(undef);
99    }
100    
101    #If we're merged, resolve the merge.
102    if (($self->EffectiveId) and
103        ($self->EffectiveId != $self->Id)) {
104            return ($self->Load($self->EffectiveId));
105        }
106
107    #Ok. we're loaded. lets get outa here.
108    return ($self->Id);
109    
110 }
111
112 # }}}
113
114 # {{{ sub LoadByURI
115
116 =head2 LoadByURI
117
118 Given a local ticket URI, loads the specified ticket.
119
120 =cut
121
122 sub LoadByURI {
123     my $self = shift;
124     my $uri = shift;
125     
126     if ($uri =~ /^$RT::TicketBaseURI(\d+)$/) {
127         my $id = $1;
128         return ($self->Load($id));
129     }
130     else {
131         return(undef);
132     }
133 }
134
135 # }}}
136
137 # {{{ sub Create
138
139 =head2 Create (ARGS)
140
141 Arguments: ARGS is a hash of named parameters.  Valid parameters are:
142
143   Queue  - Either a Queue object or a Queue Name
144   Requestor -  A reference to a list of RT::User objects, email addresses or RT user Names
145   Cc  - A reference to a list of RT::User objects, email addresses or Names
146   AdminCc  - A reference to a  list of RT::User objects, email addresses or Names
147   Type -- The ticket\'s type. ignore this for now
148   Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
149   Subject -- A string describing the subject of the ticket
150   InitialPriority -- an integer from 0 to 99
151   FinalPriority -- an integer from 0 to 99
152   Status -- any valid status (Defined in RT::Queue)
153   TimeWorked -- an integer
154   TimeLeft -- an integer
155   Starts -- an ISO date describing the ticket\'s start date and time in GMT
156   Due -- an ISO date describing the ticket\'s due date and time in GMT
157   MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
158
159   KeywordSelect-<id> -- an array of keyword ids for that keyword select
160
161
162 Returns: TICKETID, Transaction Object, Error Message
163
164
165 =begin testing
166
167 my $t = RT::Ticket->new($RT::SystemUser);
168
169 ok( $t->Create(Queue => 'General', Subject => 'This is a subject'), "Ticket Created");
170
171 ok ( my $id = $t->Id, "Got ticket id");
172
173 =end testing
174
175 =cut
176
177 sub Create {
178     my $self = shift;
179     
180     my %args = (
181                 Queue => undef,
182                 Requestor => undef,
183                 Cc => undef,
184                 AdminCc => undef,
185                 Type => 'ticket',
186                 Owner => $RT::Nobody->UserObj,
187                 Subject => '[no subject]',
188                 InitialPriority => undef,
189                 FinalPriority => undef,
190                 Status => 'new',
191                 TimeWorked => "0",
192                 TimeLeft => 0,
193                 Due => undef,
194                 Starts => undef,
195                 MIMEObj => undef,
196                 @_);
197
198     my ($ErrStr, $QueueObj, $Owner, $resolved);
199     my (@non_fatal_errors);
200     
201     my $now = RT::Date->new($self->CurrentUser);
202     $now->SetToNow();
203
204     if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) {
205         $QueueObj=RT::Queue->new($RT::SystemUser);
206         $QueueObj->Load($args{'Queue'});
207     }
208     elsif (ref($args{'Queue'}) eq 'RT::Queue') {
209         $QueueObj=RT::Queue->new($RT::SystemUser);
210         $QueueObj->Load($args{'Queue'}->Id);
211     }
212     else {
213         $RT::Logger->debug("$self ". $args{'Queue'} . 
214                          " not a recognised queue object.");
215     }
216   
217     #Can't create a ticket without a queue.
218     unless (defined ($QueueObj)) {
219         $RT::Logger->debug( "$self No queue given for ticket creation.");
220         return (0, 0,'Could not create ticket. Queue not set');
221     }
222     
223     #Now that we have a queue, Check the ACLS
224     unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket',
225                                               QueueObj => $QueueObj )) {
226         return (0,0,"No permission to create tickets in the queue '". 
227                 $QueueObj->Name."'.");
228     }
229     
230     #Since we have a queue, we can set queue defaults
231     #Initial Priority
232
233     # If there's no queue default initial priority and it's not set, set it to 0
234     $args{'InitialPriority'} = ($QueueObj->InitialPriority || 0)
235       unless (defined $args{'InitialPriority'});
236         
237     #Final priority 
238
239     # If there's no queue default final priority and it's not set, set it to 0
240     $args{'FinalPriority'} = ($QueueObj->FinalPriority  || 0)
241       unless (defined $args{'FinalPriority'});
242     
243     
244     #TODO we should see what sort of due date we're getting, rather +
245     # than assuming it's in ISO format.
246     
247     #Set the due date. if we didn't get fed one, use the queue default due in
248     my $due = new RT::Date($self->CurrentUser);
249     if (defined $args{'Due'}) {
250         $due->Set (Format => 'ISO',
251                    Value => $args{'Due'});
252     }   
253     elsif (defined ($QueueObj->DefaultDueIn)) {
254         $due->SetToNow;
255         $due->AddDays($QueueObj->DefaultDueIn);
256     }   
257     
258     my $starts = new RT::Date($self->CurrentUser);
259     if (defined $args{'Starts'}) {
260         $starts->Set (Format => 'ISO',
261                    Value => $args{'Starts'});
262     }
263
264         
265     # {{{ Deal with setting the owner
266     
267     if (ref($args{'Owner'}) eq 'RT::User') {
268         $Owner = $args{'Owner'};
269     }
270     #If we've been handed something else, try to load the user.
271     elsif ($args{'Owner'}) {
272         $Owner = new RT::User($self->CurrentUser);
273         $Owner->Load($args{'Owner'});
274         
275     }
276     #If we can't handle it, call it nobody
277     else {
278         if (ref($args{'Owner'})) {
279             $RT::Logger->warning("$ticket ->Create called with an Owner of ".
280                  "type ".ref($args{'Owner'}) .". Defaulting to nobody.\n");
281
282             push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'.";
283         }
284         else { 
285             $RT::Logger->warning("$self ->Create called with an ".
286                                  "unknown datatype for Owner: ".$args{'Owner'} .
287                                  ". Defaulting to Nobody.\n");
288         }
289     }
290     
291     #If we have a proposed owner and they don't have the right 
292     #to own a ticket, scream about it and make them not the owner
293     if ((defined ($Owner)) and
294         ($Owner->Id != $RT::Nobody->Id) and 
295         (!$Owner->HasQueueRight( QueueObj => $QueueObj, 
296                                  Right => 'OwnTicket'))) {
297         
298         $RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id .
299                              ") was proposed ".
300                              "as a ticket owner but has no rights to own ".
301                              "tickets in this queue\n");
302
303         push @non_fatal_errors, "Invalid owner. Defaulting to 'nobody'.";
304
305         $Owner = undef;
306     }
307     
308     #If we haven't been handed a valid owner, make it nobody.
309     unless (defined ($Owner)) {
310         $Owner = new RT::User($self->CurrentUser);
311         $Owner->Load($RT::Nobody->UserObj->Id);
312     }   
313
314     # }}}
315
316     unless ($self->ValidateStatus($args{'Status'})) {
317         return (0,0,'Invalid value for status');
318     }
319
320     if ($args{'Status'} eq 'resolved') {
321         $resolved = $now->ISO;
322     } else{
323         $resolved = undef;
324     }
325
326     my $id = $self->SUPER::Create(
327                                   Queue => $QueueObj->Id,
328                                   Owner => $Owner->Id,
329                                   Subject => $args{'Subject'},
330                                   InitialPriority => $args{'InitialPriority'},
331                                   FinalPriority => $args{'FinalPriority'},
332                                   Priority => $args{'InitialPriority'},
333                                   Status => $args{'Status'},
334                                   TimeWorked => $args{'TimeWorked'},
335                                   TimeLeft => $args{'TimeLeft'},
336                                   Type => $args{'Type'},        
337                                   Starts => $starts->ISO,
338                                   Resolved => $resolved,
339                                   Due => $due->ISO
340                                  );
341     #Set the ticket's effective ID now that we've created it.
342     my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id);
343     
344     unless ($val) {
345         $RT::Logger->err("$self ->Create couldn't set EffectiveId: $msg\n");
346     }   
347      
348
349     my $watcher;
350     foreach $watcher (@{$args{'Cc'}}) {
351         my ($wval, $wmsg) = 
352           $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1);
353         push @non_fatal_errors, $wmsg   unless ($wval);
354     }   
355
356     foreach $watcher (@{$args{'Requestor'}}) {
357         my ($wval, $wmsg) = 
358           $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1);
359         push @non_fatal_errors, $wmsg   unless ($wval);
360     }
361
362     foreach $watcher (@{$args{'AdminCc'}}) {
363         # Note that we're using AddWatcher, rather than _AddWatcher, as we 
364         # actually _want_ that ACL check. Otherwise, random ticket creators
365         # could make themselves adminccs and maybe get ticket rights. that would
366         # be poor
367         my ($wval, $wmsg) = 
368           $self->AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1);
369         push @non_fatal_errors, $wmsg   unless ($wval);
370     }
371
372     # Iterate through all the KeywordSelect-<int> params passed in, calling _AddKeyword
373     # for each of them
374
375
376     foreach my $key (keys %args) {
377
378         next unless ($key =~ /^KeywordSelect-(.*)$/);
379         
380         my $ks = $1;
381
382
383         my @keywords = ref($args{$key}) eq 'ARRAY' ?
384               @{$args{$key}} : ($args{$key});
385         
386         foreach my $keyword (@keywords) {  
387             my ($kval, $kmsg) = $self->_AddKeyword(KeywordSelect => $ks,
388                                                    Keyword => $keyword,
389                                                    Silent => 1);
390         }       
391         push @non_fatal_errors, $kmsg unless ($kval);
392     }
393
394     
395     
396     #Add a transaction for the create
397     my ($Trans, $Msg, $TransObj) = 
398         $self->_NewTransaction( Type => "Create",
399                                 TimeTaken => 0, 
400                                 MIMEObj=>$args{'MIMEObj'});
401     
402     # Logging
403     if ($self->Id && $Trans) {
404         $ErrStr = "Ticket ".$self->Id . " created in queue '". $QueueObj->Name. 
405           "'.\n" . join("\n", @non_fatal_errors);
406         
407         $RT::Logger->info($ErrStr);
408     }
409     else {
410         # TODO where does this get errstr from?
411         $RT::Logger->warning("Ticket couldn't be created: $ErrStr");
412     }
413     
414     return($self->Id, $TransObj->Id, $ErrStr);
415 }
416
417 # }}}
418
419 # {{{ sub Import
420
421 =head2 Import PARAMHASH
422
423 Import a ticket. 
424 Doesn\'t create a transaction. 
425 Doesn\'t supply queue defaults, etc.
426
427 Arguments are identical to Create(), with the addition of
428     Id -    Ticket Id
429
430 Returns: TICKETID
431
432 =cut
433
434
435 sub Import {
436     my $self = shift;
437     my ( $ErrStr, $QueueObj, $Owner);
438     
439     my %args = (id => undef,
440                 EffectiveId => undef,
441                 Queue => undef,
442                 Requestor => undef,
443                 Type => 'ticket',
444                 Owner => $RT::Nobody->Id,
445                 Subject => '[no subject]',
446                 InitialPriority => undef,
447                 FinalPriority => undef,
448                 Status => 'new',
449                 TimeWorked => "0",
450                 Due => undef,
451                 Created => undef,
452                 Updated => undef,
453         Resolved => undef,
454                 Told => undef,
455                 @_);
456     
457     if ( (defined($args{'Queue'})) && (!ref($args{'Queue'})) ) {
458         $QueueObj=RT::Queue->new($RT::SystemUser);
459         $QueueObj->Load($args{'Queue'});
460         #TODO error check this and return 0 if it\'s not loading properly +++
461     }
462     elsif (ref($args{'Queue'}) eq 'RT::Queue') {
463         $QueueObj=RT::Queue->new($RT::SystemUser);
464         $QueueObj->Load($args{'Queue'}->Id);
465     }
466     else {
467         $RT::Logger->debug("$self ". $args{'Queue'} . 
468                            " not a recognised queue object.");
469     }
470     
471     #Can't create a ticket without a queue.
472     unless (defined ($QueueObj) and $QueueObj->Id) {
473         $RT::Logger->debug( "$self No queue given for ticket creation.");
474         return (0,'Could not create ticket. Queue not set');
475     }
476     
477     #Now that we have a queue, Check the ACLS
478     unless ($self->CurrentUser->HasQueueRight(Right => 'CreateTicket',
479                                               QueueObj => $QueueObj )) {
480         return (0,"No permission to create tickets in the queue '". 
481                 $QueueObj->Name."'.");
482     }
483     
484     
485
486
487     # {{{ Deal with setting the owner
488       
489     # Attempt to take user object, user name or user id.
490     # Assign to nobody if lookup fails.
491     if (defined ($args{'Owner'})) { 
492         if ( ref($args{'Owner'}) ) {
493             $Owner = $args{'Owner'};
494         }
495         else {
496             $Owner = new RT::User($self->CurrentUser);
497             $Owner->Load($args{'Owner'});
498             if ( ! defined($Owner->id) ) {
499                 $Owner->Load($RT::Nobody->id);
500             }
501         }
502     }
503     
504
505     #If we have a proposed owner and they don't have the right 
506     #to own a ticket, scream about it and make them not the owner
507     if ((defined ($Owner)) and
508         ($Owner->Id != $RT::Nobody->Id) and 
509         (!$Owner->HasQueueRight( QueueObj => $QueueObj, 
510                                  Right => 'OwnTicket'))) {
511         
512         $RT::Logger->warning("$self user ".$Owner->Name . "(".$Owner->id .
513                              ") was proposed ".
514                              "as a ticket owner but has no rights to own ".
515                              "tickets in '".$QueueObj->Name."'\n");
516         
517         $Owner = undef;
518     }
519     
520     #If we haven't been handed a valid owner, make it nobody.
521     unless (defined ($Owner)) {
522         $Owner = new RT::User($self->CurrentUser);
523         $Owner->Load($RT::Nobody->UserObj->Id);
524     }   
525
526     # }}}
527
528     unless ($self->ValidateStatus($args{'Status'})) {
529         return (0,"'$args{'Status'}' is an invalid value for status");
530     }
531     
532     $self->{'_AccessibleCache'}{Created} = { 'read'=>1, 'write'=>1 };
533     $self->{'_AccessibleCache'}{Creator} = { 'read'=>1, 'auto'=>1 };
534     $self->{'_AccessibleCache'}{LastUpdated} = { 'read'=>1, 'write'=>1 };
535     $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read'=>1, 'auto'=>1 };
536
537
538     # If we're coming in with an id, set that now.
539     my $EffectiveId = undef;
540     if ($args{'id'}) {
541         $EffectiveId = $args{'id'};
542
543     }
544
545
546     my $id = $self->SUPER::Create(
547                                   id => $args{'id'},
548                                   EffectiveId => $EffectiveId,
549                                   Queue => $QueueObj->Id,
550                                   Owner => $Owner->Id,
551                                   Subject => $args{'Subject'},
552                                   InitialPriority => $args{'InitialPriority'},
553                                   FinalPriority => $args{'FinalPriority'},
554                                   Priority => $args{'InitialPriority'},
555                                   Status => $args{'Status'},
556                                   TimeWorked => $args{'TimeWorked'},
557                                   Type => $args{'Type'},        
558                                   Created => $args{'Created'},
559                                   Told => $args{'Told'},
560                                   LastUpdated => $args{'Updated'},
561         Resolved => $args{Resolved},
562                                   Due => $args{'Due'},
563                                  );
564
565
566
567     # If the ticket didn't have an id
568     # Set the ticket's effective ID now that we've created it.
569     if ($args{'id'} ) { 
570           $self->Load($args{'id'});
571     }
572     else {
573            my ($val, $msg) = $self->__Set(Field => 'EffectiveId', Value => $id);
574     
575            unless ($val) {
576             $RT::Logger->err($self."->Import couldn't set EffectiveId: $msg\n");
577            }    
578     } 
579
580     my $watcher;
581     foreach $watcher (@{$args{'Cc'}}) {
582         $self->_AddWatcher( Type => 'Cc', Person => $watcher, Silent => 1);
583     }   
584     foreach $watcher (@{$args{'AdminCc'}}) {
585         $self->_AddWatcher( Type => 'AdminCc', Person => $watcher, Silent => 1);
586     }   
587     foreach $watcher (@{$args{'Requestor'}}) {
588         $self->_AddWatcher( Type => 'Requestor', Person => $watcher, Silent => 1);
589     }
590     
591     return($self->Id, $ErrStr);
592 }
593
594 # }}}
595
596 # {{{ sub Delete
597
598 sub Delete {
599     my $self = shift;
600     return (0, 'Deleting this object would violate referential integrity.'.
601             ' That\'s bad.');
602 }
603 # }}}
604
605 # {{{ Routines dealing with watchers.
606
607 # {{{ Routines dealing with adding new watchers
608
609 # {{{ sub AddWatcher
610
611 =head2 AddWatcher
612
613 AddWatcher takes a parameter hash. The keys are as follows:
614
615 Email
616 Type
617 Owner
618
619 If the watcher you\'re trying to set has an RT account, set the Owner paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
620
621 =cut
622
623 sub AddWatcher {
624     my $self = shift;
625     my %args = ( Email => undef,
626                  Type => undef,
627                  Owner => undef,
628                  @_
629                );
630
631     # {{{ Check ACLS
632     #If the watcher we're trying to add is for the current user
633     if ( ( $self->CurrentUser->EmailAddress &&
634            ($args{'Email'} eq $self->CurrentUser->EmailAddress) ) or
635            ($args{'Owner'} eq $self->CurrentUser->Id) 
636         ) {
637
638         
639         #  If it's an AdminCc and they don't have 
640         #   'WatchAsAdminCc' or 'ModifyTicket', bail
641         if ($args{'Type'} eq 'AdminCc') {
642             unless ($self->CurrentUserHasRight('ModifyTicket') or 
643                     $self->CurrentUserHasRight('WatchAsAdminCc')) {
644                 return(0, 'Permission Denied');
645             }
646         }
647
648         #  If it's a Requestor or Cc and they don't have
649         #   'Watch' or 'ModifyTicket', bail
650         elsif (($args{'Type'} eq 'Cc') or 
651                ($args{'Type'} eq 'Requestor')) {
652                    
653             unless ($self->CurrentUserHasRight('ModifyTicket') or 
654                     $self->CurrentUserHasRight('Watch')) {
655                 return(0, 'Permission Denied');
656             }
657         }
658         else {
659             $RT::Logger->warn("$self -> AddWatcher hit code".
660                               " it never should. We got passed ".
661                               " a type of ". $args{'Type'});
662             return (0,'Error in parameters to TicketAddWatcher');
663         }
664     }
665     # If the watcher isn't the current user 
666     # and the current user  doesn't have 'ModifyTicket'
667     # bail
668     else {
669         unless ($self->CurrentUserHasRight('ModifyTicket')) {
670             return (0, "Permission Denied");
671         }
672     }
673     # }}}
674
675     return ($self->_AddWatcher(%args));
676 }
677
678
679 #This contains the meat of AddWatcher. but can be called from a routine like
680 # Create, which doesn't need the additional acl check
681 sub _AddWatcher {
682     my $self = shift;
683     my %args = (
684                 Type => undef,
685                 Silent => undef,
686                 Email => undef,
687                 Owner => 0,
688                 Person => undef,
689                 @_ );
690     
691     
692     
693     #clear the watchers cache
694     $self->{'watchers_cache'} = undef;
695     
696     if (defined $args{'Person'}) {
697         #if it's an RT::User object, pull out the id and shove it in Owner
698         if (ref ($args{'Person'}) =~ /RT::User/) {
699             $args{'Owner'} = $args{'Person'}->id;
700         }       
701         #if it's an int, shove it in Owner
702         elsif ($args{'Person'} =~ /^\d+$/) {
703             $args{'Owner'} = $args{'Person'};
704         }
705         #if it's an email address, shove it in Email
706        else {
707            $args{'Email'} = $args{'Person'};
708        }        
709     }   
710
711     # Turn an email address int a watcher if we possibly can.
712     if ($args{'Email'}) {
713         my $watcher = new RT::User($self->CurrentUser);
714         $watcher->LoadByEmail($args{'Email'});
715         if ($watcher->Id) {
716                 $args{'Owner'} = $watcher->Id;
717                 delete $args{'Email'};
718         }
719     }
720
721
722     # see if this user is already a watcher. if we have an owner, check it
723     # otherwise, we've got an email-address watcher. use that.
724
725     if ($self->IsWatcher(Type => $args{'Type'},
726                          Id => ($args{'Owner'} || $args{'Email'}) ) ) {
727
728
729         return(0, 'That user is already that sort of watcher for this ticket');
730     }
731
732     
733     require RT::Watcher;
734     my $Watcher = new RT::Watcher ($self->CurrentUser);
735     my ($retval, $msg) = ($Watcher->Create( Value => $self->Id,
736                                             Scope => 'Ticket',
737                                             Email => $args{'Email'},
738                                             Type => $args{'Type'},
739                                             Owner => $args{'Owner'},
740                                           ));
741     
742     unless ($args{'Silent'}) {
743         $self->_NewTransaction( Type => 'AddWatcher',
744                                 NewValue => $Watcher->Email,
745                                 Field => $Watcher->Type);
746     }
747     
748     return ($retval, $msg);
749 }
750
751 # }}}
752
753 # {{{ sub AddRequestor
754
755 =head2 AddRequestor
756
757 AddRequestor takes what AddWatcher does, except it presets
758 the "Type" parameter to \'Requestor\'
759
760 =cut
761
762 sub AddRequestor {
763    my $self = shift;
764    return ($self->AddWatcher ( Type => 'Requestor', @_));
765 }
766
767 # }}}
768
769 # {{{ sub AddCc
770
771 =head2 AddCc
772
773 AddCc takes what AddWatcher does, except it presets
774 the "Type" parameter to \'Cc\'
775
776 =cut
777
778 sub AddCc {
779    my $self = shift;
780    return ($self->AddWatcher ( Type => 'Cc', @_));
781 }
782 # }}}
783         
784 # {{{ sub AddAdminCc
785
786 =head2 AddAdminCc
787
788 AddAdminCc takes what AddWatcher does, except it presets
789 the "Type" parameter to \'AdminCc\'
790
791 =cut
792
793 sub AddAdminCc {
794    my $self = shift;
795    return ($self->AddWatcher ( Type => 'AdminCc', @_));
796 }
797
798 # }}}
799
800 # }}}
801
802 # {{{ sub DeleteWatcher
803
804 =head2 DeleteWatcher id [type]
805
806 DeleteWatcher takes a single argument which is either an email address 
807 or a watcher id.  
808 If the first argument is an email address, you need to specify the watcher type you're talking
809 about as the second argument. Valid values are 'Requestor', 'Cc' or 'AdminCc'.
810 It removes that watcher from this Ticket\'s list of watchers.
811
812
813 =cut
814
815 #TODO It is lame that you can't call this the same way you can call AddWatcher
816
817 sub DeleteWatcher {
818     my $self = shift;
819     my $id = shift;
820
821     my $type;
822     
823     $type = shift if (@_);
824     
825     my $Watcher = new RT::Watcher($self->CurrentUser);
826     
827     #If it\'s a numeric watcherid
828     if ($id =~ /^(\d*)$/) {
829         $Watcher->Load($id);
830     }
831     
832     #Otherwise, we'll assume it's an email address
833     elsif ($type) {
834         my ($result, $msg) = 
835           $Watcher->LoadByValue( Email => $id,
836                                  Scope => 'Ticket',
837                                  Value => $self->id,
838                                  Type => $type);
839         return (0,$msg) unless ($result);
840     }
841     
842     else {
843         return(0,"Can\'t delete a watcher by email address without specifying a type");
844     }
845     
846     # {{{ Check ACLS 
847
848     #If the watcher we're trying to delete is for the current user
849     if ($Watcher->Email eq $self->CurrentUser->EmailAddress) {
850                 
851         #  If it's an AdminCc and they don't have 
852         #   'WatchAsAdminCc' or 'ModifyTicket', bail
853         if ($Watcher->Type eq 'AdminCc') {
854             unless ($self->CurrentUserHasRight('ModifyTicket') or 
855                     $self->CurrentUserHasRight('WatchAsAdminCc')) {
856                 return(0, 'Permission Denied');
857             }
858         }
859
860         #  If it's a Requestor or Cc and they don't have
861         #   'Watch' or 'ModifyTicket', bail
862         elsif (($Watcher->Type eq 'Cc') or 
863                ($Watcher->Type eq 'Requestor')) {
864                    
865             unless ($self->CurrentUserHasRight('ModifyTicket') or 
866                     $self->CurrentUserHasRight('Watch')) {
867                 return(0, 'Permission Denied');
868             }
869         }
870         else {
871             $RT::Logger->warn("$self -> DeleteWatcher hit code".
872                               " it never should. We got passed ".
873                               " a type of ". $args{'Type'});
874             return (0,'Error in parameters to $self DeleteWatcher');
875         }
876     }
877     # If the watcher isn't the current user 
878     # and the current user  doesn't have 'ModifyTicket'
879     # bail
880     else {
881         unless ($self->CurrentUserHasRight('ModifyTicket')) {
882             return (0, "Permission Denied");
883         }
884     }   
885     
886     # }}}
887     
888     unless (($Watcher->Scope eq 'Ticket') and
889             ($Watcher->Value == $self->id) ) {
890         return (0, "Not a watcher for this ticket");
891     }
892
893
894     #Clear out the watchers hash.
895     $self->{'watchers'} = undef;
896     
897     #If we\'ve validated that it is a watcher for this ticket 
898     $self->_NewTransaction ( Type => 'DelWatcher',        
899                              OldValue => $Watcher->Email,
900                              Field => $Watcher->Type,
901                            );
902     
903     my $retval = $Watcher->Delete();
904     
905     unless ($retval) {
906         return(0,"Watcher could not be deleted. Database inconsistency possible.");
907     }
908     
909     return(1, "Watcher deleted");
910 }
911
912 # {{{ sub DeleteRequestor
913
914 =head2 DeleteRequestor EMAIL
915
916 Takes an email address. It calls DeleteWatcher with a preset 
917 type of 'Requestor'
918
919
920 =cut
921
922 sub DeleteRequestor {
923    my $self = shift;
924    my $id = shift;
925    return ($self->DeleteWatcher ($id, 'Requestor'))
926 }
927
928 # }}}
929
930 # {{{ sub DeleteCc
931
932 =head2 DeleteCc EMAIL
933
934 Takes an email address. It calls DeleteWatcher with a preset 
935 type of 'Cc'
936
937
938 =cut
939
940 sub DeleteCc {
941    my $self = shift;
942    my $id = shift;
943    return ($self->DeleteWatcher ($id, 'Cc'))
944 }
945
946 # }}}
947
948 # {{{ sub DeleteAdminCc
949
950 =head2 DeleteAdminCc EMAIL
951
952 Takes an email address. It calls DeleteWatcher with a preset 
953 type of 'AdminCc'
954
955
956 =cut
957
958 sub DeleteAdminCc {
959    my $self = shift;
960    my $id = shift;
961    return ($self->DeleteWatcher ($id, 'AdminCc'))
962 }
963
964 # }}}
965
966
967 # }}}
968
969 # {{{ sub Watchers
970
971 =head2 Watchers
972
973 Watchers returns a Watchers object preloaded with this ticket\'s watchers.
974
975 # It should return only the ticket watchers. the actual FooAsString
976 # methods capture the queue watchers too. I don't feel thrilled about this,
977 # but we don't want the Cc Requestors and AdminCc objects to get filled up
978 # with all the queue watchers too. we've got seperate objects for that.
979   # should we rename these as s/(.*)AsString/$1Addresses/ or somesuch?
980
981 =cut
982
983 sub Watchers {
984   my $self = shift;
985   
986   require RT::Watchers;
987   my $watchers=RT::Watchers->new($self->CurrentUser);
988   if ($self->CurrentUserHasRight('ShowTicket')) {
989       $watchers->LimitToTicket($self->id);
990   }
991   
992   return($watchers);
993   
994 }
995
996 # }}}
997
998 # {{{ a set of  [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
999
1000 =head2 RequestorsAsString
1001
1002  B<Returns> String: All Ticket Requestor email addresses as a string.
1003
1004 =cut
1005
1006 sub RequestorsAsString {
1007     my $self=shift;
1008
1009     unless ($self->CurrentUserHasRight('ShowTicket')) {
1010         return undef;
1011     }
1012     
1013     return ($self->Requestors->EmailsAsString() );
1014 }
1015
1016 =head2 WatchersAsString
1017
1018 B<Returns> String: All Ticket Watchers email addresses as a string
1019
1020 =cut
1021
1022 sub WatchersAsString {
1023     my $self=shift;
1024
1025     unless ($self->CurrentUserHasRight('ShowTicket')) {
1026         return (0, "Permission Denied");
1027     }
1028     
1029     return ($self->Watchers->EmailsAsString());
1030
1031 }
1032
1033 =head2 AdminCcAsString
1034
1035 returns String: All Ticket AdminCc email addresses as a string
1036
1037 =cut
1038
1039
1040 sub AdminCcAsString {
1041     my $self=shift;
1042
1043     unless ($self->CurrentUserHasRight('ShowTicket')) {
1044         return undef;
1045     }
1046     
1047     return ($self->AdminCc->EmailsAsString());
1048     
1049 }
1050
1051 =head2 CcAsString
1052
1053 returns String: All Ticket Ccs as a string of email addresses
1054
1055 =cut
1056
1057 sub CcAsString {
1058     my $self=shift;
1059
1060     unless ($self->CurrentUserHasRight('ShowTicket')) {
1061         return undef; 
1062     }
1063     
1064     return ($self->Cc->EmailsAsString());
1065
1066 }
1067
1068 # }}}
1069
1070 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1071
1072 # {{{ sub Requestors
1073
1074 =head2 Requestors
1075
1076 Takes nothing.
1077 Returns this ticket's Requestors as an RT::Watchers object
1078
1079 =cut
1080
1081 sub Requestors {
1082     my $self = shift;
1083     
1084     my $requestors = $self->Watchers();
1085     if ($self->CurrentUserHasRight('ShowTicket')) {
1086         $requestors->LimitToRequestors();
1087     }   
1088     
1089     return($requestors);
1090     
1091 }
1092
1093 # }}}
1094
1095 # {{{ sub Cc
1096
1097 =head2 Cc
1098
1099 Takes nothing.
1100 Returns a watchers object which contains this ticket's Cc watchers
1101
1102 =cut
1103
1104 sub Cc {
1105     my $self = shift;
1106     
1107     my $cc = $self->Watchers();
1108     
1109     if ($self->CurrentUserHasRight('ShowTicket')) {
1110         $cc->LimitToCc();
1111     }
1112     
1113     return($cc);
1114     
1115 }
1116
1117 # }}}
1118
1119 # {{{ sub AdminCc
1120
1121 =head2 AdminCc
1122
1123 Takes nothing.
1124 Returns this ticket\'s administrative Ccs as an RT::Watchers object
1125
1126 =cut
1127
1128 sub AdminCc {
1129     my $self = shift;
1130     
1131     my $admincc = $self->Watchers();
1132     if ($self->CurrentUserHasRight('ShowTicket')) {
1133         $admincc->LimitToAdminCc();
1134     }
1135     return($admincc);
1136 }
1137
1138 # }}}
1139
1140 # }}}
1141
1142 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1143
1144 # {{{ sub IsWatcher
1145 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1146
1147 =head2 IsWatcher
1148
1149 Takes a param hash with the attributes Type and User. User is either a user object or string containing an email address. Returns true if that user or string
1150 is a ticket watcher. Returns undef otherwise
1151
1152 =cut
1153
1154 sub IsWatcher {
1155     my $self = shift;
1156
1157     my %args = ( Type => 'Requestor',
1158                  Email => undef,
1159                  Id => undef,
1160                  @_
1161                );
1162     
1163     my %cols = ('Type' => $args{'Type'},
1164                 'Scope' => 'Ticket',
1165                 'Value' => $self->Id,
1166                 'Owner' => undef,
1167                 'Email' => undef
1168                );
1169     
1170     if (ref($args{'Id'})){ 
1171         #If it's a ref, it's an RT::User object;
1172         $cols{'Owner'} = $args{'Id'}->Id;
1173     }
1174     elsif ($args{'Id'} =~ /^\d+$/) { 
1175         # if it's an integer, it's a reference to an RT::User obj
1176         $cols{'Owner'} = $args{'Id'};
1177     }
1178     else {
1179         $cols{'Email'} = $args{'Id'};
1180     }   
1181     
1182     if ($args{'Email'}) {
1183         $cols{'Email'} = $args{'Email'};
1184     }
1185
1186     my $description = join(":",%cols);
1187     
1188     #If we've cached a positive match...
1189     if (defined $self->{'watchers_cache'}->{"$description"}) {
1190         if ($self->{'watchers_cache'}->{"$description"} == 1) {
1191             return(1);
1192         }
1193         else { #If we've cached a negative match...
1194             return(undef);
1195         }
1196     }
1197     
1198     
1199     my $watcher = new RT::Watcher($self->CurrentUser);
1200     $watcher->LoadByCols(%cols);
1201     
1202     
1203     if ($watcher->id) {
1204         $self->{'watchers_cache'}->{"$description"} = 1;
1205         return(1);
1206     }   
1207     else {
1208         $self->{'watchers_cache'}->{"$description"} = 0;
1209         return(undef);
1210     }
1211     
1212 }
1213 # }}}
1214
1215 # {{{ sub IsRequestor
1216
1217 =head2 IsRequestor
1218   
1219   Takes an email address, RT::User object or integer (RT user id)
1220   Returns true if the string is a requestor of the current ticket.
1221
1222
1223 =cut
1224
1225 sub IsRequestor {
1226     my $self = shift;
1227     my $person = shift;
1228
1229     return ($self->IsWatcher(Type => 'Requestor', Id => $person));
1230             
1231 };
1232
1233 # }}}
1234
1235 # {{{ sub IsCc
1236
1237 =head2 IsCc
1238
1239 Takes a string. Returns true if the string is a Cc watcher of the current ticket.
1240
1241 =cut
1242
1243 sub IsCc {
1244   my $self = shift;
1245   my $cc = shift;
1246   
1247   return ($self->IsWatcher( Type => 'Cc', Id => $cc ));
1248   
1249 }
1250
1251 # }}}
1252
1253 # {{{ sub IsAdminCc
1254
1255 =head2 IsAdminCc
1256
1257 Takes a string. Returns true if the string is an AdminCc watcher of the current ticket.
1258
1259 =cut
1260
1261 sub IsAdminCc {
1262   my $self = shift;
1263   my $person = shift;
1264   
1265   return ($self->IsWatcher( Type => 'AdminCc', Id => $person ));
1266   
1267 }
1268
1269 # }}}
1270
1271 # {{{ sub IsOwner
1272
1273 =head2 IsOwner
1274
1275   Takes an RT::User object. Returns true if that user is this ticket's owner.
1276 returns undef otherwise
1277
1278 =cut
1279
1280 sub IsOwner {
1281     my $self = shift;
1282     my $person = shift;
1283   
1284
1285     # no ACL check since this is used in acl decisions
1286     # unless ($self->CurrentUserHasRight('ShowTicket')) {
1287     #   return(undef);
1288     #   }       
1289
1290     
1291     #Tickets won't yet have owners when they're being created.
1292     unless ($self->OwnerObj->id) {
1293         return(undef);
1294     }
1295     
1296     if ($person->id == $self->OwnerObj->id) {
1297         return(1);
1298     }
1299     else {
1300         return(undef);
1301     }
1302 }
1303
1304
1305 # }}}
1306
1307 # }}}
1308
1309 # }}}
1310
1311 # {{{ Routines dealing with queues 
1312
1313 # {{{ sub ValidateQueue
1314
1315 sub ValidateQueue {
1316   my $self = shift;
1317   my $Value = shift;
1318   
1319   #TODO I don't think this should be here. We shouldn't allow anything to have an undef queue,
1320   if (!$Value) {
1321     $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1322     return (1);
1323   }
1324   
1325   my $QueueObj = RT::Queue->new($self->CurrentUser);
1326   my $id = $QueueObj->Load($Value);
1327   
1328   if ($id) {
1329     return (1);
1330   }
1331   else {
1332     return (undef);
1333   }
1334 }
1335
1336 # }}}
1337
1338 # {{{ sub SetQueue  
1339
1340 sub SetQueue {
1341     my $self = shift;
1342     my $NewQueue = shift;
1343
1344     #Redundant. ACL gets checked in _Set;
1345     unless ($self->CurrentUserHasRight('ModifyTicket')) {
1346         return (0, "Permission Denied");
1347     }
1348    
1349  
1350     my $NewQueueObj = RT::Queue->new($self->CurrentUser);
1351     $NewQueueObj->Load($NewQueue);
1352     
1353     unless ($NewQueueObj->Id()) {
1354         return (0, "That queue does not exist");
1355     }
1356     
1357     if ($NewQueueObj->Id == $self->QueueObj->Id) {
1358         return (0, 'That is the same value');
1359     }
1360     unless ($self->CurrentUser->HasQueueRight(Right =>'CreateTicket',
1361                                               QueueObj => $NewQueueObj )) {
1362         return (0, "You may not create requests in that queue.");
1363     }
1364     
1365     unless ($self->OwnerObj->HasQueueRight(Right=> 'OwnTicket',  
1366                                            QueueObj => $NewQueueObj)) {
1367             $self->Untake();
1368     }
1369
1370     return($self->_Set(Field => 'Queue', Value => $NewQueueObj->Id()));
1371     
1372 }
1373
1374 # }}}
1375
1376 # {{{ sub QueueObj
1377
1378 =head2 QueueObj
1379
1380 Takes nothing. returns this ticket's queue object
1381
1382 =cut
1383
1384 sub QueueObj {
1385     my $self = shift;
1386     
1387     my $queue_obj = RT::Queue->new($self->CurrentUser);
1388     #We call __Value so that we can avoid the ACL decision and some deep recursion
1389     my ($result) = $queue_obj->Load($self->__Value('Queue'));
1390     return ($queue_obj);
1391 }
1392
1393
1394 # }}}
1395
1396 # }}}
1397
1398 # {{{ Date printing routines
1399
1400 # {{{ sub DueObj
1401
1402 =head2 DueObj
1403
1404   Returns an RT::Date object containing this ticket's due date
1405
1406 =cut
1407 sub DueObj {
1408     my $self = shift;
1409     
1410     my $time = new RT::Date($self->CurrentUser);
1411
1412     # -1 is RT::Date slang for never
1413     if ($self->Due) {
1414         $time->Set(Format => 'sql', Value => $self->Due );
1415     }
1416     else {
1417         $time->Set(Format => 'unix', Value => -1);
1418     }
1419     
1420     return $time;
1421 }
1422 # }}}
1423
1424 # {{{ sub DueAsString 
1425
1426 =head2 DueAsString
1427
1428 Returns this ticket's due date as a human readable string
1429
1430 =cut
1431
1432 sub DueAsString {
1433   my $self = shift;
1434   return $self->DueObj->AsString();
1435 }
1436
1437 # }}}
1438
1439 # {{{ sub GraceTimeAsString 
1440
1441 =head2 GraceTimeAsString
1442
1443 Return the time until this ticket is due as a string
1444
1445 =cut
1446
1447 # TODO This should be deprecated 
1448
1449 sub GraceTimeAsString {
1450     my $self=shift;
1451     
1452     if ($self->Due) {
1453         return ($self->DueObj->AgeAsString());
1454     } else {
1455         return "";
1456     }
1457 }
1458
1459 # }}}
1460
1461
1462 # {{{ sub ResolvedObj
1463
1464 =head2 ResolvedObj
1465
1466   Returns an RT::Date object of this ticket's 'resolved' time.
1467
1468 =cut
1469
1470 sub ResolvedObj {
1471   my $self = shift;
1472
1473   my $time = new RT::Date($self->CurrentUser);
1474   $time->Set(Format => 'sql', Value => $self->Resolved);
1475   return $time;
1476 }
1477 # }}}
1478
1479 # {{{ sub SetStarted
1480
1481 =head2 SetStarted
1482
1483 Takes a date in ISO format or undef
1484 Returns a transaction id and a message
1485 The client calls "Start" to note that the project was started on the date in $date.
1486 A null date means "now"
1487
1488 =cut
1489   
1490 sub SetStarted {
1491     my $self = shift;
1492     my $time = shift || 0;
1493     
1494
1495     unless ($self->CurrentUserHasRight('ModifyTicket')) {
1496         return (0, "Permission Denied");
1497     }
1498
1499     #We create a date object to catch date weirdness
1500     my $time_obj = new RT::Date($self->CurrentUser());
1501     if ($time != 0)  {
1502         $time_obj->Set(Format => 'ISO', Value => $time);
1503     }
1504     else {
1505         $time_obj->SetToNow();
1506     }
1507     
1508     #Now that we're starting, open this ticket
1509     #TODO do we really want to force this as policy? it should be a scrip
1510     
1511     #We need $TicketAsSystem, in case the current user doesn't have
1512     #ShowTicket
1513     #
1514     my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1515     $TicketAsSystem->Load($self->Id);   
1516     if ($TicketAsSystem->Status eq 'new') {
1517         $TicketAsSystem->Open();
1518     }   
1519     
1520     return ($self->_Set(Field => 'Started', Value =>$time_obj->ISO));
1521     
1522 }
1523
1524 # }}}
1525
1526 # {{{ sub StartedObj
1527
1528 =head2 StartedObj
1529
1530   Returns an RT::Date object which contains this ticket's 
1531 'Started' time.
1532
1533 =cut
1534
1535
1536 sub StartedObj {
1537     my $self = shift;
1538     
1539     my $time = new RT::Date($self->CurrentUser);
1540     $time->Set(Format => 'sql', Value => $self->Started);
1541     return $time;
1542 }
1543 # }}}
1544
1545 # {{{ sub StartsObj
1546
1547 =head2 StartsObj
1548
1549   Returns an RT::Date object which contains this ticket's 
1550 'Starts' time.
1551
1552 =cut
1553
1554 sub StartsObj {
1555   my $self = shift;
1556   
1557   my $time = new RT::Date($self->CurrentUser);
1558   $time->Set(Format => 'sql', Value => $self->Starts);
1559   return $time;
1560 }
1561 # }}}
1562
1563 # {{{ sub ToldObj
1564
1565 =head2 ToldObj
1566
1567   Returns an RT::Date object which contains this ticket's 
1568 'Told' time.
1569
1570 =cut
1571
1572
1573 sub ToldObj {
1574   my $self = shift;
1575   
1576   my $time = new RT::Date($self->CurrentUser);
1577   $time->Set(Format => 'sql', Value => $self->Told);
1578   return $time;
1579 }
1580
1581 # }}}
1582
1583 # {{{ sub LongSinceToldAsString
1584
1585 # TODO this should be deprecated
1586
1587
1588 sub LongSinceToldAsString {
1589   my $self = shift;
1590
1591   if ($self->Told) {
1592       return $self->ToldObj->AgeAsString();
1593   } else {
1594       return "Never";
1595   }
1596 }
1597 # }}}
1598
1599 # {{{ sub ToldAsString
1600
1601 =head2 ToldAsString
1602
1603 A convenience method that returns ToldObj->AsString
1604
1605 TODO: This should be deprecated
1606
1607 =cut
1608
1609
1610 sub ToldAsString {
1611     my $self = shift;
1612     if ($self->Told) {
1613         return $self->ToldObj->AsString();
1614     }
1615     else {
1616         return("Never");
1617     }
1618 }
1619 # }}}
1620
1621 # {{{ sub TimeWorkedAsString
1622
1623 =head2 TimeWorkedAsString
1624
1625 Returns the amount of time worked on this ticket as a Text String
1626
1627 =cut
1628
1629 sub TimeWorkedAsString {
1630     my $self=shift;
1631     return "0" unless $self->TimeWorked;
1632     
1633     #This is not really a date object, but if we diff a number of seconds 
1634     #vs the epoch, we'll get a nice description of time worked.
1635     
1636     my $worked = new RT::Date($self->CurrentUser);
1637     #return the  #of minutes worked turned into seconds and written as
1638     # a simple text string
1639
1640     return($worked->DurationAsString($self->TimeWorked*60));
1641 }
1642
1643 # }}}
1644
1645
1646 # }}}
1647
1648 # {{{ Routines dealing with correspondence/comments
1649
1650 # {{{ sub Comment
1651
1652 =head2 Comment
1653
1654 Comment on this ticket.
1655 Takes a hashref with the follwoing attributes:
1656
1657 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo
1658
1659 =cut
1660
1661 sub Comment {
1662   my $self = shift;
1663   
1664   my %args = (
1665           CcMessageTo => undef,
1666           BccMessageTo => undef,
1667               MIMEObj => undef,
1668               TimeTaken => 0,
1669               @_ );
1670
1671   unless (($self->CurrentUserHasRight('CommentOnTicket')) or
1672           ($self->CurrentUserHasRight('ModifyTicket'))) {
1673       return (0, "Permission Denied");
1674   }
1675  
1676    unless ($args{'MIMEObj'}) {
1677        return(0,"No correspondence attached");
1678    }
1679
1680   # If we've been passed in CcMessageTo and BccMessageTo fields,
1681   # add them to the mime object for passing on to the transaction handler
1682   # The "NotifyOtherRecipients" scripAction will look for RT--Send-Cc: and
1683   # RT-Send-Bcc: headers
1684  
1685   $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'});
1686   $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'});
1687
1688   #Record the correspondence (write the transaction)
1689   my ($Trans, $Msg, $TransObj) = $self->_NewTransaction( Type => 'Comment',
1690                                       Data =>($args{'MIMEObj'}->head->get('subject') || 'No Subject'),
1691                                       TimeTaken => $args{'TimeTaken'},
1692                                       MIMEObj => $args{'MIMEObj'}
1693                                     );
1694   
1695   
1696   return ($Trans, "The comment has been recorded");
1697 }
1698
1699 # }}}
1700
1701 # {{{ sub Correspond
1702
1703 =head2 Correspond
1704
1705 Correspond on this ticket.
1706 Takes a hashref with the following attributes:
1707
1708
1709 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo
1710
1711 =cut
1712
1713 sub Correspond {
1714     my $self = shift;
1715     my %args = (
1716           CcMessageTo => undef,
1717           BccMessageTo => undef,
1718                 MIMEObj => undef,
1719                 TimeTaken => 0,
1720                 @_ );
1721     
1722     unless (($self->CurrentUserHasRight('ReplyToTicket')) or
1723             ($self->CurrentUserHasRight('ModifyTicket'))) {
1724         return (0, "Permission Denied");
1725     }
1726     
1727     unless ($args{'MIMEObj'}) {
1728         return(0,"No correspondence attached");
1729     }
1730     
1731   # If we've been passed in CcMessageTo and BccMessageTo fields,
1732   # add them to the mime object for passing on to the transaction handler
1733   # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
1734   # headers
1735  
1736   $args{'MIMEObj'}->head->add('RT-Send-Cc', $args{'CcMessageTo'});
1737   $args{'MIMEObj'}->head->add('RT-Send-Bcc', $args{'BccMessageTo'});
1738
1739     #Record the correspondence (write the transaction)
1740     my ($Trans,$msg, $TransObj) = $self->_NewTransaction
1741       (Type => 'Correspond',
1742        Data => ($args{'MIMEObj'}->head->get('subject') || 'No Subject'),
1743        TimeTaken => $args{'TimeTaken'},
1744        MIMEObj=> $args{'MIMEObj'}     
1745       );
1746     
1747     # TODO this bit of logic should really become a scrip for 2.2
1748     my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
1749     $TicketAsSystem->Load($self->Id);
1750         
1751     if (
1752         ($TicketAsSystem->Status ne 'open') and
1753         ($TicketAsSystem->Status ne 'new')
1754        ) {
1755         
1756         my $oldstatus = $TicketAsSystem->Status();
1757         $TicketAsSystem->__Set(Field => 'Status', Value => 'open');
1758         $TicketAsSystem->_NewTransaction 
1759           ( Type => 'Set',
1760             Field => 'Status',
1761             OldValue => $oldstatus,
1762             NewValue => 'open',
1763             Data => 'Ticket auto-opened on incoming correspondence'
1764           );
1765     }
1766     
1767     unless ($Trans) {
1768         $RT::Logger->err("$self couldn't init a transaction ($msg)\n");
1769         return ($Trans, "correspondence (probably) not sent", $args{'MIMEObj'});
1770     }
1771     
1772     #Set the last told date to now if this isn't mail from the requestor.
1773     #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
1774     
1775     unless ($TransObj->IsInbound) {
1776         $self->_SetTold;
1777     }
1778     
1779     return ($Trans, "correspondence sent");
1780 }
1781
1782 # }}}
1783
1784 # }}}
1785
1786 # {{{ Routines dealing with Links and Relations between tickets
1787
1788 # {{{ Link Collections
1789
1790 # {{{ sub Members
1791
1792 =head2 Members
1793
1794   This returns an RT::Links object which references all the tickets 
1795 which are 'MembersOf' this ticket
1796
1797 =cut
1798
1799 sub Members {
1800    my $self = shift;
1801    return ($self->_Links('Target', 'MemberOf'));
1802 }
1803
1804 # }}}
1805
1806 # {{{ sub MemberOf
1807
1808 =head2 MemberOf
1809
1810   This returns an RT::Links object which references all the tickets that this
1811 ticket is a 'MemberOf'
1812
1813 =cut
1814
1815 sub MemberOf {
1816    my $self = shift;
1817    return ($self->_Links('Base', 'MemberOf'));
1818 }
1819
1820 # }}}
1821
1822 # {{{ RefersTo
1823
1824 =head2 RefersTo
1825
1826   This returns an RT::Links object which shows all references for which this ticket is a base
1827
1828 =cut
1829
1830 sub RefersTo {
1831     my $self = shift;
1832     return ($self->_Links('Base', 'RefersTo'));
1833 }
1834
1835 # }}}
1836
1837 # {{{ ReferredToBy
1838
1839 =head2 ReferredToBy
1840
1841   This returns an RT::Links object which shows all references for which this ticket is a target
1842
1843 =cut
1844
1845 sub ReferredToBy {
1846     my $self = shift;
1847     return ($self->_Links('Target', 'RefersTo'));
1848 }
1849
1850 # }}}
1851
1852 # {{{ DependedOnBy
1853
1854 =head2 DependedOnBy
1855
1856   This returns an RT::Links object which references all the tickets that depend on this one
1857
1858 =cut
1859 sub DependedOnBy {
1860     my $self = shift;
1861     return ($self->_Links('Target','DependsOn'));
1862 }
1863
1864 # }}}
1865
1866 # {{{ DependsOn
1867
1868 =head2 DependsOn
1869
1870   This returns an RT::Links object which references all the tickets that this ticket depends on
1871
1872 =cut
1873 sub DependsOn {
1874    my $self = shift;
1875     return ($self->_Links('Base','DependsOn'));
1876 }
1877
1878 # }}}
1879
1880 # {{{ sub _Links 
1881
1882 sub _Links {
1883     my $self = shift;
1884     
1885     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
1886     #tobias meant by $f
1887     my $field = shift;
1888     my $type =shift || "";
1889
1890     unless ($self->{"$field$type"}) {
1891         $self->{"$field$type"} = new RT::Links($self->CurrentUser);
1892         if ($self->CurrentUserHasRight('ShowTicket')) {
1893             
1894             $self->{"$field$type"}->Limit(FIELD=>$field, VALUE=>$self->URI);
1895             $self->{"$field$type"}->Limit(FIELD=>'Type', 
1896                                           VALUE=>$type) if ($type);
1897         }
1898     }
1899     return ($self->{"$field$type"});
1900 }
1901
1902 # }}}
1903
1904 # }}}
1905
1906
1907 # {{{ sub DeleteLink 
1908
1909 =head2 DeleteLink
1910
1911 Delete a link. takes a paramhash of Base, Target and Type.
1912 Either Base or Target must be null. The null value will 
1913 be replaced with this ticket\'s id
1914
1915 =cut 
1916
1917 sub DeleteLink {
1918     my $self = shift;
1919     my %args = ( Base =>  undef,
1920                  Target => undef,
1921                  Type => undef,
1922                  @_ );
1923     
1924     #check acls
1925     unless ($self->CurrentUserHasRight('ModifyTicket')) {
1926         $RT::Logger->debug("No permission to delete links\n"); 
1927         return (0, 'Permission Denied');
1928
1929     
1930     }
1931     
1932     #we want one of base and target. we don't care which
1933     #but we only want _one_
1934
1935     if ($args{'Base'} and $args{'Target'}) {
1936         $RT::Logger->debug("$self ->_DeleteLink. got both Base and Target\n");
1937         return (0, 'Can\'t specifiy both base and target');
1938     }
1939     elsif ($args{'Base'}) {
1940         $args{'Target'} = $self->Id();
1941     }
1942     elsif ($args{'Target'}) {
1943         $args{'Base'} = $self->Id();
1944     }
1945     else {  
1946         $RT::Logger->debug("$self: Base or Target must be specified\n");
1947         return (0, 'Either base or target must be specified');
1948     }
1949      
1950     my $link = new RT::Link($self->CurrentUser);
1951     $RT::Logger->debug("Trying to load link: ". $args{'Base'}." ". $args{'Type'}. " ". $args{'Target'}. "\n");
1952     
1953     $link->Load($args{'Base'}, $args{'Type'}, $args{'Target'});
1954     
1955     
1956     
1957     #it's a real link. 
1958     if ($link->id) {
1959         $RT::Logger->debug("We're going to delete link ".$link->id."\n");
1960         $link->Delete();
1961
1962         my $TransString=
1963           "Ticket $args{'Base'} no longer $args{Type} ticket $args{'Target'}.";
1964         my ($Trans, $Msg, $TransObj) = $self->_NewTransaction
1965           (Type => 'DeleteLink',
1966            Field => $args{'Type'},
1967            Data => $TransString,
1968            TimeTaken => 0
1969           );
1970         
1971         return ($linkid, "Link deleted ($TransString)", $transactionid);
1972     }
1973     #if it's not a link we can find
1974     else {
1975         $RT::Logger->debug("Couldn't find that link\n");
1976         return (0, "Link not found");
1977     }
1978 }
1979
1980 # }}}
1981
1982 # {{{ sub AddLink
1983
1984 =head2 AddLink
1985
1986 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
1987
1988
1989 =cut
1990
1991 sub AddLink {
1992     my $self = shift;
1993     my %args = ( Target => '',
1994                  Base => '',
1995                  Type => '',
1996                  @_ );
1997     
1998     unless ($self->CurrentUserHasRight('ModifyTicket')) {
1999         return (0, "Permission Denied");
2000     }
2001     
2002     if ($args{'Base'} and $args{'Target'}) {
2003         $RT::Logger->debug("$self tried to delete a link. both base and target were specified\n");
2004         return (0, 'Can\'t specifiy both base and target');
2005     }
2006     elsif ($args{'Base'}) {
2007         $args{'Target'} = $self->Id();
2008     }
2009     elsif ($args{'Target'}) {
2010         $args{'Base'} = $self->Id();
2011     }
2012     else {  
2013         return (0, 'Either base or target must be specified');
2014     }
2015     
2016     # {{{ We don't want references to ourself
2017     if ($args{Base} eq $args{Target}) {
2018         return (0, "Can\'t link a ticket to itself");
2019     }   
2020                 
2021     # }}}
2022     
2023     # If the base isn't a URI, make it a URI. 
2024     # If the target isn't a URI, make it a URI. 
2025         
2026     # {{{ Check if the link already exists - we don't want duplicates
2027     my $old_link= new RT::Link ($self->CurrentUser);
2028     $old_link->Load($args{'Base'}, $args{'Type'}, $args{'Target'});
2029     if ($old_link->Id) {
2030         $RT::Logger->debug("$self Somebody tried to duplicate a link");
2031         return ($old_link->id, "Link already exists",0);
2032     }
2033     # }}}
2034     
2035     # Storing the link in the DB.
2036     my $link = RT::Link->new($self->CurrentUser);
2037     my ($linkid) = $link->Create(Target => $args{Target}, 
2038                                  Base => $args{Base}, 
2039                                  Type => $args{Type});
2040     
2041     unless ($linkid) {
2042         return (0,"Link could not be created");
2043     }
2044         #Write the transaction
2045     
2046     my $TransString="Ticket $args{'Base'} $args{Type} ticket $args{'Target'}.";
2047     
2048     my ($Trans, $Msg, $TransObj) = $self->_NewTransaction
2049       (Type => 'AddLink',
2050        Field => $args{'Type'},
2051        Data => $TransString,
2052        TimeTaken => 0
2053       );
2054     
2055     return ($Trans, "Link created ($TransString)");
2056         
2057         
2058 }
2059 # }}}
2060
2061 # {{{ sub URI 
2062
2063 =head2 URI
2064
2065 Returns this ticket's URI
2066
2067 =cut
2068
2069 sub URI {
2070     my $self = shift;
2071     return $RT::TicketBaseURI.$self->id;
2072 }
2073
2074 # }}}
2075
2076 # {{{ sub MergeInto
2077
2078 =head2 MergeInto
2079 MergeInto take the id of the ticket to merge this ticket into.
2080
2081 =cut
2082
2083 sub MergeInto {
2084     my $self = shift;
2085     my $MergeInto = shift;
2086     
2087     unless ($self->CurrentUserHasRight('ModifyTicket')) {
2088         return (0, "Permission Denied");
2089     }
2090     
2091     # Load up the new ticket.
2092     my $NewTicket = RT::Ticket->new($RT::SystemUser);
2093     $NewTicket->Load($MergeInto);
2094
2095     # make sure it exists.
2096     unless (defined $NewTicket->Id) {
2097         return (0, 'New ticket doesn\'t exist');
2098     }
2099
2100     
2101     # Make sure the current user can modify the new ticket.
2102     unless ($NewTicket->CurrentUserHasRight('ModifyTicket')) {
2103         $RT::Logger->debug("failed...");
2104         return (0, "Permission Denied");
2105     }
2106     
2107     $RT::Logger->debug("checking if the new ticket has the same id and effective id...");
2108     unless ($NewTicket->id == $NewTicket->EffectiveId) {
2109         $RT::Logger->err('$self trying to merge into '.$NewTicket->Id .
2110                          ' which is itself merged.\n');
2111         return (0, "Can't merge into a merged ticket. ".
2112                 "You should never get this error");
2113     }
2114
2115     
2116     # We use EffectiveId here even though it duplicates information from
2117     # the links table becasue of the massive performance hit we'd take
2118     # by trying to do a seperate database query for merge info everytime 
2119     # loaded a ticket. 
2120     
2121     
2122     #update this ticket's effective id to the new ticket's id.
2123     my ($id_val, $id_msg) = $self->__Set(Field => 'EffectiveId', 
2124                                          Value => $NewTicket->Id());
2125     
2126     unless ($id_val) {
2127         $RT::Logger->error("Couldn't set effective ID for ".$self->Id.
2128                            ": $id_msg");
2129         return(0,"Merge failed. Couldn't set EffectiveId");
2130     }
2131     
2132     my ($status_val, $status_msg) = $self->__Set(Field => 'Status',
2133                                                  Value => 'resolved');
2134     
2135     unless ($status_val) {
2136         $RT::Logger->error("$self couldn't set status to resolved.".
2137                            "RT's Database may be inconsistent.");
2138     }       
2139     
2140     #make a new link: this ticket is merged into that other ticket.
2141     $self->AddLink( Type =>'MergedInto',
2142                     Target => $NewTicket->Id() );
2143     
2144     #add all of this ticket's watchers to that ticket.
2145     my $watchers = $self->Watchers();
2146     
2147     while (my $watcher = $watchers->Next()) {
2148         unless (
2149                 ($watcher->Owner && 
2150                 $NewTicket->IsWatcher (Type => $watcher->Type,
2151                                        Id => $watcher->Owner)) or 
2152                 ($watcher->Email && 
2153                  $NewTicket->IsWatcher (Type => $watcher->Type,
2154                                         Id => $watcher->Email)) 
2155                ) {
2156             
2157             
2158             
2159             $NewTicket->_AddWatcher(Silent => 1, 
2160                                     Type => $watcher->Type, 
2161                                     Email => $watcher->Email,
2162                                     Owner => $watcher->Owner);
2163         }
2164     }
2165     
2166     
2167     #find all of the tickets that were merged into this ticket. 
2168     my $old_mergees = new RT::Tickets($self->CurrentUser);
2169     $old_mergees->Limit( FIELD => 'EffectiveId',
2170                          OPERATOR => '=',
2171                          VALUE => $self->Id );
2172     
2173     #   update their EffectiveId fields to the new ticket's id
2174     while (my $ticket = $old_mergees->Next()) {
2175         my ($val, $msg) = $ticket->__Set(Field => 'EffectiveId', 
2176                                          Value => $NewTicket->Id());
2177     }   
2178     $NewTicket->_SetLastUpdated;
2179
2180     return ($TransactionObj, "Merge Successful");
2181 }  
2182
2183 # }}}
2184
2185 # }}}
2186
2187 # {{{ Routines dealing with keywords
2188
2189 # {{{ sub KeywordsObj
2190
2191 =head2 KeywordsObj [KEYWORD_SELECT_ID]
2192
2193   Returns an B<RT::ObjectKeywords> object preloaded with this ticket's ObjectKeywords.
2194 If the optional KEYWORD_SELECT_ID parameter is set, limit the keywords object to that keyword
2195 select.
2196
2197 =cut
2198
2199 sub KeywordsObj {
2200     my $self = shift;
2201     my $keyword_select; 
2202     
2203     $keyword_select = shift if (@_);
2204     
2205     use RT::ObjectKeywords;
2206     my $Keywords = new RT::ObjectKeywords($self->CurrentUser);
2207
2208     #ACL check
2209     if ($self->CurrentUserHasRight('ShowTicket')) {
2210         $Keywords->LimitToTicket($self->id);
2211         if ($keyword_select) {
2212             $Keywords->LimitToKeywordSelect($keyword_select);
2213         }       
2214     }
2215     return ($Keywords);
2216 }
2217 # }}}
2218
2219 # {{{ sub AddKeyword
2220
2221 =head2 AddKeyword
2222
2223 Takes a paramhash of Keyword and KeywordSelect.  If Keyword is a valid choice
2224 for KeywordSelect, creates a KeywordObject.  If the KeywordSelect says this should
2225 be a single KeywordObject, automatically removes the old value.
2226
2227  Issues: probably doesn't enforce the depth restrictions or make sure that keywords
2228 are coming from the right part of the tree. really should.
2229
2230 =cut
2231
2232 sub AddKeyword {
2233     my $self = shift;
2234    #ACL check
2235     unless ($self->CurrentUserHasRight('ModifyTicket')) {
2236         return (0, 'Permission Denied');
2237     }
2238     
2239     return($self->_AddKeyword(@_));
2240     
2241 }
2242
2243
2244 # Helper version of AddKeyword without that pesky ACL check
2245 sub _AddKeyword {
2246     my $self = shift;
2247     my %args = ( KeywordSelect => undef,  # id of a keyword select record
2248                  Keyword => undef, #id of the keyword to add
2249                  Silent => 0,
2250                  @_
2251                );
2252     
2253     my ($OldValue);
2254
2255     #TODO make sure that $args{'Keyword'} is valid for $args{'KeywordSelect'}
2256
2257     #TODO: make sure that $args{'KeywordSelect'} applies to this ticket's queue.
2258     
2259     my $Keyword = new RT::Keyword($self->CurrentUser);
2260     unless ($Keyword->Load($args{'Keyword'}) ) {
2261         $RT::Logger->err("$self Couldn't load Keyword ".$args{'Keyword'} ."\n");
2262         return(0, "Couldn't load keyword");
2263     }
2264     
2265     my $KeywordSelectObj = new RT::KeywordSelect($self->CurrentUser);
2266     unless ($KeywordSelectObj->Load($args{'KeywordSelect'})) {
2267         $RT::Logger->err("$self Couldn't load KeywordSelect ".$args{'KeywordSelect'});
2268         return(0, "Couldn't load keywordselect");
2269     }
2270     
2271     my $Keywords = $self->KeywordsObj($KeywordSelectObj->id);
2272
2273     #If the ticket already has this keyword, just get out of here.
2274     if ($Keywords->HasEntry($Keyword->id)) {
2275         return(0, "That is already the current value");
2276     }   
2277
2278     #If the keywordselect wants this to be a singleton:
2279
2280     if ($KeywordSelectObj->Single) {
2281
2282         #Whack any old values...keep track of the last value that we get.
2283         #we shouldn't need a loop ehre, but we do it anyway, to try to 
2284         # help keep the database clean.
2285         while (my $OldKey = $Keywords->Next) {
2286             $OldValue = $OldKey->KeywordObj->Name;
2287             $OldKey->Delete();
2288         }       
2289         
2290         
2291     }
2292
2293     # create the new objectkeyword 
2294     my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser);
2295     my $result = $ObjectKeyword->Create( Keyword => $Keyword->Id,
2296                                          ObjectType => 'Ticket',
2297                                          ObjectId => $self->Id,
2298                                          KeywordSelect => $KeywordSelectObj->Id );
2299     
2300
2301     # record a single transaction, unless we were told not to
2302     unless ($args{'Silent'}) {
2303         my ($TransactionId, $Msg, $TransactionObj) = 
2304           $self->_NewTransaction( Type => 'Keyword',
2305                                   Field => $KeywordSelectObj->Id,
2306                                   OldValue => $OldValue,
2307                                   NewValue => $Keyword->Name );
2308     }
2309     return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." added.");    
2310
2311 }       
2312
2313 # }}}
2314
2315 # {{{ sub DeleteKeyword
2316
2317 =head2 DeleteKeyword
2318   
2319   Takes a paramhash. Deletes the Keyword denoted by the I<Keyword> parameter from this
2320   ticket's object keywords.
2321
2322 =cut
2323
2324 sub DeleteKeyword {
2325     my $self = shift;
2326     my %args = ( Keyword => undef,
2327                  KeywordSelect => undef,
2328                  @_ );
2329
2330    #ACL check
2331     unless ($self->CurrentUserHasRight('ModifyTicket')) {    
2332         return (0, 'Permission Denied');
2333     }
2334
2335     
2336     #Load up the ObjectKeyword we\'re talking about
2337     my $ObjectKeyword = new RT::ObjectKeyword($self->CurrentUser);
2338     $ObjectKeyword->LoadByCols(Keyword => $args{'Keyword'},
2339                                KeywordSelect => $args{'KeywordSelect'},
2340                                ObjectType => 'Ticket',
2341                                ObjectId => $self->id()
2342                               );
2343     
2344     #if we can\'t find it, bail
2345     unless ($ObjectKeyword->id) {
2346         $RT::Logger->err("Couldn't find the keyword ".$args{'Keyword'} .
2347                          " for keywordselect ". $args{'KeywordSelect'} . 
2348                          "for ticket ".$self->id );
2349         return (undef, "Couldn't load keyword while trying to delete it.");
2350     };
2351     
2352     #record transaction here.
2353     my ($TransactionId, $Msg, $TransObj) = 
2354       $self->_NewTransaction( Type => 'Keyword', 
2355                               OldValue => $ObjectKeyword->KeywordObj->Name);
2356     
2357     $ObjectKeyword->Delete();
2358     
2359     return ($TransactionId, "Keyword ".$ObjectKeyword->KeywordObj->Name ." deleted.");
2360     
2361 }
2362
2363 # }}}
2364
2365 # }}}
2366
2367 # {{{ Routines dealing with ownership
2368
2369 # {{{ sub OwnerObj
2370
2371 =head2 OwnerObj
2372
2373 Takes nothing and returns an RT::User object of 
2374 this ticket's owner
2375
2376 =cut
2377
2378 sub OwnerObj {
2379     my $self = shift;
2380     
2381     #If this gets ACLed, we lose on a rights check in User.pm and
2382     #get deep recursion. if we need ACLs here, we need
2383     #an equiv without ACLs
2384     
2385     $owner = new RT::User ($self->CurrentUser);
2386     $owner->Load($self->__Value('Owner'));
2387     
2388     #Return the owner object
2389     return ($owner);
2390 }
2391
2392 # }}}
2393
2394 # {{{ sub OwnerAsString 
2395
2396 =head2 OwnerAsString
2397
2398 Returns the owner's email address
2399
2400 =cut
2401
2402 sub OwnerAsString {
2403   my $self = shift;
2404   return($self->OwnerObj->EmailAddress);
2405
2406 }
2407
2408 # }}}
2409
2410 # {{{ sub SetOwner
2411
2412 =head2 SetOwner
2413
2414 Takes two arguments:
2415      the Id or Name of the owner 
2416 and  (optionally) the type of the SetOwner Transaction. It defaults
2417 to 'Give'.  'Steal' is also a valid option.
2418
2419 =cut
2420
2421 sub SetOwner {
2422     my $self = shift;
2423     my $NewOwner = shift;
2424     my $Type = shift || "Give";
2425     
2426     unless ($self->CurrentUserHasRight('ModifyTicket')) {
2427         return (0, "Permission Denied");
2428     }  
2429     
2430     my $NewOwnerObj = RT::User->new($self->CurrentUser);
2431     my $OldOwnerObj = $self->OwnerObj;
2432   
2433     $NewOwnerObj->Load($NewOwner);
2434     if (!$NewOwnerObj->Id) {
2435             return (0, "That user does not exist");
2436     }
2437     
2438     #If thie ticket has an owner and it's not the current user
2439     
2440     if (($Type ne 'Steal' ) and ($Type ne 'Force') and  #If we're not stealing
2441         ($self->OwnerObj->Id != $RT::Nobody->Id ) and #and the owner is set
2442         ($self->CurrentUser->Id ne $self->OwnerObj->Id())) { #and it's not us
2443         return(0, "You can only reassign tickets that you own or that are unowned");
2444     }
2445     
2446     #If we've specified a new owner and that user can't modify the ticket
2447     elsif (($NewOwnerObj->Id) and 
2448            (!$NewOwnerObj->HasQueueRight(Right => 'OwnTicket',
2449                                          QueueObj => $self->QueueObj,
2450                                          TicketObj => $self))
2451           ) {
2452         return (0, "That user may not own requests in that queue");
2453     }
2454   
2455   
2456     #If the ticket has an owner and it's the new owner, we don't need
2457     #To do anything
2458     elsif (($self->OwnerObj) and ($NewOwnerObj->Id eq $self->OwnerObj->Id)) {
2459         return(0, "That user already owns that request");
2460     }
2461   
2462   
2463     my ($trans,$msg)=$self->_Set(Field => 'Owner',
2464                                  Value => $NewOwnerObj->Id, 
2465                                  TimeTaken => 0,
2466                                  TransactionType => $Type);
2467   
2468     if ($trans) {
2469         $msg = "Owner changed from ".$OldOwnerObj->Name." to ".$NewOwnerObj->Name;
2470     }
2471     return ($trans, $msg);
2472           
2473 }
2474
2475 # }}}
2476
2477 # {{{ sub Take
2478
2479 =head2 Take
2480
2481 A convenince method to set the ticket's owner to the current user
2482
2483 =cut
2484
2485 sub Take {
2486     my $self = shift;
2487     return ($self->SetOwner($self->CurrentUser->Id, 'Take'));
2488 }
2489
2490 # }}}
2491
2492 # {{{ sub Untake
2493
2494 =head2 Untake
2495
2496 Convenience method to set the owner to 'nobody' if the current user is the owner.
2497
2498 =cut
2499
2500 sub Untake {
2501     my $self = shift;
2502     return($self->SetOwner($RT::Nobody->UserObj->Id, 'Untake'));
2503 }
2504 # }}}
2505
2506 # {{{ sub Steal 
2507
2508 =head2 Steal
2509
2510 A convenience method to change the owner of the current ticket to the
2511 current user. Even if it's owned by another user.
2512
2513 =cut
2514
2515 sub Steal {
2516     my $self = shift;
2517   
2518     if ($self->IsOwner($self->CurrentUser)) {
2519         return (0,"You already own this ticket"); 
2520     } else {
2521         return($self->SetOwner($self->CurrentUser->Id, 'Steal'));
2522       
2523     }
2524   
2525 }
2526
2527 # }}}
2528
2529 # }}}
2530
2531 # {{{ Routines dealing with status
2532
2533 # {{{ sub ValidateStatus 
2534
2535 =head2 ValidateStatus STATUS
2536
2537 Takes a string. Returns true if that status is a valid status for this ticket.
2538 Returns false otherwise.
2539
2540 =cut
2541
2542 sub ValidateStatus {
2543     my $self = shift;
2544     my $status = shift;
2545
2546     #Make sure the status passed in is valid
2547     unless ($self->QueueObj->IsValidStatus($status)) {
2548         return (undef);
2549     }
2550     
2551     return (1);
2552
2553 }
2554
2555
2556 # }}}
2557
2558 # {{{ sub SetStatus
2559
2560 =head2 SetStatus STATUS
2561
2562 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved or dead.
2563
2564 =cut
2565
2566 sub SetStatus { 
2567     my $self = shift;
2568     my $status = shift;
2569
2570     #Check ACL
2571     unless ($self->CurrentUserHasRight('ModifyTicket')) {
2572         return (0, 'Permission Denied');
2573     }
2574
2575     my $now = new RT::Date($self->CurrentUser);
2576     $now->SetToNow();
2577     
2578     #If we're changing the status from new, record that we've started
2579     if (($self->Status =~ /new/) && ($status ne 'new')) {
2580         #Set the Started time to "now"
2581         $self->_Set(Field => 'Started',
2582                     Value => $now->ISO,
2583                     RecordTransaction => 0);
2584     }
2585     
2586
2587     if ($status eq 'resolved') {
2588         #When we resolve a ticket, set the 'Resolved' attribute to now.
2589         $self->_Set(Field => 'Resolved',
2590                     Value => $now->ISO, 
2591                     RecordTransaction => 0);
2592     }
2593     
2594     
2595     #Actually update the status
2596     return($self->_Set(Field => 'Status', 
2597                        Value => $status,
2598                        TimeTaken => 0,
2599                        TransactionType => 'Status'));
2600 }
2601
2602 # }}}
2603
2604 # {{{ sub Kill
2605
2606 =head2 Kill
2607
2608 Takes no arguments. Marks this ticket for garbage collection
2609
2610 =cut
2611
2612 sub Kill {
2613   my $self = shift;
2614   return ($self->SetStatus('dead'));
2615   # TODO: garbage collection
2616 }
2617
2618 # }}}
2619
2620 # {{{ sub Stall
2621
2622 =head2 Stall
2623
2624 Sets this ticket's status to stalled
2625
2626 =cut
2627
2628 sub Stall {
2629   my $self = shift;
2630   return ($self->SetStatus('stalled'));
2631 }
2632
2633 # }}}
2634
2635 # {{{ sub Open
2636
2637 =head2 Open
2638
2639 Sets this ticket\'s status to Open
2640
2641 =cut
2642
2643 sub Open {
2644     my $self = shift;
2645     return ($self->SetStatus('open'));
2646 }
2647
2648 # }}}
2649
2650 # {{{ sub Resolve
2651
2652 =head2 Resolve
2653
2654 Sets this ticket\'s status to Resolved
2655
2656 =cut
2657
2658 sub Resolve {
2659     my $self = shift;
2660     return ($self->SetStatus('resolved'));
2661 }
2662
2663 # }}}
2664
2665 # }}}
2666
2667 # {{{ Actions + Routines dealing with transactions
2668
2669 # {{{ sub SetTold and _SetTold
2670
2671 =head2 SetTold ISO  [TIMETAKEN]
2672
2673 Updates the told and records a transaction
2674
2675 =cut
2676
2677 sub SetTold {
2678     my $self=shift;
2679     my $told;
2680     $told = shift if (@_);
2681     my $timetaken=shift || 0;
2682    
2683     unless ($self->CurrentUserHasRight('ModifyTicket')) {
2684         return (0, "Permission Denied");
2685     }
2686
2687     my $datetold = new RT::Date($self->CurrentUser);
2688     if ($told) {
2689         $datetold->Set( Format => 'iso',
2690                         Value => $told);
2691     }
2692     else {
2693          $datetold->SetToNow(); 
2694     }
2695     
2696     return($self->_Set(Field => 'Told', 
2697                        Value => $datetold->ISO,
2698                        TimeTaken => $timetaken,
2699                        TransactionType => 'Told'));
2700 }
2701
2702 =head2 _SetTold
2703
2704 Updates the told without a transaction or acl check. Useful when we're sending replies.
2705
2706 =cut
2707
2708 sub _SetTold {
2709     my $self=shift;
2710     
2711     my $now = new RT::Date($self->CurrentUser);
2712     $now->SetToNow();
2713     #use __Set to get no ACLs ;)
2714     return($self->__Set(Field => 'Told',
2715                         Value => $now->ISO));
2716 }
2717
2718 # }}}
2719
2720 # {{{ sub Transactions 
2721
2722 =head2 Transactions
2723
2724   Returns an RT::Transactions object of all transactions on this ticket
2725
2726 =cut
2727   
2728 sub Transactions {
2729     my $self = shift;
2730     
2731     use RT::Transactions;
2732     my $transactions = RT::Transactions->new($self->CurrentUser);
2733
2734     #If the user has no rights, return an empty object
2735     if ($self->CurrentUserHasRight('ShowTicket')) {
2736         my $tickets = $transactions->NewAlias('Tickets');
2737         $transactions->Join( ALIAS1 => 'main',
2738                               FIELD1 => 'Ticket',
2739                               ALIAS2 => $tickets,
2740                               FIELD2 => 'id');
2741         $transactions->Limit( ALIAS => $tickets,
2742                               FIELD => 'EffectiveId',
2743                               VALUE => $self->id());
2744         # if the user may not see comments do not return them
2745         unless ($self->CurrentUserHasRight('ShowTicketComments')) {
2746             $transactions->Limit( FIELD => 'Type',
2747                                   OPERATOR => '!=',
2748                                   VALUE => "Comment");
2749         }
2750     }
2751     
2752     return($transactions);
2753 }
2754
2755 # }}}
2756
2757 # {{{ sub _NewTransaction
2758
2759 sub _NewTransaction {
2760     my $self = shift;
2761     my %args = ( TimeTaken => 0,
2762                  Type => undef,
2763                  OldValue => undef,
2764                  NewValue => undef,
2765                  Data => undef,
2766                  Field => undef,
2767                  MIMEObj => undef,
2768                  @_ );
2769     
2770     
2771     require RT::Transaction;
2772     my $trans = new RT::Transaction($self->CurrentUser);
2773     my ($transaction, $msg) = 
2774       $trans->Create( Ticket => $self->Id,
2775                       TimeTaken => $args{'TimeTaken'},
2776                       Type => $args{'Type'},
2777                       Data => $args{'Data'},
2778                       Field => $args{'Field'},
2779                       NewValue => $args{'NewValue'},
2780                       OldValue => $args{'OldValue'},
2781                       MIMEObj => $args{'MIMEObj'}
2782                     );
2783     
2784     $RT::Logger->warning($msg) unless $transaction;
2785     
2786     $self->_SetLastUpdated;
2787     
2788     if (defined $args{'TimeTaken'} ) {
2789         $self->_UpdateTimeTaken($args{'TimeTaken'}); 
2790     }
2791     return($transaction, $msg, $trans);
2792 }
2793
2794 # }}}
2795
2796 # }}}
2797
2798 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
2799
2800 # {{{ sub _ClassAccessible
2801
2802 sub _ClassAccessible {
2803     {
2804         EffectiveId => { 'read' => 1, 'write' => 1, 'public' => 1 },
2805         Queue => { 'read' => 1, 'write' => 1 },
2806         Requestors => { 'read' => 1, 'write' => 1 },
2807         Owner => { 'read' => 1, 'write' => 1 },
2808         Subject => { 'read' => 1, 'write' => 1 },
2809         InitialPriority => { 'read' => 1, 'write' => 1 },
2810         FinalPriority => { 'read' => 1, 'write' => 1 },
2811         Priority => { 'read' => 1, 'write' => 1 },
2812         Status => { 'read' => 1, 'write' => 1 },
2813         TimeWorked => { 'read' => 1, 'write' => 1 },
2814         TimeLeft => { 'read' => 1, 'write' => 1 },
2815         Created => { 'read' => 1, 'auto' => 1 },
2816         Creator => { 'read' => 1,  'auto' => 1 },
2817         Told => { 'read' => 1, 'write' => 1 },
2818         Resolved => {'read' => 1},
2819         Starts => { 'read' => 1, 'write' => 1 },
2820         Started => { 'read' => 1, 'write' => 1 },
2821         Due => { 'read' => 1, 'write' => 1 },
2822         Creator => { 'read' => 1, 'auto' => 1 },
2823         Created => { 'read' => 1, 'auto' => 1 },
2824         LastUpdatedBy => { 'read' => 1, 'auto' => 1 },
2825         LastUpdated => { 'read' => 1, 'auto' => 1 }
2826     };
2827
2828 }    
2829
2830 # }}}
2831
2832 # {{{ sub _Set
2833
2834 sub _Set {
2835     my $self = shift;
2836     
2837     unless ($self->CurrentUserHasRight('ModifyTicket')) {
2838         return (0, "Permission Denied");
2839     }
2840     
2841     my %args = (Field => undef,
2842                 Value => undef,
2843                 TimeTaken => 0,
2844                 RecordTransaction => 1,
2845                 TransactionType => 'Set',
2846                 @_
2847                );
2848     #if the user is trying to modify the record
2849     
2850     #Take care of the old value we really don't want to get in an ACL loop.
2851     # so ask the super::_Value
2852     my $Old=$self->SUPER::_Value("$args{'Field'}");
2853     
2854     #Set the new value
2855     my ($ret, $msg)=$self->SUPER::_Set(Field => $args{'Field'}, 
2856                                        Value=> $args{'Value'});
2857     
2858     #If we can't actually set the field to the value, don't record
2859     # a transaction. instead, get out of here.
2860     if ($ret==0) {return (0,$msg);}
2861     
2862     if ($args{'RecordTransaction'} == 1) {
2863         
2864         my ($Trans, $Msg, $TransObj) =  
2865           $self->_NewTransaction(Type => $args{'TransactionType'},
2866                                  Field => $args{'Field'},
2867                                  NewValue => $args{'Value'},
2868                                  OldValue =>  $Old,
2869                                  TimeTaken => $args{'TimeTaken'},
2870                                 );
2871       return ($Trans,$TransObj->Description);
2872     }
2873     else {
2874         return ($ret, $msg);
2875   }
2876 }
2877
2878 # }}}
2879
2880 # {{{ sub _Value 
2881
2882 =head2 _Value
2883
2884 Takes the name of a table column.
2885 Returns its value as a string, if the user passes an ACL check
2886
2887 =cut
2888
2889 sub _Value  {
2890
2891   my $self = shift;
2892   my $field = shift;
2893
2894   
2895   #if the field is public, return it.
2896   if ($self->_Accessible($field, 'public')) {
2897       #$RT::Logger->debug("Skipping ACL check for $field\n");
2898       return($self->SUPER::_Value($field));
2899       
2900   }
2901   
2902   #If the current user doesn't have ACLs, don't let em at it.  
2903   
2904   unless ($self->CurrentUserHasRight('ShowTicket')) {
2905       return (undef);
2906   }
2907   return($self->SUPER::_Value($field));
2908   
2909 }
2910
2911 # }}}
2912
2913 # {{{ sub _UpdateTimeTaken
2914
2915 =head2 _UpdateTimeTaken
2916
2917 This routine will increment the timeworked counter. it should
2918 only be called from _NewTransaction 
2919
2920 =cut
2921
2922 sub _UpdateTimeTaken {
2923   my $self = shift;
2924   my $Minutes = shift;
2925   my ($Total);
2926    
2927   $Total = $self->SUPER::_Value("TimeWorked");
2928   $Total = ($Total || 0) + ($Minutes || 0);
2929   $self->SUPER::_Set(Field => "TimeWorked", 
2930                      Value => $Total);
2931
2932   return ($Total);
2933 }
2934
2935 # }}}
2936
2937 # }}}
2938
2939 # {{{ Routines dealing with ACCESS CONTROL
2940
2941 # {{{ sub CurrentUserHasRight 
2942
2943 =head2 CurrentUserHasRight
2944
2945   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
2946 1 if the user has that right. It returns 0 if the user doesn't have that right.
2947
2948 =cut
2949
2950 sub CurrentUserHasRight {
2951   my $self = shift;
2952   my $right = shift;
2953   
2954   return ($self->HasRight( Principal=> $self->CurrentUser->UserObj(),
2955                             Right => "$right"));
2956
2957 }
2958
2959 # }}}
2960
2961 # {{{ sub HasRight 
2962
2963 =head2 HasRight
2964
2965  Takes a paramhash with the attributes 'Right' and 'Principal'
2966   'Right' is a ticket-scoped textual right from RT::ACE 
2967   'Principal' is an RT::User object
2968
2969   Returns 1 if the principal has the right. Returns undef if not.
2970
2971 =cut
2972
2973 sub HasRight {
2974     my $self = shift;
2975     my %args = ( Right => undef,
2976                  Principal => undef,
2977                  @_);
2978     
2979     unless ((defined $args{'Principal'}) and (ref($args{'Principal'}))) {
2980         $RT::Logger->warning("Principal attrib undefined for Ticket::HasRight");
2981     }
2982     
2983     return($args{'Principal'}->HasQueueRight(TicketObj => $self,
2984                                              Right => $args{'Right'}));
2985 }
2986
2987 # }}}
2988
2989 # }}}
2990
2991
2992 1;
2993
2994 =head1 AUTHOR
2995
2996 Jesse Vincent, jesse@fsck.com
2997
2998 =head1 SEE ALSO
2999
3000 RT
3001
3002 =cut
3003
3004