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