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