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