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->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->warning("$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 FirstInactiveStatus
1993
1994 Returns the first inactive status that the ticket could transition to,
1995 according to its current Queue's lifecycle.  May return undef if there
1996 is no such possible status to transition to, or we are already in it.
1997 This is used in resolve action in UnsafeEmailCommands, for instance.
1998
1999 =cut
2000
2001 sub FirstInactiveStatus {
2002     my $self = shift;
2003
2004     my $lifecycle = $self->QueueObj->Lifecycle;
2005     my $status = $self->Status;
2006     my @inactive = $lifecycle->Inactive;
2007     # no change if no inactive statuses in the lifecycle
2008     return undef unless @inactive;
2009
2010     # no change if the ticket is already has first status from the list of inactive
2011     return undef if lc $status eq lc $inactive[0];
2012
2013     my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
2014     return $next;
2015 }
2016
2017 =head2 SetStarted
2018
2019 Takes a date in ISO format or undef
2020 Returns a transaction id and a message
2021 The client calls "Start" to note that the project was started on the date in $date.
2022 A null date means "now"
2023
2024 =cut
2025
2026 sub SetStarted {
2027     my $self = shift;
2028     my $time = shift || 0;
2029
2030     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2031         return ( 0, $self->loc("Permission Denied") );
2032     }
2033
2034     #We create a date object to catch date weirdness
2035     my $time_obj = RT::Date->new( $self->CurrentUser() );
2036     if ( $time ) {
2037         $time_obj->Set( Format => 'ISO', Value => $time );
2038     }
2039     else {
2040         $time_obj->SetToNow();
2041     }
2042
2043     # We need $TicketAsSystem, in case the current user doesn't have
2044     # ShowTicket
2045     my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
2046     $TicketAsSystem->Load( $self->Id );
2047     # Now that we're starting, open this ticket
2048     # TODO: do we really want to force this as policy? it should be a scrip
2049     my $next = $TicketAsSystem->FirstActiveStatus;
2050
2051     $self->SetStatus( $next ) if defined $next;
2052
2053     return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2054
2055 }
2056
2057
2058
2059 =head2 StartedObj
2060
2061   Returns an RT::Date object which contains this ticket's 
2062 'Started' time.
2063
2064 =cut
2065
2066 sub StartedObj {
2067     my $self = shift;
2068
2069     my $time = RT::Date->new( $self->CurrentUser );
2070     $time->Set( Format => 'sql', Value => $self->Started );
2071     return $time;
2072 }
2073
2074
2075
2076 =head2 StartsObj
2077
2078   Returns an RT::Date object which contains this ticket's 
2079 'Starts' time.
2080
2081 =cut
2082
2083 sub StartsObj {
2084     my $self = shift;
2085
2086     my $time = RT::Date->new( $self->CurrentUser );
2087     $time->Set( Format => 'sql', Value => $self->Starts );
2088     return $time;
2089 }
2090
2091
2092
2093 =head2 ToldObj
2094
2095   Returns an RT::Date object which contains this ticket's 
2096 'Told' time.
2097
2098 =cut
2099
2100 sub ToldObj {
2101     my $self = shift;
2102
2103     my $time = RT::Date->new( $self->CurrentUser );
2104     $time->Set( Format => 'sql', Value => $self->Told );
2105     return $time;
2106 }
2107
2108
2109
2110 =head2 ToldAsString
2111
2112 A convenience method that returns ToldObj->AsString
2113
2114 TODO: This should be deprecated
2115
2116 =cut
2117
2118 sub ToldAsString {
2119     my $self = shift;
2120     if ( $self->Told ) {
2121         return $self->ToldObj->AsString();
2122     }
2123     else {
2124         return ("Never");
2125     }
2126 }
2127
2128
2129
2130 =head2 TimeWorkedAsString
2131
2132 Returns the amount of time worked on this ticket as a Text String
2133
2134 =cut
2135
2136 sub TimeWorkedAsString {
2137     my $self = shift;
2138     my $value = $self->TimeWorked;
2139
2140     # return the # of minutes worked turned into seconds and written as
2141     # a simple text string, this is not really a date object, but if we
2142     # diff a number of seconds vs the epoch, we'll get a nice description
2143     # of time worked.
2144     return "" unless $value;
2145     return RT::Date->new( $self->CurrentUser )
2146         ->DurationAsString( $value * 60 );
2147 }
2148
2149
2150
2151 =head2  TimeLeftAsString
2152
2153 Returns the amount of time left on this ticket as a Text String
2154
2155 =cut
2156
2157 sub TimeLeftAsString {
2158     my $self = shift;
2159     my $value = $self->TimeLeft;
2160     return "" unless $value;
2161     return RT::Date->new( $self->CurrentUser )
2162         ->DurationAsString( $value * 60 );
2163 }
2164
2165
2166
2167
2168 =head2 Comment
2169
2170 Comment on this ticket.
2171 Takes a hash with the following attributes:
2172 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2173 comment.
2174
2175 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2176
2177 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2178 They will, however, be prepared and you'll be able to access them through the TransactionObj
2179
2180 Returns: Transaction id, Error Message, Transaction Object
2181 (note the different order from Create()!)
2182
2183 =cut
2184
2185 sub Comment {
2186     my $self = shift;
2187
2188     my %args = ( CcMessageTo  => undef,
2189                  BccMessageTo => undef,
2190                  MIMEObj      => undef,
2191                  Content      => undef,
2192                  TimeTaken => 0,
2193                  DryRun     => 0, 
2194                  @_ );
2195
2196     unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
2197              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2198         return ( 0, $self->loc("Permission Denied"), undef );
2199     }
2200     $args{'NoteType'} = 'Comment';
2201
2202     if ($args{'DryRun'}) {
2203         $RT::Handle->BeginTransaction();
2204         $args{'CommitScrips'} = 0;
2205     }
2206
2207     my @results = $self->_RecordNote(%args);
2208     if ($args{'DryRun'}) {
2209         $RT::Handle->Rollback();
2210     }
2211
2212     return(@results);
2213 }
2214
2215
2216 =head2 Correspond
2217
2218 Correspond on this ticket.
2219 Takes a hashref with the following attributes:
2220
2221
2222 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2223
2224 if there's no MIMEObj, Content is used to build a MIME::Entity object
2225
2226 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2227 They will, however, be prepared and you'll be able to access them through the TransactionObj
2228
2229 Returns: Transaction id, Error Message, Transaction Object
2230 (note the different order from Create()!)
2231
2232
2233 =cut
2234
2235 sub Correspond {
2236     my $self = shift;
2237     my %args = ( CcMessageTo  => undef,
2238                  BccMessageTo => undef,
2239                  MIMEObj      => undef,
2240                  Content      => undef,
2241                  TimeTaken    => 0,
2242                  @_ );
2243
2244     unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
2245              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2246         return ( 0, $self->loc("Permission Denied"), undef );
2247     }
2248
2249     $args{'NoteType'} = 'Correspond'; 
2250     if ($args{'DryRun'}) {
2251         $RT::Handle->BeginTransaction();
2252         $args{'CommitScrips'} = 0;
2253     }
2254
2255     my @results = $self->_RecordNote(%args);
2256
2257     #Set the last told date to now if this isn't mail from the requestor.
2258     #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2259     unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2260         my %squelch;
2261         $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2262         $self->_SetTold
2263             if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2264     }
2265
2266     if ($args{'DryRun'}) {
2267         $RT::Handle->Rollback();
2268     }
2269
2270     return (@results);
2271
2272 }
2273
2274
2275
2276 =head2 _RecordNote
2277
2278 the meat of both comment and correspond. 
2279
2280 Performs no access control checks. hence, dangerous.
2281
2282 =cut
2283
2284 sub _RecordNote {
2285     my $self = shift;
2286     my %args = ( 
2287         CcMessageTo  => undef,
2288         BccMessageTo => undef,
2289         Encrypt      => undef,
2290         Sign         => undef,
2291         MIMEObj      => undef,
2292         Content      => undef,
2293         NoteType     => 'Correspond',
2294         TimeTaken    => 0,
2295         CommitScrips => 1,
2296         SquelchMailTo => undef,
2297         CustomFields => {},
2298         @_
2299     );
2300
2301     unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2302         return ( 0, $self->loc("No message attached"), undef );
2303     }
2304
2305     unless ( $args{'MIMEObj'} ) {
2306         $args{'MIMEObj'} = MIME::Entity->build(
2307             Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2308         );
2309     }
2310
2311     # convert text parts into utf-8
2312     RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2313
2314     # If we've been passed in CcMessageTo and BccMessageTo fields,
2315     # add them to the mime object for passing on to the transaction handler
2316     # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2317     # RT-Send-Bcc: headers
2318
2319
2320     foreach my $type (qw/Cc Bcc/) {
2321         if ( defined $args{ $type . 'MessageTo' } ) {
2322
2323             my $addresses = join ', ', (
2324                 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2325                     Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2326             $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2327         }
2328     }
2329
2330     foreach my $argument (qw(Encrypt Sign)) {
2331         $args{'MIMEObj'}->head->replace(
2332             "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2333         ) if defined $args{ $argument };
2334     }
2335
2336     # If this is from an external source, we need to come up with its
2337     # internal Message-ID now, so all emails sent because of this
2338     # message have a common Message-ID
2339     my $org = RT->Config->Get('Organization');
2340     my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2341     unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2342         $args{'MIMEObj'}->head->set(
2343             'RT-Message-ID' => Encode::encode_utf8(
2344                 RT::Interface::Email::GenMessageId( Ticket => $self )
2345             )
2346         );
2347     }
2348
2349     #Record the correspondence (write the transaction)
2350     my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2351              Type => $args{'NoteType'},
2352              Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2353              TimeTaken => $args{'TimeTaken'},
2354              MIMEObj   => $args{'MIMEObj'}, 
2355              CommitScrips => $args{'CommitScrips'},
2356              SquelchMailTo => $args{'SquelchMailTo'},
2357              CustomFields => $args{'CustomFields'},
2358     );
2359
2360     unless ($Trans) {
2361         $RT::Logger->err("$self couldn't init a transaction $msg");
2362         return ( $Trans, $self->loc("Message could not be recorded"), undef );
2363     }
2364
2365     return ( $Trans, $self->loc("Message recorded"), $TransObj );
2366 }
2367
2368
2369 =head2 DryRun
2370
2371 Builds a MIME object from the given C<UpdateSubject> and
2372 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2373 C<< DryRun => 1 >>, and returns the transaction so produced.
2374
2375 =cut
2376
2377 sub DryRun {
2378     my $self = shift;
2379     my %args = @_;
2380     my $action;
2381     if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2382         $action = 'Correspond';
2383     } else {
2384         $action = 'Comment';
2385     }
2386
2387     my $Message = MIME::Entity->build(
2388         Type    => 'text/plain',
2389         Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2390         Charset => 'UTF-8',
2391         Data    => $args{'UpdateContent'} || "",
2392     );
2393
2394     my ( $Transaction, $Description, $Object ) = $self->$action(
2395         CcMessageTo  => $args{'UpdateCc'},
2396         BccMessageTo => $args{'UpdateBcc'},
2397         MIMEObj      => $Message,
2398         TimeTaken    => $args{'UpdateTimeWorked'},
2399         DryRun       => 1,
2400     );
2401     unless ( $Transaction ) {
2402         $RT::Logger->error("Couldn't fire '$action' action: $Description");
2403     }
2404
2405     return $Object;
2406 }
2407
2408 =head2 DryRunCreate
2409
2410 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2411 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2412 the resulting L<RT::Transaction>.
2413
2414 =cut
2415
2416 sub DryRunCreate {
2417     my $self = shift;
2418     my %args = @_;
2419     my $Message = MIME::Entity->build(
2420         Type    => 'text/plain',
2421         Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2422         (defined $args{'Cc'} ?
2423              ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2424         Charset => 'UTF-8',
2425         Data    => $args{'Content'} || "",
2426     );
2427
2428     my ( $Transaction, $Object, $Description ) = $self->Create(
2429         Type            => $args{'Type'} || 'ticket',
2430         Queue           => $args{'Queue'},
2431         Owner           => $args{'Owner'},
2432         Requestor       => $args{'Requestors'},
2433         Cc              => $args{'Cc'},
2434         AdminCc         => $args{'AdminCc'},
2435         InitialPriority => $args{'InitialPriority'},
2436         FinalPriority   => $args{'FinalPriority'},
2437         TimeLeft        => $args{'TimeLeft'},
2438         TimeEstimated   => $args{'TimeEstimated'},
2439         TimeWorked      => $args{'TimeWorked'},
2440         Subject         => $args{'Subject'},
2441         Status          => $args{'Status'},
2442         MIMEObj         => $Message,
2443         DryRun          => 1,
2444     );
2445     unless ( $Transaction ) {
2446         $RT::Logger->error("Couldn't fire Create action: $Description");
2447     }
2448
2449     return $Object;
2450 }
2451
2452
2453
2454 sub _Links {
2455     my $self = shift;
2456
2457     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2458     #tobias meant by $f
2459     my $field = shift;
2460     my $type  = shift || "";
2461
2462     my $cache_key = "$field$type";
2463     return $self->{ $cache_key } if $self->{ $cache_key };
2464
2465     my $links = $self->{ $cache_key }
2466               = RT::Links->new( $self->CurrentUser );
2467     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2468         $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2469         return $links;
2470     }
2471
2472     # Maybe this ticket is a merge ticket
2473     #my $limit_on = 'Local'. $field;
2474     # at least to myself
2475     $links->Limit(
2476         FIELD           => $field, #$limit_on,
2477         OPERATOR        => 'MATCHES',
2478         VALUE           => 'fsck.com-rt://%/ticket/'. $self->id,
2479         ENTRYAGGREGATOR => 'OR',
2480     );
2481     $links->Limit(
2482         FIELD           => $field, #$limit_on,
2483         OPERATOR        => 'MATCHES',
2484         VALUE           => 'fsck.com-rt://%/ticket/'. $_,
2485         ENTRYAGGREGATOR => 'OR',
2486     ) foreach $self->Merged;
2487     $links->Limit(
2488         FIELD => 'Type',
2489         VALUE => $type,
2490     ) if $type;
2491
2492     return $links;
2493 }
2494
2495
2496
2497 =head2 DeleteLink
2498
2499 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2500 SilentBase and SilentTarget. Either Base or Target must be null.
2501 The null value will be replaced with this ticket\'s id.
2502
2503 If Silent is true then no transaction would be recorded, in other
2504 case you can control creation of transactions on both base and
2505 target with SilentBase and SilentTarget respectively. By default
2506 both transactions are created.
2507
2508 =cut 
2509
2510 sub DeleteLink {
2511     my $self = shift;
2512     my %args = (
2513         Base   => undef,
2514         Target => undef,
2515         Type   => undef,
2516         Silent => undef,
2517         SilentBase   => undef,
2518         SilentTarget => undef,
2519         @_
2520     );
2521
2522     unless ( $args{'Target'} || $args{'Base'} ) {
2523         $RT::Logger->error("Base or Target must be specified");
2524         return ( 0, $self->loc('Either base or target must be specified') );
2525     }
2526
2527     #check acls
2528     my $right = 0;
2529     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2530     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2531         return ( 0, $self->loc("Permission Denied") );
2532     }
2533
2534     # If the other URI is an RT::Ticket, we want to make sure the user
2535     # can modify it too...
2536     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2537     return (0, $msg) unless $status;
2538     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2539         $right++;
2540     }
2541     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2542          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2543     {
2544         return ( 0, $self->loc("Permission Denied") );
2545     }
2546
2547     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2548     return ( 0, $Msg ) unless $val;
2549
2550     return ( $val, $Msg ) if $args{'Silent'};
2551
2552     my ($direction, $remote_link);
2553
2554     if ( $args{'Base'} ) {
2555         $remote_link = $args{'Base'};
2556         $direction = 'Target';
2557     }
2558     elsif ( $args{'Target'} ) {
2559         $remote_link = $args{'Target'};
2560         $direction = 'Base';
2561     } 
2562
2563     my $remote_uri = RT::URI->new( $self->CurrentUser );
2564     $remote_uri->FromURI( $remote_link );
2565
2566     unless ( $args{ 'Silent'. $direction } ) {
2567         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2568             Type      => 'DeleteLink',
2569             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2570             OldValue  => $remote_uri->URI || $remote_link,
2571             TimeTaken => 0
2572         );
2573         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2574     }
2575
2576     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2577         my $OtherObj = $remote_uri->Object;
2578         my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2579             Type           => 'DeleteLink',
2580             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2581                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2582             OldValue       => $self->URI,
2583             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2584             TimeTaken      => 0,
2585         );
2586         $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2587     }
2588
2589     return ( $val, $Msg );
2590 }
2591
2592
2593
2594 =head2 AddLink
2595
2596 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2597
2598 If Silent is true then no transaction would be recorded, in other
2599 case you can control creation of transactions on both base and
2600 target with SilentBase and SilentTarget respectively. By default
2601 both transactions are created.
2602
2603 =cut
2604
2605 sub AddLink {
2606     my $self = shift;
2607     my %args = ( Target       => '',
2608                  Base         => '',
2609                  Type         => '',
2610                  Silent       => undef,
2611                  SilentBase   => undef,
2612                  SilentTarget => undef,
2613                  @_ );
2614
2615     unless ( $args{'Target'} || $args{'Base'} ) {
2616         $RT::Logger->error("Base or Target must be specified");
2617         return ( 0, $self->loc('Either base or target must be specified') );
2618     }
2619
2620     my $right = 0;
2621     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2622     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2623         return ( 0, $self->loc("Permission Denied") );
2624     }
2625
2626     # If the other URI is an RT::Ticket, we want to make sure the user
2627     # can modify it too...
2628     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2629     return (0, $msg) unless $status;
2630     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2631         $right++;
2632     }
2633     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2634          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2635     {
2636         return ( 0, $self->loc("Permission Denied") );
2637     }
2638
2639     return ( 0, "Can't link to a deleted ticket" )
2640       if $other_ticket && $other_ticket->Status eq 'deleted';
2641
2642     return $self->_AddLink(%args);
2643 }
2644
2645 sub __GetTicketFromURI {
2646     my $self = shift;
2647     my %args = ( URI => '', @_ );
2648
2649     # If the other URI is an RT::Ticket, we want to make sure the user
2650     # can modify it too...
2651     my $uri_obj = RT::URI->new( $self->CurrentUser );
2652     $uri_obj->FromURI( $args{'URI'} );
2653
2654     unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2655         my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2656         $RT::Logger->warning( $msg );
2657         return( 0, $msg );
2658     }
2659     my $obj = $uri_obj->Resolver->Object;
2660     unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2661         return (1, 'Found not a ticket', undef);
2662     }
2663     return (1, 'Found ticket', $obj);
2664 }
2665
2666 =head2 _AddLink  
2667
2668 Private non-acled variant of AddLink so that links can be added during create.
2669
2670 =cut
2671
2672 sub _AddLink {
2673     my $self = shift;
2674     my %args = ( Target       => '',
2675                  Base         => '',
2676                  Type         => '',
2677                  Silent       => undef,
2678                  SilentBase   => undef,
2679                  SilentTarget => undef,
2680                  @_ );
2681
2682     my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2683     return ($val, $msg) if !$val || $exist;
2684     return ($val, $msg) if $args{'Silent'};
2685
2686     my ($direction, $remote_link);
2687     if ( $args{'Target'} ) {
2688         $remote_link  = $args{'Target'};
2689         $direction    = 'Base';
2690     } elsif ( $args{'Base'} ) {
2691         $remote_link  = $args{'Base'};
2692         $direction    = 'Target';
2693     }
2694
2695     my $remote_uri = RT::URI->new( $self->CurrentUser );
2696     $remote_uri->FromURI( $remote_link );
2697
2698     unless ( $args{ 'Silent'. $direction } ) {
2699         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2700             Type      => 'AddLink',
2701             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2702             NewValue  =>  $remote_uri->URI || $remote_link,
2703             TimeTaken => 0
2704         );
2705         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2706     }
2707
2708     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2709         my $OtherObj = $remote_uri->Object;
2710         my ( $val, $msg ) = $OtherObj->_NewTransaction(
2711             Type           => 'AddLink',
2712             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2713                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2714             NewValue       => $self->URI,
2715             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2716             TimeTaken      => 0,
2717         );
2718         $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2719     }
2720
2721     return ( $val, $msg );
2722 }
2723
2724
2725
2726
2727 =head2 MergeInto
2728
2729 MergeInto take the id of the ticket to merge this ticket into.
2730
2731 =cut
2732
2733 sub MergeInto {
2734     my $self      = shift;
2735     my $ticket_id = shift;
2736
2737     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2738         return ( 0, $self->loc("Permission Denied") );
2739     }
2740
2741     # Load up the new ticket.
2742     my $MergeInto = RT::Ticket->new($self->CurrentUser);
2743     $MergeInto->Load($ticket_id);
2744
2745     # make sure it exists.
2746     unless ( $MergeInto->Id ) {
2747         return ( 0, $self->loc("New ticket doesn't exist") );
2748     }
2749
2750     # Make sure the current user can modify the new ticket.
2751     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2752         return ( 0, $self->loc("Permission Denied") );
2753     }
2754
2755     delete $MERGE_CACHE{'effective'}{ $self->id };
2756     delete @{ $MERGE_CACHE{'merged'} }{
2757         $ticket_id, $MergeInto->id, $self->id
2758     };
2759
2760     $RT::Handle->BeginTransaction();
2761
2762     $self->_MergeInto( $MergeInto );
2763
2764     $RT::Handle->Commit();
2765
2766     return ( 1, $self->loc("Merge Successful") );
2767 }
2768
2769 sub _MergeInto {
2770     my $self      = shift;
2771     my $MergeInto = shift;
2772
2773
2774     # We use EffectiveId here even though it duplicates information from
2775     # the links table becasue of the massive performance hit we'd take
2776     # by trying to do a separate database query for merge info everytime 
2777     # loaded a ticket. 
2778
2779     #update this ticket's effective id to the new ticket's id.
2780     my ( $id_val, $id_msg ) = $self->__Set(
2781         Field => 'EffectiveId',
2782         Value => $MergeInto->Id()
2783     );
2784
2785     unless ($id_val) {
2786         $RT::Handle->Rollback();
2787         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2788     }
2789
2790
2791     my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2792     if ( $force_status && $force_status ne $self->__Value('Status') ) {
2793         my ( $status_val, $status_msg )
2794             = $self->__Set( Field => 'Status', Value => $force_status );
2795
2796         unless ($status_val) {
2797             $RT::Handle->Rollback();
2798             $RT::Logger->error(
2799                 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2800             );
2801             return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2802         }
2803     }
2804
2805     # update all the links that point to that old ticket
2806     my $old_links_to = RT::Links->new($self->CurrentUser);
2807     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2808
2809     my %old_seen;
2810     while (my $link = $old_links_to->Next) {
2811         if (exists $old_seen{$link->Base."-".$link->Type}) {
2812             $link->Delete;
2813         }   
2814         elsif ($link->Base eq $MergeInto->URI) {
2815             $link->Delete;
2816         } else {
2817             # First, make sure the link doesn't already exist. then move it over.
2818             my $tmp = RT::Link->new(RT->SystemUser);
2819             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2820             if ($tmp->id)   {
2821                     $link->Delete;
2822             } else { 
2823                 $link->SetTarget($MergeInto->URI);
2824                 $link->SetLocalTarget($MergeInto->id);
2825             }
2826             $old_seen{$link->Base."-".$link->Type} =1;
2827         }
2828
2829     }
2830
2831     my $old_links_from = RT::Links->new($self->CurrentUser);
2832     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2833
2834     while (my $link = $old_links_from->Next) {
2835         if (exists $old_seen{$link->Type."-".$link->Target}) {
2836             $link->Delete;
2837         }   
2838         if ($link->Target eq $MergeInto->URI) {
2839             $link->Delete;
2840         } else {
2841             # First, make sure the link doesn't already exist. then move it over.
2842             my $tmp = RT::Link->new(RT->SystemUser);
2843             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2844             if ($tmp->id)   {
2845                     $link->Delete;
2846             } else { 
2847                 $link->SetBase($MergeInto->URI);
2848                 $link->SetLocalBase($MergeInto->id);
2849                 $old_seen{$link->Type."-".$link->Target} =1;
2850             }
2851         }
2852
2853     }
2854
2855     # Update time fields
2856     foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2857
2858         my $mutator = "Set$type";
2859         $MergeInto->$mutator(
2860             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2861
2862     }
2863 #add all of this ticket's watchers to that ticket.
2864     foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2865
2866         my $people = $self->$watcher_type->MembersObj;
2867         my $addwatcher_type =  $watcher_type;
2868         $addwatcher_type  =~ s/s$//;
2869
2870         while ( my $watcher = $people->Next ) {
2871             
2872            my ($val, $msg) =  $MergeInto->_AddWatcher(
2873                 Type        => $addwatcher_type,
2874                 Silent => 1,
2875                 PrincipalId => $watcher->MemberId
2876             );
2877             unless ($val) {
2878                 $RT::Logger->debug($msg);
2879             }
2880     }
2881
2882     }
2883
2884     #find all of the tickets that were merged into this ticket. 
2885     my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2886     $old_mergees->Limit(
2887         FIELD    => 'EffectiveId',
2888         OPERATOR => '=',
2889         VALUE    => $self->Id
2890     );
2891
2892     #   update their EffectiveId fields to the new ticket's id
2893     while ( my $ticket = $old_mergees->Next() ) {
2894         my ( $val, $msg ) = $ticket->__Set(
2895             Field => 'EffectiveId',
2896             Value => $MergeInto->Id()
2897         );
2898     }
2899
2900     #make a new link: this ticket is merged into that other ticket.
2901     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2902
2903     $MergeInto->_SetLastUpdated;    
2904 }
2905
2906 =head2 Merged
2907
2908 Returns list of tickets' ids that's been merged into this ticket.
2909
2910 =cut
2911
2912 sub Merged {
2913     my $self = shift;
2914
2915     my $id = $self->id;
2916     return @{ $MERGE_CACHE{'merged'}{ $id } }
2917         if $MERGE_CACHE{'merged'}{ $id };
2918
2919     my $mergees = RT::Tickets->new( $self->CurrentUser );
2920     $mergees->Limit(
2921         FIELD    => 'EffectiveId',
2922         VALUE    => $id,
2923     );
2924     $mergees->Limit(
2925         FIELD    => 'id',
2926         OPERATOR => '!=',
2927         VALUE    => $id,
2928     );
2929     return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2930         = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2931 }
2932
2933
2934
2935
2936
2937 =head2 OwnerObj
2938
2939 Takes nothing and returns an RT::User object of 
2940 this ticket's owner
2941
2942 =cut
2943
2944 sub OwnerObj {
2945     my $self = shift;
2946
2947     #If this gets ACLed, we lose on a rights check in User.pm and
2948     #get deep recursion. if we need ACLs here, we need
2949     #an equiv without ACLs
2950
2951     my $owner = RT::User->new( $self->CurrentUser );
2952     $owner->Load( $self->__Value('Owner') );
2953
2954     #Return the owner object
2955     return ($owner);
2956 }
2957
2958
2959
2960 =head2 OwnerAsString
2961
2962 Returns the owner's email address
2963
2964 =cut
2965
2966 sub OwnerAsString {
2967     my $self = shift;
2968     return ( $self->OwnerObj->EmailAddress );
2969
2970 }
2971
2972
2973
2974 =head2 SetOwner
2975
2976 Takes two arguments:
2977      the Id or Name of the owner 
2978 and  (optionally) the type of the SetOwner Transaction. It defaults
2979 to 'Set'.  'Steal' is also a valid option.
2980
2981
2982 =cut
2983
2984 sub SetOwner {
2985     my $self     = shift;
2986     my $NewOwner = shift;
2987     my $Type     = shift || "Set";
2988
2989     $RT::Handle->BeginTransaction();
2990
2991     $self->_SetLastUpdated(); # lock the ticket
2992     $self->Load( $self->id ); # in case $self changed while waiting for lock
2993
2994     my $OldOwnerObj = $self->OwnerObj;
2995
2996     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2997     $NewOwnerObj->Load( $NewOwner );
2998     unless ( $NewOwnerObj->Id ) {
2999         $RT::Handle->Rollback();
3000         return ( 0, $self->loc("That user does not exist") );
3001     }
3002
3003
3004     # must have ModifyTicket rights
3005     # or TakeTicket/StealTicket and $NewOwner is self
3006     # see if it's a take
3007     if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
3008         unless (    $self->CurrentUserHasRight('ModifyTicket')
3009                  || $self->CurrentUserHasRight('TakeTicket') ) {
3010             $RT::Handle->Rollback();
3011             return ( 0, $self->loc("Permission Denied") );
3012         }
3013     }
3014
3015     # see if it's a steal
3016     elsif (    $OldOwnerObj->Id != RT->Nobody->Id
3017             && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3018
3019         unless (    $self->CurrentUserHasRight('ModifyTicket')
3020                  || $self->CurrentUserHasRight('StealTicket') ) {
3021             $RT::Handle->Rollback();
3022             return ( 0, $self->loc("Permission Denied") );
3023         }
3024     }
3025     else {
3026         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3027             $RT::Handle->Rollback();
3028             return ( 0, $self->loc("Permission Denied") );
3029         }
3030     }
3031
3032     # If we're not stealing and the ticket has an owner and it's not
3033     # the current user
3034     if ( $Type ne 'Steal' and $Type ne 'Force'
3035          and $OldOwnerObj->Id != RT->Nobody->Id
3036          and $OldOwnerObj->Id != $self->CurrentUser->Id )
3037     {
3038         $RT::Handle->Rollback();
3039         return ( 0, $self->loc("You can only take tickets that are unowned") )
3040             if $NewOwnerObj->id == $self->CurrentUser->id;
3041         return (
3042             0,
3043             $self->loc("You can only reassign tickets that you own or that are unowned" )
3044         );
3045     }
3046
3047     #If we've specified a new owner and that user can't modify the ticket
3048     elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3049         $RT::Handle->Rollback();
3050         return ( 0, $self->loc("That user may not own tickets in that queue") );
3051     }
3052
3053     # If the ticket has an owner and it's the new owner, we don't need
3054     # To do anything
3055     elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3056         $RT::Handle->Rollback();
3057         return ( 0, $self->loc("That user already owns that ticket") );
3058     }
3059
3060     # Delete the owner in the owner group, then add a new one
3061     # TODO: is this safe? it's not how we really want the API to work
3062     # for most things, but it's fast.
3063     my ( $del_id, $del_msg );
3064     for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
3065         ($del_id, $del_msg) = $owner->Delete();
3066         last unless ($del_id);
3067     }
3068
3069     unless ($del_id) {
3070         $RT::Handle->Rollback();
3071         return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3072     }
3073
3074     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3075                                        PrincipalId => $NewOwnerObj->PrincipalId,
3076                                        InsideTransaction => 1 );
3077     unless ($add_id) {
3078         $RT::Handle->Rollback();
3079         return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3080     }
3081
3082     # We call set twice with slightly different arguments, so
3083     # as to not have an SQL transaction span two RT transactions
3084
3085     my ( $val, $msg ) = $self->_Set(
3086                       Field             => 'Owner',
3087                       RecordTransaction => 0,
3088                       Value             => $NewOwnerObj->Id,
3089                       TimeTaken         => 0,
3090                       TransactionType   => 'Set',
3091                       CheckACL          => 0,                  # don't check acl
3092     );
3093
3094     unless ($val) {
3095         $RT::Handle->Rollback;
3096         return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3097     }
3098
3099     ($val, $msg) = $self->_NewTransaction(
3100         Type      => 'Set',
3101         Field     => 'Owner',
3102         NewValue  => $NewOwnerObj->Id,
3103         OldValue  => $OldOwnerObj->Id,
3104         TimeTaken => 0,
3105     );
3106
3107     if ( $val ) {
3108         $msg = $self->loc( "Owner changed from [_1] to [_2]",
3109                            $OldOwnerObj->Name, $NewOwnerObj->Name );
3110     }
3111     else {
3112         $RT::Handle->Rollback();
3113         return ( 0, $msg );
3114     }
3115
3116     $RT::Handle->Commit();
3117
3118     return ( $val, $msg );
3119 }
3120
3121
3122
3123 =head2 Take
3124
3125 A convenince method to set the ticket's owner to the current user
3126
3127 =cut
3128
3129 sub Take {
3130     my $self = shift;
3131     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3132 }
3133
3134
3135
3136 =head2 Untake
3137
3138 Convenience method to set the owner to 'nobody' if the current user is the owner.
3139
3140 =cut
3141
3142 sub Untake {
3143     my $self = shift;
3144     return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3145 }
3146
3147
3148
3149 =head2 Steal
3150
3151 A convenience method to change the owner of the current ticket to the
3152 current user. Even if it's owned by another user.
3153
3154 =cut
3155
3156 sub Steal {
3157     my $self = shift;
3158
3159     if ( $self->IsOwner( $self->CurrentUser ) ) {
3160         return ( 0, $self->loc("You already own this ticket") );
3161     }
3162     else {
3163         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3164
3165     }
3166
3167 }
3168
3169
3170
3171
3172
3173 =head2 ValidateStatus STATUS
3174
3175 Takes a string. Returns true if that status is a valid status for this ticket.
3176 Returns false otherwise.
3177
3178 =cut
3179
3180 sub ValidateStatus {
3181     my $self   = shift;
3182     my $status = shift;
3183
3184     #Make sure the status passed in is valid
3185     return 1 if $self->QueueObj->IsValidStatus($status);
3186
3187     my $i = 0;
3188     while ( my $caller = (caller($i++))[3] ) {
3189         return 1 if $caller eq 'RT::Ticket::SetQueue';
3190     }
3191
3192     return 0;
3193 }
3194
3195
3196
3197 =head2 SetStatus STATUS
3198
3199 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3200
3201 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3202 If FORCE is true, ignore unresolved dependencies and force a status change.
3203 if SETSTARTED is true( it's the default value), set Started to current datetime if Started 
3204 is not set and the status is changed from initial to not initial. 
3205
3206 =cut
3207
3208 sub SetStatus {
3209     my $self = shift;
3210     my %args;
3211     if (@_ == 1) {
3212         $args{Status} = shift;
3213     }
3214     else {
3215         %args = (@_);
3216     }
3217
3218     # this only allows us to SetStarted, not we must SetStarted.
3219     # this option was added for rtir initially
3220     $args{SetStarted} = 1 unless exists $args{SetStarted};
3221
3222
3223     my $lifecycle = $self->QueueObj->Lifecycle;
3224
3225     my $new = $args{'Status'};
3226     unless ( $lifecycle->IsValid( $new ) ) {
3227         return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3228     }
3229
3230     my $old = $self->__Value('Status');
3231     unless ( $lifecycle->IsTransition( $old => $new ) ) {
3232         return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3233     }
3234
3235     my $check_right = $lifecycle->CheckRight( $old => $new );
3236     unless ( $self->CurrentUserHasRight( $check_right ) ) {
3237         return ( 0, $self->loc('Permission Denied') );
3238     }
3239
3240     if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3241         return (0, $self->loc('That ticket has unresolved dependencies'));
3242     }
3243
3244     my $now = RT::Date->new( $self->CurrentUser );
3245     $now->SetToNow();
3246
3247     my $raw_started = RT::Date->new(RT->SystemUser);
3248     $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3249
3250     #If we're changing the status from new, record that we've started
3251     if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3252         #Set the Started time to "now"
3253         $self->_Set(
3254             Field             => 'Started',
3255             Value             => $now->ISO,
3256             RecordTransaction => 0
3257         );
3258     }
3259
3260     #When we close a ticket, set the 'Resolved' attribute to now.
3261     # It's misnamed, but that's just historical.
3262     if ( $lifecycle->IsInactive($new) ) {
3263         $self->_Set(
3264             Field             => 'Resolved',
3265             Value             => $now->ISO,
3266             RecordTransaction => 0,
3267         );
3268     }
3269
3270     #Actually update the status
3271     my ($val, $msg)= $self->_Set(
3272         Field           => 'Status',
3273         Value           => $args{Status},
3274         TimeTaken       => 0,
3275         CheckACL        => 0,
3276         TransactionType => 'Status',
3277     );
3278     return ($val, $msg);
3279 }
3280
3281
3282
3283 =head2 Delete
3284
3285 Takes no arguments. Marks this ticket for garbage collection
3286
3287 =cut
3288
3289 sub Delete {
3290     my $self = shift;
3291     unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3292         return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3293     }
3294     return ( $self->SetStatus('deleted') );
3295 }
3296
3297
3298 =head2 SetTold ISO  [TIMETAKEN]
3299
3300 Updates the told and records a transaction
3301
3302 =cut
3303
3304 sub SetTold {
3305     my $self = shift;
3306     my $told;
3307     $told = shift if (@_);
3308     my $timetaken = shift || 0;
3309
3310     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3311         return ( 0, $self->loc("Permission Denied") );
3312     }
3313
3314     my $datetold = RT::Date->new( $self->CurrentUser );
3315     if ($told) {
3316         $datetold->Set( Format => 'iso',
3317                         Value  => $told );
3318     }
3319     else {
3320         $datetold->SetToNow();
3321     }
3322
3323     return ( $self->_Set( Field           => 'Told',
3324                           Value           => $datetold->ISO,
3325                           TimeTaken       => $timetaken,
3326                           TransactionType => 'Told' ) );
3327 }
3328
3329 =head2 _SetTold
3330
3331 Updates the told without a transaction or acl check. Useful when we're sending replies.
3332
3333 =cut
3334
3335 sub _SetTold {
3336     my $self = shift;
3337
3338     my $now = RT::Date->new( $self->CurrentUser );
3339     $now->SetToNow();
3340
3341     #use __Set to get no ACLs ;)
3342     return ( $self->__Set( Field => 'Told',
3343                            Value => $now->ISO ) );
3344 }
3345
3346 =head2 SeenUpTo
3347
3348
3349 =cut
3350
3351 sub SeenUpTo {
3352     my $self = shift;
3353     my $uid = $self->CurrentUser->id;
3354     my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3355     return if $attr && $attr->Content gt $self->LastUpdated;
3356
3357     my $txns = $self->Transactions;
3358     $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3359     $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3360     $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3361     $txns->Limit(
3362         FIELD => 'Created',
3363         OPERATOR => '>',
3364         VALUE => $attr->Content
3365     ) if $attr;
3366     $txns->RowsPerPage(1);
3367     return $txns->First;
3368 }
3369
3370 =head2 RanTransactionBatch
3371
3372 Acts as a guard around running TransactionBatch scrips.
3373
3374 Should be false until you enter the code that runs TransactionBatch scrips
3375
3376 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3377
3378 =cut
3379
3380 sub RanTransactionBatch {
3381     my $self = shift;
3382     my $val = shift;
3383
3384     if ( defined $val ) {
3385         return $self->{_RanTransactionBatch} = $val;
3386     } else {
3387         return $self->{_RanTransactionBatch};
3388     }
3389
3390 }
3391
3392
3393 =head2 TransactionBatch
3394
3395 Returns an array reference of all transactions created on this ticket during
3396 this ticket object's lifetime or since last application of a batch, or undef
3397 if there were none.
3398
3399 Only works when the C<UseTransactionBatch> config option is set to true.
3400
3401 =cut
3402
3403 sub TransactionBatch {
3404     my $self = shift;
3405     return $self->{_TransactionBatch};
3406 }
3407
3408 =head2 ApplyTransactionBatch
3409
3410 Applies scrips on the current batch of transactions and shinks it. Usually
3411 batch is applied when object is destroyed, but in some cases it's too late.
3412
3413 =cut
3414
3415 sub ApplyTransactionBatch {
3416     my $self = shift;
3417
3418     my $batch = $self->TransactionBatch;
3419     return unless $batch && @$batch;
3420
3421     $self->_ApplyTransactionBatch;
3422
3423     $self->{_TransactionBatch} = [];
3424 }
3425
3426 sub _ApplyTransactionBatch {
3427     my $self = shift;
3428
3429     return if $self->RanTransactionBatch;
3430     $self->RanTransactionBatch(1);
3431
3432     my $still_exists = RT::Ticket->new( RT->SystemUser );
3433     $still_exists->Load( $self->Id );
3434     if (not $still_exists->Id) {
3435         # The ticket has been removed from the database, but we still
3436         # have pending TransactionBatch txns for it.  Unfortunately,
3437         # because it isn't in the DB anymore, attempting to run scrips
3438         # on it may produce unpredictable results; simply drop the
3439         # batched transactions.
3440         $RT::Logger->warning("TransactionBatch was fired on a ticket that no longer exists; unable to run scrips!  Call ->ApplyTransactionBatch before shredding the ticket, for consistent results.");
3441         return;
3442     }
3443
3444     my $batch = $self->TransactionBatch;
3445
3446     my %seen;
3447     my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3448
3449     require RT::Scrips;
3450     RT::Scrips->new(RT->SystemUser)->Apply(
3451         Stage          => 'TransactionBatch',
3452         TicketObj      => $self,
3453         TransactionObj => $batch->[0],
3454         Type           => $types,
3455     );
3456
3457     # Entry point of the rule system
3458     my $rules = RT::Ruleset->FindAllRules(
3459         Stage          => 'TransactionBatch',
3460         TicketObj      => $self,
3461         TransactionObj => $batch->[0],
3462         Type           => $types,
3463     );
3464     RT::Ruleset->CommitRules($rules);
3465 }
3466
3467 sub DESTROY {
3468     my $self = shift;
3469
3470     # DESTROY methods need to localize $@, or it may unset it.  This
3471     # causes $m->abort to not bubble all of the way up.  See perlbug
3472     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3473     local $@;
3474
3475     # The following line eliminates reentrancy.
3476     # It protects against the fact that perl doesn't deal gracefully
3477     # when an object's refcount is changed in its destructor.
3478     return if $self->{_Destroyed}++;
3479
3480     if (in_global_destruction()) {
3481        unless ($ENV{'HARNESS_ACTIVE'}) {
3482             warn "Too late to safely run transaction-batch scrips!"
3483                 ." This is typically caused by using ticket objects"
3484                 ." at the top-level of a script which uses the RT API."
3485                ." Be sure to explicitly undef such ticket objects,"
3486                 ." or put them inside of a lexical scope.";
3487         }
3488         return;
3489     }
3490
3491     return $self->ApplyTransactionBatch;
3492 }
3493
3494
3495
3496
3497 sub _OverlayAccessible {
3498     {
3499         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3500           Queue           => { 'read' => 1,  'write' => 1 },
3501           Requestors      => { 'read' => 1,  'write' => 1 },
3502           Owner           => { 'read' => 1,  'write' => 1 },
3503           Subject         => { 'read' => 1,  'write' => 1 },
3504           InitialPriority => { 'read' => 1,  'write' => 1 },
3505           FinalPriority   => { 'read' => 1,  'write' => 1 },
3506           Priority        => { 'read' => 1,  'write' => 1 },
3507           Status          => { 'read' => 1,  'write' => 1 },
3508           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3509           TimeWorked      => { 'read' => 1,  'write' => 1 },
3510           TimeLeft        => { 'read' => 1,  'write' => 1 },
3511           Told            => { 'read' => 1,  'write' => 1 },
3512           Resolved        => { 'read' => 1 },
3513           Type            => { 'read' => 1 },
3514           Starts        => { 'read' => 1, 'write' => 1 },
3515           Started       => { 'read' => 1, 'write' => 1 },
3516           Due           => { 'read' => 1, 'write' => 1 },
3517           Creator       => { 'read' => 1, 'auto'  => 1 },
3518           Created       => { 'read' => 1, 'auto'  => 1 },
3519           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3520           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3521     };
3522
3523 }
3524
3525
3526
3527 sub _Set {
3528     my $self = shift;
3529
3530     my %args = ( Field             => undef,
3531                  Value             => undef,
3532                  TimeTaken         => 0,
3533                  RecordTransaction => 1,
3534                  UpdateTicket      => 1,
3535                  CheckACL          => 1,
3536                  TransactionType   => 'Set',
3537                  @_ );
3538
3539     if ($args{'CheckACL'}) {
3540       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3541           return ( 0, $self->loc("Permission Denied"));
3542       }
3543    }
3544
3545     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3546         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3547         return(0, $self->loc("Internal Error"));
3548     }
3549
3550     #if the user is trying to modify the record
3551
3552     #Take care of the old value we really don't want to get in an ACL loop.
3553     # so ask the super::_Value
3554     my $Old = $self->SUPER::_Value("$args{'Field'}");
3555     
3556     my ($ret, $msg);
3557     if ( $args{'UpdateTicket'}  ) {
3558
3559         #Set the new value
3560         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3561                                                 Value => $args{'Value'} );
3562     
3563         #If we can't actually set the field to the value, don't record
3564         # a transaction. instead, get out of here.
3565         return ( 0, $msg ) unless $ret;
3566     }
3567
3568     if ( $args{'RecordTransaction'} == 1 ) {
3569
3570         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3571                                                Type => $args{'TransactionType'},
3572                                                Field     => $args{'Field'},
3573                                                NewValue  => $args{'Value'},
3574                                                OldValue  => $Old,
3575                                                TimeTaken => $args{'TimeTaken'},
3576         );
3577         return ( $Trans, scalar $TransObj->BriefDescription );
3578     }
3579     else {
3580         return ( $ret, $msg );
3581     }
3582 }
3583
3584
3585
3586 =head2 _Value
3587
3588 Takes the name of a table column.
3589 Returns its value as a string, if the user passes an ACL check
3590
3591 =cut
3592
3593 sub _Value {
3594
3595     my $self  = shift;
3596     my $field = shift;
3597
3598     #if the field is public, return it.
3599     if ( $self->_Accessible( $field, 'public' ) ) {
3600
3601         #$RT::Logger->debug("Skipping ACL check for $field");
3602         return ( $self->SUPER::_Value($field) );
3603
3604     }
3605
3606     #If the current user doesn't have ACLs, don't let em at it.  
3607
3608     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3609         return (undef);
3610     }
3611     return ( $self->SUPER::_Value($field) );
3612
3613 }
3614
3615
3616
3617 =head2 _UpdateTimeTaken
3618
3619 This routine will increment the timeworked counter. it should
3620 only be called from _NewTransaction 
3621
3622 =cut
3623
3624 sub _UpdateTimeTaken {
3625     my $self    = shift;
3626     my $Minutes = shift;
3627     my ($Total);
3628
3629     $Total = $self->SUPER::_Value("TimeWorked");
3630     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3631     $self->SUPER::_Set(
3632         Field => "TimeWorked",
3633         Value => $Total
3634     );
3635
3636     return ($Total);
3637 }
3638
3639
3640
3641
3642
3643 =head2 CurrentUserHasRight
3644
3645   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3646 1 if the user has that right. It returns 0 if the user doesn't have that right.
3647
3648 =cut
3649
3650 sub CurrentUserHasRight {
3651     my $self  = shift;
3652     my $right = shift;
3653
3654     return $self->CurrentUser->PrincipalObj->HasRight(
3655         Object => $self,
3656         Right  => $right,
3657     )
3658 }
3659
3660
3661 =head2 CurrentUserCanSee
3662
3663 Returns true if the current user can see the ticket, using ShowTicket
3664
3665 =cut
3666
3667 sub CurrentUserCanSee {
3668     my $self = shift;
3669     return $self->CurrentUserHasRight('ShowTicket');
3670 }
3671
3672 =head2 HasRight
3673
3674  Takes a paramhash with the attributes 'Right' and 'Principal'
3675   'Right' is a ticket-scoped textual right from RT::ACE 
3676   'Principal' is an RT::User object
3677
3678   Returns 1 if the principal has the right. Returns undef if not.
3679
3680 =cut
3681
3682 sub HasRight {
3683     my $self = shift;
3684     my %args = (
3685         Right     => undef,
3686         Principal => undef,
3687         @_
3688     );
3689
3690     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3691     {
3692         Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3693         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3694         return(undef);
3695     }
3696
3697     return (
3698         $args{'Principal'}->HasRight(
3699             Object => $self,
3700             Right     => $args{'Right'}
3701           )
3702     );
3703 }
3704
3705
3706
3707 =head2 Reminders
3708
3709 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3710 It isn't acutally a searchbuilder collection itself.
3711
3712 =cut
3713
3714 sub Reminders {
3715     my $self = shift;
3716     
3717     unless ($self->{'__reminders'}) {
3718         $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3719         $self->{'__reminders'}->Ticket($self->id);
3720     }
3721     return $self->{'__reminders'};
3722
3723 }
3724
3725
3726
3727
3728 =head2 Transactions
3729
3730   Returns an RT::Transactions object of all transactions on this ticket
3731
3732 =cut
3733
3734 sub Transactions {
3735     my $self = shift;
3736
3737     my $transactions = RT::Transactions->new( $self->CurrentUser );
3738
3739     #If the user has no rights, return an empty object
3740     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3741         $transactions->LimitToTicket($self->id);
3742
3743         # if the user may not see comments do not return them
3744         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3745             $transactions->Limit(
3746                 SUBCLAUSE => 'acl',
3747                 FIELD    => 'Type',
3748                 OPERATOR => '!=',
3749                 VALUE    => "Comment"
3750             );
3751             $transactions->Limit(
3752                 SUBCLAUSE => 'acl',
3753                 FIELD    => 'Type',
3754                 OPERATOR => '!=',
3755                 VALUE    => "CommentEmailRecord",
3756                 ENTRYAGGREGATOR => 'AND'
3757             );
3758
3759         }
3760     } else {
3761         $transactions->Limit(
3762             SUBCLAUSE => 'acl',
3763             FIELD    => 'id',
3764             VALUE    => 0,
3765             ENTRYAGGREGATOR => 'AND'
3766         );
3767     }
3768
3769     return ($transactions);
3770 }
3771
3772
3773
3774
3775 =head2 TransactionCustomFields
3776
3777     Returns the custom fields that transactions on tickets will have.
3778
3779 =cut
3780
3781 sub TransactionCustomFields {
3782     my $self = shift;
3783     my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3784     $cfs->SetContextObject( $self );
3785     return $cfs;
3786 }
3787
3788
3789
3790 =head2 CustomFieldValues
3791
3792 # Do name => id mapping (if needed) before falling back to
3793 # RT::Record's CustomFieldValues
3794
3795 See L<RT::Record>
3796
3797 =cut
3798
3799 sub CustomFieldValues {
3800     my $self  = shift;
3801     my $field = shift;
3802
3803     return $self->SUPER::CustomFieldValues( $field ) if !$field || $field =~ /^\d+$/;
3804
3805     my $cf = RT::CustomField->new( $self->CurrentUser );
3806     $cf->SetContextObject( $self );
3807     $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3808     unless ( $cf->id ) {
3809         $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3810     }
3811
3812     # If we didn't find a valid cfid, give up.
3813     return RT::ObjectCustomFieldValues->new( $self->CurrentUser ) unless $cf->id;
3814
3815     return $self->SUPER::CustomFieldValues( $cf->id );
3816 }
3817
3818
3819
3820 =head2 CustomFieldLookupType
3821
3822 Returns the RT::Ticket lookup type, which can be passed to 
3823 RT::CustomField->Create() via the 'LookupType' hash key.
3824
3825 =cut
3826
3827
3828 sub CustomFieldLookupType {
3829     "RT::Queue-RT::Ticket";
3830 }
3831
3832 =head2 ACLEquivalenceObjects
3833
3834 This method returns a list of objects for which a user's rights also apply
3835 to this ticket. Generally, this is only the ticket's queue, but some RT 
3836 extensions may make other objects available too.
3837
3838 This method is called from L<RT::Principal/HasRight>.
3839
3840 =cut
3841
3842 sub ACLEquivalenceObjects {
3843     my $self = shift;
3844     return $self->QueueObj;
3845
3846 }
3847
3848
3849 1;
3850
3851 =head1 AUTHOR
3852
3853 Jesse Vincent, jesse@bestpractical.com
3854
3855 =head1 SEE ALSO
3856
3857 RT
3858
3859 =cut
3860
3861
3862 use RT::Queue;
3863 use base 'RT::Record';
3864
3865 sub Table {'Tickets'}
3866
3867
3868
3869
3870
3871
3872 =head2 id
3873
3874 Returns the current value of id.
3875 (In the database, id is stored as int(11).)
3876
3877
3878 =cut
3879
3880
3881 =head2 EffectiveId
3882
3883 Returns the current value of EffectiveId.
3884 (In the database, EffectiveId is stored as int(11).)
3885
3886
3887
3888 =head2 SetEffectiveId VALUE
3889
3890
3891 Set EffectiveId to VALUE.
3892 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3893 (In the database, EffectiveId will be stored as a int(11).)
3894
3895
3896 =cut
3897
3898
3899 =head2 Queue
3900
3901 Returns the current value of Queue.
3902 (In the database, Queue is stored as int(11).)
3903
3904
3905
3906 =head2 SetQueue VALUE
3907
3908
3909 Set Queue to VALUE.
3910 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3911 (In the database, Queue will be stored as a int(11).)
3912
3913
3914 =cut
3915
3916
3917 =head2 Type
3918
3919 Returns the current value of Type.
3920 (In the database, Type is stored as varchar(16).)
3921
3922
3923
3924 =head2 SetType VALUE
3925
3926
3927 Set Type to VALUE.
3928 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3929 (In the database, Type will be stored as a varchar(16).)
3930
3931
3932 =cut
3933
3934
3935 =head2 IssueStatement
3936
3937 Returns the current value of IssueStatement.
3938 (In the database, IssueStatement is stored as int(11).)
3939
3940
3941
3942 =head2 SetIssueStatement VALUE
3943
3944
3945 Set IssueStatement to VALUE.
3946 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3947 (In the database, IssueStatement will be stored as a int(11).)
3948
3949
3950 =cut
3951
3952
3953 =head2 Resolution
3954
3955 Returns the current value of Resolution.
3956 (In the database, Resolution is stored as int(11).)
3957
3958
3959
3960 =head2 SetResolution VALUE
3961
3962
3963 Set Resolution to VALUE.
3964 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3965 (In the database, Resolution will be stored as a int(11).)
3966
3967
3968 =cut
3969
3970
3971 =head2 Owner
3972
3973 Returns the current value of Owner.
3974 (In the database, Owner is stored as int(11).)
3975
3976
3977
3978 =head2 SetOwner VALUE
3979
3980
3981 Set Owner to VALUE.
3982 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3983 (In the database, Owner will be stored as a int(11).)
3984
3985
3986 =cut
3987
3988
3989 =head2 Subject
3990
3991 Returns the current value of Subject.
3992 (In the database, Subject is stored as varchar(200).)
3993
3994
3995
3996 =head2 SetSubject VALUE
3997
3998
3999 Set Subject to VALUE.
4000 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4001 (In the database, Subject will be stored as a varchar(200).)
4002
4003
4004 =cut
4005
4006
4007 =head2 InitialPriority
4008
4009 Returns the current value of InitialPriority.
4010 (In the database, InitialPriority is stored as int(11).)
4011
4012
4013
4014 =head2 SetInitialPriority VALUE
4015
4016
4017 Set InitialPriority to VALUE.
4018 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4019 (In the database, InitialPriority will be stored as a int(11).)
4020
4021
4022 =cut
4023
4024
4025 =head2 FinalPriority
4026
4027 Returns the current value of FinalPriority.
4028 (In the database, FinalPriority is stored as int(11).)
4029
4030
4031
4032 =head2 SetFinalPriority VALUE
4033
4034
4035 Set FinalPriority to VALUE.
4036 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4037 (In the database, FinalPriority will be stored as a int(11).)
4038
4039
4040 =cut
4041
4042
4043 =head2 Priority
4044
4045 Returns the current value of Priority.
4046 (In the database, Priority is stored as int(11).)
4047
4048
4049
4050 =head2 SetPriority VALUE
4051
4052
4053 Set Priority to VALUE.
4054 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4055 (In the database, Priority will be stored as a int(11).)
4056
4057
4058 =cut
4059
4060
4061 =head2 TimeEstimated
4062
4063 Returns the current value of TimeEstimated.
4064 (In the database, TimeEstimated is stored as int(11).)
4065
4066
4067
4068 =head2 SetTimeEstimated VALUE
4069
4070
4071 Set TimeEstimated to VALUE.
4072 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4073 (In the database, TimeEstimated will be stored as a int(11).)
4074
4075
4076 =cut
4077
4078
4079 =head2 TimeWorked
4080
4081 Returns the current value of TimeWorked.
4082 (In the database, TimeWorked is stored as int(11).)
4083
4084
4085
4086 =head2 SetTimeWorked VALUE
4087
4088
4089 Set TimeWorked to VALUE.
4090 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4091 (In the database, TimeWorked will be stored as a int(11).)
4092
4093
4094 =cut
4095
4096
4097 =head2 Status
4098
4099 Returns the current value of Status.
4100 (In the database, Status is stored as varchar(64).)
4101
4102
4103
4104 =head2 SetStatus VALUE
4105
4106
4107 Set Status to VALUE.
4108 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4109 (In the database, Status will be stored as a varchar(64).)
4110
4111
4112 =cut
4113
4114
4115 =head2 TimeLeft
4116
4117 Returns the current value of TimeLeft.
4118 (In the database, TimeLeft is stored as int(11).)
4119
4120
4121
4122 =head2 SetTimeLeft VALUE
4123
4124
4125 Set TimeLeft to VALUE.
4126 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4127 (In the database, TimeLeft will be stored as a int(11).)
4128
4129
4130 =cut
4131
4132
4133 =head2 Told
4134
4135 Returns the current value of Told.
4136 (In the database, Told is stored as datetime.)
4137
4138
4139
4140 =head2 SetTold VALUE
4141
4142
4143 Set Told to VALUE.
4144 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4145 (In the database, Told will be stored as a datetime.)
4146
4147
4148 =cut
4149
4150
4151 =head2 Starts
4152
4153 Returns the current value of Starts.
4154 (In the database, Starts is stored as datetime.)
4155
4156
4157
4158 =head2 SetStarts VALUE
4159
4160
4161 Set Starts to VALUE.
4162 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4163 (In the database, Starts will be stored as a datetime.)
4164
4165
4166 =cut
4167
4168
4169 =head2 Started
4170
4171 Returns the current value of Started.
4172 (In the database, Started is stored as datetime.)
4173
4174
4175
4176 =head2 SetStarted VALUE
4177
4178
4179 Set Started to VALUE.
4180 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4181 (In the database, Started will be stored as a datetime.)
4182
4183
4184 =cut
4185
4186
4187 =head2 Due
4188
4189 Returns the current value of Due.
4190 (In the database, Due is stored as datetime.)
4191
4192
4193
4194 =head2 SetDue VALUE
4195
4196
4197 Set Due to VALUE.
4198 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4199 (In the database, Due will be stored as a datetime.)
4200
4201
4202 =cut
4203
4204
4205 =head2 Resolved
4206
4207 Returns the current value of Resolved.
4208 (In the database, Resolved is stored as datetime.)
4209
4210
4211
4212 =head2 SetResolved VALUE
4213
4214
4215 Set Resolved to VALUE.
4216 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4217 (In the database, Resolved will be stored as a datetime.)
4218
4219
4220 =cut
4221
4222
4223 =head2 LastUpdatedBy
4224
4225 Returns the current value of LastUpdatedBy.
4226 (In the database, LastUpdatedBy is stored as int(11).)
4227
4228
4229 =cut
4230
4231
4232 =head2 LastUpdated
4233
4234 Returns the current value of LastUpdated.
4235 (In the database, LastUpdated is stored as datetime.)
4236
4237
4238 =cut
4239
4240
4241 =head2 Creator
4242
4243 Returns the current value of Creator.
4244 (In the database, Creator is stored as int(11).)
4245
4246
4247 =cut
4248
4249
4250 =head2 Created
4251
4252 Returns the current value of Created.
4253 (In the database, Created is stored as datetime.)
4254
4255
4256 =cut
4257
4258
4259 =head2 Disabled
4260
4261 Returns the current value of Disabled.
4262 (In the database, Disabled is stored as smallint(6).)
4263
4264
4265
4266 =head2 SetDisabled VALUE
4267
4268
4269 Set Disabled to VALUE.
4270 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4271 (In the database, Disabled will be stored as a smallint(6).)
4272
4273
4274 =cut
4275
4276
4277
4278 sub _CoreAccessible {
4279     {
4280
4281         id =>
4282                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
4283         EffectiveId =>
4284                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4285         Queue =>
4286                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4287         Type =>
4288                 {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
4289         IssueStatement =>
4290                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4291         Resolution =>
4292                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4293         Owner =>
4294                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4295         Subject =>
4296                 {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => '[no subject]'},
4297         InitialPriority =>
4298                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4299         FinalPriority =>
4300                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4301         Priority =>
4302                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4303         TimeEstimated =>
4304                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4305         TimeWorked =>
4306                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4307         Status =>
4308                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
4309         TimeLeft =>
4310                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4311         Told =>
4312                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4313         Starts =>
4314                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4315         Started =>
4316                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4317         Due =>
4318                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4319         Resolved =>
4320                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4321         LastUpdatedBy =>
4322                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4323         LastUpdated =>
4324                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4325         Creator =>
4326                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4327         Created =>
4328                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4329         Disabled =>
4330                 {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
4331
4332  }
4333 };
4334
4335 RT::Base->_ImportOverlays();
4336
4337 1;