removing unnecessary code spouting (thankfully harmless) warnings
[freeside.git] / rt / lib / RT / Ticket.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 SYNOPSIS
50
51   use RT::Ticket;
52   my $ticket = RT::Ticket->new($CurrentUser);
53   $ticket->Load($ticket_id);
54
55 =head1 DESCRIPTION
56
57 This module lets you manipulate RT\'s ticket object.
58
59
60 =head1 METHODS
61
62
63 =cut
64
65
66 package RT::Ticket;
67
68 use strict;
69 use warnings;
70
71
72 use RT::Queue;
73 use RT::User;
74 use RT::Record;
75 use RT::Links;
76 use RT::Date;
77 use RT::CustomFields;
78 use RT::Tickets;
79 use RT::Transactions;
80 use RT::Reminders;
81 use RT::URI::fsck_com_rt;
82 use RT::URI;
83 use RT::URI::freeside;
84 use MIME::Entity;
85 use Devel::GlobalDestruction;
86
87
88 # A helper table for links mapping to make it easier
89 # to build and parse links between tickets
90
91 our %LINKTYPEMAP = (
92     MemberOf => { Type => 'MemberOf',
93                   Mode => 'Target', },
94     Parents => { Type => 'MemberOf',
95          Mode => 'Target', },
96     Members => { Type => 'MemberOf',
97                  Mode => 'Base', },
98     Children => { Type => 'MemberOf',
99           Mode => 'Base', },
100     HasMember => { Type => 'MemberOf',
101                    Mode => 'Base', },
102     RefersTo => { Type => 'RefersTo',
103                   Mode => 'Target', },
104     ReferredToBy => { Type => 'RefersTo',
105                       Mode => 'Base', },
106     DependsOn => { Type => 'DependsOn',
107                    Mode => 'Target', },
108     DependedOnBy => { Type => 'DependsOn',
109                       Mode => 'Base', },
110     MergedInto => { Type => 'MergedInto',
111                    Mode => 'Target', },
112
113 );
114
115
116 # A helper table for links mapping to make it easier
117 # to build and parse links between tickets
118
119 our %LINKDIRMAP = (
120     MemberOf => { Base => 'MemberOf',
121                   Target => 'HasMember', },
122     RefersTo => { Base => 'RefersTo',
123                 Target => 'ReferredToBy', },
124     DependsOn => { Base => 'DependsOn',
125                    Target => 'DependedOnBy', },
126     MergedInto => { Base => 'MergedInto',
127                    Target => 'MergedInto', },
128
129 );
130
131
132 sub LINKTYPEMAP   { return \%LINKTYPEMAP   }
133 sub LINKDIRMAP   { return \%LINKDIRMAP   }
134
135 our %MERGE_CACHE = (
136     effective => {},
137     merged => {},
138 );
139
140
141 =head2 Load
142
143 Takes a single argument. This can be a ticket id, ticket alias or 
144 local ticket uri.  If the ticket can't be loaded, returns undef.
145 Otherwise, returns the ticket id.
146
147 =cut
148
149 sub Load {
150     my $self = shift;
151     my $id   = shift;
152     $id = '' unless defined $id;
153
154     # TODO: modify this routine to look at EffectiveId and
155     # do the recursive load thing. be careful to cache all
156     # the interim tickets we try so we don't loop forever.
157
158     unless ( $id =~ /^\d+$/ ) {
159         $RT::Logger->debug("Tried to load a bogus ticket id: '$id'");
160         return (undef);
161     }
162
163     $id = $MERGE_CACHE{'effective'}{ $id }
164         if $MERGE_CACHE{'effective'}{ $id };
165
166     my ($ticketid, $msg) = $self->LoadById( $id );
167     unless ( $self->Id ) {
168         $RT::Logger->debug("$self tried to load a bogus ticket: $id");
169         return (undef);
170     }
171
172     #If we're merged, resolve the merge.
173     if ( $self->EffectiveId && $self->EffectiveId != $self->Id ) {
174         $RT::Logger->debug(
175             "We found a merged ticket. "
176             . $self->id ."/". $self->EffectiveId
177         );
178         my $real_id = $self->Load( $self->EffectiveId );
179         $MERGE_CACHE{'effective'}{ $id } = $real_id;
180         return $real_id;
181     }
182
183     #Ok. we're loaded. lets get outa here.
184     return $self->Id;
185 }
186
187
188
189 =head2 Create (ARGS)
190
191 Arguments: ARGS is a hash of named parameters.  Valid parameters are:
192
193   id 
194   Queue  - Either a Queue object or a Queue Name
195   Requestor -  A reference to a list of  email addresses or RT user Names
196   Cc  - A reference to a list of  email addresses or Names
197   AdminCc  - A reference to a  list of  email addresses or Names
198   SquelchMailTo - A reference to a list of email addresses - 
199                   who should this ticket not mail
200   Type -- The ticket\'s type. ignore this for now
201   Owner -- This ticket\'s owner. either an RT::User object or this user\'s id
202   Subject -- A string describing the subject of the ticket
203   Priority -- an integer from 0 to 99
204   InitialPriority -- an integer from 0 to 99
205   FinalPriority -- an integer from 0 to 99
206   Status -- any valid status (Defined in RT::Queue)
207   TimeEstimated -- an integer. estimated time for this task in minutes
208   TimeWorked -- an integer. time worked so far in minutes
209   TimeLeft -- an integer. time remaining in minutes
210   Starts -- an ISO date describing the ticket\'s start date and time in GMT
211   Due -- an ISO date describing the ticket\'s due date and time in GMT
212   MIMEObj -- a MIME::Entity object with the content of the initial ticket request.
213   CustomField-<n> -- a scalar or array of values for the customfield with the id <n>
214
215 Ticket links can be set up during create by passing the link type as a hask key and
216 the ticket id to be linked to as a value (or a URI when linking to other objects).
217 Multiple links of the same type can be created by passing an array ref. For example:
218
219   Parents => 45,
220   DependsOn => [ 15, 22 ],
221   RefersTo => 'http://www.bestpractical.com',
222
223 Supported link types are C<MemberOf>, C<HasMember>, C<RefersTo>, C<ReferredToBy>,
224 C<DependsOn> and C<DependedOnBy>. Also, C<Parents> is alias for C<MemberOf> and
225 C<Members> and C<Children> are aliases for C<HasMember>.
226
227 Returns: TICKETID, Transaction Object, Error Message
228
229
230 =cut
231
232 sub Create {
233     my $self = shift;
234
235     my %args = (
236         id                 => undef,
237         EffectiveId        => undef,
238         Queue              => undef,
239         Requestor          => undef,
240         Cc                 => undef,
241         AdminCc            => undef,
242         SquelchMailTo      => undef,
243         TransSquelchMailTo => undef,
244         Type               => 'ticket',
245         Owner              => undef,
246         Subject            => '',
247         InitialPriority    => undef,
248         FinalPriority      => undef,
249         Priority           => undef,
250         Status             => undef,
251         TimeWorked         => "0",
252         TimeLeft           => 0,
253         TimeEstimated      => 0,
254         Due                => undef,
255         Starts             => undef,
256         Started            => undef,
257         Resolved           => undef,
258         MIMEObj            => undef,
259         _RecordTransaction => 1,
260         DryRun             => 0,
261         @_
262     );
263
264     my ($ErrStr, @non_fatal_errors);
265
266     my $QueueObj = RT::Queue->new( RT->SystemUser );
267     if ( ref $args{'Queue'} eq 'RT::Queue' ) {
268         $QueueObj->Load( $args{'Queue'}->Id );
269     }
270     elsif ( $args{'Queue'} ) {
271         $QueueObj->Load( $args{'Queue'} );
272     }
273     else {
274         $RT::Logger->debug("'". ( $args{'Queue'} ||''). "' not a recognised queue object." );
275     }
276
277     #Can't create a ticket without a queue.
278     unless ( $QueueObj->Id ) {
279         $RT::Logger->debug("$self No queue given for ticket creation.");
280         return ( 0, 0, $self->loc('Could not create ticket. Queue not set') );
281     }
282
283
284     #Now that we have a queue, Check the ACLS
285     unless (
286         $self->CurrentUser->HasRight(
287             Right  => 'CreateTicket',
288             Object => $QueueObj
289         )
290       )
291     {
292         return (
293             0, 0,
294             $self->loc( "No permission to create tickets in the queue '[_1]'", $QueueObj->Name));
295     }
296
297     my $cycle = $QueueObj->Lifecycle;
298     unless ( defined $args{'Status'} && length $args{'Status'} ) {
299         $args{'Status'} = $cycle->DefaultOnCreate;
300     }
301
302     unless ( $cycle->IsValid( $args{'Status'} ) ) {
303         return ( 0, 0,
304             $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.",
305                 $self->loc($args{'Status'}))
306         );
307     }
308
309     unless ( $cycle->IsTransition( '' => $args{'Status'} ) ) {
310         return ( 0, 0,
311             $self->loc("New tickets can not have status '[_1]' in this queue.",
312                 $self->loc($args{'Status'}))
313         );
314     }
315
316
317
318     #Since we have a queue, we can set queue defaults
319
320     #Initial Priority
321     # If there's no queue default initial priority and it's not set, set it to 0
322     $args{'InitialPriority'} = $QueueObj->InitialPriority || 0
323         unless defined $args{'InitialPriority'};
324
325     #Final priority
326     # If there's no queue default final priority and it's not set, set it to 0
327     $args{'FinalPriority'} = $QueueObj->FinalPriority || 0
328         unless defined $args{'FinalPriority'};
329
330     # Priority may have changed from InitialPriority, for the case
331     # where we're importing tickets (eg, from an older RT version.)
332     $args{'Priority'} = $args{'InitialPriority'}
333         unless defined $args{'Priority'};
334
335     # Dates
336     #TODO we should see what sort of due date we're getting, rather +
337     # than assuming it's in ISO format.
338
339     #Set the due date. if we didn't get fed one, use the queue default due in
340     my $Due = RT::Date->new( $self->CurrentUser );
341     if ( defined $args{'Due'} ) {
342         $Due->Set( Format => 'ISO', Value => $args{'Due'} );
343     }
344     elsif ( my $due_in = $QueueObj->DefaultDueIn ) {
345         $Due->SetToNow;
346         $Due->AddDays( $due_in );
347     }
348
349     my $Starts = RT::Date->new( $self->CurrentUser );
350     if ( defined $args{'Starts'} ) {
351         $Starts->Set( Format => 'ISO', Value => $args{'Starts'} );
352     }
353
354     my $Started = RT::Date->new( $self->CurrentUser );
355     if ( defined $args{'Started'} ) {
356         $Started->Set( Format => 'ISO', Value => $args{'Started'} );
357     }
358
359     # If the status is not an initial status, set the started date
360     elsif ( !$cycle->IsInitial($args{'Status'}) ) {
361         $Started->SetToNow;
362     }
363
364     my $Resolved = RT::Date->new( $self->CurrentUser );
365     if ( defined $args{'Resolved'} ) {
366         $Resolved->Set( Format => 'ISO', Value => $args{'Resolved'} );
367     }
368
369     #If the status is an inactive status, set the resolved date
370     elsif ( $cycle->IsInactive( $args{'Status'} ) )
371     {
372         $RT::Logger->debug( "Got a ". $args{'Status'}
373             ."(inactive) ticket with undefined resolved date. Setting to now."
374         );
375         $Resolved->SetToNow;
376     }
377
378     # }}}
379
380     # Dealing with time fields
381
382     $args{'TimeEstimated'} = 0 unless defined $args{'TimeEstimated'};
383     $args{'TimeWorked'}    = 0 unless defined $args{'TimeWorked'};
384     $args{'TimeLeft'}      = 0 unless defined $args{'TimeLeft'};
385
386     # }}}
387
388     # Deal with setting the owner
389
390     my $Owner;
391     if ( ref( $args{'Owner'} ) eq 'RT::User' ) {
392         if ( $args{'Owner'}->id ) {
393             $Owner = $args{'Owner'};
394         } else {
395             $RT::Logger->error('Passed an empty RT::User for owner');
396             push @non_fatal_errors,
397                 $self->loc("Owner could not be set.") . " ".
398             $self->loc("Invalid value for [_1]",loc('owner'));
399             $Owner = undef;
400         }
401     }
402
403     #If we've been handed something else, try to load the user.
404     elsif ( $args{'Owner'} ) {
405         $Owner = RT::User->new( $self->CurrentUser );
406         $Owner->Load( $args{'Owner'} );
407         if (!$Owner->id) {
408             $Owner->LoadByEmail( $args{'Owner'} )
409         }
410         unless ( $Owner->Id ) {
411             push @non_fatal_errors,
412                 $self->loc("Owner could not be set.") . " "
413               . $self->loc( "User '[_1]' could not be found.", $args{'Owner'} );
414             $Owner = undef;
415         }
416     }
417
418     #If we have a proposed owner and they don't have the right
419     #to own a ticket, scream about it and make them not the owner
420    
421     my $DeferOwner;  
422     if ( $Owner && $Owner->Id != RT->Nobody->Id 
423         && !$Owner->HasRight( Object => $QueueObj, Right  => 'OwnTicket' ) )
424     {
425         $DeferOwner = $Owner;
426         $Owner = undef;
427         $RT::Logger->debug('going to deffer setting owner');
428
429     }
430
431     #If we haven't been handed a valid owner, make it nobody.
432     unless ( defined($Owner) && $Owner->Id ) {
433         $Owner = RT::User->new( $self->CurrentUser );
434         $Owner->Load( RT->Nobody->Id );
435     }
436
437     # }}}
438
439 # We attempt to load or create each of the people who might have a role for this ticket
440 # _outside_ the transaction, so we don't get into ticket creation races
441     foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
442         $args{ $type } = [ $args{ $type } ] unless ref $args{ $type };
443         foreach my $watcher ( splice @{ $args{$type} } ) {
444             next unless $watcher;
445             if ( $watcher =~ /^\d+$/ ) {
446                 push @{ $args{$type} }, $watcher;
447             } else {
448                 my @addresses = RT::EmailParser->ParseEmailAddress( $watcher );
449                 foreach my $address( @addresses ) {
450                     my $user = RT::User->new( RT->SystemUser );
451                     my ($uid, $msg) = $user->LoadOrCreateByEmail( $address );
452                     unless ( $uid ) {
453                         push @non_fatal_errors,
454                             $self->loc("Couldn't load or create user: [_1]", $msg);
455                     } else {
456                         push @{ $args{$type} }, $user->id;
457                     }
458                 }
459             }
460         }
461     }
462
463     $RT::Handle->BeginTransaction();
464
465     my %params = (
466         Queue           => $QueueObj->Id,
467         Owner           => $Owner->Id,
468         Subject         => $args{'Subject'},
469         InitialPriority => $args{'InitialPriority'},
470         FinalPriority   => $args{'FinalPriority'},
471         Priority        => $args{'Priority'},
472         Status          => $args{'Status'},
473         TimeWorked      => $args{'TimeWorked'},
474         TimeEstimated   => $args{'TimeEstimated'},
475         TimeLeft        => $args{'TimeLeft'},
476         Type            => $args{'Type'},
477         Starts          => $Starts->ISO,
478         Started         => $Started->ISO,
479         Resolved        => $Resolved->ISO,
480         Due             => $Due->ISO
481     );
482
483 # Parameters passed in during an import that we probably don't want to touch, otherwise
484     foreach my $attr (qw(id Creator Created LastUpdated LastUpdatedBy)) {
485         $params{$attr} = $args{$attr} if $args{$attr};
486     }
487
488     # Delete null integer parameters
489     foreach my $attr
490         (qw(TimeWorked TimeLeft TimeEstimated InitialPriority FinalPriority))
491     {
492         delete $params{$attr}
493           unless ( exists $params{$attr} && $params{$attr} );
494     }
495
496     # Delete the time worked if we're counting it in the transaction
497     delete $params{'TimeWorked'} if $args{'_RecordTransaction'};
498
499     my ($id,$ticket_message) = $self->SUPER::Create( %params );
500     unless ($id) {
501         $RT::Logger->crit( "Couldn't create a ticket: " . $ticket_message );
502         $RT::Handle->Rollback();
503         return ( 0, 0,
504             $self->loc("Ticket could not be created due to an internal error")
505         );
506     }
507
508     #Set the ticket's effective ID now that we've created it.
509     my ( $val, $msg ) = $self->__Set(
510         Field => 'EffectiveId',
511         Value => ( $args{'EffectiveId'} || $id )
512     );
513     unless ( $val ) {
514         $RT::Logger->crit("Couldn't set EffectiveId: $msg");
515         $RT::Handle->Rollback;
516         return ( 0, 0,
517             $self->loc("Ticket could not be created due to an internal error")
518         );
519     }
520
521     my $create_groups_ret = $self->_CreateTicketGroups();
522     unless ($create_groups_ret) {
523         $RT::Logger->crit( "Couldn't create ticket groups for ticket "
524               . $self->Id
525               . ". aborting Ticket creation." );
526         $RT::Handle->Rollback();
527         return ( 0, 0,
528             $self->loc("Ticket could not be created due to an internal error")
529         );
530     }
531
532     # Set the owner in the Groups table
533     # We denormalize it into the Ticket table too because doing otherwise would
534     # kill performance, bigtime. It gets kept in lockstep thanks to the magic of transactionalization
535     $self->OwnerGroup->_AddMember(
536         PrincipalId       => $Owner->PrincipalId,
537         InsideTransaction => 1
538     ) unless $DeferOwner;
539
540
541
542     # Deal with setting up watchers
543
544     foreach my $type ( "Cc", "AdminCc", "Requestor" ) {
545         # we know it's an array ref
546         foreach my $watcher ( @{ $args{$type} } ) {
547
548             # Note that we're using AddWatcher, rather than _AddWatcher, as we
549             # actually _want_ that ACL check. Otherwise, random ticket creators
550             # could make themselves adminccs and maybe get ticket rights. that would
551             # be poor
552             my $method = $type eq 'AdminCc'? 'AddWatcher': '_AddWatcher';
553
554             my ($val, $msg) = $self->$method(
555                 Type   => $type,
556                 PrincipalId => $watcher,
557                 Silent => 1,
558             );
559             push @non_fatal_errors, $self->loc("Couldn't set [_1] watcher: [_2]", $type, $msg)
560                 unless $val;
561         }
562     } 
563
564     if ($args{'SquelchMailTo'}) {
565        my @squelch = ref( $args{'SquelchMailTo'} ) ? @{ $args{'SquelchMailTo'} }
566         : $args{'SquelchMailTo'};
567         $self->_SquelchMailTo( @squelch );
568     }
569
570
571     # }}}
572
573     # Add all the custom fields
574
575     foreach my $arg ( keys %args ) {
576         next unless $arg =~ /^CustomField-(\d+)$/i;
577         my $cfid = $1;
578
579         foreach my $value (
580             UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
581         {
582             next unless defined $value && length $value;
583
584             # Allow passing in uploaded LargeContent etc by hash reference
585             my ($status, $msg) = $self->_AddCustomFieldValue(
586                 (UNIVERSAL::isa( $value => 'HASH' )
587                     ? %$value
588                     : (Value => $value)
589                 ),
590                 Field             => $cfid,
591                 RecordTransaction => 0,
592             );
593             push @non_fatal_errors, $msg unless $status;
594         }
595     }
596
597     # }}}
598
599     # Deal with setting up links
600
601     # TODO: Adding link may fire scrips on other end and those scrips
602     # could create transactions on this ticket before 'Create' transaction.
603     #
604     # We should implement different lifecycle: record 'Create' transaction,
605     # create links and only then fire create transaction's scrips.
606     #
607     # Ideal variant: add all links without firing scrips, record create
608     # transaction and only then fire scrips on the other ends of links.
609     #
610     # //RUZ
611
612     foreach my $type ( keys %LINKTYPEMAP ) {
613         next unless ( defined $args{$type} );
614         foreach my $link (
615             ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) )
616         {
617             my ( $val, $msg, $obj ) = $self->__GetTicketFromURI( URI => $link );
618             unless ($val) {
619                 push @non_fatal_errors, $msg;
620                 next;
621             }
622
623             # Check rights on the other end of the link if we must
624             # then run _AddLink that doesn't check for ACLs
625             if ( RT->Config->Get( 'StrictLinkACL' ) ) {
626                 if ( $obj && !$obj->CurrentUserHasRight('ModifyTicket') ) {
627                     push @non_fatal_errors, $self->loc('Linking. Permission denied');
628                     next;
629                 }
630             }
631
632             if ( $obj && $obj->Status eq 'deleted' ) {
633                 push @non_fatal_errors,
634                   $self->loc("Linking. Can't link to a deleted ticket");
635                 next;
636             }
637
638             my ( $wval, $wmsg ) = $self->_AddLink(
639                 Type                          => $LINKTYPEMAP{$type}->{'Type'},
640                 $LINKTYPEMAP{$type}->{'Mode'} => $link,
641                 Silent                        => !$args{'_RecordTransaction'} || $self->Type eq 'reminder',
642                 'Silent'. ( $LINKTYPEMAP{$type}->{'Mode'} eq 'Base'? 'Target': 'Base' )
643                                               => 1,
644             );
645
646             push @non_fatal_errors, $wmsg unless ($wval);
647         }
648     }
649
650     # }}}
651
652     # {{{ Deal with auto-customer association
653
654     #unless we already have (a) customer(s)...
655     unless ( $self->Customers->Count ) {
656
657       #first find any requestors with emails but *without* customer targets
658       my @NoCust_Requestors =
659         grep { $_->EmailAddress && ! $_->Customers->Count }
660              @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
661
662       for my $Requestor (@NoCust_Requestors) {
663
664          #perhaps the stuff in here should be in a User method??
665          my @Customers =
666            &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
667
668          foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
669
670            ## false laziness w/RT/Interface/Web_Vendor.pm
671            my @link = ( 'Type'   => 'MemberOf',
672                         'Target' => "freeside://freeside/cust_main/$custnum",
673                       );
674
675            my( $val, $msg ) = $Requestor->_AddLink(@link);
676            #XXX should do something with $msg# push @non_fatal_errors, $msg;
677
678          }
679
680       }
681
682       #find any requestors with customer targets
683   
684       my %cust_target = ();
685
686       my @Requestors =
687         grep { $_->Customers->Count }
688              @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
689   
690       foreach my $Requestor ( @Requestors ) {
691         foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
692           $cust_target{ $cust_link->Target } = 1;
693         }
694       }
695   
696       #and then auto-associate this ticket with those customers
697   
698       foreach my $cust_target ( keys %cust_target ) {
699   
700         my @link = ( 'Type'   => 'MemberOf',
701                      #'Target' => "freeside://freeside/cust_main/$custnum",
702                      'Target' => $cust_target,
703                    );
704   
705         my( $val, $msg ) = $self->_AddLink(@link);
706         push @non_fatal_errors, $msg;
707   
708       }
709
710     }
711
712     # }}}
713
714     # Now that we've created the ticket and set up its metadata, we can actually go and check OwnTicket on the ticket itself. 
715     # This might be different than before in cases where extensions like RTIR are doing clever things with RT's ACL system
716     if (  $DeferOwner ) { 
717             if (!$DeferOwner->HasRight( Object => $self, Right  => 'OwnTicket')) {
718     
719             $RT::Logger->warning( "User " . $DeferOwner->Name . "(" . $DeferOwner->id 
720                 . ") was proposed as a ticket owner but has no rights to own "
721                 . "tickets in " . $QueueObj->Name );
722             push @non_fatal_errors, $self->loc(
723                 "Owner '[_1]' does not have rights to own this ticket.",
724                 $DeferOwner->Name
725             );
726         } else {
727             $Owner = $DeferOwner;
728             $self->__Set(Field => 'Owner', Value => $Owner->id);
729
730         }
731         $self->OwnerGroup->_AddMember(
732             PrincipalId       => $Owner->PrincipalId,
733             InsideTransaction => 1
734         );
735     }
736
737     #don't make a transaction or fire off any scrips for reminders either
738     if ( $args{'_RecordTransaction'} && $self->Type ne 'reminder' ) {
739
740         # Add a transaction for the create
741         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
742             Type         => "Create",
743             TimeTaken    => $args{'TimeWorked'},
744             MIMEObj      => $args{'MIMEObj'},
745             CommitScrips => !$args{'DryRun'},
746             SquelchMailTo => $args{'TransSquelchMailTo'},
747         );
748
749         if ( $self->Id && $Trans ) {
750
751           #$TransObj->UpdateCustomFields(ARGSRef => \%args);
752             $TransObj->UpdateCustomFields(%args);
753
754             $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
755             $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
756             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
757         }
758         else {
759             $RT::Handle->Rollback();
760
761             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
762             $RT::Logger->error("Ticket couldn't be created: $ErrStr");
763             return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
764         }
765
766         if ( $args{'DryRun'} ) {
767             $RT::Handle->Rollback();
768             return ($self->id, $TransObj, $ErrStr);
769         }
770         $RT::Handle->Commit();
771         return ( $self->Id, $TransObj->Id, $ErrStr );
772
773         # }}}
774     }
775     else {
776
777         # Not going to record a transaction
778         $RT::Handle->Commit();
779         $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
780         $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
781         return ( $self->Id, 0, $ErrStr );
782
783     }
784 }
785
786
787
788
789 =head2 _Parse822HeadersForAttributes Content
790
791 Takes an RFC822 style message and parses its attributes into a hash.
792
793 =cut
794
795 sub _Parse822HeadersForAttributes {
796     my $self    = shift;
797     my $content = shift;
798     my %args;
799
800     my @lines = ( split ( /\n/, $content ) );
801     while ( defined( my $line = shift @lines ) ) {
802         if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
803             my $value = $2;
804             my $tag   = lc($1);
805
806             $tag =~ s/-//g;
807             if ( defined( $args{$tag} ) )
808             {    #if we're about to get a second value, make it an array
809                 $args{$tag} = [ $args{$tag} ];
810             }
811             if ( ref( $args{$tag} ) )
812             {    #If it's an array, we want to push the value
813                 push @{ $args{$tag} }, $value;
814             }
815             else {    #if there's nothing there, just set the value
816                 $args{$tag} = $value;
817             }
818         } elsif ($line =~ /^$/) {
819
820             #TODO: this won't work, since "" isn't of the form "foo:value"
821
822                 while ( defined( my $l = shift @lines ) ) {
823                     push @{ $args{'content'} }, $l;
824                 }
825             }
826         
827     }
828
829     foreach my $date (qw(due starts started resolved)) {
830         my $dateobj = RT::Date->new(RT->SystemUser);
831         if ( defined ($args{$date}) and $args{$date} =~ /^\d+$/ ) {
832             $dateobj->Set( Format => 'unix', Value => $args{$date} );
833         }
834         else {
835             $dateobj->Set( Format => 'unknown', Value => $args{$date} );
836         }
837         $args{$date} = $dateobj->ISO;
838     }
839     $args{'mimeobj'} = MIME::Entity->new();
840     $args{'mimeobj'}->build(
841         Type => ( $args{'contenttype'} || 'text/plain' ),
842         Data => ($args{'content'} || '')
843     );
844
845     return (%args);
846 }
847
848
849
850 =head2 Import PARAMHASH
851
852 Import a ticket. 
853 Doesn\'t create a transaction. 
854 Doesn\'t supply queue defaults, etc.
855
856 Returns: TICKETID
857
858 =cut
859
860 sub Import {
861     my $self = shift;
862     my ( $ErrStr, $QueueObj, $Owner );
863
864     my %args = (
865         id              => undef,
866         EffectiveId     => undef,
867         Queue           => undef,
868         Requestor       => undef,
869         Type            => 'ticket',
870         Owner           => RT->Nobody->Id,
871         Subject         => '[no subject]',
872         InitialPriority => undef,
873         FinalPriority   => undef,
874         Status          => 'new',
875         TimeWorked      => "0",
876         Due             => undef,
877         Created         => undef,
878         Updated         => undef,
879         Resolved        => undef,
880         Told            => undef,
881         @_
882     );
883
884     if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
885         $QueueObj = RT::Queue->new(RT->SystemUser);
886         $QueueObj->Load( $args{'Queue'} );
887
888         #TODO error check this and return 0 if it\'s not loading properly +++
889     }
890     elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
891         $QueueObj = RT::Queue->new(RT->SystemUser);
892         $QueueObj->Load( $args{'Queue'}->Id );
893     }
894     else {
895         $RT::Logger->debug(
896             "$self " . $args{'Queue'} . " not a recognised queue object." );
897     }
898
899     #Can't create a ticket without a queue.
900     unless ( defined($QueueObj) and $QueueObj->Id ) {
901         $RT::Logger->debug("$self No queue given for ticket creation.");
902         return ( 0, $self->loc('Could not create ticket. Queue not set') );
903     }
904
905     #Now that we have a queue, Check the ACLS
906     unless (
907         $self->CurrentUser->HasRight(
908             Right    => 'CreateTicket',
909             Object => $QueueObj
910         )
911       )
912     {
913         return ( 0,
914             $self->loc("No permission to create tickets in the queue '[_1]'"
915               , $QueueObj->Name));
916     }
917
918     # Deal with setting the owner
919
920     # Attempt to take user object, user name or user id.
921     # Assign to nobody if lookup fails.
922     if ( defined( $args{'Owner'} ) ) {
923         if ( ref( $args{'Owner'} ) ) {
924             $Owner = $args{'Owner'};
925         }
926         else {
927             $Owner = RT::User->new( $self->CurrentUser );
928             $Owner->Load( $args{'Owner'} );
929             if ( !defined( $Owner->id ) ) {
930                 $Owner->Load( RT->Nobody->id );
931             }
932         }
933     }
934
935     #If we have a proposed owner and they don't have the right 
936     #to own a ticket, scream about it and make them not the owner
937     if (
938         ( defined($Owner) )
939         and ( $Owner->Id != RT->Nobody->Id )
940         and (
941             !$Owner->HasRight(
942                 Object => $QueueObj,
943                 Right    => 'OwnTicket'
944             )
945         )
946       )
947     {
948
949         $RT::Logger->warning( "$self user "
950               . $Owner->Name . "("
951               . $Owner->id
952               . ") was proposed "
953               . "as a ticket owner but has no rights to own "
954               . "tickets in '"
955               . $QueueObj->Name . "'" );
956
957         $Owner = undef;
958     }
959
960     #If we haven't been handed a valid owner, make it nobody.
961     unless ( defined($Owner) ) {
962         $Owner = RT::User->new( $self->CurrentUser );
963         $Owner->Load( RT->Nobody->UserObj->Id );
964     }
965
966     # }}}
967
968     unless ( $self->ValidateStatus( $args{'Status'} ) ) {
969         return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
970     }
971
972     $self->{'_AccessibleCache'}{Created}       = { 'read' => 1, 'write' => 1 };
973     $self->{'_AccessibleCache'}{Creator}       = { 'read' => 1, 'auto'  => 1 };
974     $self->{'_AccessibleCache'}{LastUpdated}   = { 'read' => 1, 'write' => 1 };
975     $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto'  => 1 };
976
977     # If we're coming in with an id, set that now.
978     my $EffectiveId = undef;
979     if ( $args{'id'} ) {
980         $EffectiveId = $args{'id'};
981
982     }
983
984     my $id = $self->SUPER::Create(
985         id              => $args{'id'},
986         EffectiveId     => $EffectiveId,
987         Queue           => $QueueObj->Id,
988         Owner           => $Owner->Id,
989         Subject         => $args{'Subject'},        # loc
990         InitialPriority => $args{'InitialPriority'},    # loc
991         FinalPriority   => $args{'FinalPriority'},    # loc
992         Priority        => $args{'InitialPriority'},    # loc
993         Status          => $args{'Status'},        # loc
994         TimeWorked      => $args{'TimeWorked'},        # loc
995         Type            => $args{'Type'},        # loc
996         Created         => $args{'Created'},        # loc
997         Told            => $args{'Told'},        # loc
998         LastUpdated     => $args{'Updated'},        # loc
999         Resolved        => $args{'Resolved'},        # loc
1000         Due             => $args{'Due'},        # loc
1001     );
1002
1003     # If the ticket didn't have an id
1004     # Set the ticket's effective ID now that we've created it.
1005     if ( $args{'id'} ) {
1006         $self->Load( $args{'id'} );
1007     }
1008     else {
1009         my ( $val, $msg ) =
1010           $self->__Set( Field => 'EffectiveId', Value => $id );
1011
1012         unless ($val) {
1013             $RT::Logger->err(
1014                 $self . "->Import couldn't set EffectiveId: $msg" );
1015         }
1016     }
1017
1018     my $create_groups_ret = $self->_CreateTicketGroups();
1019     unless ($create_groups_ret) {
1020         $RT::Logger->crit(
1021             "Couldn't create ticket groups for ticket " . $self->Id );
1022     }
1023
1024     $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1025
1026     foreach my $watcher ( @{ $args{'Cc'} } ) {
1027         $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1028     }
1029     foreach my $watcher ( @{ $args{'AdminCc'} } ) {
1030         $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1031             Silent => 1 );
1032     }
1033     foreach my $watcher ( @{ $args{'Requestor'} } ) {
1034         $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1035             Silent => 1 );
1036     }
1037
1038     return ( $self->Id, $ErrStr );
1039 }
1040
1041
1042
1043
1044 =head2 _CreateTicketGroups
1045
1046 Create the ticket groups and links for this ticket. 
1047 This routine expects to be called from Ticket->Create _inside of a transaction_
1048
1049 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1050
1051 It will return true on success and undef on failure.
1052
1053
1054 =cut
1055
1056
1057 sub _CreateTicketGroups {
1058     my $self = shift;
1059     
1060     my @types = (qw(Requestor Owner Cc AdminCc));
1061
1062     foreach my $type (@types) {
1063         my $type_obj = RT::Group->new($self->CurrentUser);
1064         my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1065                                                        Instance => $self->Id, 
1066                                                        Type => $type);
1067         unless ($id) {
1068             $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1069                                $self->Id.": ".$msg);     
1070             return(undef);
1071         }
1072      }
1073     return(1);
1074     
1075 }
1076
1077
1078
1079 =head2 OwnerGroup
1080
1081 A constructor which returns an RT::Group object containing the owner of this ticket.
1082
1083 =cut
1084
1085 sub OwnerGroup {
1086     my $self = shift;
1087     my $owner_obj = RT::Group->new($self->CurrentUser);
1088     $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id,  Type => 'Owner');
1089     return ($owner_obj);
1090 }
1091
1092
1093
1094
1095 =head2 AddWatcher
1096
1097 AddWatcher takes a parameter hash. The keys are as follows:
1098
1099 Type        One of Requestor, Cc, AdminCc
1100
1101 PrincipalId The RT::Principal id of the user or group that's being added as a watcher
1102
1103 Email       The email address of the new watcher. If a user with this 
1104             email address can't be found, a new nonprivileged user will be created.
1105
1106 If the watcher you\'re trying to set has an RT account, set the PrincipalId paremeter to their User Id. Otherwise, set the Email parameter to their Email address.
1107
1108 =cut
1109
1110 sub AddWatcher {
1111     my $self = shift;
1112     my %args = (
1113         Type  => undef,
1114         PrincipalId => undef,
1115         Email => undef,
1116         @_
1117     );
1118
1119     # ModifyTicket works in any case
1120     return $self->_AddWatcher( %args )
1121         if $self->CurrentUserHasRight('ModifyTicket');
1122     if ( $args{'Email'} ) {
1123         my ($addr) = RT::EmailParser->ParseEmailAddress( $args{'Email'} );
1124         return (0, $self->loc("Couldn't parse address from '[_1]' string", $args{'Email'} ))
1125             unless $addr;
1126
1127         if ( lc $self->CurrentUser->UserObj->EmailAddress
1128             eq lc RT::User->CanonicalizeEmailAddress( $addr->address ) )
1129         {
1130             $args{'PrincipalId'} = $self->CurrentUser->id;
1131             delete $args{'Email'};
1132         }
1133     }
1134
1135     # If the watcher isn't the current user then the current user has no right
1136     # bail
1137     unless ( $args{'PrincipalId'} && $self->CurrentUser->id == $args{'PrincipalId'} ) {
1138         return ( 0, $self->loc("Permission Denied") );
1139     }
1140
1141     #  If it's an AdminCc and they don't have 'WatchAsAdminCc', bail
1142     if ( $args{'Type'} eq 'AdminCc' ) {
1143         unless ( $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1144             return ( 0, $self->loc('Permission Denied') );
1145         }
1146     }
1147
1148     #  If it's a Requestor or Cc and they don't have 'Watch', bail
1149     elsif ( $args{'Type'} eq 'Cc' || $args{'Type'} eq 'Requestor' ) {
1150         unless ( $self->CurrentUserHasRight('Watch') ) {
1151             return ( 0, $self->loc('Permission Denied') );
1152         }
1153     }
1154     else {
1155         $RT::Logger->warning( "AddWatcher got passed a bogus type");
1156         return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1157     }
1158
1159     return $self->_AddWatcher( %args );
1160 }
1161
1162 #This contains the meat of AddWatcher. but can be called from a routine like
1163 # Create, which doesn't need the additional acl check
1164 sub _AddWatcher {
1165     my $self = shift;
1166     my %args = (
1167         Type   => undef,
1168         Silent => undef,
1169         PrincipalId => undef,
1170         Email => undef,
1171         @_
1172     );
1173
1174
1175     my $principal = RT::Principal->new($self->CurrentUser);
1176     if ($args{'Email'}) {
1177         if ( RT::EmailParser->IsRTAddress( $args{'Email'} ) ) {
1178             return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $args{'Email'}, $self->loc($args{'Type'})));
1179         }
1180         my $user = RT::User->new(RT->SystemUser);
1181         my ($pid, $msg) = $user->LoadOrCreateByEmail( $args{'Email'} );
1182         $args{'PrincipalId'} = $pid if $pid; 
1183     }
1184     if ($args{'PrincipalId'}) {
1185         $principal->Load($args{'PrincipalId'});
1186         if ( $principal->id and $principal->IsUser and my $email = $principal->Object->EmailAddress ) {
1187             return (0, $self->loc("[_1] is an address RT receives mail at. Adding it as a '[_2]' would create a mail loop", $email, $self->loc($args{'Type'})))
1188                 if RT::EmailParser->IsRTAddress( $email );
1189
1190         }
1191     } 
1192
1193  
1194     # If we can't find this watcher, we need to bail.
1195     unless ($principal->Id) {
1196             $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1197         return(0, $self->loc("Could not find or create that user"));
1198     }
1199
1200
1201     my $group = RT::Group->new($self->CurrentUser);
1202     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1203     unless ($group->id) {
1204         return(0,$self->loc("Group not found"));
1205     }
1206
1207     if ( $group->HasMember( $principal)) {
1208
1209         return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1210     }
1211
1212
1213     my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1214                                                InsideTransaction => 1 );
1215     unless ($m_id) {
1216         $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1217
1218         return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1219     }
1220
1221     unless ( $args{'Silent'} ) {
1222         $self->_NewTransaction(
1223             Type     => 'AddWatcher',
1224             NewValue => $principal->Id,
1225             Field    => $args{'Type'}
1226         );
1227     }
1228
1229         return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1230 }
1231
1232
1233
1234
1235 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1236
1237
1238 Deletes a Ticket watcher.  Takes two arguments:
1239
1240 Type  (one of Requestor,Cc,AdminCc)
1241
1242 and one of
1243
1244 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1245     OR
1246 Email (the email address of an existing wathcer)
1247
1248
1249 =cut
1250
1251
1252 sub DeleteWatcher {
1253     my $self = shift;
1254
1255     my %args = ( Type        => undef,
1256                  PrincipalId => undef,
1257                  Email       => undef,
1258                  @_ );
1259
1260     unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1261         return ( 0, $self->loc("No principal specified") );
1262     }
1263     my $principal = RT::Principal->new( $self->CurrentUser );
1264     if ( $args{'PrincipalId'} ) {
1265
1266         $principal->Load( $args{'PrincipalId'} );
1267     }
1268     else {
1269         my $user = RT::User->new( $self->CurrentUser );
1270         $user->LoadByEmail( $args{'Email'} );
1271         $principal->Load( $user->Id );
1272     }
1273
1274     # If we can't find this watcher, we need to bail.
1275     unless ( $principal->Id ) {
1276         return ( 0, $self->loc("Could not find that principal") );
1277     }
1278
1279     my $group = RT::Group->new( $self->CurrentUser );
1280     $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1281     unless ( $group->id ) {
1282         return ( 0, $self->loc("Group not found") );
1283     }
1284
1285     # Check ACLS
1286     #If the watcher we're trying to add is for the current user
1287     if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1288
1289         #  If it's an AdminCc and they don't have
1290         #   'WatchAsAdminCc' or 'ModifyTicket', bail
1291         if ( $args{'Type'} eq 'AdminCc' ) {
1292             unless (    $self->CurrentUserHasRight('ModifyTicket')
1293                      or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1294                 return ( 0, $self->loc('Permission Denied') );
1295             }
1296         }
1297
1298         #  If it's a Requestor or Cc and they don't have
1299         #   'Watch' or 'ModifyTicket', bail
1300         elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1301         {
1302             unless (    $self->CurrentUserHasRight('ModifyTicket')
1303                      or $self->CurrentUserHasRight('Watch') ) {
1304                 return ( 0, $self->loc('Permission Denied') );
1305             }
1306         }
1307         else {
1308             $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1309             return ( 0,
1310                      $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1311         }
1312     }
1313
1314     # If the watcher isn't the current user
1315     # and the current user  doesn't have 'ModifyTicket' bail
1316     else {
1317         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1318             return ( 0, $self->loc("Permission Denied") );
1319         }
1320     }
1321
1322     # }}}
1323
1324     # see if this user is already a watcher.
1325
1326     unless ( $group->HasMember($principal) ) {
1327         return ( 0,
1328                  $self->loc( 'That principal is not a [_1] for this ticket',
1329                              $args{'Type'} ) );
1330     }
1331
1332     my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1333     unless ($m_id) {
1334         $RT::Logger->error( "Failed to delete "
1335                             . $principal->Id
1336                             . " as a member of group "
1337                             . $group->Id . ": "
1338                             . $m_msg );
1339
1340         return (0,
1341                 $self->loc(
1342                     'Could not remove that principal as a [_1] for this ticket',
1343                     $args{'Type'} ) );
1344     }
1345
1346     unless ( $args{'Silent'} ) {
1347         $self->_NewTransaction( Type     => 'DelWatcher',
1348                                 OldValue => $principal->Id,
1349                                 Field    => $args{'Type'} );
1350     }
1351
1352     return ( 1,
1353              $self->loc( "[_1] is no longer a [_2] for this ticket.",
1354                          $principal->Object->Name,
1355                          $args{'Type'} ) );
1356 }
1357
1358
1359
1360
1361
1362 =head2 SquelchMailTo [EMAIL]
1363
1364 Takes an optional email address to never email about updates to this ticket.
1365
1366
1367 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1368
1369
1370 =cut
1371
1372 sub SquelchMailTo {
1373     my $self = shift;
1374     if (@_) {
1375         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1376             return ();
1377         }
1378     } else {
1379         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1380             return ();
1381         }
1382
1383     }
1384     return $self->_SquelchMailTo(@_);
1385 }
1386
1387 sub _SquelchMailTo {
1388     my $self = shift;
1389     if (@_) {
1390         my $attr = shift;
1391         $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1392             unless grep { $_->Content eq $attr }
1393                 $self->Attributes->Named('SquelchMailTo');
1394     }
1395     my @attributes = $self->Attributes->Named('SquelchMailTo');
1396     return (@attributes);
1397 }
1398
1399
1400 =head2 UnsquelchMailTo ADDRESS
1401
1402 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1403
1404 Returns a tuple of (status, message)
1405
1406 =cut
1407
1408 sub UnsquelchMailTo {
1409     my $self = shift;
1410
1411     my $address = shift;
1412     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1413         return ( 0, $self->loc("Permission Denied") );
1414     }
1415
1416     my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1417     return ($val, $msg);
1418 }
1419
1420
1421
1422 =head2 RequestorAddresses
1423
1424  B<Returns> String: All Ticket Requestor email addresses as a string.
1425
1426 =cut
1427
1428 sub RequestorAddresses {
1429     my $self = shift;
1430
1431     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1432         return undef;
1433     }
1434
1435     return ( $self->Requestors->MemberEmailAddressesAsString );
1436 }
1437
1438
1439 =head2 AdminCcAddresses
1440
1441 returns String: All Ticket AdminCc email addresses as a string
1442
1443 =cut
1444
1445 sub AdminCcAddresses {
1446     my $self = shift;
1447
1448     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1449         return undef;
1450     }
1451
1452     return ( $self->AdminCc->MemberEmailAddressesAsString )
1453
1454 }
1455
1456 =head2 CcAddresses
1457
1458 returns String: All Ticket Ccs as a string of email addresses
1459
1460 =cut
1461
1462 sub CcAddresses {
1463     my $self = shift;
1464
1465     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1466         return undef;
1467     }
1468     return ( $self->Cc->MemberEmailAddressesAsString);
1469
1470 }
1471
1472
1473
1474
1475 =head2 Requestors
1476
1477 Takes nothing.
1478 Returns this ticket's Requestors as an RT::Group object
1479
1480 =cut
1481
1482 sub Requestors {
1483     my $self = shift;
1484
1485     my $group = RT::Group->new($self->CurrentUser);
1486     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1487         $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1488     }
1489     return ($group);
1490
1491 }
1492
1493 =head2 _Requestors
1494
1495 Private non-ACLed variant of Reqeustors so that we can look them up for the
1496 purposes of customer auto-association during create.
1497
1498 =cut
1499
1500 sub _Requestors {
1501     my $self = shift;
1502
1503     my $group = RT::Group->new($RT::SystemUser);
1504     $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1505     return ($group);
1506 }
1507
1508 =head2 Cc
1509
1510 Takes nothing.
1511 Returns an RT::Group object which contains this ticket's Ccs.
1512 If the user doesn't have "ShowTicket" permission, returns an empty group
1513
1514 =cut
1515
1516 sub Cc {
1517     my $self = shift;
1518
1519     my $group = RT::Group->new($self->CurrentUser);
1520     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1521         $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1522     }
1523     return ($group);
1524
1525 }
1526
1527
1528
1529 =head2 AdminCc
1530
1531 Takes nothing.
1532 Returns an RT::Group object which contains this ticket's AdminCcs.
1533 If the user doesn't have "ShowTicket" permission, returns an empty group
1534
1535 =cut
1536
1537 sub AdminCc {
1538     my $self = shift;
1539
1540     my $group = RT::Group->new($self->CurrentUser);
1541     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1542         $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1543     }
1544     return ($group);
1545
1546 }
1547
1548
1549
1550
1551 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1552
1553 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1554
1555 Takes a param hash with the attributes Type and either PrincipalId or Email
1556
1557 Type is one of Requestor, Cc, AdminCc and Owner
1558
1559 PrincipalId is an RT::Principal id, and Email is an email address.
1560
1561 Returns true if the specified principal (or the one corresponding to the
1562 specified address) is a member of the group Type for this ticket.
1563
1564 XX TODO: This should be Memoized. 
1565
1566 =cut
1567
1568 sub IsWatcher {
1569     my $self = shift;
1570
1571     my %args = ( Type  => 'Requestor',
1572         PrincipalId    => undef,
1573         Email          => undef,
1574         @_
1575     );
1576
1577     # Load the relevant group. 
1578     my $group = RT::Group->new($self->CurrentUser);
1579     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1580
1581     # Find the relevant principal.
1582     if (!$args{PrincipalId} && $args{Email}) {
1583         # Look up the specified user.
1584         my $user = RT::User->new($self->CurrentUser);
1585         $user->LoadByEmail($args{Email});
1586         if ($user->Id) {
1587             $args{PrincipalId} = $user->PrincipalId;
1588         }
1589         else {
1590             # A non-existent user can't be a group member.
1591             return 0;
1592         }
1593     }
1594
1595     # Ask if it has the member in question
1596     return $group->HasMember( $args{'PrincipalId'} );
1597 }
1598
1599
1600
1601 =head2 IsRequestor PRINCIPAL_ID
1602   
1603 Takes an L<RT::Principal> id.
1604
1605 Returns true if the principal is a requestor of the current ticket.
1606
1607 =cut
1608
1609 sub IsRequestor {
1610     my $self   = shift;
1611     my $person = shift;
1612
1613     return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1614
1615 };
1616
1617
1618
1619 =head2 IsCc PRINCIPAL_ID
1620
1621   Takes an RT::Principal id.
1622   Returns true if the principal is a Cc of the current ticket.
1623
1624
1625 =cut
1626
1627 sub IsCc {
1628     my $self = shift;
1629     my $cc   = shift;
1630
1631     return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1632
1633 }
1634
1635
1636
1637 =head2 IsAdminCc PRINCIPAL_ID
1638
1639   Takes an RT::Principal id.
1640   Returns true if the principal is an AdminCc of the current ticket.
1641
1642 =cut
1643
1644 sub IsAdminCc {
1645     my $self   = shift;
1646     my $person = shift;
1647
1648     return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1649
1650 }
1651
1652
1653
1654 =head2 IsOwner
1655
1656   Takes an RT::User object. Returns true if that user is this ticket's owner.
1657 returns undef otherwise
1658
1659 =cut
1660
1661 sub IsOwner {
1662     my $self   = shift;
1663     my $person = shift;
1664
1665     # no ACL check since this is used in acl decisions
1666     # unless ($self->CurrentUserHasRight('ShowTicket')) {
1667     #    return(undef);
1668     #   }    
1669
1670     #Tickets won't yet have owners when they're being created.
1671     unless ( $self->OwnerObj->id ) {
1672         return (undef);
1673     }
1674
1675     if ( $person->id == $self->OwnerObj->id ) {
1676         return (1);
1677     }
1678     else {
1679         return (undef);
1680     }
1681 }
1682
1683
1684
1685
1686
1687 =head2 TransactionAddresses
1688
1689 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1690 all this ticket's Create, Comment or Correspond transactions. The keys are
1691 stringified email addresses. Each value is an L<Email::Address> object.
1692
1693 NOTE: For performance reasons, this method might want to skip transactions and go straight for attachments. But to make that work right, we're going to need to go and walk around the access control in Attachment.pm's sub _Value.
1694
1695 =cut
1696
1697
1698 sub TransactionAddresses {
1699     my $self = shift;
1700     my $txns = $self->Transactions;
1701
1702     my %addresses = ();
1703
1704     my $attachments = RT::Attachments->new( $self->CurrentUser );
1705     $attachments->LimitByTicket( $self->id );
1706     $attachments->Columns( qw( id Headers TransactionId));
1707
1708
1709     foreach my $type (qw(Create Comment Correspond)) {
1710         $attachments->Limit( ALIAS    => $attachments->TransactionAlias,
1711                              FIELD    => 'Type',
1712                              OPERATOR => '=',
1713                              VALUE    => $type,
1714                              ENTRYAGGREGATOR => 'OR',
1715                              CASESENSITIVE   => 1
1716                            );
1717     }
1718
1719     while ( my $att = $attachments->Next ) {
1720         foreach my $addrlist ( values %{$att->Addresses } ) {
1721             foreach my $addr (@$addrlist) {
1722
1723 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1724                 next
1725                     if (    $addresses{ $addr->address }
1726                          && $addresses{ $addr->address }->phrase
1727                          && not $addr->phrase );
1728
1729                 # skips "comment-only" addresses
1730                 next unless ( $addr->address );
1731                 $addresses{ $addr->address } = $addr;
1732             }
1733         }
1734     }
1735
1736     return \%addresses;
1737
1738 }
1739
1740
1741
1742
1743
1744
1745 sub ValidateQueue {
1746     my $self  = shift;
1747     my $Value = shift;
1748
1749     if ( !$Value ) {
1750         $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1751         return (1);
1752     }
1753
1754     my $QueueObj = RT::Queue->new( $self->CurrentUser );
1755     my $id       = $QueueObj->Load($Value);
1756
1757     if ($id) {
1758         return (1);
1759     }
1760     else {
1761         return (undef);
1762     }
1763 }
1764
1765
1766
1767 sub SetQueue {
1768     my $self     = shift;
1769     my $NewQueue = shift;
1770
1771     #Redundant. ACL gets checked in _Set;
1772     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1773         return ( 0, $self->loc("Permission Denied") );
1774     }
1775
1776     my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1777     $NewQueueObj->Load($NewQueue);
1778
1779     unless ( $NewQueueObj->Id() ) {
1780         return ( 0, $self->loc("That queue does not exist") );
1781     }
1782
1783     if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1784         return ( 0, $self->loc('That is the same value') );
1785     }
1786     unless ( $self->CurrentUser->HasRight( Right    => 'CreateTicket', Object => $NewQueueObj)) {
1787         return ( 0, $self->loc("You may not create requests in that queue.") );
1788     }
1789
1790     my $new_status;
1791     my $old_lifecycle = $self->QueueObj->Lifecycle;
1792     my $new_lifecycle = $NewQueueObj->Lifecycle;
1793     if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
1794         unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
1795             return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
1796         }
1797         $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
1798         return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
1799             unless $new_status;
1800     }
1801
1802     if ( $new_status ) {
1803         my $clone = RT::Ticket->new( RT->SystemUser );
1804         $clone->Load( $self->Id );
1805         unless ( $clone->Id ) {
1806             return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1807         }
1808
1809         my $now = RT::Date->new( $self->CurrentUser );
1810         $now->SetToNow;
1811
1812         my $old_status = $clone->Status;
1813
1814         #If we're changing the status from initial in old to not intial in new,
1815         # record that we've started
1816         if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status)  && $clone->StartedObj->Unix == 0 ) {
1817             #Set the Started time to "now"
1818             $clone->_Set(
1819                 Field             => 'Started',
1820                 Value             => $now->ISO,
1821                 RecordTransaction => 0
1822             );
1823         }
1824
1825         #When we close a ticket, set the 'Resolved' attribute to now.
1826         # It's misnamed, but that's just historical.
1827         if ( $new_lifecycle->IsInactive($new_status) ) {
1828             $clone->_Set(
1829                 Field             => 'Resolved',
1830                 Value             => $now->ISO,
1831                 RecordTransaction => 0,
1832             );
1833         }
1834
1835         #Actually update the status
1836         my ($val, $msg)= $clone->_Set(
1837             Field             => 'Status',
1838             Value             => $new_status,
1839             RecordTransaction => 0,
1840         );
1841         $RT::Logger->error( 'Status change failed on queue change: '. $msg )
1842             unless $val;
1843     }
1844
1845     my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1846
1847     if ( $status ) {
1848         # Clear the queue object cache;
1849         $self->{_queue_obj} = undef;
1850
1851         # Untake the ticket if we have no permissions in the new queue
1852         unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
1853             my $clone = RT::Ticket->new( RT->SystemUser );
1854             $clone->Load( $self->Id );
1855             unless ( $clone->Id ) {
1856                 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1857             }
1858             my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1859             $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1860         }
1861
1862         # On queue change, change queue for reminders too
1863         my $reminder_collection = $self->Reminders->Collection;
1864         while ( my $reminder = $reminder_collection->Next ) {
1865             my ($status, $msg) = $reminder->SetQueue($NewQueue);
1866             $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1867         }
1868     }
1869
1870     return ($status, $msg);
1871 }
1872
1873
1874
1875 =head2 QueueObj
1876
1877 Takes nothing. returns this ticket's queue object
1878
1879 =cut
1880
1881 sub QueueObj {
1882     my $self = shift;
1883
1884     if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1885
1886         $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1887
1888         #We call __Value so that we can avoid the ACL decision and some deep recursion
1889         my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1890     }
1891     return ($self->{_queue_obj});
1892 }
1893
1894 =head2 SubjectTag
1895
1896 Takes nothing. Returns SubjectTag for this ticket. Includes
1897 queue's subject tag or rtname if that is not set, ticket
1898 id and braces, for example:
1899
1900     [support.example.com #123456]
1901
1902 =cut
1903
1904 sub SubjectTag {
1905     my $self = shift;
1906     return
1907         '['
1908         . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1909         .' #'. $self->id
1910         .']'
1911     ;
1912 }
1913
1914
1915 =head2 DueObj
1916
1917   Returns an RT::Date object containing this ticket's due date
1918
1919 =cut
1920
1921 sub DueObj {
1922     my $self = shift;
1923
1924     my $time = RT::Date->new( $self->CurrentUser );
1925
1926     # -1 is RT::Date slang for never
1927     if ( my $due = $self->Due ) {
1928         $time->Set( Format => 'sql', Value => $due );
1929     }
1930     else {
1931         $time->Set( Format => 'unix', Value => -1 );
1932     }
1933
1934     return $time;
1935 }
1936
1937
1938
1939 =head2 DueAsString
1940
1941 Returns this ticket's due date as a human readable string
1942
1943 =cut
1944
1945 sub DueAsString {
1946     my $self = shift;
1947     return $self->DueObj->AsString();
1948 }
1949
1950
1951
1952 =head2 ResolvedObj
1953
1954   Returns an RT::Date object of this ticket's 'resolved' time.
1955
1956 =cut
1957
1958 sub ResolvedObj {
1959     my $self = shift;
1960
1961     my $time = RT::Date->new( $self->CurrentUser );
1962     $time->Set( Format => 'sql', Value => $self->Resolved );
1963     return $time;
1964 }
1965
1966
1967 =head2 FirstActiveStatus
1968
1969 Returns the first active status that the ticket could transition to,
1970 according to its current Queue's lifecycle.  May return undef if there
1971 is no such possible status to transition to, or we are already in it.
1972 This is used in L<RT::Action::AutoOpen>, for instance.
1973
1974 =cut
1975
1976 sub FirstActiveStatus {
1977     my $self = shift;
1978
1979     my $lifecycle = $self->QueueObj->Lifecycle;
1980     my $status = $self->Status;
1981     my @active = $lifecycle->Active;
1982     # no change if no active statuses in the lifecycle
1983     return undef unless @active;
1984
1985     # no change if the ticket is already has first status from the list of active
1986     return undef if lc $status eq lc $active[0];
1987
1988     my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1989     return $next;
1990 }
1991
1992 =head2 SetStarted
1993
1994 Takes a date in ISO format or undef
1995 Returns a transaction id and a message
1996 The client calls "Start" to note that the project was started on the date in $date.
1997 A null date means "now"
1998
1999 =cut
2000
2001 sub SetStarted {
2002     my $self = shift;
2003     my $time = shift || 0;
2004
2005     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2006         return ( 0, $self->loc("Permission Denied") );
2007     }
2008
2009     #We create a date object to catch date weirdness
2010     my $time_obj = RT::Date->new( $self->CurrentUser() );
2011     if ( $time ) {
2012         $time_obj->Set( Format => 'ISO', Value => $time );
2013     }
2014     else {
2015         $time_obj->SetToNow();
2016     }
2017
2018     # We need $TicketAsSystem, in case the current user doesn't have
2019     # ShowTicket
2020     my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
2021     $TicketAsSystem->Load( $self->Id );
2022     # Now that we're starting, open this ticket
2023     # TODO: do we really want to force this as policy? it should be a scrip
2024     my $next = $TicketAsSystem->FirstActiveStatus;
2025
2026     $self->SetStatus( $next ) if defined $next;
2027
2028     return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2029
2030 }
2031
2032
2033
2034 =head2 StartedObj
2035
2036   Returns an RT::Date object which contains this ticket's 
2037 'Started' time.
2038
2039 =cut
2040
2041 sub StartedObj {
2042     my $self = shift;
2043
2044     my $time = RT::Date->new( $self->CurrentUser );
2045     $time->Set( Format => 'sql', Value => $self->Started );
2046     return $time;
2047 }
2048
2049
2050
2051 =head2 StartsObj
2052
2053   Returns an RT::Date object which contains this ticket's 
2054 'Starts' time.
2055
2056 =cut
2057
2058 sub StartsObj {
2059     my $self = shift;
2060
2061     my $time = RT::Date->new( $self->CurrentUser );
2062     $time->Set( Format => 'sql', Value => $self->Starts );
2063     return $time;
2064 }
2065
2066
2067
2068 =head2 ToldObj
2069
2070   Returns an RT::Date object which contains this ticket's 
2071 'Told' time.
2072
2073 =cut
2074
2075 sub ToldObj {
2076     my $self = shift;
2077
2078     my $time = RT::Date->new( $self->CurrentUser );
2079     $time->Set( Format => 'sql', Value => $self->Told );
2080     return $time;
2081 }
2082
2083
2084
2085 =head2 ToldAsString
2086
2087 A convenience method that returns ToldObj->AsString
2088
2089 TODO: This should be deprecated
2090
2091 =cut
2092
2093 sub ToldAsString {
2094     my $self = shift;
2095     if ( $self->Told ) {
2096         return $self->ToldObj->AsString();
2097     }
2098     else {
2099         return ("Never");
2100     }
2101 }
2102
2103
2104
2105 =head2 TimeWorkedAsString
2106
2107 Returns the amount of time worked on this ticket as a Text String
2108
2109 =cut
2110
2111 sub TimeWorkedAsString {
2112     my $self = shift;
2113     my $value = $self->TimeWorked;
2114
2115     # return the # of minutes worked turned into seconds and written as
2116     # a simple text string, this is not really a date object, but if we
2117     # diff a number of seconds vs the epoch, we'll get a nice description
2118     # of time worked.
2119     return "" unless $value;
2120     return RT::Date->new( $self->CurrentUser )
2121         ->DurationAsString( $value * 60 );
2122 }
2123
2124
2125
2126 =head2  TimeLeftAsString
2127
2128 Returns the amount of time left on this ticket as a Text String
2129
2130 =cut
2131
2132 sub TimeLeftAsString {
2133     my $self = shift;
2134     my $value = $self->TimeLeft;
2135     return "" unless $value;
2136     return RT::Date->new( $self->CurrentUser )
2137         ->DurationAsString( $value * 60 );
2138 }
2139
2140
2141
2142
2143 =head2 Comment
2144
2145 Comment on this ticket.
2146 Takes a hash with the following attributes:
2147 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2148 comment.
2149
2150 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2151
2152 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2153 They will, however, be prepared and you'll be able to access them through the TransactionObj
2154
2155 Returns: Transaction id, Error Message, Transaction Object
2156 (note the different order from Create()!)
2157
2158 =cut
2159
2160 sub Comment {
2161     my $self = shift;
2162
2163     my %args = ( CcMessageTo  => undef,
2164                  BccMessageTo => undef,
2165                  MIMEObj      => undef,
2166                  Content      => undef,
2167                  TimeTaken => 0,
2168                  DryRun     => 0, 
2169                  @_ );
2170
2171     unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
2172              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2173         return ( 0, $self->loc("Permission Denied"), undef );
2174     }
2175     $args{'NoteType'} = 'Comment';
2176
2177     if ($args{'DryRun'}) {
2178         $RT::Handle->BeginTransaction();
2179         $args{'CommitScrips'} = 0;
2180     }
2181
2182     my @results = $self->_RecordNote(%args);
2183     if ($args{'DryRun'}) {
2184         $RT::Handle->Rollback();
2185     }
2186
2187     return(@results);
2188 }
2189
2190
2191 =head2 Correspond
2192
2193 Correspond on this ticket.
2194 Takes a hashref with the following attributes:
2195
2196
2197 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2198
2199 if there's no MIMEObj, Content is used to build a MIME::Entity object
2200
2201 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2202 They will, however, be prepared and you'll be able to access them through the TransactionObj
2203
2204 Returns: Transaction id, Error Message, Transaction Object
2205 (note the different order from Create()!)
2206
2207
2208 =cut
2209
2210 sub Correspond {
2211     my $self = shift;
2212     my %args = ( CcMessageTo  => undef,
2213                  BccMessageTo => undef,
2214                  MIMEObj      => undef,
2215                  Content      => undef,
2216                  TimeTaken    => 0,
2217                  @_ );
2218
2219     unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
2220              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2221         return ( 0, $self->loc("Permission Denied"), undef );
2222     }
2223
2224     $args{'NoteType'} = 'Correspond'; 
2225     if ($args{'DryRun'}) {
2226         $RT::Handle->BeginTransaction();
2227         $args{'CommitScrips'} = 0;
2228     }
2229
2230     my @results = $self->_RecordNote(%args);
2231
2232     #Set the last told date to now if this isn't mail from the requestor.
2233     #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2234     unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2235         my %squelch;
2236         $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2237         $self->_SetTold
2238             if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2239     }
2240
2241     if ($args{'DryRun'}) {
2242         $RT::Handle->Rollback();
2243     }
2244
2245     return (@results);
2246
2247 }
2248
2249
2250
2251 =head2 _RecordNote
2252
2253 the meat of both comment and correspond. 
2254
2255 Performs no access control checks. hence, dangerous.
2256
2257 =cut
2258
2259 sub _RecordNote {
2260     my $self = shift;
2261     my %args = ( 
2262         CcMessageTo  => undef,
2263         BccMessageTo => undef,
2264         Encrypt      => undef,
2265         Sign         => undef,
2266         MIMEObj      => undef,
2267         Content      => undef,
2268         NoteType     => 'Correspond',
2269         TimeTaken    => 0,
2270         CommitScrips => 1,
2271         SquelchMailTo => undef,
2272         CustomFields => {},
2273         @_
2274     );
2275
2276     unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2277         return ( 0, $self->loc("No message attached"), undef );
2278     }
2279
2280     unless ( $args{'MIMEObj'} ) {
2281         $args{'MIMEObj'} = MIME::Entity->build(
2282             Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2283         );
2284     }
2285
2286     # convert text parts into utf-8
2287     RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2288
2289     # If we've been passed in CcMessageTo and BccMessageTo fields,
2290     # add them to the mime object for passing on to the transaction handler
2291     # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2292     # RT-Send-Bcc: headers
2293
2294
2295     foreach my $type (qw/Cc Bcc/) {
2296         if ( defined $args{ $type . 'MessageTo' } ) {
2297
2298             my $addresses = join ', ', (
2299                 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2300                     Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2301             $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2302         }
2303     }
2304
2305     foreach my $argument (qw(Encrypt Sign)) {
2306         $args{'MIMEObj'}->head->replace(
2307             "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2308         ) if defined $args{ $argument };
2309     }
2310
2311     # If this is from an external source, we need to come up with its
2312     # internal Message-ID now, so all emails sent because of this
2313     # message have a common Message-ID
2314     my $org = RT->Config->Get('Organization');
2315     my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2316     unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2317         $args{'MIMEObj'}->head->set(
2318             'RT-Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $self )
2319         );
2320     }
2321
2322     #Record the correspondence (write the transaction)
2323     my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2324              Type => $args{'NoteType'},
2325              Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2326              TimeTaken => $args{'TimeTaken'},
2327              MIMEObj   => $args{'MIMEObj'}, 
2328              CommitScrips => $args{'CommitScrips'},
2329              SquelchMailTo => $args{'SquelchMailTo'},
2330              CustomFields => $args{'CustomFields'},
2331     );
2332
2333     unless ($Trans) {
2334         $RT::Logger->err("$self couldn't init a transaction $msg");
2335         return ( $Trans, $self->loc("Message could not be recorded"), undef );
2336     }
2337
2338     return ( $Trans, $self->loc("Message recorded"), $TransObj );
2339 }
2340
2341
2342 =head2 DryRun
2343
2344 Builds a MIME object from the given C<UpdateSubject> and
2345 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2346 C<< DryRun => 1 >>, and returns the transaction so produced.
2347
2348 =cut
2349
2350 sub DryRun {
2351     my $self = shift;
2352     my %args = @_;
2353     my $action;
2354     if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2355         $action = 'Correspond';
2356     } else {
2357         $action = 'Comment';
2358     }
2359
2360     my $Message = MIME::Entity->build(
2361         Type    => 'text/plain',
2362         Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2363         Charset => 'UTF-8',
2364         Data    => $args{'UpdateContent'} || "",
2365     );
2366
2367     my ( $Transaction, $Description, $Object ) = $self->$action(
2368         CcMessageTo  => $args{'UpdateCc'},
2369         BccMessageTo => $args{'UpdateBcc'},
2370         MIMEObj      => $Message,
2371         TimeTaken    => $args{'UpdateTimeWorked'},
2372         DryRun       => 1,
2373     );
2374     unless ( $Transaction ) {
2375         $RT::Logger->error("Couldn't fire '$action' action: $Description");
2376     }
2377
2378     return $Object;
2379 }
2380
2381 =head2 DryRunCreate
2382
2383 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2384 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2385 the resulting L<RT::Transaction>.
2386
2387 =cut
2388
2389 sub DryRunCreate {
2390     my $self = shift;
2391     my %args = @_;
2392     my $Message = MIME::Entity->build(
2393         Type    => 'text/plain',
2394         Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2395         (defined $args{'Cc'} ?
2396              ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2397         Charset => 'UTF-8',
2398         Data    => $args{'Content'} || "",
2399     );
2400
2401     my ( $Transaction, $Object, $Description ) = $self->Create(
2402         Type            => $args{'Type'} || 'ticket',
2403         Queue           => $args{'Queue'},
2404         Owner           => $args{'Owner'},
2405         Requestor       => $args{'Requestors'},
2406         Cc              => $args{'Cc'},
2407         AdminCc         => $args{'AdminCc'},
2408         InitialPriority => $args{'InitialPriority'},
2409         FinalPriority   => $args{'FinalPriority'},
2410         TimeLeft        => $args{'TimeLeft'},
2411         TimeEstimated   => $args{'TimeEstimated'},
2412         TimeWorked      => $args{'TimeWorked'},
2413         Subject         => $args{'Subject'},
2414         Status          => $args{'Status'},
2415         MIMEObj         => $Message,
2416         DryRun          => 1,
2417     );
2418     unless ( $Transaction ) {
2419         $RT::Logger->error("Couldn't fire Create action: $Description");
2420     }
2421
2422     return $Object;
2423 }
2424
2425
2426
2427 sub _Links {
2428     my $self = shift;
2429
2430     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2431     #tobias meant by $f
2432     my $field = shift;
2433     my $type  = shift || "";
2434
2435     my $cache_key = "$field$type";
2436     return $self->{ $cache_key } if $self->{ $cache_key };
2437
2438     my $links = $self->{ $cache_key }
2439               = RT::Links->new( $self->CurrentUser );
2440     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2441         $links->Limit( FIELD => 'id', VALUE => 0 );
2442         return $links;
2443     }
2444
2445     # Maybe this ticket is a merge ticket
2446     my $limit_on = 'Local'. $field;
2447     # at least to myself
2448     $links->Limit(
2449         FIELD           => $limit_on,
2450         VALUE           => $self->id,
2451         ENTRYAGGREGATOR => 'OR',
2452     );
2453     $links->Limit(
2454         FIELD           => $limit_on,
2455         VALUE           => $_,
2456         ENTRYAGGREGATOR => 'OR',
2457     ) foreach $self->Merged;
2458     $links->Limit(
2459         FIELD => 'Type',
2460         VALUE => $type,
2461     ) if $type;
2462
2463     return $links;
2464 }
2465
2466
2467
2468 =head2 DeleteLink
2469
2470 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2471 SilentBase and SilentTarget. Either Base or Target must be null.
2472 The null value will be replaced with this ticket\'s id.
2473
2474 If Silent is true then no transaction would be recorded, in other
2475 case you can control creation of transactions on both base and
2476 target with SilentBase and SilentTarget respectively. By default
2477 both transactions are created.
2478
2479 =cut 
2480
2481 sub DeleteLink {
2482     my $self = shift;
2483     my %args = (
2484         Base   => undef,
2485         Target => undef,
2486         Type   => undef,
2487         Silent => undef,
2488         SilentBase   => undef,
2489         SilentTarget => undef,
2490         @_
2491     );
2492
2493     unless ( $args{'Target'} || $args{'Base'} ) {
2494         $RT::Logger->error("Base or Target must be specified");
2495         return ( 0, $self->loc('Either base or target must be specified') );
2496     }
2497
2498     #check acls
2499     my $right = 0;
2500     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2501     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2502         return ( 0, $self->loc("Permission Denied") );
2503     }
2504
2505     # If the other URI is an RT::Ticket, we want to make sure the user
2506     # can modify it too...
2507     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2508     return (0, $msg) unless $status;
2509     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2510         $right++;
2511     }
2512     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2513          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2514     {
2515         return ( 0, $self->loc("Permission Denied") );
2516     }
2517
2518     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2519     return ( 0, $Msg ) unless $val;
2520
2521     return ( $val, $Msg ) if $args{'Silent'};
2522
2523     my ($direction, $remote_link);
2524
2525     if ( $args{'Base'} ) {
2526         $remote_link = $args{'Base'};
2527         $direction = 'Target';
2528     }
2529     elsif ( $args{'Target'} ) {
2530         $remote_link = $args{'Target'};
2531         $direction = 'Base';
2532     } 
2533
2534     my $remote_uri = RT::URI->new( $self->CurrentUser );
2535     $remote_uri->FromURI( $remote_link );
2536
2537     unless ( $args{ 'Silent'. $direction } ) {
2538         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2539             Type      => 'DeleteLink',
2540             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2541             OldValue  => $remote_uri->URI || $remote_link,
2542             TimeTaken => 0
2543         );
2544         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2545     }
2546
2547     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2548         my $OtherObj = $remote_uri->Object;
2549         my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2550             Type           => 'DeleteLink',
2551             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2552                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2553             OldValue       => $self->URI,
2554             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2555             TimeTaken      => 0,
2556         );
2557         $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2558     }
2559
2560     return ( $val, $Msg );
2561 }
2562
2563
2564
2565 =head2 AddLink
2566
2567 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2568
2569 If Silent is true then no transaction would be recorded, in other
2570 case you can control creation of transactions on both base and
2571 target with SilentBase and SilentTarget respectively. By default
2572 both transactions are created.
2573
2574 =cut
2575
2576 sub AddLink {
2577     my $self = shift;
2578     my %args = ( Target       => '',
2579                  Base         => '',
2580                  Type         => '',
2581                  Silent       => undef,
2582                  SilentBase   => undef,
2583                  SilentTarget => undef,
2584                  @_ );
2585
2586     unless ( $args{'Target'} || $args{'Base'} ) {
2587         $RT::Logger->error("Base or Target must be specified");
2588         return ( 0, $self->loc('Either base or target must be specified') );
2589     }
2590
2591     my $right = 0;
2592     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2593     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2594         return ( 0, $self->loc("Permission Denied") );
2595     }
2596
2597     # If the other URI is an RT::Ticket, we want to make sure the user
2598     # can modify it too...
2599     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2600     return (0, $msg) unless $status;
2601     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2602         $right++;
2603     }
2604     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2605          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2606     {
2607         return ( 0, $self->loc("Permission Denied") );
2608     }
2609
2610     return ( 0, "Can't link to a deleted ticket" )
2611       if $other_ticket && $other_ticket->Status eq 'deleted';
2612
2613     return $self->_AddLink(%args);
2614 }
2615
2616 sub __GetTicketFromURI {
2617     my $self = shift;
2618     my %args = ( URI => '', @_ );
2619
2620     # If the other URI is an RT::Ticket, we want to make sure the user
2621     # can modify it too...
2622     my $uri_obj = RT::URI->new( $self->CurrentUser );
2623     $uri_obj->FromURI( $args{'URI'} );
2624
2625     unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2626         my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2627         $RT::Logger->warning( $msg );
2628         return( 0, $msg );
2629     }
2630     my $obj = $uri_obj->Resolver->Object;
2631     unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2632         return (1, 'Found not a ticket', undef);
2633     }
2634     return (1, 'Found ticket', $obj);
2635 }
2636
2637 =head2 _AddLink  
2638
2639 Private non-acled variant of AddLink so that links can be added during create.
2640
2641 =cut
2642
2643 sub _AddLink {
2644     my $self = shift;
2645     my %args = ( Target       => '',
2646                  Base         => '',
2647                  Type         => '',
2648                  Silent       => undef,
2649                  SilentBase   => undef,
2650                  SilentTarget => undef,
2651                  @_ );
2652
2653     my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2654     return ($val, $msg) if !$val || $exist;
2655     return ($val, $msg) if $args{'Silent'};
2656
2657     my ($direction, $remote_link);
2658     if ( $args{'Target'} ) {
2659         $remote_link  = $args{'Target'};
2660         $direction    = 'Base';
2661     } elsif ( $args{'Base'} ) {
2662         $remote_link  = $args{'Base'};
2663         $direction    = 'Target';
2664     }
2665
2666     my $remote_uri = RT::URI->new( $self->CurrentUser );
2667     $remote_uri->FromURI( $remote_link );
2668
2669     unless ( $args{ 'Silent'. $direction } ) {
2670         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2671             Type      => 'AddLink',
2672             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2673             NewValue  =>  $remote_uri->URI || $remote_link,
2674             TimeTaken => 0
2675         );
2676         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2677     }
2678
2679     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2680         my $OtherObj = $remote_uri->Object;
2681         my ( $val, $msg ) = $OtherObj->_NewTransaction(
2682             Type           => 'AddLink',
2683             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2684                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2685             NewValue       => $self->URI,
2686             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2687             TimeTaken      => 0,
2688         );
2689         $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2690     }
2691
2692     return ( $val, $msg );
2693 }
2694
2695
2696
2697
2698 =head2 MergeInto
2699
2700 MergeInto take the id of the ticket to merge this ticket into.
2701
2702 =cut
2703
2704 sub MergeInto {
2705     my $self      = shift;
2706     my $ticket_id = shift;
2707
2708     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2709         return ( 0, $self->loc("Permission Denied") );
2710     }
2711
2712     # Load up the new ticket.
2713     my $MergeInto = RT::Ticket->new($self->CurrentUser);
2714     $MergeInto->Load($ticket_id);
2715
2716     # make sure it exists.
2717     unless ( $MergeInto->Id ) {
2718         return ( 0, $self->loc("New ticket doesn't exist") );
2719     }
2720
2721     # Make sure the current user can modify the new ticket.
2722     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2723         return ( 0, $self->loc("Permission Denied") );
2724     }
2725
2726     delete $MERGE_CACHE{'effective'}{ $self->id };
2727     delete @{ $MERGE_CACHE{'merged'} }{
2728         $ticket_id, $MergeInto->id, $self->id
2729     };
2730
2731     $RT::Handle->BeginTransaction();
2732
2733     $self->_MergeInto( $MergeInto );
2734
2735     $RT::Handle->Commit();
2736
2737     return ( 1, $self->loc("Merge Successful") );
2738 }
2739
2740 sub _MergeInto {
2741     my $self      = shift;
2742     my $MergeInto = shift;
2743
2744
2745     # We use EffectiveId here even though it duplicates information from
2746     # the links table becasue of the massive performance hit we'd take
2747     # by trying to do a separate database query for merge info everytime 
2748     # loaded a ticket. 
2749
2750     #update this ticket's effective id to the new ticket's id.
2751     my ( $id_val, $id_msg ) = $self->__Set(
2752         Field => 'EffectiveId',
2753         Value => $MergeInto->Id()
2754     );
2755
2756     unless ($id_val) {
2757         $RT::Handle->Rollback();
2758         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2759     }
2760
2761
2762     my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2763     if ( $force_status && $force_status ne $self->__Value('Status') ) {
2764         my ( $status_val, $status_msg )
2765             = $self->__Set( Field => 'Status', Value => $force_status );
2766
2767         unless ($status_val) {
2768             $RT::Handle->Rollback();
2769             $RT::Logger->error(
2770                 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2771             );
2772             return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2773         }
2774     }
2775
2776     # update all the links that point to that old ticket
2777     my $old_links_to = RT::Links->new($self->CurrentUser);
2778     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2779
2780     my %old_seen;
2781     while (my $link = $old_links_to->Next) {
2782         if (exists $old_seen{$link->Base."-".$link->Type}) {
2783             $link->Delete;
2784         }   
2785         elsif ($link->Base eq $MergeInto->URI) {
2786             $link->Delete;
2787         } else {
2788             # First, make sure the link doesn't already exist. then move it over.
2789             my $tmp = RT::Link->new(RT->SystemUser);
2790             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2791             if ($tmp->id)   {
2792                     $link->Delete;
2793             } else { 
2794                 $link->SetTarget($MergeInto->URI);
2795                 $link->SetLocalTarget($MergeInto->id);
2796             }
2797             $old_seen{$link->Base."-".$link->Type} =1;
2798         }
2799
2800     }
2801
2802     my $old_links_from = RT::Links->new($self->CurrentUser);
2803     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2804
2805     while (my $link = $old_links_from->Next) {
2806         if (exists $old_seen{$link->Type."-".$link->Target}) {
2807             $link->Delete;
2808         }   
2809         if ($link->Target eq $MergeInto->URI) {
2810             $link->Delete;
2811         } else {
2812             # First, make sure the link doesn't already exist. then move it over.
2813             my $tmp = RT::Link->new(RT->SystemUser);
2814             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2815             if ($tmp->id)   {
2816                     $link->Delete;
2817             } else { 
2818                 $link->SetBase($MergeInto->URI);
2819                 $link->SetLocalBase($MergeInto->id);
2820                 $old_seen{$link->Type."-".$link->Target} =1;
2821             }
2822         }
2823
2824     }
2825
2826     # Update time fields
2827     foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2828
2829         my $mutator = "Set$type";
2830         $MergeInto->$mutator(
2831             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2832
2833     }
2834 #add all of this ticket's watchers to that ticket.
2835     foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2836
2837         my $people = $self->$watcher_type->MembersObj;
2838         my $addwatcher_type =  $watcher_type;
2839         $addwatcher_type  =~ s/s$//;
2840
2841         while ( my $watcher = $people->Next ) {
2842             
2843            my ($val, $msg) =  $MergeInto->_AddWatcher(
2844                 Type        => $addwatcher_type,
2845                 Silent => 1,
2846                 PrincipalId => $watcher->MemberId
2847             );
2848             unless ($val) {
2849                 $RT::Logger->debug($msg);
2850             }
2851     }
2852
2853     }
2854
2855     #find all of the tickets that were merged into this ticket. 
2856     my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2857     $old_mergees->Limit(
2858         FIELD    => 'EffectiveId',
2859         OPERATOR => '=',
2860         VALUE    => $self->Id
2861     );
2862
2863     #   update their EffectiveId fields to the new ticket's id
2864     while ( my $ticket = $old_mergees->Next() ) {
2865         my ( $val, $msg ) = $ticket->__Set(
2866             Field => 'EffectiveId',
2867             Value => $MergeInto->Id()
2868         );
2869     }
2870
2871     #make a new link: this ticket is merged into that other ticket.
2872     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2873
2874     $MergeInto->_SetLastUpdated;    
2875 }
2876
2877 =head2 Merged
2878
2879 Returns list of tickets' ids that's been merged into this ticket.
2880
2881 =cut
2882
2883 sub Merged {
2884     my $self = shift;
2885
2886     my $id = $self->id;
2887     return @{ $MERGE_CACHE{'merged'}{ $id } }
2888         if $MERGE_CACHE{'merged'}{ $id };
2889
2890     my $mergees = RT::Tickets->new( $self->CurrentUser );
2891     $mergees->Limit(
2892         FIELD    => 'EffectiveId',
2893         VALUE    => $id,
2894     );
2895     $mergees->Limit(
2896         FIELD    => 'id',
2897         OPERATOR => '!=',
2898         VALUE    => $id,
2899     );
2900     return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2901         = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2902 }
2903
2904
2905
2906
2907
2908 =head2 OwnerObj
2909
2910 Takes nothing and returns an RT::User object of 
2911 this ticket's owner
2912
2913 =cut
2914
2915 sub OwnerObj {
2916     my $self = shift;
2917
2918     #If this gets ACLed, we lose on a rights check in User.pm and
2919     #get deep recursion. if we need ACLs here, we need
2920     #an equiv without ACLs
2921
2922     my $owner = RT::User->new( $self->CurrentUser );
2923     $owner->Load( $self->__Value('Owner') );
2924
2925     #Return the owner object
2926     return ($owner);
2927 }
2928
2929
2930
2931 =head2 OwnerAsString
2932
2933 Returns the owner's email address
2934
2935 =cut
2936
2937 sub OwnerAsString {
2938     my $self = shift;
2939     return ( $self->OwnerObj->EmailAddress );
2940
2941 }
2942
2943
2944
2945 =head2 SetOwner
2946
2947 Takes two arguments:
2948      the Id or Name of the owner 
2949 and  (optionally) the type of the SetOwner Transaction. It defaults
2950 to 'Set'.  'Steal' is also a valid option.
2951
2952
2953 =cut
2954
2955 sub SetOwner {
2956     my $self     = shift;
2957     my $NewOwner = shift;
2958     my $Type     = shift || "Set";
2959
2960     $RT::Handle->BeginTransaction();
2961
2962     $self->_SetLastUpdated(); # lock the ticket
2963     $self->Load( $self->id ); # in case $self changed while waiting for lock
2964
2965     my $OldOwnerObj = $self->OwnerObj;
2966
2967     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2968     $NewOwnerObj->Load( $NewOwner );
2969     unless ( $NewOwnerObj->Id ) {
2970         $RT::Handle->Rollback();
2971         return ( 0, $self->loc("That user does not exist") );
2972     }
2973
2974
2975     # must have ModifyTicket rights
2976     # or TakeTicket/StealTicket and $NewOwner is self
2977     # see if it's a take
2978     if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2979         unless (    $self->CurrentUserHasRight('ModifyTicket')
2980                  || $self->CurrentUserHasRight('TakeTicket') ) {
2981             $RT::Handle->Rollback();
2982             return ( 0, $self->loc("Permission Denied") );
2983         }
2984     }
2985
2986     # see if it's a steal
2987     elsif (    $OldOwnerObj->Id != RT->Nobody->Id
2988             && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2989
2990         unless (    $self->CurrentUserHasRight('ModifyTicket')
2991                  || $self->CurrentUserHasRight('StealTicket') ) {
2992             $RT::Handle->Rollback();
2993             return ( 0, $self->loc("Permission Denied") );
2994         }
2995     }
2996     else {
2997         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2998             $RT::Handle->Rollback();
2999             return ( 0, $self->loc("Permission Denied") );
3000         }
3001     }
3002
3003     # If we're not stealing and the ticket has an owner and it's not
3004     # the current user
3005     if ( $Type ne 'Steal' and $Type ne 'Force'
3006          and $OldOwnerObj->Id != RT->Nobody->Id
3007          and $OldOwnerObj->Id != $self->CurrentUser->Id )
3008     {
3009         $RT::Handle->Rollback();
3010         return ( 0, $self->loc("You can only take tickets that are unowned") )
3011             if $NewOwnerObj->id == $self->CurrentUser->id;
3012         return (
3013             0,
3014             $self->loc("You can only reassign tickets that you own or that are unowned" )
3015         );
3016     }
3017
3018     #If we've specified a new owner and that user can't modify the ticket
3019     elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3020         $RT::Handle->Rollback();
3021         return ( 0, $self->loc("That user may not own tickets in that queue") );
3022     }
3023
3024     # If the ticket has an owner and it's the new owner, we don't need
3025     # To do anything
3026     elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3027         $RT::Handle->Rollback();
3028         return ( 0, $self->loc("That user already owns that ticket") );
3029     }
3030
3031     # Delete the owner in the owner group, then add a new one
3032     # TODO: is this safe? it's not how we really want the API to work
3033     # for most things, but it's fast.
3034     my ( $del_id, $del_msg );
3035     for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
3036         ($del_id, $del_msg) = $owner->Delete();
3037         last unless ($del_id);
3038     }
3039
3040     unless ($del_id) {
3041         $RT::Handle->Rollback();
3042         return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3043     }
3044
3045     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3046                                        PrincipalId => $NewOwnerObj->PrincipalId,
3047                                        InsideTransaction => 1 );
3048     unless ($add_id) {
3049         $RT::Handle->Rollback();
3050         return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3051     }
3052
3053     # We call set twice with slightly different arguments, so
3054     # as to not have an SQL transaction span two RT transactions
3055
3056     my ( $val, $msg ) = $self->_Set(
3057                       Field             => 'Owner',
3058                       RecordTransaction => 0,
3059                       Value             => $NewOwnerObj->Id,
3060                       TimeTaken         => 0,
3061                       TransactionType   => 'Set',
3062                       CheckACL          => 0,                  # don't check acl
3063     );
3064
3065     unless ($val) {
3066         $RT::Handle->Rollback;
3067         return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3068     }
3069
3070     ($val, $msg) = $self->_NewTransaction(
3071         Type      => 'Set',
3072         Field     => 'Owner',
3073         NewValue  => $NewOwnerObj->Id,
3074         OldValue  => $OldOwnerObj->Id,
3075         TimeTaken => 0,
3076     );
3077
3078     if ( $val ) {
3079         $msg = $self->loc( "Owner changed from [_1] to [_2]",
3080                            $OldOwnerObj->Name, $NewOwnerObj->Name );
3081     }
3082     else {
3083         $RT::Handle->Rollback();
3084         return ( 0, $msg );
3085     }
3086
3087     $RT::Handle->Commit();
3088
3089     return ( $val, $msg );
3090 }
3091
3092
3093
3094 =head2 Take
3095
3096 A convenince method to set the ticket's owner to the current user
3097
3098 =cut
3099
3100 sub Take {
3101     my $self = shift;
3102     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3103 }
3104
3105
3106
3107 =head2 Untake
3108
3109 Convenience method to set the owner to 'nobody' if the current user is the owner.
3110
3111 =cut
3112
3113 sub Untake {
3114     my $self = shift;
3115     return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3116 }
3117
3118
3119
3120 =head2 Steal
3121
3122 A convenience method to change the owner of the current ticket to the
3123 current user. Even if it's owned by another user.
3124
3125 =cut
3126
3127 sub Steal {
3128     my $self = shift;
3129
3130     if ( $self->IsOwner( $self->CurrentUser ) ) {
3131         return ( 0, $self->loc("You already own this ticket") );
3132     }
3133     else {
3134         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3135
3136     }
3137
3138 }
3139
3140
3141
3142
3143
3144 =head2 ValidateStatus STATUS
3145
3146 Takes a string. Returns true if that status is a valid status for this ticket.
3147 Returns false otherwise.
3148
3149 =cut
3150
3151 sub ValidateStatus {
3152     my $self   = shift;
3153     my $status = shift;
3154
3155     #Make sure the status passed in is valid
3156     return 1 if $self->QueueObj->IsValidStatus($status);
3157
3158     my $i = 0;
3159     while ( my $caller = (caller($i++))[3] ) {
3160         return 1 if $caller eq 'RT::Ticket::SetQueue';
3161     }
3162
3163     return 0;
3164 }
3165
3166
3167
3168 =head2 SetStatus STATUS
3169
3170 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3171
3172 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3173 If FORCE is true, ignore unresolved dependencies and force a status change.
3174 if SETSTARTED is true( it's the default value), set Started to current datetime if Started 
3175 is not set and the status is changed from initial to not initial. 
3176
3177 =cut
3178
3179 sub SetStatus {
3180     my $self = shift;
3181     my %args;
3182     if (@_ == 1) {
3183         $args{Status} = shift;
3184     }
3185     else {
3186         %args = (@_);
3187     }
3188
3189     # this only allows us to SetStarted, not we must SetStarted.
3190     # this option was added for rtir initially
3191     $args{SetStarted} = 1 unless exists $args{SetStarted};
3192
3193
3194     my $lifecycle = $self->QueueObj->Lifecycle;
3195
3196     my $new = $args{'Status'};
3197     unless ( $lifecycle->IsValid( $new ) ) {
3198         return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3199     }
3200
3201     my $old = $self->__Value('Status');
3202     unless ( $lifecycle->IsTransition( $old => $new ) ) {
3203         return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3204     }
3205
3206     my $check_right = $lifecycle->CheckRight( $old => $new );
3207     unless ( $self->CurrentUserHasRight( $check_right ) ) {
3208         return ( 0, $self->loc('Permission Denied') );
3209     }
3210
3211     if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3212         return (0, $self->loc('That ticket has unresolved dependencies'));
3213     }
3214
3215     my $now = RT::Date->new( $self->CurrentUser );
3216     $now->SetToNow();
3217
3218     my $raw_started = RT::Date->new(RT->SystemUser);
3219     $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3220
3221     #If we're changing the status from new, record that we've started
3222     if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3223         #Set the Started time to "now"
3224         $self->_Set(
3225             Field             => 'Started',
3226             Value             => $now->ISO,
3227             RecordTransaction => 0
3228         );
3229     }
3230
3231     #When we close a ticket, set the 'Resolved' attribute to now.
3232     # It's misnamed, but that's just historical.
3233     if ( $lifecycle->IsInactive($new) ) {
3234         $self->_Set(
3235             Field             => 'Resolved',
3236             Value             => $now->ISO,
3237             RecordTransaction => 0,
3238         );
3239     }
3240
3241     #Actually update the status
3242     my ($val, $msg)= $self->_Set(
3243         Field           => 'Status',
3244         Value           => $args{Status},
3245         TimeTaken       => 0,
3246         CheckACL        => 0,
3247         TransactionType => 'Status',
3248     );
3249     return ($val, $msg);
3250 }
3251
3252
3253
3254 =head2 Delete
3255
3256 Takes no arguments. Marks this ticket for garbage collection
3257
3258 =cut
3259
3260 sub Delete {
3261     my $self = shift;
3262     unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3263         return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3264     }
3265     return ( $self->SetStatus('deleted') );
3266 }
3267
3268
3269 =head2 SetTold ISO  [TIMETAKEN]
3270
3271 Updates the told and records a transaction
3272
3273 =cut
3274
3275 sub SetTold {
3276     my $self = shift;
3277     my $told;
3278     $told = shift if (@_);
3279     my $timetaken = shift || 0;
3280
3281     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3282         return ( 0, $self->loc("Permission Denied") );
3283     }
3284
3285     my $datetold = RT::Date->new( $self->CurrentUser );
3286     if ($told) {
3287         $datetold->Set( Format => 'iso',
3288                         Value  => $told );
3289     }
3290     else {
3291         $datetold->SetToNow();
3292     }
3293
3294     return ( $self->_Set( Field           => 'Told',
3295                           Value           => $datetold->ISO,
3296                           TimeTaken       => $timetaken,
3297                           TransactionType => 'Told' ) );
3298 }
3299
3300 =head2 _SetTold
3301
3302 Updates the told without a transaction or acl check. Useful when we're sending replies.
3303
3304 =cut
3305
3306 sub _SetTold {
3307     my $self = shift;
3308
3309     my $now = RT::Date->new( $self->CurrentUser );
3310     $now->SetToNow();
3311
3312     #use __Set to get no ACLs ;)
3313     return ( $self->__Set( Field => 'Told',
3314                            Value => $now->ISO ) );
3315 }
3316
3317 =head2 SeenUpTo
3318
3319
3320 =cut
3321
3322 sub SeenUpTo {
3323     my $self = shift;
3324     my $uid = $self->CurrentUser->id;
3325     my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3326     return if $attr && $attr->Content gt $self->LastUpdated;
3327
3328     my $txns = $self->Transactions;
3329     $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3330     $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3331     $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3332     $txns->Limit(
3333         FIELD => 'Created',
3334         OPERATOR => '>',
3335         VALUE => $attr->Content
3336     ) if $attr;
3337     $txns->RowsPerPage(1);
3338     return $txns->First;
3339 }
3340
3341
3342 =head2 TransactionBatch
3343
3344 Returns an array reference of all transactions created on this ticket during
3345 this ticket object's lifetime or since last application of a batch, or undef
3346 if there were none.
3347
3348 Only works when the C<UseTransactionBatch> config option is set to true.
3349
3350 =cut
3351
3352 sub TransactionBatch {
3353     my $self = shift;
3354     return $self->{_TransactionBatch};
3355 }
3356
3357 =head2 ApplyTransactionBatch
3358
3359 Applies scrips on the current batch of transactions and shinks it. Usually
3360 batch is applied when object is destroyed, but in some cases it's too late.
3361
3362 =cut
3363
3364 sub ApplyTransactionBatch {
3365     my $self = shift;
3366
3367     my $batch = $self->TransactionBatch;
3368     return unless $batch && @$batch;
3369
3370     $self->_ApplyTransactionBatch;
3371
3372     $self->{_TransactionBatch} = [];
3373 }
3374
3375 sub _ApplyTransactionBatch {
3376     my $self = shift;
3377     my $batch = $self->TransactionBatch;
3378
3379     my %seen;
3380     my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3381
3382     require RT::Scrips;
3383     RT::Scrips->new(RT->SystemUser)->Apply(
3384         Stage          => 'TransactionBatch',
3385         TicketObj      => $self,
3386         TransactionObj => $batch->[0],
3387         Type           => $types,
3388     );
3389
3390     # Entry point of the rule system
3391     my $rules = RT::Ruleset->FindAllRules(
3392         Stage          => 'TransactionBatch',
3393         TicketObj      => $self,
3394         TransactionObj => $batch->[0],
3395         Type           => $types,
3396     );
3397     RT::Ruleset->CommitRules($rules);
3398 }
3399
3400 sub DESTROY {
3401     my $self = shift;
3402
3403     # DESTROY methods need to localize $@, or it may unset it.  This
3404     # causes $m->abort to not bubble all of the way up.  See perlbug
3405     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3406     local $@;
3407
3408     # The following line eliminates reentrancy.
3409     # It protects against the fact that perl doesn't deal gracefully
3410     # when an object's refcount is changed in its destructor.
3411     return if $self->{_Destroyed}++;
3412
3413     if (in_global_destruction()) {
3414        unless ($ENV{'HARNESS_ACTIVE'}) {
3415             warn "Too late to safely run transaction-batch scrips!"
3416                 ." This is typically caused by using ticket objects"
3417                 ." at the top-level of a script which uses the RT API."
3418                ." Be sure to explicitly undef such ticket objects,"
3419                 ." or put them inside of a lexical scope.";
3420         }
3421         return;
3422     }
3423
3424     my $batch = $self->TransactionBatch;
3425     return unless $batch && @$batch;
3426
3427     return $self->_ApplyTransactionBatch;
3428 }
3429
3430
3431
3432
3433 sub _OverlayAccessible {
3434     {
3435         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3436           Queue           => { 'read' => 1,  'write' => 1 },
3437           Requestors      => { 'read' => 1,  'write' => 1 },
3438           Owner           => { 'read' => 1,  'write' => 1 },
3439           Subject         => { 'read' => 1,  'write' => 1 },
3440           InitialPriority => { 'read' => 1,  'write' => 1 },
3441           FinalPriority   => { 'read' => 1,  'write' => 1 },
3442           Priority        => { 'read' => 1,  'write' => 1 },
3443           Status          => { 'read' => 1,  'write' => 1 },
3444           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3445           TimeWorked      => { 'read' => 1,  'write' => 1 },
3446           TimeLeft        => { 'read' => 1,  'write' => 1 },
3447           Told            => { 'read' => 1,  'write' => 1 },
3448           Resolved        => { 'read' => 1 },
3449           Type            => { 'read' => 1 },
3450           Starts        => { 'read' => 1, 'write' => 1 },
3451           Started       => { 'read' => 1, 'write' => 1 },
3452           Due           => { 'read' => 1, 'write' => 1 },
3453           Creator       => { 'read' => 1, 'auto'  => 1 },
3454           Created       => { 'read' => 1, 'auto'  => 1 },
3455           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3456           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3457     };
3458
3459 }
3460
3461
3462
3463 sub _Set {
3464     my $self = shift;
3465
3466     my %args = ( Field             => undef,
3467                  Value             => undef,
3468                  TimeTaken         => 0,
3469                  RecordTransaction => 1,
3470                  UpdateTicket      => 1,
3471                  CheckACL          => 1,
3472                  TransactionType   => 'Set',
3473                  @_ );
3474
3475     if ($args{'CheckACL'}) {
3476       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3477           return ( 0, $self->loc("Permission Denied"));
3478       }
3479    }
3480
3481     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3482         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3483         return(0, $self->loc("Internal Error"));
3484     }
3485
3486     #if the user is trying to modify the record
3487
3488     #Take care of the old value we really don't want to get in an ACL loop.
3489     # so ask the super::_Value
3490     my $Old = $self->SUPER::_Value("$args{'Field'}");
3491     
3492     my ($ret, $msg);
3493     if ( $args{'UpdateTicket'}  ) {
3494
3495         #Set the new value
3496         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3497                                                 Value => $args{'Value'} );
3498     
3499         #If we can't actually set the field to the value, don't record
3500         # a transaction. instead, get out of here.
3501         return ( 0, $msg ) unless $ret;
3502     }
3503
3504     if ( $args{'RecordTransaction'} == 1 ) {
3505
3506         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3507                                                Type => $args{'TransactionType'},
3508                                                Field     => $args{'Field'},
3509                                                NewValue  => $args{'Value'},
3510                                                OldValue  => $Old,
3511                                                TimeTaken => $args{'TimeTaken'},
3512         );
3513         return ( $Trans, scalar $TransObj->BriefDescription );
3514     }
3515     else {
3516         return ( $ret, $msg );
3517     }
3518 }
3519
3520
3521
3522 =head2 _Value
3523
3524 Takes the name of a table column.
3525 Returns its value as a string, if the user passes an ACL check
3526
3527 =cut
3528
3529 sub _Value {
3530
3531     my $self  = shift;
3532     my $field = shift;
3533
3534     #if the field is public, return it.
3535     if ( $self->_Accessible( $field, 'public' ) ) {
3536
3537         #$RT::Logger->debug("Skipping ACL check for $field");
3538         return ( $self->SUPER::_Value($field) );
3539
3540     }
3541
3542     #If the current user doesn't have ACLs, don't let em at it.  
3543
3544     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3545         return (undef);
3546     }
3547     return ( $self->SUPER::_Value($field) );
3548
3549 }
3550
3551
3552
3553 =head2 _UpdateTimeTaken
3554
3555 This routine will increment the timeworked counter. it should
3556 only be called from _NewTransaction 
3557
3558 =cut
3559
3560 sub _UpdateTimeTaken {
3561     my $self    = shift;
3562     my $Minutes = shift;
3563     my ($Total);
3564
3565     $Total = $self->SUPER::_Value("TimeWorked");
3566     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3567     $self->SUPER::_Set(
3568         Field => "TimeWorked",
3569         Value => $Total
3570     );
3571
3572     return ($Total);
3573 }
3574
3575
3576
3577
3578
3579 =head2 CurrentUserHasRight
3580
3581   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3582 1 if the user has that right. It returns 0 if the user doesn't have that right.
3583
3584 =cut
3585
3586 sub CurrentUserHasRight {
3587     my $self  = shift;
3588     my $right = shift;
3589
3590     return $self->CurrentUser->PrincipalObj->HasRight(
3591         Object => $self,
3592         Right  => $right,
3593     )
3594 }
3595
3596
3597
3598 =head2 HasRight
3599
3600  Takes a paramhash with the attributes 'Right' and 'Principal'
3601   'Right' is a ticket-scoped textual right from RT::ACE 
3602   'Principal' is an RT::User object
3603
3604   Returns 1 if the principal has the right. Returns undef if not.
3605
3606 =cut
3607
3608 sub HasRight {
3609     my $self = shift;
3610     my %args = (
3611         Right     => undef,
3612         Principal => undef,
3613         @_
3614     );
3615
3616     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3617     {
3618         Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3619         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3620         return(undef);
3621     }
3622
3623     return (
3624         $args{'Principal'}->HasRight(
3625             Object => $self,
3626             Right     => $args{'Right'}
3627           )
3628     );
3629 }
3630
3631
3632
3633 =head2 Reminders
3634
3635 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3636 It isn't acutally a searchbuilder collection itself.
3637
3638 =cut
3639
3640 sub Reminders {
3641     my $self = shift;
3642     
3643     unless ($self->{'__reminders'}) {
3644         $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3645         $self->{'__reminders'}->Ticket($self->id);
3646     }
3647     return $self->{'__reminders'};
3648
3649 }
3650
3651
3652
3653
3654 =head2 Transactions
3655
3656   Returns an RT::Transactions object of all transactions on this ticket
3657
3658 =cut
3659
3660 sub Transactions {
3661     my $self = shift;
3662
3663     my $transactions = RT::Transactions->new( $self->CurrentUser );
3664
3665     #If the user has no rights, return an empty object
3666     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3667         $transactions->LimitToTicket($self->id);
3668
3669         # if the user may not see comments do not return them
3670         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3671             $transactions->Limit(
3672                 SUBCLAUSE => 'acl',
3673                 FIELD    => 'Type',
3674                 OPERATOR => '!=',
3675                 VALUE    => "Comment"
3676             );
3677             $transactions->Limit(
3678                 SUBCLAUSE => 'acl',
3679                 FIELD    => 'Type',
3680                 OPERATOR => '!=',
3681                 VALUE    => "CommentEmailRecord",
3682                 ENTRYAGGREGATOR => 'AND'
3683             );
3684
3685         }
3686     } else {
3687         $transactions->Limit(
3688             SUBCLAUSE => 'acl',
3689             FIELD    => 'id',
3690             VALUE    => 0,
3691             ENTRYAGGREGATOR => 'AND'
3692         );
3693     }
3694
3695     return ($transactions);
3696 }
3697
3698
3699
3700
3701 =head2 TransactionCustomFields
3702
3703     Returns the custom fields that transactions on tickets will have.
3704
3705 =cut
3706
3707 sub TransactionCustomFields {
3708     my $self = shift;
3709     return $self->QueueObj->TicketTransactionCustomFields;
3710 }
3711
3712
3713
3714 =head2 CustomFieldValues
3715
3716 # Do name => id mapping (if needed) before falling back to
3717 # RT::Record's CustomFieldValues
3718
3719 See L<RT::Record>
3720
3721 =cut
3722
3723 sub CustomFieldValues {
3724     my $self  = shift;
3725     my $field = shift;
3726
3727     return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3728
3729     my $cf = RT::CustomField->new( $self->CurrentUser );
3730     $cf->SetContextObject( $self );
3731     $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3732     unless ( $cf->id ) {
3733         $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3734     }
3735
3736     # If we didn't find a valid cfid, give up.
3737     return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3738
3739     return $self->SUPER::CustomFieldValues( $cf->id );
3740 }
3741
3742
3743
3744 =head2 CustomFieldLookupType
3745
3746 Returns the RT::Ticket lookup type, which can be passed to 
3747 RT::CustomField->Create() via the 'LookupType' hash key.
3748
3749 =cut
3750
3751
3752 sub CustomFieldLookupType {
3753     "RT::Queue-RT::Ticket";
3754 }
3755
3756 =head2 ACLEquivalenceObjects
3757
3758 This method returns a list of objects for which a user's rights also apply
3759 to this ticket. Generally, this is only the ticket's queue, but some RT 
3760 extensions may make other objects available too.
3761
3762 This method is called from L<RT::Principal/HasRight>.
3763
3764 =cut
3765
3766 sub ACLEquivalenceObjects {
3767     my $self = shift;
3768     return $self->QueueObj;
3769
3770 }
3771
3772
3773 1;
3774
3775 =head1 AUTHOR
3776
3777 Jesse Vincent, jesse@bestpractical.com
3778
3779 =head1 SEE ALSO
3780
3781 RT
3782
3783 =cut
3784
3785
3786 use RT::Queue;
3787 use base 'RT::Record';
3788
3789 sub Table {'Tickets'}
3790
3791
3792
3793
3794
3795
3796 =head2 id
3797
3798 Returns the current value of id.
3799 (In the database, id is stored as int(11).)
3800
3801
3802 =cut
3803
3804
3805 =head2 EffectiveId
3806
3807 Returns the current value of EffectiveId.
3808 (In the database, EffectiveId is stored as int(11).)
3809
3810
3811
3812 =head2 SetEffectiveId VALUE
3813
3814
3815 Set EffectiveId to VALUE.
3816 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3817 (In the database, EffectiveId will be stored as a int(11).)
3818
3819
3820 =cut
3821
3822
3823 =head2 Queue
3824
3825 Returns the current value of Queue.
3826 (In the database, Queue is stored as int(11).)
3827
3828
3829
3830 =head2 SetQueue VALUE
3831
3832
3833 Set Queue to VALUE.
3834 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3835 (In the database, Queue will be stored as a int(11).)
3836
3837
3838 =cut
3839
3840
3841 =head2 Type
3842
3843 Returns the current value of Type.
3844 (In the database, Type is stored as varchar(16).)
3845
3846
3847
3848 =head2 SetType VALUE
3849
3850
3851 Set Type to VALUE.
3852 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3853 (In the database, Type will be stored as a varchar(16).)
3854
3855
3856 =cut
3857
3858
3859 =head2 IssueStatement
3860
3861 Returns the current value of IssueStatement.
3862 (In the database, IssueStatement is stored as int(11).)
3863
3864
3865
3866 =head2 SetIssueStatement VALUE
3867
3868
3869 Set IssueStatement to VALUE.
3870 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3871 (In the database, IssueStatement will be stored as a int(11).)
3872
3873
3874 =cut
3875
3876
3877 =head2 Resolution
3878
3879 Returns the current value of Resolution.
3880 (In the database, Resolution is stored as int(11).)
3881
3882
3883
3884 =head2 SetResolution VALUE
3885
3886
3887 Set Resolution to VALUE.
3888 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3889 (In the database, Resolution will be stored as a int(11).)
3890
3891
3892 =cut
3893
3894
3895 =head2 Owner
3896
3897 Returns the current value of Owner.
3898 (In the database, Owner is stored as int(11).)
3899
3900
3901
3902 =head2 SetOwner VALUE
3903
3904
3905 Set Owner to VALUE.
3906 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3907 (In the database, Owner will be stored as a int(11).)
3908
3909
3910 =cut
3911
3912
3913 =head2 Subject
3914
3915 Returns the current value of Subject.
3916 (In the database, Subject is stored as varchar(200).)
3917
3918
3919
3920 =head2 SetSubject VALUE
3921
3922
3923 Set Subject to VALUE.
3924 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3925 (In the database, Subject will be stored as a varchar(200).)
3926
3927
3928 =cut
3929
3930
3931 =head2 InitialPriority
3932
3933 Returns the current value of InitialPriority.
3934 (In the database, InitialPriority is stored as int(11).)
3935
3936
3937
3938 =head2 SetInitialPriority VALUE
3939
3940
3941 Set InitialPriority to VALUE.
3942 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3943 (In the database, InitialPriority will be stored as a int(11).)
3944
3945
3946 =cut
3947
3948
3949 =head2 FinalPriority
3950
3951 Returns the current value of FinalPriority.
3952 (In the database, FinalPriority is stored as int(11).)
3953
3954
3955
3956 =head2 SetFinalPriority VALUE
3957
3958
3959 Set FinalPriority to VALUE.
3960 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3961 (In the database, FinalPriority will be stored as a int(11).)
3962
3963
3964 =cut
3965
3966
3967 =head2 Priority
3968
3969 Returns the current value of Priority.
3970 (In the database, Priority is stored as int(11).)
3971
3972
3973
3974 =head2 SetPriority VALUE
3975
3976
3977 Set Priority to VALUE.
3978 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3979 (In the database, Priority will be stored as a int(11).)
3980
3981
3982 =cut
3983
3984
3985 =head2 TimeEstimated
3986
3987 Returns the current value of TimeEstimated.
3988 (In the database, TimeEstimated is stored as int(11).)
3989
3990
3991
3992 =head2 SetTimeEstimated VALUE
3993
3994
3995 Set TimeEstimated to VALUE.
3996 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3997 (In the database, TimeEstimated will be stored as a int(11).)
3998
3999
4000 =cut
4001
4002
4003 =head2 TimeWorked
4004
4005 Returns the current value of TimeWorked.
4006 (In the database, TimeWorked is stored as int(11).)
4007
4008
4009
4010 =head2 SetTimeWorked VALUE
4011
4012
4013 Set TimeWorked to VALUE.
4014 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4015 (In the database, TimeWorked will be stored as a int(11).)
4016
4017
4018 =cut
4019
4020
4021 =head2 Status
4022
4023 Returns the current value of Status.
4024 (In the database, Status is stored as varchar(64).)
4025
4026
4027
4028 =head2 SetStatus VALUE
4029
4030
4031 Set Status to VALUE.
4032 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4033 (In the database, Status will be stored as a varchar(64).)
4034
4035
4036 =cut
4037
4038
4039 =head2 TimeLeft
4040
4041 Returns the current value of TimeLeft.
4042 (In the database, TimeLeft is stored as int(11).)
4043
4044
4045
4046 =head2 SetTimeLeft VALUE
4047
4048
4049 Set TimeLeft to VALUE.
4050 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4051 (In the database, TimeLeft will be stored as a int(11).)
4052
4053
4054 =cut
4055
4056
4057 =head2 Told
4058
4059 Returns the current value of Told.
4060 (In the database, Told is stored as datetime.)
4061
4062
4063
4064 =head2 SetTold VALUE
4065
4066
4067 Set Told to VALUE.
4068 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4069 (In the database, Told will be stored as a datetime.)
4070
4071
4072 =cut
4073
4074
4075 =head2 Starts
4076
4077 Returns the current value of Starts.
4078 (In the database, Starts is stored as datetime.)
4079
4080
4081
4082 =head2 SetStarts VALUE
4083
4084
4085 Set Starts to VALUE.
4086 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4087 (In the database, Starts will be stored as a datetime.)
4088
4089
4090 =cut
4091
4092
4093 =head2 Started
4094
4095 Returns the current value of Started.
4096 (In the database, Started is stored as datetime.)
4097
4098
4099
4100 =head2 SetStarted VALUE
4101
4102
4103 Set Started to VALUE.
4104 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4105 (In the database, Started will be stored as a datetime.)
4106
4107
4108 =cut
4109
4110
4111 =head2 Due
4112
4113 Returns the current value of Due.
4114 (In the database, Due is stored as datetime.)
4115
4116
4117
4118 =head2 SetDue VALUE
4119
4120
4121 Set Due to VALUE.
4122 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4123 (In the database, Due will be stored as a datetime.)
4124
4125
4126 =cut
4127
4128
4129 =head2 Resolved
4130
4131 Returns the current value of Resolved.
4132 (In the database, Resolved is stored as datetime.)
4133
4134
4135
4136 =head2 SetResolved VALUE
4137
4138
4139 Set Resolved to VALUE.
4140 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4141 (In the database, Resolved will be stored as a datetime.)
4142
4143
4144 =cut
4145
4146
4147 =head2 LastUpdatedBy
4148
4149 Returns the current value of LastUpdatedBy.
4150 (In the database, LastUpdatedBy is stored as int(11).)
4151
4152
4153 =cut
4154
4155
4156 =head2 LastUpdated
4157
4158 Returns the current value of LastUpdated.
4159 (In the database, LastUpdated is stored as datetime.)
4160
4161
4162 =cut
4163
4164
4165 =head2 Creator
4166
4167 Returns the current value of Creator.
4168 (In the database, Creator is stored as int(11).)
4169
4170
4171 =cut
4172
4173
4174 =head2 Created
4175
4176 Returns the current value of Created.
4177 (In the database, Created is stored as datetime.)
4178
4179
4180 =cut
4181
4182
4183 =head2 Disabled
4184
4185 Returns the current value of Disabled.
4186 (In the database, Disabled is stored as smallint(6).)
4187
4188
4189
4190 =head2 SetDisabled VALUE
4191
4192
4193 Set Disabled to VALUE.
4194 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4195 (In the database, Disabled will be stored as a smallint(6).)
4196
4197
4198 =cut
4199
4200
4201
4202 sub _CoreAccessible {
4203     {
4204
4205         id =>
4206                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
4207         EffectiveId =>
4208                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4209         Queue =>
4210                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4211         Type =>
4212                 {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
4213         IssueStatement =>
4214                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4215         Resolution =>
4216                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4217         Owner =>
4218                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4219         Subject =>
4220                 {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => '[no subject]'},
4221         InitialPriority =>
4222                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4223         FinalPriority =>
4224                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4225         Priority =>
4226                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4227         TimeEstimated =>
4228                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4229         TimeWorked =>
4230                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4231         Status =>
4232                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
4233         TimeLeft =>
4234                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4235         Told =>
4236                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4237         Starts =>
4238                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4239         Started =>
4240                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4241         Due =>
4242                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4243         Resolved =>
4244                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4245         LastUpdatedBy =>
4246                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4247         LastUpdated =>
4248                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4249         Creator =>
4250                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4251         Created =>
4252                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4253         Disabled =>
4254                 {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
4255
4256  }
4257 };
4258
4259 RT::Base->_ImportOverlays();
4260
4261 1;