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