Merge branch 'master' of git.freeside.biz:/home/git/freeside
[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, SUBCLAUSE => 'acl' );
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           => $field, #$limit_on,
2450         OPERATOR        => 'MATCHES',
2451         VALUE           => 'fsck.com-rt://%/ticket/'. $self->id,
2452         ENTRYAGGREGATOR => 'OR',
2453     );
2454     $links->Limit(
2455         FIELD           => $field, #$limit_on,
2456         OPERATOR        => 'MATCHES',
2457         VALUE           => 'fsck.com-rt://%/ticket/'. $_,
2458         ENTRYAGGREGATOR => 'OR',
2459     ) foreach $self->Merged;
2460     $links->Limit(
2461         FIELD => 'Type',
2462         VALUE => $type,
2463     ) if $type;
2464
2465     return $links;
2466 }
2467
2468
2469
2470 =head2 DeleteLink
2471
2472 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2473 SilentBase and SilentTarget. Either Base or Target must be null.
2474 The null value will be replaced with this ticket\'s id.
2475
2476 If Silent is true then no transaction would be recorded, in other
2477 case you can control creation of transactions on both base and
2478 target with SilentBase and SilentTarget respectively. By default
2479 both transactions are created.
2480
2481 =cut 
2482
2483 sub DeleteLink {
2484     my $self = shift;
2485     my %args = (
2486         Base   => undef,
2487         Target => undef,
2488         Type   => undef,
2489         Silent => undef,
2490         SilentBase   => undef,
2491         SilentTarget => undef,
2492         @_
2493     );
2494
2495     unless ( $args{'Target'} || $args{'Base'} ) {
2496         $RT::Logger->error("Base or Target must be specified");
2497         return ( 0, $self->loc('Either base or target must be specified') );
2498     }
2499
2500     #check acls
2501     my $right = 0;
2502     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2503     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2504         return ( 0, $self->loc("Permission Denied") );
2505     }
2506
2507     # If the other URI is an RT::Ticket, we want to make sure the user
2508     # can modify it too...
2509     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2510     return (0, $msg) unless $status;
2511     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2512         $right++;
2513     }
2514     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2515          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2516     {
2517         return ( 0, $self->loc("Permission Denied") );
2518     }
2519
2520     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2521     return ( 0, $Msg ) unless $val;
2522
2523     return ( $val, $Msg ) if $args{'Silent'};
2524
2525     my ($direction, $remote_link);
2526
2527     if ( $args{'Base'} ) {
2528         $remote_link = $args{'Base'};
2529         $direction = 'Target';
2530     }
2531     elsif ( $args{'Target'} ) {
2532         $remote_link = $args{'Target'};
2533         $direction = 'Base';
2534     } 
2535
2536     my $remote_uri = RT::URI->new( $self->CurrentUser );
2537     $remote_uri->FromURI( $remote_link );
2538
2539     unless ( $args{ 'Silent'. $direction } ) {
2540         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2541             Type      => 'DeleteLink',
2542             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2543             OldValue  => $remote_uri->URI || $remote_link,
2544             TimeTaken => 0
2545         );
2546         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2547     }
2548
2549     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2550         my $OtherObj = $remote_uri->Object;
2551         my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2552             Type           => 'DeleteLink',
2553             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2554                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2555             OldValue       => $self->URI,
2556             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2557             TimeTaken      => 0,
2558         );
2559         $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2560     }
2561
2562     return ( $val, $Msg );
2563 }
2564
2565
2566
2567 =head2 AddLink
2568
2569 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2570
2571 If Silent is true then no transaction would be recorded, in other
2572 case you can control creation of transactions on both base and
2573 target with SilentBase and SilentTarget respectively. By default
2574 both transactions are created.
2575
2576 =cut
2577
2578 sub AddLink {
2579     my $self = shift;
2580     my %args = ( Target       => '',
2581                  Base         => '',
2582                  Type         => '',
2583                  Silent       => undef,
2584                  SilentBase   => undef,
2585                  SilentTarget => undef,
2586                  @_ );
2587
2588     unless ( $args{'Target'} || $args{'Base'} ) {
2589         $RT::Logger->error("Base or Target must be specified");
2590         return ( 0, $self->loc('Either base or target must be specified') );
2591     }
2592
2593     my $right = 0;
2594     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2595     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2596         return ( 0, $self->loc("Permission Denied") );
2597     }
2598
2599     # If the other URI is an RT::Ticket, we want to make sure the user
2600     # can modify it too...
2601     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2602     return (0, $msg) unless $status;
2603     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2604         $right++;
2605     }
2606     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2607          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2608     {
2609         return ( 0, $self->loc("Permission Denied") );
2610     }
2611
2612     return ( 0, "Can't link to a deleted ticket" )
2613       if $other_ticket && $other_ticket->Status eq 'deleted';
2614
2615     return $self->_AddLink(%args);
2616 }
2617
2618 sub __GetTicketFromURI {
2619     my $self = shift;
2620     my %args = ( URI => '', @_ );
2621
2622     # If the other URI is an RT::Ticket, we want to make sure the user
2623     # can modify it too...
2624     my $uri_obj = RT::URI->new( $self->CurrentUser );
2625     $uri_obj->FromURI( $args{'URI'} );
2626
2627     unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2628         my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2629         $RT::Logger->warning( $msg );
2630         return( 0, $msg );
2631     }
2632     my $obj = $uri_obj->Resolver->Object;
2633     unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2634         return (1, 'Found not a ticket', undef);
2635     }
2636     return (1, 'Found ticket', $obj);
2637 }
2638
2639 =head2 _AddLink  
2640
2641 Private non-acled variant of AddLink so that links can be added during create.
2642
2643 =cut
2644
2645 sub _AddLink {
2646     my $self = shift;
2647     my %args = ( Target       => '',
2648                  Base         => '',
2649                  Type         => '',
2650                  Silent       => undef,
2651                  SilentBase   => undef,
2652                  SilentTarget => undef,
2653                  @_ );
2654
2655     my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2656     return ($val, $msg) if !$val || $exist;
2657     return ($val, $msg) if $args{'Silent'};
2658
2659     my ($direction, $remote_link);
2660     if ( $args{'Target'} ) {
2661         $remote_link  = $args{'Target'};
2662         $direction    = 'Base';
2663     } elsif ( $args{'Base'} ) {
2664         $remote_link  = $args{'Base'};
2665         $direction    = 'Target';
2666     }
2667
2668     my $remote_uri = RT::URI->new( $self->CurrentUser );
2669     $remote_uri->FromURI( $remote_link );
2670
2671     unless ( $args{ 'Silent'. $direction } ) {
2672         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2673             Type      => 'AddLink',
2674             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2675             NewValue  =>  $remote_uri->URI || $remote_link,
2676             TimeTaken => 0
2677         );
2678         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2679     }
2680
2681     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2682         my $OtherObj = $remote_uri->Object;
2683         my ( $val, $msg ) = $OtherObj->_NewTransaction(
2684             Type           => 'AddLink',
2685             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2686                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2687             NewValue       => $self->URI,
2688             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2689             TimeTaken      => 0,
2690         );
2691         $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2692     }
2693
2694     return ( $val, $msg );
2695 }
2696
2697
2698
2699
2700 =head2 MergeInto
2701
2702 MergeInto take the id of the ticket to merge this ticket into.
2703
2704 =cut
2705
2706 sub MergeInto {
2707     my $self      = shift;
2708     my $ticket_id = shift;
2709
2710     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2711         return ( 0, $self->loc("Permission Denied") );
2712     }
2713
2714     # Load up the new ticket.
2715     my $MergeInto = RT::Ticket->new($self->CurrentUser);
2716     $MergeInto->Load($ticket_id);
2717
2718     # make sure it exists.
2719     unless ( $MergeInto->Id ) {
2720         return ( 0, $self->loc("New ticket doesn't exist") );
2721     }
2722
2723     # Make sure the current user can modify the new ticket.
2724     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2725         return ( 0, $self->loc("Permission Denied") );
2726     }
2727
2728     delete $MERGE_CACHE{'effective'}{ $self->id };
2729     delete @{ $MERGE_CACHE{'merged'} }{
2730         $ticket_id, $MergeInto->id, $self->id
2731     };
2732
2733     $RT::Handle->BeginTransaction();
2734
2735     $self->_MergeInto( $MergeInto );
2736
2737     $RT::Handle->Commit();
2738
2739     return ( 1, $self->loc("Merge Successful") );
2740 }
2741
2742 sub _MergeInto {
2743     my $self      = shift;
2744     my $MergeInto = shift;
2745
2746
2747     # We use EffectiveId here even though it duplicates information from
2748     # the links table becasue of the massive performance hit we'd take
2749     # by trying to do a separate database query for merge info everytime 
2750     # loaded a ticket. 
2751
2752     #update this ticket's effective id to the new ticket's id.
2753     my ( $id_val, $id_msg ) = $self->__Set(
2754         Field => 'EffectiveId',
2755         Value => $MergeInto->Id()
2756     );
2757
2758     unless ($id_val) {
2759         $RT::Handle->Rollback();
2760         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2761     }
2762
2763
2764     my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2765     if ( $force_status && $force_status ne $self->__Value('Status') ) {
2766         my ( $status_val, $status_msg )
2767             = $self->__Set( Field => 'Status', Value => $force_status );
2768
2769         unless ($status_val) {
2770             $RT::Handle->Rollback();
2771             $RT::Logger->error(
2772                 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2773             );
2774             return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2775         }
2776     }
2777
2778     # update all the links that point to that old ticket
2779     my $old_links_to = RT::Links->new($self->CurrentUser);
2780     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2781
2782     my %old_seen;
2783     while (my $link = $old_links_to->Next) {
2784         if (exists $old_seen{$link->Base."-".$link->Type}) {
2785             $link->Delete;
2786         }   
2787         elsif ($link->Base eq $MergeInto->URI) {
2788             $link->Delete;
2789         } else {
2790             # First, make sure the link doesn't already exist. then move it over.
2791             my $tmp = RT::Link->new(RT->SystemUser);
2792             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2793             if ($tmp->id)   {
2794                     $link->Delete;
2795             } else { 
2796                 $link->SetTarget($MergeInto->URI);
2797                 $link->SetLocalTarget($MergeInto->id);
2798             }
2799             $old_seen{$link->Base."-".$link->Type} =1;
2800         }
2801
2802     }
2803
2804     my $old_links_from = RT::Links->new($self->CurrentUser);
2805     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2806
2807     while (my $link = $old_links_from->Next) {
2808         if (exists $old_seen{$link->Type."-".$link->Target}) {
2809             $link->Delete;
2810         }   
2811         if ($link->Target eq $MergeInto->URI) {
2812             $link->Delete;
2813         } else {
2814             # First, make sure the link doesn't already exist. then move it over.
2815             my $tmp = RT::Link->new(RT->SystemUser);
2816             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2817             if ($tmp->id)   {
2818                     $link->Delete;
2819             } else { 
2820                 $link->SetBase($MergeInto->URI);
2821                 $link->SetLocalBase($MergeInto->id);
2822                 $old_seen{$link->Type."-".$link->Target} =1;
2823             }
2824         }
2825
2826     }
2827
2828     # Update time fields
2829     foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2830
2831         my $mutator = "Set$type";
2832         $MergeInto->$mutator(
2833             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2834
2835     }
2836 #add all of this ticket's watchers to that ticket.
2837     foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2838
2839         my $people = $self->$watcher_type->MembersObj;
2840         my $addwatcher_type =  $watcher_type;
2841         $addwatcher_type  =~ s/s$//;
2842
2843         while ( my $watcher = $people->Next ) {
2844             
2845            my ($val, $msg) =  $MergeInto->_AddWatcher(
2846                 Type        => $addwatcher_type,
2847                 Silent => 1,
2848                 PrincipalId => $watcher->MemberId
2849             );
2850             unless ($val) {
2851                 $RT::Logger->debug($msg);
2852             }
2853     }
2854
2855     }
2856
2857     #find all of the tickets that were merged into this ticket. 
2858     my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2859     $old_mergees->Limit(
2860         FIELD    => 'EffectiveId',
2861         OPERATOR => '=',
2862         VALUE    => $self->Id
2863     );
2864
2865     #   update their EffectiveId fields to the new ticket's id
2866     while ( my $ticket = $old_mergees->Next() ) {
2867         my ( $val, $msg ) = $ticket->__Set(
2868             Field => 'EffectiveId',
2869             Value => $MergeInto->Id()
2870         );
2871     }
2872
2873     #make a new link: this ticket is merged into that other ticket.
2874     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2875
2876     $MergeInto->_SetLastUpdated;    
2877 }
2878
2879 =head2 Merged
2880
2881 Returns list of tickets' ids that's been merged into this ticket.
2882
2883 =cut
2884
2885 sub Merged {
2886     my $self = shift;
2887
2888     my $id = $self->id;
2889     return @{ $MERGE_CACHE{'merged'}{ $id } }
2890         if $MERGE_CACHE{'merged'}{ $id };
2891
2892     my $mergees = RT::Tickets->new( $self->CurrentUser );
2893     $mergees->Limit(
2894         FIELD    => 'EffectiveId',
2895         VALUE    => $id,
2896     );
2897     $mergees->Limit(
2898         FIELD    => 'id',
2899         OPERATOR => '!=',
2900         VALUE    => $id,
2901     );
2902     return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2903         = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2904 }
2905
2906
2907
2908
2909
2910 =head2 OwnerObj
2911
2912 Takes nothing and returns an RT::User object of 
2913 this ticket's owner
2914
2915 =cut
2916
2917 sub OwnerObj {
2918     my $self = shift;
2919
2920     #If this gets ACLed, we lose on a rights check in User.pm and
2921     #get deep recursion. if we need ACLs here, we need
2922     #an equiv without ACLs
2923
2924     my $owner = RT::User->new( $self->CurrentUser );
2925     $owner->Load( $self->__Value('Owner') );
2926
2927     #Return the owner object
2928     return ($owner);
2929 }
2930
2931
2932
2933 =head2 OwnerAsString
2934
2935 Returns the owner's email address
2936
2937 =cut
2938
2939 sub OwnerAsString {
2940     my $self = shift;
2941     return ( $self->OwnerObj->EmailAddress );
2942
2943 }
2944
2945
2946
2947 =head2 SetOwner
2948
2949 Takes two arguments:
2950      the Id or Name of the owner 
2951 and  (optionally) the type of the SetOwner Transaction. It defaults
2952 to 'Set'.  'Steal' is also a valid option.
2953
2954
2955 =cut
2956
2957 sub SetOwner {
2958     my $self     = shift;
2959     my $NewOwner = shift;
2960     my $Type     = shift || "Set";
2961
2962     $RT::Handle->BeginTransaction();
2963
2964     $self->_SetLastUpdated(); # lock the ticket
2965     $self->Load( $self->id ); # in case $self changed while waiting for lock
2966
2967     my $OldOwnerObj = $self->OwnerObj;
2968
2969     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2970     $NewOwnerObj->Load( $NewOwner );
2971     unless ( $NewOwnerObj->Id ) {
2972         $RT::Handle->Rollback();
2973         return ( 0, $self->loc("That user does not exist") );
2974     }
2975
2976
2977     # must have ModifyTicket rights
2978     # or TakeTicket/StealTicket and $NewOwner is self
2979     # see if it's a take
2980     if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2981         unless (    $self->CurrentUserHasRight('ModifyTicket')
2982                  || $self->CurrentUserHasRight('TakeTicket') ) {
2983             $RT::Handle->Rollback();
2984             return ( 0, $self->loc("Permission Denied") );
2985         }
2986     }
2987
2988     # see if it's a steal
2989     elsif (    $OldOwnerObj->Id != RT->Nobody->Id
2990             && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2991
2992         unless (    $self->CurrentUserHasRight('ModifyTicket')
2993                  || $self->CurrentUserHasRight('StealTicket') ) {
2994             $RT::Handle->Rollback();
2995             return ( 0, $self->loc("Permission Denied") );
2996         }
2997     }
2998     else {
2999         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3000             $RT::Handle->Rollback();
3001             return ( 0, $self->loc("Permission Denied") );
3002         }
3003     }
3004
3005     # If we're not stealing and the ticket has an owner and it's not
3006     # the current user
3007     if ( $Type ne 'Steal' and $Type ne 'Force'
3008          and $OldOwnerObj->Id != RT->Nobody->Id
3009          and $OldOwnerObj->Id != $self->CurrentUser->Id )
3010     {
3011         $RT::Handle->Rollback();
3012         return ( 0, $self->loc("You can only take tickets that are unowned") )
3013             if $NewOwnerObj->id == $self->CurrentUser->id;
3014         return (
3015             0,
3016             $self->loc("You can only reassign tickets that you own or that are unowned" )
3017         );
3018     }
3019
3020     #If we've specified a new owner and that user can't modify the ticket
3021     elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3022         $RT::Handle->Rollback();
3023         return ( 0, $self->loc("That user may not own tickets in that queue") );
3024     }
3025
3026     # If the ticket has an owner and it's the new owner, we don't need
3027     # To do anything
3028     elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3029         $RT::Handle->Rollback();
3030         return ( 0, $self->loc("That user already owns that ticket") );
3031     }
3032
3033     # Delete the owner in the owner group, then add a new one
3034     # TODO: is this safe? it's not how we really want the API to work
3035     # for most things, but it's fast.
3036     my ( $del_id, $del_msg );
3037     for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
3038         ($del_id, $del_msg) = $owner->Delete();
3039         last unless ($del_id);
3040     }
3041
3042     unless ($del_id) {
3043         $RT::Handle->Rollback();
3044         return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3045     }
3046
3047     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3048                                        PrincipalId => $NewOwnerObj->PrincipalId,
3049                                        InsideTransaction => 1 );
3050     unless ($add_id) {
3051         $RT::Handle->Rollback();
3052         return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3053     }
3054
3055     # We call set twice with slightly different arguments, so
3056     # as to not have an SQL transaction span two RT transactions
3057
3058     my ( $val, $msg ) = $self->_Set(
3059                       Field             => 'Owner',
3060                       RecordTransaction => 0,
3061                       Value             => $NewOwnerObj->Id,
3062                       TimeTaken         => 0,
3063                       TransactionType   => 'Set',
3064                       CheckACL          => 0,                  # don't check acl
3065     );
3066
3067     unless ($val) {
3068         $RT::Handle->Rollback;
3069         return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3070     }
3071
3072     ($val, $msg) = $self->_NewTransaction(
3073         Type      => 'Set',
3074         Field     => 'Owner',
3075         NewValue  => $NewOwnerObj->Id,
3076         OldValue  => $OldOwnerObj->Id,
3077         TimeTaken => 0,
3078     );
3079
3080     if ( $val ) {
3081         $msg = $self->loc( "Owner changed from [_1] to [_2]",
3082                            $OldOwnerObj->Name, $NewOwnerObj->Name );
3083     }
3084     else {
3085         $RT::Handle->Rollback();
3086         return ( 0, $msg );
3087     }
3088
3089     $RT::Handle->Commit();
3090
3091     return ( $val, $msg );
3092 }
3093
3094
3095
3096 =head2 Take
3097
3098 A convenince method to set the ticket's owner to the current user
3099
3100 =cut
3101
3102 sub Take {
3103     my $self = shift;
3104     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3105 }
3106
3107
3108
3109 =head2 Untake
3110
3111 Convenience method to set the owner to 'nobody' if the current user is the owner.
3112
3113 =cut
3114
3115 sub Untake {
3116     my $self = shift;
3117     return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3118 }
3119
3120
3121
3122 =head2 Steal
3123
3124 A convenience method to change the owner of the current ticket to the
3125 current user. Even if it's owned by another user.
3126
3127 =cut
3128
3129 sub Steal {
3130     my $self = shift;
3131
3132     if ( $self->IsOwner( $self->CurrentUser ) ) {
3133         return ( 0, $self->loc("You already own this ticket") );
3134     }
3135     else {
3136         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3137
3138     }
3139
3140 }
3141
3142
3143
3144
3145
3146 =head2 ValidateStatus STATUS
3147
3148 Takes a string. Returns true if that status is a valid status for this ticket.
3149 Returns false otherwise.
3150
3151 =cut
3152
3153 sub ValidateStatus {
3154     my $self   = shift;
3155     my $status = shift;
3156
3157     #Make sure the status passed in is valid
3158     return 1 if $self->QueueObj->IsValidStatus($status);
3159
3160     my $i = 0;
3161     while ( my $caller = (caller($i++))[3] ) {
3162         return 1 if $caller eq 'RT::Ticket::SetQueue';
3163     }
3164
3165     return 0;
3166 }
3167
3168
3169
3170 =head2 SetStatus STATUS
3171
3172 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3173
3174 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3175 If FORCE is true, ignore unresolved dependencies and force a status change.
3176 if SETSTARTED is true( it's the default value), set Started to current datetime if Started 
3177 is not set and the status is changed from initial to not initial. 
3178
3179 =cut
3180
3181 sub SetStatus {
3182     my $self = shift;
3183     my %args;
3184     if (@_ == 1) {
3185         $args{Status} = shift;
3186     }
3187     else {
3188         %args = (@_);
3189     }
3190
3191     # this only allows us to SetStarted, not we must SetStarted.
3192     # this option was added for rtir initially
3193     $args{SetStarted} = 1 unless exists $args{SetStarted};
3194
3195
3196     my $lifecycle = $self->QueueObj->Lifecycle;
3197
3198     my $new = $args{'Status'};
3199     unless ( $lifecycle->IsValid( $new ) ) {
3200         return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3201     }
3202
3203     my $old = $self->__Value('Status');
3204     unless ( $lifecycle->IsTransition( $old => $new ) ) {
3205         return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3206     }
3207
3208     my $check_right = $lifecycle->CheckRight( $old => $new );
3209     unless ( $self->CurrentUserHasRight( $check_right ) ) {
3210         return ( 0, $self->loc('Permission Denied') );
3211     }
3212
3213     if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3214         return (0, $self->loc('That ticket has unresolved dependencies'));
3215     }
3216
3217     my $now = RT::Date->new( $self->CurrentUser );
3218     $now->SetToNow();
3219
3220     my $raw_started = RT::Date->new(RT->SystemUser);
3221     $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3222
3223     #If we're changing the status from new, record that we've started
3224     if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3225         #Set the Started time to "now"
3226         $self->_Set(
3227             Field             => 'Started',
3228             Value             => $now->ISO,
3229             RecordTransaction => 0
3230         );
3231     }
3232
3233     #When we close a ticket, set the 'Resolved' attribute to now.
3234     # It's misnamed, but that's just historical.
3235     if ( $lifecycle->IsInactive($new) ) {
3236         $self->_Set(
3237             Field             => 'Resolved',
3238             Value             => $now->ISO,
3239             RecordTransaction => 0,
3240         );
3241     }
3242
3243     #Actually update the status
3244     my ($val, $msg)= $self->_Set(
3245         Field           => 'Status',
3246         Value           => $args{Status},
3247         TimeTaken       => 0,
3248         CheckACL        => 0,
3249         TransactionType => 'Status',
3250     );
3251     return ($val, $msg);
3252 }
3253
3254
3255
3256 =head2 Delete
3257
3258 Takes no arguments. Marks this ticket for garbage collection
3259
3260 =cut
3261
3262 sub Delete {
3263     my $self = shift;
3264     unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3265         return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3266     }
3267     return ( $self->SetStatus('deleted') );
3268 }
3269
3270
3271 =head2 SetTold ISO  [TIMETAKEN]
3272
3273 Updates the told and records a transaction
3274
3275 =cut
3276
3277 sub SetTold {
3278     my $self = shift;
3279     my $told;
3280     $told = shift if (@_);
3281     my $timetaken = shift || 0;
3282
3283     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3284         return ( 0, $self->loc("Permission Denied") );
3285     }
3286
3287     my $datetold = RT::Date->new( $self->CurrentUser );
3288     if ($told) {
3289         $datetold->Set( Format => 'iso',
3290                         Value  => $told );
3291     }
3292     else {
3293         $datetold->SetToNow();
3294     }
3295
3296     return ( $self->_Set( Field           => 'Told',
3297                           Value           => $datetold->ISO,
3298                           TimeTaken       => $timetaken,
3299                           TransactionType => 'Told' ) );
3300 }
3301
3302 =head2 _SetTold
3303
3304 Updates the told without a transaction or acl check. Useful when we're sending replies.
3305
3306 =cut
3307
3308 sub _SetTold {
3309     my $self = shift;
3310
3311     my $now = RT::Date->new( $self->CurrentUser );
3312     $now->SetToNow();
3313
3314     #use __Set to get no ACLs ;)
3315     return ( $self->__Set( Field => 'Told',
3316                            Value => $now->ISO ) );
3317 }
3318
3319 =head2 SeenUpTo
3320
3321
3322 =cut
3323
3324 sub SeenUpTo {
3325     my $self = shift;
3326     my $uid = $self->CurrentUser->id;
3327     my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3328     return if $attr && $attr->Content gt $self->LastUpdated;
3329
3330     my $txns = $self->Transactions;
3331     $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3332     $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3333     $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3334     $txns->Limit(
3335         FIELD => 'Created',
3336         OPERATOR => '>',
3337         VALUE => $attr->Content
3338     ) if $attr;
3339     $txns->RowsPerPage(1);
3340     return $txns->First;
3341 }
3342
3343
3344 =head2 TransactionBatch
3345
3346 Returns an array reference of all transactions created on this ticket during
3347 this ticket object's lifetime or since last application of a batch, or undef
3348 if there were none.
3349
3350 Only works when the C<UseTransactionBatch> config option is set to true.
3351
3352 =cut
3353
3354 sub TransactionBatch {
3355     my $self = shift;
3356     return $self->{_TransactionBatch};
3357 }
3358
3359 =head2 ApplyTransactionBatch
3360
3361 Applies scrips on the current batch of transactions and shinks it. Usually
3362 batch is applied when object is destroyed, but in some cases it's too late.
3363
3364 =cut
3365
3366 sub ApplyTransactionBatch {
3367     my $self = shift;
3368
3369     my $batch = $self->TransactionBatch;
3370     return unless $batch && @$batch;
3371
3372     $self->_ApplyTransactionBatch;
3373
3374     $self->{_TransactionBatch} = [];
3375 }
3376
3377 sub _ApplyTransactionBatch {
3378     my $self = shift;
3379     my $batch = $self->TransactionBatch;
3380
3381     my %seen;
3382     my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3383
3384     require RT::Scrips;
3385     RT::Scrips->new(RT->SystemUser)->Apply(
3386         Stage          => 'TransactionBatch',
3387         TicketObj      => $self,
3388         TransactionObj => $batch->[0],
3389         Type           => $types,
3390     );
3391
3392     # Entry point of the rule system
3393     my $rules = RT::Ruleset->FindAllRules(
3394         Stage          => 'TransactionBatch',
3395         TicketObj      => $self,
3396         TransactionObj => $batch->[0],
3397         Type           => $types,
3398     );
3399     RT::Ruleset->CommitRules($rules);
3400 }
3401
3402 sub DESTROY {
3403     my $self = shift;
3404
3405     # DESTROY methods need to localize $@, or it may unset it.  This
3406     # causes $m->abort to not bubble all of the way up.  See perlbug
3407     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3408     local $@;
3409
3410     # The following line eliminates reentrancy.
3411     # It protects against the fact that perl doesn't deal gracefully
3412     # when an object's refcount is changed in its destructor.
3413     return if $self->{_Destroyed}++;
3414
3415     if (in_global_destruction()) {
3416        unless ($ENV{'HARNESS_ACTIVE'}) {
3417             warn "Too late to safely run transaction-batch scrips!"
3418                 ." This is typically caused by using ticket objects"
3419                 ." at the top-level of a script which uses the RT API."
3420                ." Be sure to explicitly undef such ticket objects,"
3421                 ." or put them inside of a lexical scope.";
3422         }
3423         return;
3424     }
3425
3426     my $batch = $self->TransactionBatch;
3427     return unless $batch && @$batch;
3428
3429     return $self->_ApplyTransactionBatch;
3430 }
3431
3432
3433
3434
3435 sub _OverlayAccessible {
3436     {
3437         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3438           Queue           => { 'read' => 1,  'write' => 1 },
3439           Requestors      => { 'read' => 1,  'write' => 1 },
3440           Owner           => { 'read' => 1,  'write' => 1 },
3441           Subject         => { 'read' => 1,  'write' => 1 },
3442           InitialPriority => { 'read' => 1,  'write' => 1 },
3443           FinalPriority   => { 'read' => 1,  'write' => 1 },
3444           Priority        => { 'read' => 1,  'write' => 1 },
3445           Status          => { 'read' => 1,  'write' => 1 },
3446           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3447           TimeWorked      => { 'read' => 1,  'write' => 1 },
3448           TimeLeft        => { 'read' => 1,  'write' => 1 },
3449           Told            => { 'read' => 1,  'write' => 1 },
3450           Resolved        => { 'read' => 1 },
3451           Type            => { 'read' => 1 },
3452           Starts        => { 'read' => 1, 'write' => 1 },
3453           Started       => { 'read' => 1, 'write' => 1 },
3454           Due           => { 'read' => 1, 'write' => 1 },
3455           Creator       => { 'read' => 1, 'auto'  => 1 },
3456           Created       => { 'read' => 1, 'auto'  => 1 },
3457           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3458           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3459     };
3460
3461 }
3462
3463
3464
3465 sub _Set {
3466     my $self = shift;
3467
3468     my %args = ( Field             => undef,
3469                  Value             => undef,
3470                  TimeTaken         => 0,
3471                  RecordTransaction => 1,
3472                  UpdateTicket      => 1,
3473                  CheckACL          => 1,
3474                  TransactionType   => 'Set',
3475                  @_ );
3476
3477     if ($args{'CheckACL'}) {
3478       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3479           return ( 0, $self->loc("Permission Denied"));
3480       }
3481    }
3482
3483     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3484         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3485         return(0, $self->loc("Internal Error"));
3486     }
3487
3488     #if the user is trying to modify the record
3489
3490     #Take care of the old value we really don't want to get in an ACL loop.
3491     # so ask the super::_Value
3492     my $Old = $self->SUPER::_Value("$args{'Field'}");
3493     
3494     my ($ret, $msg);
3495     if ( $args{'UpdateTicket'}  ) {
3496
3497         #Set the new value
3498         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3499                                                 Value => $args{'Value'} );
3500     
3501         #If we can't actually set the field to the value, don't record
3502         # a transaction. instead, get out of here.
3503         return ( 0, $msg ) unless $ret;
3504     }
3505
3506     if ( $args{'RecordTransaction'} == 1 ) {
3507
3508         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3509                                                Type => $args{'TransactionType'},
3510                                                Field     => $args{'Field'},
3511                                                NewValue  => $args{'Value'},
3512                                                OldValue  => $Old,
3513                                                TimeTaken => $args{'TimeTaken'},
3514         );
3515         return ( $Trans, scalar $TransObj->BriefDescription );
3516     }
3517     else {
3518         return ( $ret, $msg );
3519     }
3520 }
3521
3522
3523
3524 =head2 _Value
3525
3526 Takes the name of a table column.
3527 Returns its value as a string, if the user passes an ACL check
3528
3529 =cut
3530
3531 sub _Value {
3532
3533     my $self  = shift;
3534     my $field = shift;
3535
3536     #if the field is public, return it.
3537     if ( $self->_Accessible( $field, 'public' ) ) {
3538
3539         #$RT::Logger->debug("Skipping ACL check for $field");
3540         return ( $self->SUPER::_Value($field) );
3541
3542     }
3543
3544     #If the current user doesn't have ACLs, don't let em at it.  
3545
3546     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3547         return (undef);
3548     }
3549     return ( $self->SUPER::_Value($field) );
3550
3551 }
3552
3553
3554
3555 =head2 _UpdateTimeTaken
3556
3557 This routine will increment the timeworked counter. it should
3558 only be called from _NewTransaction 
3559
3560 =cut
3561
3562 sub _UpdateTimeTaken {
3563     my $self    = shift;
3564     my $Minutes = shift;
3565     my ($Total);
3566
3567     $Total = $self->SUPER::_Value("TimeWorked");
3568     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3569     $self->SUPER::_Set(
3570         Field => "TimeWorked",
3571         Value => $Total
3572     );
3573
3574     return ($Total);
3575 }
3576
3577
3578
3579
3580
3581 =head2 CurrentUserHasRight
3582
3583   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3584 1 if the user has that right. It returns 0 if the user doesn't have that right.
3585
3586 =cut
3587
3588 sub CurrentUserHasRight {
3589     my $self  = shift;
3590     my $right = shift;
3591
3592     return $self->CurrentUser->PrincipalObj->HasRight(
3593         Object => $self,
3594         Right  => $right,
3595     )
3596 }
3597
3598
3599 =head2 CurrentUserCanSee
3600
3601 Returns true if the current user can see the ticket, using ShowTicket
3602
3603 =cut
3604
3605 sub CurrentUserCanSee {
3606     my $self = shift;
3607     return $self->CurrentUserHasRight('ShowTicket');
3608 }
3609
3610 =head2 HasRight
3611
3612  Takes a paramhash with the attributes 'Right' and 'Principal'
3613   'Right' is a ticket-scoped textual right from RT::ACE 
3614   'Principal' is an RT::User object
3615
3616   Returns 1 if the principal has the right. Returns undef if not.
3617
3618 =cut
3619
3620 sub HasRight {
3621     my $self = shift;
3622     my %args = (
3623         Right     => undef,
3624         Principal => undef,
3625         @_
3626     );
3627
3628     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3629     {
3630         Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3631         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3632         return(undef);
3633     }
3634
3635     return (
3636         $args{'Principal'}->HasRight(
3637             Object => $self,
3638             Right     => $args{'Right'}
3639           )
3640     );
3641 }
3642
3643
3644
3645 =head2 Reminders
3646
3647 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3648 It isn't acutally a searchbuilder collection itself.
3649
3650 =cut
3651
3652 sub Reminders {
3653     my $self = shift;
3654     
3655     unless ($self->{'__reminders'}) {
3656         $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3657         $self->{'__reminders'}->Ticket($self->id);
3658     }
3659     return $self->{'__reminders'};
3660
3661 }
3662
3663
3664
3665
3666 =head2 Transactions
3667
3668   Returns an RT::Transactions object of all transactions on this ticket
3669
3670 =cut
3671
3672 sub Transactions {
3673     my $self = shift;
3674
3675     my $transactions = RT::Transactions->new( $self->CurrentUser );
3676
3677     #If the user has no rights, return an empty object
3678     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3679         $transactions->LimitToTicket($self->id);
3680
3681         # if the user may not see comments do not return them
3682         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3683             $transactions->Limit(
3684                 SUBCLAUSE => 'acl',
3685                 FIELD    => 'Type',
3686                 OPERATOR => '!=',
3687                 VALUE    => "Comment"
3688             );
3689             $transactions->Limit(
3690                 SUBCLAUSE => 'acl',
3691                 FIELD    => 'Type',
3692                 OPERATOR => '!=',
3693                 VALUE    => "CommentEmailRecord",
3694                 ENTRYAGGREGATOR => 'AND'
3695             );
3696
3697         }
3698     } else {
3699         $transactions->Limit(
3700             SUBCLAUSE => 'acl',
3701             FIELD    => 'id',
3702             VALUE    => 0,
3703             ENTRYAGGREGATOR => 'AND'
3704         );
3705     }
3706
3707     return ($transactions);
3708 }
3709
3710
3711
3712
3713 =head2 TransactionCustomFields
3714
3715     Returns the custom fields that transactions on tickets will have.
3716
3717 =cut
3718
3719 sub TransactionCustomFields {
3720     my $self = shift;
3721     my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3722     $cfs->SetContextObject( $self );
3723     return $cfs;
3724 }
3725
3726
3727
3728 =head2 CustomFieldValues
3729
3730 # Do name => id mapping (if needed) before falling back to
3731 # RT::Record's CustomFieldValues
3732
3733 See L<RT::Record>
3734
3735 =cut
3736
3737 sub CustomFieldValues {
3738     my $self  = shift;
3739     my $field = shift;
3740
3741     return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3742
3743     my $cf = RT::CustomField->new( $self->CurrentUser );
3744     $cf->SetContextObject( $self );
3745     $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3746     unless ( $cf->id ) {
3747         $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3748     }
3749
3750     # If we didn't find a valid cfid, give up.
3751     return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3752
3753     return $self->SUPER::CustomFieldValues( $cf->id );
3754 }
3755
3756
3757
3758 =head2 CustomFieldLookupType
3759
3760 Returns the RT::Ticket lookup type, which can be passed to 
3761 RT::CustomField->Create() via the 'LookupType' hash key.
3762
3763 =cut
3764
3765
3766 sub CustomFieldLookupType {
3767     "RT::Queue-RT::Ticket";
3768 }
3769
3770 =head2 ACLEquivalenceObjects
3771
3772 This method returns a list of objects for which a user's rights also apply
3773 to this ticket. Generally, this is only the ticket's queue, but some RT 
3774 extensions may make other objects available too.
3775
3776 This method is called from L<RT::Principal/HasRight>.
3777
3778 =cut
3779
3780 sub ACLEquivalenceObjects {
3781     my $self = shift;
3782     return $self->QueueObj;
3783
3784 }
3785
3786
3787 1;
3788
3789 =head1 AUTHOR
3790
3791 Jesse Vincent, jesse@bestpractical.com
3792
3793 =head1 SEE ALSO
3794
3795 RT
3796
3797 =cut
3798
3799
3800 use RT::Queue;
3801 use base 'RT::Record';
3802
3803 sub Table {'Tickets'}
3804
3805
3806
3807
3808
3809
3810 =head2 id
3811
3812 Returns the current value of id.
3813 (In the database, id is stored as int(11).)
3814
3815
3816 =cut
3817
3818
3819 =head2 EffectiveId
3820
3821 Returns the current value of EffectiveId.
3822 (In the database, EffectiveId is stored as int(11).)
3823
3824
3825
3826 =head2 SetEffectiveId VALUE
3827
3828
3829 Set EffectiveId to VALUE.
3830 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3831 (In the database, EffectiveId will be stored as a int(11).)
3832
3833
3834 =cut
3835
3836
3837 =head2 Queue
3838
3839 Returns the current value of Queue.
3840 (In the database, Queue is stored as int(11).)
3841
3842
3843
3844 =head2 SetQueue VALUE
3845
3846
3847 Set Queue to VALUE.
3848 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3849 (In the database, Queue will be stored as a int(11).)
3850
3851
3852 =cut
3853
3854
3855 =head2 Type
3856
3857 Returns the current value of Type.
3858 (In the database, Type is stored as varchar(16).)
3859
3860
3861
3862 =head2 SetType VALUE
3863
3864
3865 Set Type to VALUE.
3866 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3867 (In the database, Type will be stored as a varchar(16).)
3868
3869
3870 =cut
3871
3872
3873 =head2 IssueStatement
3874
3875 Returns the current value of IssueStatement.
3876 (In the database, IssueStatement is stored as int(11).)
3877
3878
3879
3880 =head2 SetIssueStatement VALUE
3881
3882
3883 Set IssueStatement to VALUE.
3884 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3885 (In the database, IssueStatement will be stored as a int(11).)
3886
3887
3888 =cut
3889
3890
3891 =head2 Resolution
3892
3893 Returns the current value of Resolution.
3894 (In the database, Resolution is stored as int(11).)
3895
3896
3897
3898 =head2 SetResolution VALUE
3899
3900
3901 Set Resolution to VALUE.
3902 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3903 (In the database, Resolution will be stored as a int(11).)
3904
3905
3906 =cut
3907
3908
3909 =head2 Owner
3910
3911 Returns the current value of Owner.
3912 (In the database, Owner is stored as int(11).)
3913
3914
3915
3916 =head2 SetOwner VALUE
3917
3918
3919 Set Owner to VALUE.
3920 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3921 (In the database, Owner will be stored as a int(11).)
3922
3923
3924 =cut
3925
3926
3927 =head2 Subject
3928
3929 Returns the current value of Subject.
3930 (In the database, Subject is stored as varchar(200).)
3931
3932
3933
3934 =head2 SetSubject VALUE
3935
3936
3937 Set Subject to VALUE.
3938 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3939 (In the database, Subject will be stored as a varchar(200).)
3940
3941
3942 =cut
3943
3944
3945 =head2 InitialPriority
3946
3947 Returns the current value of InitialPriority.
3948 (In the database, InitialPriority is stored as int(11).)
3949
3950
3951
3952 =head2 SetInitialPriority VALUE
3953
3954
3955 Set InitialPriority to VALUE.
3956 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3957 (In the database, InitialPriority will be stored as a int(11).)
3958
3959
3960 =cut
3961
3962
3963 =head2 FinalPriority
3964
3965 Returns the current value of FinalPriority.
3966 (In the database, FinalPriority is stored as int(11).)
3967
3968
3969
3970 =head2 SetFinalPriority VALUE
3971
3972
3973 Set FinalPriority to VALUE.
3974 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3975 (In the database, FinalPriority will be stored as a int(11).)
3976
3977
3978 =cut
3979
3980
3981 =head2 Priority
3982
3983 Returns the current value of Priority.
3984 (In the database, Priority is stored as int(11).)
3985
3986
3987
3988 =head2 SetPriority VALUE
3989
3990
3991 Set Priority to VALUE.
3992 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3993 (In the database, Priority will be stored as a int(11).)
3994
3995
3996 =cut
3997
3998
3999 =head2 TimeEstimated
4000
4001 Returns the current value of TimeEstimated.
4002 (In the database, TimeEstimated is stored as int(11).)
4003
4004
4005
4006 =head2 SetTimeEstimated VALUE
4007
4008
4009 Set TimeEstimated to VALUE.
4010 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4011 (In the database, TimeEstimated will be stored as a int(11).)
4012
4013
4014 =cut
4015
4016
4017 =head2 TimeWorked
4018
4019 Returns the current value of TimeWorked.
4020 (In the database, TimeWorked is stored as int(11).)
4021
4022
4023
4024 =head2 SetTimeWorked VALUE
4025
4026
4027 Set TimeWorked to VALUE.
4028 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4029 (In the database, TimeWorked will be stored as a int(11).)
4030
4031
4032 =cut
4033
4034
4035 =head2 Status
4036
4037 Returns the current value of Status.
4038 (In the database, Status is stored as varchar(64).)
4039
4040
4041
4042 =head2 SetStatus VALUE
4043
4044
4045 Set Status to VALUE.
4046 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4047 (In the database, Status will be stored as a varchar(64).)
4048
4049
4050 =cut
4051
4052
4053 =head2 TimeLeft
4054
4055 Returns the current value of TimeLeft.
4056 (In the database, TimeLeft is stored as int(11).)
4057
4058
4059
4060 =head2 SetTimeLeft VALUE
4061
4062
4063 Set TimeLeft to VALUE.
4064 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4065 (In the database, TimeLeft will be stored as a int(11).)
4066
4067
4068 =cut
4069
4070
4071 =head2 Told
4072
4073 Returns the current value of Told.
4074 (In the database, Told is stored as datetime.)
4075
4076
4077
4078 =head2 SetTold VALUE
4079
4080
4081 Set Told to VALUE.
4082 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4083 (In the database, Told will be stored as a datetime.)
4084
4085
4086 =cut
4087
4088
4089 =head2 Starts
4090
4091 Returns the current value of Starts.
4092 (In the database, Starts is stored as datetime.)
4093
4094
4095
4096 =head2 SetStarts VALUE
4097
4098
4099 Set Starts to VALUE.
4100 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4101 (In the database, Starts will be stored as a datetime.)
4102
4103
4104 =cut
4105
4106
4107 =head2 Started
4108
4109 Returns the current value of Started.
4110 (In the database, Started is stored as datetime.)
4111
4112
4113
4114 =head2 SetStarted VALUE
4115
4116
4117 Set Started to VALUE.
4118 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4119 (In the database, Started will be stored as a datetime.)
4120
4121
4122 =cut
4123
4124
4125 =head2 Due
4126
4127 Returns the current value of Due.
4128 (In the database, Due is stored as datetime.)
4129
4130
4131
4132 =head2 SetDue VALUE
4133
4134
4135 Set Due to VALUE.
4136 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4137 (In the database, Due will be stored as a datetime.)
4138
4139
4140 =cut
4141
4142
4143 =head2 Resolved
4144
4145 Returns the current value of Resolved.
4146 (In the database, Resolved is stored as datetime.)
4147
4148
4149
4150 =head2 SetResolved VALUE
4151
4152
4153 Set Resolved to VALUE.
4154 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4155 (In the database, Resolved will be stored as a datetime.)
4156
4157
4158 =cut
4159
4160
4161 =head2 LastUpdatedBy
4162
4163 Returns the current value of LastUpdatedBy.
4164 (In the database, LastUpdatedBy is stored as int(11).)
4165
4166
4167 =cut
4168
4169
4170 =head2 LastUpdated
4171
4172 Returns the current value of LastUpdated.
4173 (In the database, LastUpdated is stored as datetime.)
4174
4175
4176 =cut
4177
4178
4179 =head2 Creator
4180
4181 Returns the current value of Creator.
4182 (In the database, Creator is stored as int(11).)
4183
4184
4185 =cut
4186
4187
4188 =head2 Created
4189
4190 Returns the current value of Created.
4191 (In the database, Created is stored as datetime.)
4192
4193
4194 =cut
4195
4196
4197 =head2 Disabled
4198
4199 Returns the current value of Disabled.
4200 (In the database, Disabled is stored as smallint(6).)
4201
4202
4203
4204 =head2 SetDisabled VALUE
4205
4206
4207 Set Disabled to VALUE.
4208 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4209 (In the database, Disabled will be stored as a smallint(6).)
4210
4211
4212 =cut
4213
4214
4215
4216 sub _CoreAccessible {
4217     {
4218
4219         id =>
4220                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
4221         EffectiveId =>
4222                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4223         Queue =>
4224                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4225         Type =>
4226                 {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
4227         IssueStatement =>
4228                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4229         Resolution =>
4230                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4231         Owner =>
4232                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4233         Subject =>
4234                 {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => '[no subject]'},
4235         InitialPriority =>
4236                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4237         FinalPriority =>
4238                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4239         Priority =>
4240                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4241         TimeEstimated =>
4242                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4243         TimeWorked =>
4244                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4245         Status =>
4246                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
4247         TimeLeft =>
4248                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4249         Told =>
4250                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4251         Starts =>
4252                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4253         Started =>
4254                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4255         Due =>
4256                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4257         Resolved =>
4258                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4259         LastUpdatedBy =>
4260                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4261         LastUpdated =>
4262                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4263         Creator =>
4264                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4265         Created =>
4266                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4267         Disabled =>
4268                 {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
4269
4270  }
4271 };
4272
4273 RT::Base->_ImportOverlays();
4274
4275 1;