Upgrade to 4.0.10.
[usit-rt.git] / lib / RT / Ticket.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =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('[_1] is already a [_2] for this ticket',
1144                     $principal->Object->Name, $self->loc($args{'Type'})) );
1145     }
1146
1147
1148     my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1149                                                InsideTransaction => 1 );
1150     unless ($m_id) {
1151         $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id.": ".$m_msg);
1152
1153         return ( 0, $self->loc('Could not make [_1] a [_2] for this ticket',
1154                     $principal->Object->Name, $self->loc($args{'Type'})) );
1155     }
1156
1157     unless ( $args{'Silent'} ) {
1158         $self->_NewTransaction(
1159             Type     => 'AddWatcher',
1160             NewValue => $principal->Id,
1161             Field    => $args{'Type'}
1162         );
1163     }
1164
1165     return ( 1, $self->loc('Added [_1] as a [_2] for this ticket',
1166                 $principal->Object->Name, $self->loc($args{'Type'})) );
1167 }
1168
1169
1170
1171
1172 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1173
1174
1175 Deletes a Ticket watcher.  Takes two arguments:
1176
1177 Type  (one of Requestor,Cc,AdminCc)
1178
1179 and one of
1180
1181 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1182     OR
1183 Email (the email address of an existing wathcer)
1184
1185
1186 =cut
1187
1188
1189 sub DeleteWatcher {
1190     my $self = shift;
1191
1192     my %args = ( Type        => undef,
1193                  PrincipalId => undef,
1194                  Email       => undef,
1195                  @_ );
1196
1197     unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1198         return ( 0, $self->loc("No principal specified") );
1199     }
1200     my $principal = RT::Principal->new( $self->CurrentUser );
1201     if ( $args{'PrincipalId'} ) {
1202
1203         $principal->Load( $args{'PrincipalId'} );
1204     }
1205     else {
1206         my $user = RT::User->new( $self->CurrentUser );
1207         $user->LoadByEmail( $args{'Email'} );
1208         $principal->Load( $user->Id );
1209     }
1210
1211     # If we can't find this watcher, we need to bail.
1212     unless ( $principal->Id ) {
1213         return ( 0, $self->loc("Could not find that principal") );
1214     }
1215
1216     my $group = RT::Group->new( $self->CurrentUser );
1217     $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1218     unless ( $group->id ) {
1219         return ( 0, $self->loc("Group not found") );
1220     }
1221
1222     # Check ACLS
1223     #If the watcher we're trying to add is for the current user
1224     if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1225
1226         #  If it's an AdminCc and they don't have
1227         #   'WatchAsAdminCc' or 'ModifyTicket', bail
1228         if ( $args{'Type'} eq 'AdminCc' ) {
1229             unless (    $self->CurrentUserHasRight('ModifyTicket')
1230                      or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1231                 return ( 0, $self->loc('Permission Denied') );
1232             }
1233         }
1234
1235         #  If it's a Requestor or Cc and they don't have
1236         #   'Watch' or 'ModifyTicket', bail
1237         elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1238         {
1239             unless (    $self->CurrentUserHasRight('ModifyTicket')
1240                      or $self->CurrentUserHasRight('Watch') ) {
1241                 return ( 0, $self->loc('Permission Denied') );
1242             }
1243         }
1244         else {
1245             $RT::Logger->warning("$self -> DeleteWatcher got passed a bogus type");
1246             return ( 0,
1247                      $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1248         }
1249     }
1250
1251     # If the watcher isn't the current user
1252     # and the current user  doesn't have 'ModifyTicket' bail
1253     else {
1254         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1255             return ( 0, $self->loc("Permission Denied") );
1256         }
1257     }
1258
1259     # }}}
1260
1261     # see if this user is already a watcher.
1262
1263     unless ( $group->HasMember($principal) ) {
1264         return ( 0,
1265                  $self->loc( '[_1] is not a [_2] for this ticket',
1266                              $principal->Object->Name, $args{'Type'} ) );
1267     }
1268
1269     my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1270     unless ($m_id) {
1271         $RT::Logger->error( "Failed to delete "
1272                             . $principal->Id
1273                             . " as a member of group "
1274                             . $group->Id . ": "
1275                             . $m_msg );
1276
1277         return (0,
1278                 $self->loc(
1279                     'Could not remove [_1] as a [_2] for this ticket',
1280                     $principal->Object->Name, $args{'Type'} ) );
1281     }
1282
1283     unless ( $args{'Silent'} ) {
1284         $self->_NewTransaction( Type     => 'DelWatcher',
1285                                 OldValue => $principal->Id,
1286                                 Field    => $args{'Type'} );
1287     }
1288
1289     return ( 1,
1290              $self->loc( "[_1] is no longer a [_2] for this ticket.",
1291                          $principal->Object->Name,
1292                          $args{'Type'} ) );
1293 }
1294
1295
1296
1297
1298
1299 =head2 SquelchMailTo [EMAIL]
1300
1301 Takes an optional email address to never email about updates to this ticket.
1302
1303
1304 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1305
1306
1307 =cut
1308
1309 sub SquelchMailTo {
1310     my $self = shift;
1311     if (@_) {
1312         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1313             return ();
1314         }
1315     } else {
1316         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1317             return ();
1318         }
1319
1320     }
1321     return $self->_SquelchMailTo(@_);
1322 }
1323
1324 sub _SquelchMailTo {
1325     my $self = shift;
1326     if (@_) {
1327         my $attr = shift;
1328         $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1329             unless grep { $_->Content eq $attr }
1330                 $self->Attributes->Named('SquelchMailTo');
1331     }
1332     my @attributes = $self->Attributes->Named('SquelchMailTo');
1333     return (@attributes);
1334 }
1335
1336
1337 =head2 UnsquelchMailTo ADDRESS
1338
1339 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1340
1341 Returns a tuple of (status, message)
1342
1343 =cut
1344
1345 sub UnsquelchMailTo {
1346     my $self = shift;
1347
1348     my $address = shift;
1349     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1350         return ( 0, $self->loc("Permission Denied") );
1351     }
1352
1353     my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1354     return ($val, $msg);
1355 }
1356
1357
1358
1359 =head2 RequestorAddresses
1360
1361 B<Returns> String: All Ticket Requestor email addresses as a string.
1362
1363 =cut
1364
1365 sub RequestorAddresses {
1366     my $self = shift;
1367
1368     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1369         return undef;
1370     }
1371
1372     return ( $self->Requestors->MemberEmailAddressesAsString );
1373 }
1374
1375
1376 =head2 AdminCcAddresses
1377
1378 returns String: All Ticket AdminCc email addresses as a string
1379
1380 =cut
1381
1382 sub AdminCcAddresses {
1383     my $self = shift;
1384
1385     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1386         return undef;
1387     }
1388
1389     return ( $self->AdminCc->MemberEmailAddressesAsString )
1390
1391 }
1392
1393 =head2 CcAddresses
1394
1395 returns String: All Ticket Ccs as a string of email addresses
1396
1397 =cut
1398
1399 sub CcAddresses {
1400     my $self = shift;
1401
1402     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1403         return undef;
1404     }
1405     return ( $self->Cc->MemberEmailAddressesAsString);
1406
1407 }
1408
1409
1410
1411
1412 =head2 Requestors
1413
1414 Takes nothing.
1415 Returns this ticket's Requestors as an RT::Group object
1416
1417 =cut
1418
1419 sub Requestors {
1420     my $self = shift;
1421
1422     my $group = RT::Group->new($self->CurrentUser);
1423     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1424         $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1425     }
1426     return ($group);
1427
1428 }
1429
1430
1431
1432 =head2 Cc
1433
1434 Takes nothing.
1435 Returns an RT::Group object which contains this ticket's Ccs.
1436 If the user doesn't have "ShowTicket" permission, returns an empty group
1437
1438 =cut
1439
1440 sub Cc {
1441     my $self = shift;
1442
1443     my $group = RT::Group->new($self->CurrentUser);
1444     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1445         $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1446     }
1447     return ($group);
1448
1449 }
1450
1451
1452
1453 =head2 AdminCc
1454
1455 Takes nothing.
1456 Returns an RT::Group object which contains this ticket's AdminCcs.
1457 If the user doesn't have "ShowTicket" permission, returns an empty group
1458
1459 =cut
1460
1461 sub AdminCc {
1462     my $self = shift;
1463
1464     my $group = RT::Group->new($self->CurrentUser);
1465     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1466         $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1467     }
1468     return ($group);
1469
1470 }
1471
1472
1473
1474
1475 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1476
1477 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1478
1479 Takes a param hash with the attributes Type and either PrincipalId or Email
1480
1481 Type is one of Requestor, Cc, AdminCc and Owner
1482
1483 PrincipalId is an RT::Principal id, and Email is an email address.
1484
1485 Returns true if the specified principal (or the one corresponding to the
1486 specified address) is a member of the group Type for this ticket.
1487
1488 XX TODO: This should be Memoized. 
1489
1490 =cut
1491
1492 sub IsWatcher {
1493     my $self = shift;
1494
1495     my %args = ( Type  => 'Requestor',
1496         PrincipalId    => undef,
1497         Email          => undef,
1498         @_
1499     );
1500
1501     # Load the relevant group. 
1502     my $group = RT::Group->new($self->CurrentUser);
1503     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1504
1505     # Find the relevant principal.
1506     if (!$args{PrincipalId} && $args{Email}) {
1507         # Look up the specified user.
1508         my $user = RT::User->new($self->CurrentUser);
1509         $user->LoadByEmail($args{Email});
1510         if ($user->Id) {
1511             $args{PrincipalId} = $user->PrincipalId;
1512         }
1513         else {
1514             # A non-existent user can't be a group member.
1515             return 0;
1516         }
1517     }
1518
1519     # Ask if it has the member in question
1520     return $group->HasMember( $args{'PrincipalId'} );
1521 }
1522
1523
1524
1525 =head2 IsRequestor PRINCIPAL_ID
1526   
1527 Takes an L<RT::Principal> id.
1528
1529 Returns true if the principal is a requestor of the current ticket.
1530
1531 =cut
1532
1533 sub IsRequestor {
1534     my $self   = shift;
1535     my $person = shift;
1536
1537     return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1538
1539 };
1540
1541
1542
1543 =head2 IsCc PRINCIPAL_ID
1544
1545   Takes an RT::Principal id.
1546   Returns true if the principal is a Cc of the current ticket.
1547
1548
1549 =cut
1550
1551 sub IsCc {
1552     my $self = shift;
1553     my $cc   = shift;
1554
1555     return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1556
1557 }
1558
1559
1560
1561 =head2 IsAdminCc PRINCIPAL_ID
1562
1563   Takes an RT::Principal id.
1564   Returns true if the principal is an AdminCc of the current ticket.
1565
1566 =cut
1567
1568 sub IsAdminCc {
1569     my $self   = shift;
1570     my $person = shift;
1571
1572     return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1573
1574 }
1575
1576
1577
1578 =head2 IsOwner
1579
1580   Takes an RT::User object. Returns true if that user is this ticket's owner.
1581 returns undef otherwise
1582
1583 =cut
1584
1585 sub IsOwner {
1586     my $self   = shift;
1587     my $person = shift;
1588
1589     # no ACL check since this is used in acl decisions
1590     # unless ($self->CurrentUserHasRight('ShowTicket')) {
1591     #    return(undef);
1592     #   }    
1593
1594     #Tickets won't yet have owners when they're being created.
1595     unless ( $self->OwnerObj->id ) {
1596         return (undef);
1597     }
1598
1599     if ( $person->id == $self->OwnerObj->id ) {
1600         return (1);
1601     }
1602     else {
1603         return (undef);
1604     }
1605 }
1606
1607
1608
1609
1610
1611 =head2 TransactionAddresses
1612
1613 Returns a composite hashref of the results of L<RT::Transaction/Addresses> for
1614 all this ticket's Create, Comment or Correspond transactions. The keys are
1615 stringified email addresses. Each value is an L<Email::Address> object.
1616
1617 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.
1618
1619 =cut
1620
1621
1622 sub TransactionAddresses {
1623     my $self = shift;
1624     my $txns = $self->Transactions;
1625
1626     my %addresses = ();
1627
1628     my $attachments = RT::Attachments->new( $self->CurrentUser );
1629     $attachments->LimitByTicket( $self->id );
1630     $attachments->Columns( qw( id Headers TransactionId));
1631
1632
1633     foreach my $type (qw(Create Comment Correspond)) {
1634         $attachments->Limit( ALIAS    => $attachments->TransactionAlias,
1635                              FIELD    => 'Type',
1636                              OPERATOR => '=',
1637                              VALUE    => $type,
1638                              ENTRYAGGREGATOR => 'OR',
1639                              CASESENSITIVE   => 1
1640                            );
1641     }
1642
1643     while ( my $att = $attachments->Next ) {
1644         foreach my $addrlist ( values %{$att->Addresses } ) {
1645             foreach my $addr (@$addrlist) {
1646
1647 # Skip addresses without a phrase (things that are just raw addresses) if we have a phrase
1648                 next
1649                     if (    $addresses{ $addr->address }
1650                          && $addresses{ $addr->address }->phrase
1651                          && not $addr->phrase );
1652
1653                 # skips "comment-only" addresses
1654                 next unless ( $addr->address );
1655                 $addresses{ $addr->address } = $addr;
1656             }
1657         }
1658     }
1659
1660     return \%addresses;
1661
1662 }
1663
1664
1665
1666
1667
1668
1669 sub ValidateQueue {
1670     my $self  = shift;
1671     my $Value = shift;
1672
1673     if ( !$Value ) {
1674         $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
1675         return (1);
1676     }
1677
1678     my $QueueObj = RT::Queue->new( $self->CurrentUser );
1679     my $id       = $QueueObj->Load($Value);
1680
1681     if ($id) {
1682         return (1);
1683     }
1684     else {
1685         return (undef);
1686     }
1687 }
1688
1689
1690
1691 sub SetQueue {
1692     my $self     = shift;
1693     my $NewQueue = shift;
1694
1695     #Redundant. ACL gets checked in _Set;
1696     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1697         return ( 0, $self->loc("Permission Denied") );
1698     }
1699
1700     my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
1701     $NewQueueObj->Load($NewQueue);
1702
1703     unless ( $NewQueueObj->Id() ) {
1704         return ( 0, $self->loc("That queue does not exist") );
1705     }
1706
1707     if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
1708         return ( 0, $self->loc('That is the same value') );
1709     }
1710     unless ( $self->CurrentUser->HasRight( Right    => 'CreateTicket', Object => $NewQueueObj)) {
1711         return ( 0, $self->loc("You may not create requests in that queue.") );
1712     }
1713
1714     my $new_status;
1715     my $old_lifecycle = $self->QueueObj->Lifecycle;
1716     my $new_lifecycle = $NewQueueObj->Lifecycle;
1717     if ( $old_lifecycle->Name ne $new_lifecycle->Name ) {
1718         unless ( $old_lifecycle->HasMoveMap( $new_lifecycle ) ) {
1719             return ( 0, $self->loc("There is no mapping for statuses between these queues. Contact your system administrator.") );
1720         }
1721         $new_status = $old_lifecycle->MoveMap( $new_lifecycle )->{ $self->Status };
1722         return ( 0, $self->loc("Mapping between queues' lifecycles is incomplete. Contact your system administrator.") )
1723             unless $new_status;
1724     }
1725
1726     if ( $new_status ) {
1727         my $clone = RT::Ticket->new( RT->SystemUser );
1728         $clone->Load( $self->Id );
1729         unless ( $clone->Id ) {
1730             return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1731         }
1732
1733         my $now = RT::Date->new( $self->CurrentUser );
1734         $now->SetToNow;
1735
1736         my $old_status = $clone->Status;
1737
1738         #If we're changing the status from initial in old to not intial in new,
1739         # record that we've started
1740         if ( $old_lifecycle->IsInitial($old_status) && !$new_lifecycle->IsInitial($new_status)  && $clone->StartedObj->Unix == 0 ) {
1741             #Set the Started time to "now"
1742             $clone->_Set(
1743                 Field             => 'Started',
1744                 Value             => $now->ISO,
1745                 RecordTransaction => 0
1746             );
1747         }
1748
1749         #When we close a ticket, set the 'Resolved' attribute to now.
1750         # It's misnamed, but that's just historical.
1751         if ( $new_lifecycle->IsInactive($new_status) ) {
1752             $clone->_Set(
1753                 Field             => 'Resolved',
1754                 Value             => $now->ISO,
1755                 RecordTransaction => 0,
1756             );
1757         }
1758
1759         #Actually update the status
1760         my ($val, $msg)= $clone->_Set(
1761             Field             => 'Status',
1762             Value             => $new_status,
1763             RecordTransaction => 0,
1764         );
1765         $RT::Logger->error( 'Status change failed on queue change: '. $msg )
1766             unless $val;
1767     }
1768
1769     my ($status, $msg) = $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() );
1770
1771     if ( $status ) {
1772         # Clear the queue object cache;
1773         $self->{_queue_obj} = undef;
1774
1775         # Untake the ticket if we have no permissions in the new queue
1776         unless ( $self->OwnerObj->HasRight( Right => 'OwnTicket', Object => $NewQueueObj ) ) {
1777             my $clone = RT::Ticket->new( RT->SystemUser );
1778             $clone->Load( $self->Id );
1779             unless ( $clone->Id ) {
1780                 return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
1781             }
1782             my ($status, $msg) = $clone->SetOwner( RT->Nobody->Id, 'Force' );
1783             $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
1784         }
1785
1786         # On queue change, change queue for reminders too
1787         my $reminder_collection = $self->Reminders->Collection;
1788         while ( my $reminder = $reminder_collection->Next ) {
1789             my ($status, $msg) = $reminder->SetQueue($NewQueue);
1790             $RT::Logger->error('Queue change failed for reminder #' . $reminder->Id . ': ' . $msg) unless $status;
1791         }
1792     }
1793
1794     return ($status, $msg);
1795 }
1796
1797
1798
1799 =head2 QueueObj
1800
1801 Takes nothing. returns this ticket's queue object
1802
1803 =cut
1804
1805 sub QueueObj {
1806     my $self = shift;
1807
1808     if(!$self->{_queue_obj} || ! $self->{_queue_obj}->id) {
1809
1810         $self->{_queue_obj} = RT::Queue->new( $self->CurrentUser );
1811
1812         #We call __Value so that we can avoid the ACL decision and some deep recursion
1813         my ($result) = $self->{_queue_obj}->Load( $self->__Value('Queue') );
1814     }
1815     return ($self->{_queue_obj});
1816 }
1817
1818 =head2 SubjectTag
1819
1820 Takes nothing. Returns SubjectTag for this ticket. Includes
1821 queue's subject tag or rtname if that is not set, ticket
1822 id and braces, for example:
1823
1824     [support.example.com #123456]
1825
1826 =cut
1827
1828 sub SubjectTag {
1829     my $self = shift;
1830     return
1831         '['
1832         . ($self->QueueObj->SubjectTag || RT->Config->Get('rtname'))
1833         .' #'. $self->id
1834         .']'
1835     ;
1836 }
1837
1838
1839 =head2 DueObj
1840
1841   Returns an RT::Date object containing this ticket's due date
1842
1843 =cut
1844
1845 sub DueObj {
1846     my $self = shift;
1847
1848     my $time = RT::Date->new( $self->CurrentUser );
1849
1850     # -1 is RT::Date slang for never
1851     if ( my $due = $self->Due ) {
1852         $time->Set( Format => 'sql', Value => $due );
1853     }
1854     else {
1855         $time->Set( Format => 'unix', Value => -1 );
1856     }
1857
1858     return $time;
1859 }
1860
1861
1862
1863 =head2 DueAsString
1864
1865 Returns this ticket's due date as a human readable string
1866
1867 =cut
1868
1869 sub DueAsString {
1870     my $self = shift;
1871     return $self->DueObj->AsString();
1872 }
1873
1874
1875
1876 =head2 ResolvedObj
1877
1878   Returns an RT::Date object of this ticket's 'resolved' time.
1879
1880 =cut
1881
1882 sub ResolvedObj {
1883     my $self = shift;
1884
1885     my $time = RT::Date->new( $self->CurrentUser );
1886     $time->Set( Format => 'sql', Value => $self->Resolved );
1887     return $time;
1888 }
1889
1890
1891 =head2 FirstActiveStatus
1892
1893 Returns the first active status that the ticket could transition to,
1894 according to its current Queue's lifecycle.  May return undef if there
1895 is no such possible status to transition to, or we are already in it.
1896 This is used in L<RT::Action::AutoOpen>, for instance.
1897
1898 =cut
1899
1900 sub FirstActiveStatus {
1901     my $self = shift;
1902
1903     my $lifecycle = $self->QueueObj->Lifecycle;
1904     my $status = $self->Status;
1905     my @active = $lifecycle->Active;
1906     # no change if no active statuses in the lifecycle
1907     return undef unless @active;
1908
1909     # no change if the ticket is already has first status from the list of active
1910     return undef if lc $status eq lc $active[0];
1911
1912     my ($next) = grep $lifecycle->IsActive($_), $lifecycle->Transitions($status);
1913     return $next;
1914 }
1915
1916 =head2 FirstInactiveStatus
1917
1918 Returns the first inactive status that the ticket could transition to,
1919 according to its current Queue's lifecycle.  May return undef if there
1920 is no such possible status to transition to, or we are already in it.
1921 This is used in resolve action in UnsafeEmailCommands, for instance.
1922
1923 =cut
1924
1925 sub FirstInactiveStatus {
1926     my $self = shift;
1927
1928     my $lifecycle = $self->QueueObj->Lifecycle;
1929     my $status = $self->Status;
1930     my @inactive = $lifecycle->Inactive;
1931     # no change if no inactive statuses in the lifecycle
1932     return undef unless @inactive;
1933
1934     # no change if the ticket is already has first status from the list of inactive
1935     return undef if lc $status eq lc $inactive[0];
1936
1937     my ($next) = grep $lifecycle->IsInactive($_), $lifecycle->Transitions($status);
1938     return $next;
1939 }
1940
1941 =head2 SetStarted
1942
1943 Takes a date in ISO format or undef
1944 Returns a transaction id and a message
1945 The client calls "Start" to note that the project was started on the date in $date.
1946 A null date means "now"
1947
1948 =cut
1949
1950 sub SetStarted {
1951     my $self = shift;
1952     my $time = shift || 0;
1953
1954     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1955         return ( 0, $self->loc("Permission Denied") );
1956     }
1957
1958     #We create a date object to catch date weirdness
1959     my $time_obj = RT::Date->new( $self->CurrentUser() );
1960     if ( $time ) {
1961         $time_obj->Set( Format => 'ISO', Value => $time );
1962     }
1963     else {
1964         $time_obj->SetToNow();
1965     }
1966
1967     # We need $TicketAsSystem, in case the current user doesn't have
1968     # ShowTicket
1969     my $TicketAsSystem = RT::Ticket->new(RT->SystemUser);
1970     $TicketAsSystem->Load( $self->Id );
1971     # Now that we're starting, open this ticket
1972     # TODO: do we really want to force this as policy? it should be a scrip
1973     my $next = $TicketAsSystem->FirstActiveStatus;
1974
1975     $self->SetStatus( $next ) if defined $next;
1976
1977     return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
1978
1979 }
1980
1981
1982
1983 =head2 StartedObj
1984
1985   Returns an RT::Date object which contains this ticket's 
1986 'Started' time.
1987
1988 =cut
1989
1990 sub StartedObj {
1991     my $self = shift;
1992
1993     my $time = RT::Date->new( $self->CurrentUser );
1994     $time->Set( Format => 'sql', Value => $self->Started );
1995     return $time;
1996 }
1997
1998
1999
2000 =head2 StartsObj
2001
2002   Returns an RT::Date object which contains this ticket's 
2003 'Starts' time.
2004
2005 =cut
2006
2007 sub StartsObj {
2008     my $self = shift;
2009
2010     my $time = RT::Date->new( $self->CurrentUser );
2011     $time->Set( Format => 'sql', Value => $self->Starts );
2012     return $time;
2013 }
2014
2015
2016
2017 =head2 ToldObj
2018
2019   Returns an RT::Date object which contains this ticket's 
2020 'Told' time.
2021
2022 =cut
2023
2024 sub ToldObj {
2025     my $self = shift;
2026
2027     my $time = RT::Date->new( $self->CurrentUser );
2028     $time->Set( Format => 'sql', Value => $self->Told );
2029     return $time;
2030 }
2031
2032
2033
2034 =head2 ToldAsString
2035
2036 A convenience method that returns ToldObj->AsString
2037
2038 TODO: This should be deprecated
2039
2040 =cut
2041
2042 sub ToldAsString {
2043     my $self = shift;
2044     if ( $self->Told ) {
2045         return $self->ToldObj->AsString();
2046     }
2047     else {
2048         return ("Never");
2049     }
2050 }
2051
2052
2053
2054 =head2 TimeWorkedAsString
2055
2056 Returns the amount of time worked on this ticket as a Text String
2057
2058 =cut
2059
2060 sub TimeWorkedAsString {
2061     my $self = shift;
2062     my $value = $self->TimeWorked;
2063
2064     # return the # of minutes worked turned into seconds and written as
2065     # a simple text string, this is not really a date object, but if we
2066     # diff a number of seconds vs the epoch, we'll get a nice description
2067     # of time worked.
2068     return "" unless $value;
2069     return RT::Date->new( $self->CurrentUser )
2070         ->DurationAsString( $value * 60 );
2071 }
2072
2073
2074
2075 =head2  TimeLeftAsString
2076
2077 Returns the amount of time left on this ticket as a Text String
2078
2079 =cut
2080
2081 sub TimeLeftAsString {
2082     my $self = shift;
2083     my $value = $self->TimeLeft;
2084     return "" unless $value;
2085     return RT::Date->new( $self->CurrentUser )
2086         ->DurationAsString( $value * 60 );
2087 }
2088
2089
2090
2091
2092 =head2 Comment
2093
2094 Comment on this ticket.
2095 Takes a hash with the following attributes:
2096 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2097 comment.
2098
2099 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2100
2101 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2102 They will, however, be prepared and you'll be able to access them through the TransactionObj
2103
2104 Returns: Transaction id, Error Message, Transaction Object
2105 (note the different order from Create()!)
2106
2107 =cut
2108
2109 sub Comment {
2110     my $self = shift;
2111
2112     my %args = ( CcMessageTo  => undef,
2113                  BccMessageTo => undef,
2114                  MIMEObj      => undef,
2115                  Content      => undef,
2116                  TimeTaken => 0,
2117                  DryRun     => 0, 
2118                  @_ );
2119
2120     unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
2121              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2122         return ( 0, $self->loc("Permission Denied"), undef );
2123     }
2124     $args{'NoteType'} = 'Comment';
2125
2126     $RT::Handle->BeginTransaction();
2127     if ($args{'DryRun'}) {
2128         $args{'CommitScrips'} = 0;
2129     }
2130
2131     my @results = $self->_RecordNote(%args);
2132     if ($args{'DryRun'}) {
2133         $RT::Handle->Rollback();
2134     } else {
2135         $RT::Handle->Commit();
2136     }
2137
2138     return(@results);
2139 }
2140
2141
2142 =head2 Correspond
2143
2144 Correspond on this ticket.
2145 Takes a hashref with the following attributes:
2146
2147
2148 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2149
2150 if there's no MIMEObj, Content is used to build a MIME::Entity object
2151
2152 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2153 They will, however, be prepared and you'll be able to access them through the TransactionObj
2154
2155 Returns: Transaction id, Error Message, Transaction Object
2156 (note the different order from Create()!)
2157
2158
2159 =cut
2160
2161 sub Correspond {
2162     my $self = shift;
2163     my %args = ( CcMessageTo  => undef,
2164                  BccMessageTo => undef,
2165                  MIMEObj      => undef,
2166                  Content      => undef,
2167                  TimeTaken    => 0,
2168                  @_ );
2169
2170     unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
2171              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2172         return ( 0, $self->loc("Permission Denied"), undef );
2173     }
2174     $args{'NoteType'} = 'Correspond';
2175
2176     $RT::Handle->BeginTransaction();
2177     if ($args{'DryRun'}) {
2178         $args{'CommitScrips'} = 0;
2179     }
2180
2181     my @results = $self->_RecordNote(%args);
2182
2183     unless ( $results[0] ) {
2184         $RT::Handle->Rollback();
2185         return @results;
2186     }
2187
2188     #Set the last told date to now if this isn't mail from the requestor.
2189     #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2190     unless ( $self->IsRequestor($self->CurrentUser->id) ) {
2191         my %squelch;
2192         $squelch{$_}++ for map {$_->Content} $self->SquelchMailTo, $results[2]->SquelchMailTo;
2193         $self->_SetTold
2194             if grep {not $squelch{$_}} $self->Requestors->MemberEmailAddresses;
2195     }
2196
2197     if ($args{'DryRun'}) {
2198         $RT::Handle->Rollback();
2199     } else {
2200         $RT::Handle->Commit();
2201     }
2202
2203     return (@results);
2204
2205 }
2206
2207
2208
2209 =head2 _RecordNote
2210
2211 the meat of both comment and correspond. 
2212
2213 Performs no access control checks. hence, dangerous.
2214
2215 =cut
2216
2217 sub _RecordNote {
2218     my $self = shift;
2219     my %args = ( 
2220         CcMessageTo  => undef,
2221         BccMessageTo => undef,
2222         Encrypt      => undef,
2223         Sign         => undef,
2224         MIMEObj      => undef,
2225         Content      => undef,
2226         NoteType     => 'Correspond',
2227         TimeTaken    => 0,
2228         CommitScrips => 1,
2229         SquelchMailTo => undef,
2230         @_
2231     );
2232
2233     unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2234         return ( 0, $self->loc("No message attached"), undef );
2235     }
2236
2237     unless ( $args{'MIMEObj'} ) {
2238         $args{'MIMEObj'} = MIME::Entity->build(
2239             Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2240         );
2241     }
2242
2243     $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
2244         unless $args{'MIMEObj'}->head->get('X-RT-Interface');
2245
2246     # convert text parts into utf-8
2247     RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2248
2249     # If we've been passed in CcMessageTo and BccMessageTo fields,
2250     # add them to the mime object for passing on to the transaction handler
2251     # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2252     # RT-Send-Bcc: headers
2253
2254
2255     foreach my $type (qw/Cc Bcc/) {
2256         if ( defined $args{ $type . 'MessageTo' } ) {
2257
2258             my $addresses = join ', ', (
2259                 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2260                     Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2261             $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2262         }
2263     }
2264
2265     foreach my $argument (qw(Encrypt Sign)) {
2266         $args{'MIMEObj'}->head->replace(
2267             "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2268         ) if defined $args{ $argument };
2269     }
2270
2271     # If this is from an external source, we need to come up with its
2272     # internal Message-ID now, so all emails sent because of this
2273     # message have a common Message-ID
2274     my $org = RT->Config->Get('Organization');
2275     my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2276     unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2277         $args{'MIMEObj'}->head->set(
2278             'RT-Message-ID' => Encode::encode_utf8(
2279                 RT::Interface::Email::GenMessageId( Ticket => $self )
2280             )
2281         );
2282     }
2283
2284     #Record the correspondence (write the transaction)
2285     my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2286              Type => $args{'NoteType'},
2287              Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2288              TimeTaken => $args{'TimeTaken'},
2289              MIMEObj   => $args{'MIMEObj'}, 
2290              CommitScrips => $args{'CommitScrips'},
2291              SquelchMailTo => $args{'SquelchMailTo'},
2292     );
2293
2294     unless ($Trans) {
2295         $RT::Logger->err("$self couldn't init a transaction $msg");
2296         return ( $Trans, $self->loc("Message could not be recorded"), undef );
2297     }
2298
2299     return ( $Trans, $self->loc("Message recorded"), $TransObj );
2300 }
2301
2302
2303 =head2 DryRun
2304
2305 Builds a MIME object from the given C<UpdateSubject> and
2306 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2307 C<< DryRun => 1 >>, and returns the transaction so produced.
2308
2309 =cut
2310
2311 sub DryRun {
2312     my $self = shift;
2313     my %args = @_;
2314     my $action;
2315     if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2316         $action = 'Correspond';
2317     } else {
2318         $action = 'Comment';
2319     }
2320
2321     my $Message = MIME::Entity->build(
2322         Type    => 'text/plain',
2323         Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2324         Charset => 'UTF-8',
2325         Data    => $args{'UpdateContent'} || "",
2326     );
2327
2328     my ( $Transaction, $Description, $Object ) = $self->$action(
2329         CcMessageTo  => $args{'UpdateCc'},
2330         BccMessageTo => $args{'UpdateBcc'},
2331         MIMEObj      => $Message,
2332         TimeTaken    => $args{'UpdateTimeWorked'},
2333         DryRun       => 1,
2334     );
2335     unless ( $Transaction ) {
2336         $RT::Logger->error("Couldn't fire '$action' action: $Description");
2337     }
2338
2339     return $Object;
2340 }
2341
2342 =head2 DryRunCreate
2343
2344 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2345 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2346 the resulting L<RT::Transaction>.
2347
2348 =cut
2349
2350 sub DryRunCreate {
2351     my $self = shift;
2352     my %args = @_;
2353     my $Message = MIME::Entity->build(
2354         Type    => 'text/plain',
2355         Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2356         (defined $args{'Cc'} ?
2357              ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2358         Charset => 'UTF-8',
2359         Data    => $args{'Content'} || "",
2360     );
2361
2362     my ( $Transaction, $Object, $Description ) = $self->Create(
2363         Type            => $args{'Type'} || 'ticket',
2364         Queue           => $args{'Queue'},
2365         Owner           => $args{'Owner'},
2366         Requestor       => $args{'Requestors'},
2367         Cc              => $args{'Cc'},
2368         AdminCc         => $args{'AdminCc'},
2369         InitialPriority => $args{'InitialPriority'},
2370         FinalPriority   => $args{'FinalPriority'},
2371         TimeLeft        => $args{'TimeLeft'},
2372         TimeEstimated   => $args{'TimeEstimated'},
2373         TimeWorked      => $args{'TimeWorked'},
2374         Subject         => $args{'Subject'},
2375         Status          => $args{'Status'},
2376         MIMEObj         => $Message,
2377         DryRun          => 1,
2378     );
2379     unless ( $Transaction ) {
2380         $RT::Logger->error("Couldn't fire Create action: $Description");
2381     }
2382
2383     return $Object;
2384 }
2385
2386
2387
2388 sub _Links {
2389     my $self = shift;
2390
2391     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2392     #tobias meant by $f
2393     my $field = shift;
2394     my $type  = shift || "";
2395
2396     my $cache_key = "$field$type";
2397     return $self->{ $cache_key } if $self->{ $cache_key };
2398
2399     my $links = $self->{ $cache_key }
2400               = RT::Links->new( $self->CurrentUser );
2401     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2402         $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2403         return $links;
2404     }
2405
2406     # Maybe this ticket is a merge ticket
2407     my $limit_on = 'Local'. $field;
2408     # at least to myself
2409     $links->Limit(
2410         FIELD           => $limit_on,
2411         VALUE           => $self->id,
2412         ENTRYAGGREGATOR => 'OR',
2413     );
2414     $links->Limit(
2415         FIELD           => $limit_on,
2416         VALUE           => $_,
2417         ENTRYAGGREGATOR => 'OR',
2418     ) foreach $self->Merged;
2419     $links->Limit(
2420         FIELD => 'Type',
2421         VALUE => $type,
2422     ) if $type;
2423
2424     return $links;
2425 }
2426
2427
2428
2429 =head2 DeleteLink
2430
2431 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2432 SilentBase and SilentTarget. Either Base or Target must be null.
2433 The null value will be replaced with this ticket's id.
2434
2435 If Silent is true then no transaction would be recorded, in other
2436 case you can control creation of transactions on both base and
2437 target with SilentBase and SilentTarget respectively. By default
2438 both transactions are created.
2439
2440 =cut 
2441
2442 sub DeleteLink {
2443     my $self = shift;
2444     my %args = (
2445         Base   => undef,
2446         Target => undef,
2447         Type   => undef,
2448         Silent => undef,
2449         SilentBase   => undef,
2450         SilentTarget => undef,
2451         @_
2452     );
2453
2454     unless ( $args{'Target'} || $args{'Base'} ) {
2455         $RT::Logger->error("Base or Target must be specified");
2456         return ( 0, $self->loc('Either base or target must be specified') );
2457     }
2458
2459     #check acls
2460     my $right = 0;
2461     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2462     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2463         return ( 0, $self->loc("Permission Denied") );
2464     }
2465
2466     # If the other URI is an RT::Ticket, we want to make sure the user
2467     # can modify it too...
2468     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2469     return (0, $msg) unless $status;
2470     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2471         $right++;
2472     }
2473     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2474          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2475     {
2476         return ( 0, $self->loc("Permission Denied") );
2477     }
2478
2479     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2480     return ( 0, $Msg ) unless $val;
2481
2482     return ( $val, $Msg ) if $args{'Silent'};
2483
2484     my ($direction, $remote_link);
2485
2486     if ( $args{'Base'} ) {
2487         $remote_link = $args{'Base'};
2488         $direction = 'Target';
2489     }
2490     elsif ( $args{'Target'} ) {
2491         $remote_link = $args{'Target'};
2492         $direction = 'Base';
2493     } 
2494
2495     my $remote_uri = RT::URI->new( $self->CurrentUser );
2496     $remote_uri->FromURI( $remote_link );
2497
2498     unless ( $args{ 'Silent'. $direction } ) {
2499         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2500             Type      => 'DeleteLink',
2501             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2502             OldValue  => $remote_uri->URI || $remote_link,
2503             TimeTaken => 0
2504         );
2505         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2506     }
2507
2508     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2509         my $OtherObj = $remote_uri->Object;
2510         my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2511             Type           => 'DeleteLink',
2512             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2513                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2514             OldValue       => $self->URI,
2515             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2516             TimeTaken      => 0,
2517         );
2518         $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2519     }
2520
2521     return ( $val, $Msg );
2522 }
2523
2524
2525
2526 =head2 AddLink
2527
2528 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2529
2530 If Silent is true then no transaction would be recorded, in other
2531 case you can control creation of transactions on both base and
2532 target with SilentBase and SilentTarget respectively. By default
2533 both transactions are created.
2534
2535 =cut
2536
2537 sub AddLink {
2538     my $self = shift;
2539     my %args = ( Target       => '',
2540                  Base         => '',
2541                  Type         => '',
2542                  Silent       => undef,
2543                  SilentBase   => undef,
2544                  SilentTarget => undef,
2545                  @_ );
2546
2547     unless ( $args{'Target'} || $args{'Base'} ) {
2548         $RT::Logger->error("Base or Target must be specified");
2549         return ( 0, $self->loc('Either base or target must be specified') );
2550     }
2551
2552     my $right = 0;
2553     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2554     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2555         return ( 0, $self->loc("Permission Denied") );
2556     }
2557
2558     # If the other URI is an RT::Ticket, we want to make sure the user
2559     # can modify it too...
2560     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2561     return (0, $msg) unless $status;
2562     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2563         $right++;
2564     }
2565     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2566          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2567     {
2568         return ( 0, $self->loc("Permission Denied") );
2569     }
2570
2571     return ( 0, "Can't link to a deleted ticket" )
2572       if $other_ticket && $other_ticket->Status eq 'deleted';
2573
2574     return $self->_AddLink(%args);
2575 }
2576
2577 sub __GetTicketFromURI {
2578     my $self = shift;
2579     my %args = ( URI => '', @_ );
2580
2581     # If the other URI is an RT::Ticket, we want to make sure the user
2582     # can modify it too...
2583     my $uri_obj = RT::URI->new( $self->CurrentUser );
2584     $uri_obj->FromURI( $args{'URI'} );
2585
2586     unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2587         my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2588         $RT::Logger->warning( $msg );
2589         return( 0, $msg );
2590     }
2591     my $obj = $uri_obj->Resolver->Object;
2592     unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2593         return (1, 'Found not a ticket', undef);
2594     }
2595     return (1, 'Found ticket', $obj);
2596 }
2597
2598 =head2 _AddLink  
2599
2600 Private non-acled variant of AddLink so that links can be added during create.
2601
2602 =cut
2603
2604 sub _AddLink {
2605     my $self = shift;
2606     my %args = ( Target       => '',
2607                  Base         => '',
2608                  Type         => '',
2609                  Silent       => undef,
2610                  SilentBase   => undef,
2611                  SilentTarget => undef,
2612                  @_ );
2613
2614     my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2615     return ($val, $msg) if !$val || $exist;
2616     return ($val, $msg) if $args{'Silent'};
2617
2618     my ($direction, $remote_link);
2619     if ( $args{'Target'} ) {
2620         $remote_link  = $args{'Target'};
2621         $direction    = 'Base';
2622     } elsif ( $args{'Base'} ) {
2623         $remote_link  = $args{'Base'};
2624         $direction    = 'Target';
2625     }
2626
2627     my $remote_uri = RT::URI->new( $self->CurrentUser );
2628     $remote_uri->FromURI( $remote_link );
2629
2630     unless ( $args{ 'Silent'. $direction } ) {
2631         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2632             Type      => 'AddLink',
2633             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2634             NewValue  =>  $remote_uri->URI || $remote_link,
2635             TimeTaken => 0
2636         );
2637         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2638     }
2639
2640     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2641         my $OtherObj = $remote_uri->Object;
2642         my ( $val, $msg ) = $OtherObj->_NewTransaction(
2643             Type           => 'AddLink',
2644             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2645                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2646             NewValue       => $self->URI,
2647             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2648             TimeTaken      => 0,
2649         );
2650         $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2651     }
2652
2653     return ( $val, $msg );
2654 }
2655
2656
2657
2658
2659 =head2 MergeInto
2660
2661 MergeInto take the id of the ticket to merge this ticket into.
2662
2663 =cut
2664
2665 sub MergeInto {
2666     my $self      = shift;
2667     my $ticket_id = shift;
2668
2669     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2670         return ( 0, $self->loc("Permission Denied") );
2671     }
2672
2673     # Load up the new ticket.
2674     my $MergeInto = RT::Ticket->new($self->CurrentUser);
2675     $MergeInto->Load($ticket_id);
2676
2677     # make sure it exists.
2678     unless ( $MergeInto->Id ) {
2679         return ( 0, $self->loc("New ticket doesn't exist") );
2680     }
2681
2682     # Make sure the current user can modify the new ticket.
2683     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2684         return ( 0, $self->loc("Permission Denied") );
2685     }
2686
2687     delete $MERGE_CACHE{'effective'}{ $self->id };
2688     delete @{ $MERGE_CACHE{'merged'} }{
2689         $ticket_id, $MergeInto->id, $self->id
2690     };
2691
2692     $RT::Handle->BeginTransaction();
2693
2694     $self->_MergeInto( $MergeInto );
2695
2696     $RT::Handle->Commit();
2697
2698     return ( 1, $self->loc("Merge Successful") );
2699 }
2700
2701 sub _MergeInto {
2702     my $self      = shift;
2703     my $MergeInto = shift;
2704
2705
2706     # We use EffectiveId here even though it duplicates information from
2707     # the links table becasue of the massive performance hit we'd take
2708     # by trying to do a separate database query for merge info everytime 
2709     # loaded a ticket. 
2710
2711     #update this ticket's effective id to the new ticket's id.
2712     my ( $id_val, $id_msg ) = $self->__Set(
2713         Field => 'EffectiveId',
2714         Value => $MergeInto->Id()
2715     );
2716
2717     unless ($id_val) {
2718         $RT::Handle->Rollback();
2719         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2720     }
2721
2722
2723     my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2724     if ( $force_status && $force_status ne $self->__Value('Status') ) {
2725         my ( $status_val, $status_msg )
2726             = $self->__Set( Field => 'Status', Value => $force_status );
2727
2728         unless ($status_val) {
2729             $RT::Handle->Rollback();
2730             $RT::Logger->error(
2731                 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2732             );
2733             return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2734         }
2735     }
2736
2737     # update all the links that point to that old ticket
2738     my $old_links_to = RT::Links->new($self->CurrentUser);
2739     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2740
2741     my %old_seen;
2742     while (my $link = $old_links_to->Next) {
2743         if (exists $old_seen{$link->Base."-".$link->Type}) {
2744             $link->Delete;
2745         }   
2746         elsif ($link->Base eq $MergeInto->URI) {
2747             $link->Delete;
2748         } else {
2749             # First, make sure the link doesn't already exist. then move it over.
2750             my $tmp = RT::Link->new(RT->SystemUser);
2751             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2752             if ($tmp->id)   {
2753                     $link->Delete;
2754             } else { 
2755                 $link->SetTarget($MergeInto->URI);
2756                 $link->SetLocalTarget($MergeInto->id);
2757             }
2758             $old_seen{$link->Base."-".$link->Type} =1;
2759         }
2760
2761     }
2762
2763     my $old_links_from = RT::Links->new($self->CurrentUser);
2764     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2765
2766     while (my $link = $old_links_from->Next) {
2767         if (exists $old_seen{$link->Type."-".$link->Target}) {
2768             $link->Delete;
2769         }   
2770         if ($link->Target eq $MergeInto->URI) {
2771             $link->Delete;
2772         } else {
2773             # First, make sure the link doesn't already exist. then move it over.
2774             my $tmp = RT::Link->new(RT->SystemUser);
2775             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2776             if ($tmp->id)   {
2777                     $link->Delete;
2778             } else { 
2779                 $link->SetBase($MergeInto->URI);
2780                 $link->SetLocalBase($MergeInto->id);
2781                 $old_seen{$link->Type."-".$link->Target} =1;
2782             }
2783         }
2784
2785     }
2786
2787     # Update time fields
2788     foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2789
2790         my $mutator = "Set$type";
2791         $MergeInto->$mutator(
2792             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2793
2794     }
2795 #add all of this ticket's watchers to that ticket.
2796     foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2797
2798         my $people = $self->$watcher_type->MembersObj;
2799         my $addwatcher_type =  $watcher_type;
2800         $addwatcher_type  =~ s/s$//;
2801
2802         while ( my $watcher = $people->Next ) {
2803             
2804            my ($val, $msg) =  $MergeInto->_AddWatcher(
2805                 Type        => $addwatcher_type,
2806                 Silent => 1,
2807                 PrincipalId => $watcher->MemberId
2808             );
2809             unless ($val) {
2810                 $RT::Logger->debug($msg);
2811             }
2812     }
2813
2814     }
2815
2816     #find all of the tickets that were merged into this ticket. 
2817     my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2818     $old_mergees->Limit(
2819         FIELD    => 'EffectiveId',
2820         OPERATOR => '=',
2821         VALUE    => $self->Id
2822     );
2823
2824     #   update their EffectiveId fields to the new ticket's id
2825     while ( my $ticket = $old_mergees->Next() ) {
2826         my ( $val, $msg ) = $ticket->__Set(
2827             Field => 'EffectiveId',
2828             Value => $MergeInto->Id()
2829         );
2830     }
2831
2832     #make a new link: this ticket is merged into that other ticket.
2833     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2834
2835     $MergeInto->_SetLastUpdated;    
2836 }
2837
2838 =head2 Merged
2839
2840 Returns list of tickets' ids that's been merged into this ticket.
2841
2842 =cut
2843
2844 sub Merged {
2845     my $self = shift;
2846
2847     my $id = $self->id;
2848     return @{ $MERGE_CACHE{'merged'}{ $id } }
2849         if $MERGE_CACHE{'merged'}{ $id };
2850
2851     my $mergees = RT::Tickets->new( $self->CurrentUser );
2852     $mergees->Limit(
2853         FIELD    => 'EffectiveId',
2854         VALUE    => $id,
2855     );
2856     $mergees->Limit(
2857         FIELD    => 'id',
2858         OPERATOR => '!=',
2859         VALUE    => $id,
2860     );
2861     return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2862         = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2863 }
2864
2865
2866
2867
2868
2869 =head2 OwnerObj
2870
2871 Takes nothing and returns an RT::User object of 
2872 this ticket's owner
2873
2874 =cut
2875
2876 sub OwnerObj {
2877     my $self = shift;
2878
2879     #If this gets ACLed, we lose on a rights check in User.pm and
2880     #get deep recursion. if we need ACLs here, we need
2881     #an equiv without ACLs
2882
2883     my $owner = RT::User->new( $self->CurrentUser );
2884     $owner->Load( $self->__Value('Owner') );
2885
2886     #Return the owner object
2887     return ($owner);
2888 }
2889
2890
2891
2892 =head2 OwnerAsString
2893
2894 Returns the owner's email address
2895
2896 =cut
2897
2898 sub OwnerAsString {
2899     my $self = shift;
2900     return ( $self->OwnerObj->EmailAddress );
2901
2902 }
2903
2904
2905
2906 =head2 SetOwner
2907
2908 Takes two arguments:
2909      the Id or Name of the owner 
2910 and  (optionally) the type of the SetOwner Transaction. It defaults
2911 to 'Set'.  'Steal' is also a valid option.
2912
2913
2914 =cut
2915
2916 sub SetOwner {
2917     my $self     = shift;
2918     my $NewOwner = shift;
2919     my $Type     = shift || "Set";
2920
2921     $RT::Handle->BeginTransaction();
2922
2923     $self->_SetLastUpdated(); # lock the ticket
2924     $self->Load( $self->id ); # in case $self changed while waiting for lock
2925
2926     my $OldOwnerObj = $self->OwnerObj;
2927
2928     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
2929     $NewOwnerObj->Load( $NewOwner );
2930     unless ( $NewOwnerObj->Id ) {
2931         $RT::Handle->Rollback();
2932         return ( 0, $self->loc("That user does not exist") );
2933     }
2934
2935
2936     # must have ModifyTicket rights
2937     # or TakeTicket/StealTicket and $NewOwner is self
2938     # see if it's a take
2939     if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
2940         unless (    $self->CurrentUserHasRight('ModifyTicket')
2941                  || $self->CurrentUserHasRight('TakeTicket') ) {
2942             $RT::Handle->Rollback();
2943             return ( 0, $self->loc("Permission Denied") );
2944         }
2945     }
2946
2947     # see if it's a steal
2948     elsif (    $OldOwnerObj->Id != RT->Nobody->Id
2949             && $OldOwnerObj->Id != $self->CurrentUser->id ) {
2950
2951         unless (    $self->CurrentUserHasRight('ModifyTicket')
2952                  || $self->CurrentUserHasRight('StealTicket') ) {
2953             $RT::Handle->Rollback();
2954             return ( 0, $self->loc("Permission Denied") );
2955         }
2956     }
2957     else {
2958         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2959             $RT::Handle->Rollback();
2960             return ( 0, $self->loc("Permission Denied") );
2961         }
2962     }
2963
2964     # If we're not stealing and the ticket has an owner and it's not
2965     # the current user
2966     if ( $Type ne 'Steal' and $Type ne 'Force'
2967          and $OldOwnerObj->Id != RT->Nobody->Id
2968          and $OldOwnerObj->Id != $self->CurrentUser->Id )
2969     {
2970         $RT::Handle->Rollback();
2971         return ( 0, $self->loc("You can only take tickets that are unowned") )
2972             if $NewOwnerObj->id == $self->CurrentUser->id;
2973         return (
2974             0,
2975             $self->loc("You can only reassign tickets that you own or that are unowned" )
2976         );
2977     }
2978
2979     #If we've specified a new owner and that user can't modify the ticket
2980     elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
2981         $RT::Handle->Rollback();
2982         return ( 0, $self->loc("That user may not own tickets in that queue") );
2983     }
2984
2985     # If the ticket has an owner and it's the new owner, we don't need
2986     # To do anything
2987     elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
2988         $RT::Handle->Rollback();
2989         return ( 0, $self->loc("That user already owns that ticket") );
2990     }
2991
2992     # Delete the owner in the owner group, then add a new one
2993     # TODO: is this safe? it's not how we really want the API to work
2994     # for most things, but it's fast.
2995     my ( $del_id, $del_msg );
2996     for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
2997         ($del_id, $del_msg) = $owner->Delete();
2998         last unless ($del_id);
2999     }
3000
3001     unless ($del_id) {
3002         $RT::Handle->Rollback();
3003         return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3004     }
3005
3006     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3007                                        PrincipalId => $NewOwnerObj->PrincipalId,
3008                                        InsideTransaction => 1 );
3009     unless ($add_id) {
3010         $RT::Handle->Rollback();
3011         return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3012     }
3013
3014     # We call set twice with slightly different arguments, so
3015     # as to not have an SQL transaction span two RT transactions
3016
3017     my ( $val, $msg ) = $self->_Set(
3018                       Field             => 'Owner',
3019                       RecordTransaction => 0,
3020                       Value             => $NewOwnerObj->Id,
3021                       TimeTaken         => 0,
3022                       TransactionType   => 'Set',
3023                       CheckACL          => 0,                  # don't check acl
3024     );
3025
3026     unless ($val) {
3027         $RT::Handle->Rollback;
3028         return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3029     }
3030
3031     ($val, $msg) = $self->_NewTransaction(
3032         Type      => 'Set',
3033         Field     => 'Owner',
3034         NewValue  => $NewOwnerObj->Id,
3035         OldValue  => $OldOwnerObj->Id,
3036         TimeTaken => 0,
3037     );
3038
3039     if ( $val ) {
3040         $msg = $self->loc( "Owner changed from [_1] to [_2]",
3041                            $OldOwnerObj->Name, $NewOwnerObj->Name );
3042     }
3043     else {
3044         $RT::Handle->Rollback();
3045         return ( 0, $msg );
3046     }
3047
3048     $RT::Handle->Commit();
3049
3050     return ( $val, $msg );
3051 }
3052
3053
3054
3055 =head2 Take
3056
3057 A convenince method to set the ticket's owner to the current user
3058
3059 =cut
3060
3061 sub Take {
3062     my $self = shift;
3063     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3064 }
3065
3066
3067
3068 =head2 Untake
3069
3070 Convenience method to set the owner to 'nobody' if the current user is the owner.
3071
3072 =cut
3073
3074 sub Untake {
3075     my $self = shift;
3076     return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3077 }
3078
3079
3080
3081 =head2 Steal
3082
3083 A convenience method to change the owner of the current ticket to the
3084 current user. Even if it's owned by another user.
3085
3086 =cut
3087
3088 sub Steal {
3089     my $self = shift;
3090
3091     if ( $self->IsOwner( $self->CurrentUser ) ) {
3092         return ( 0, $self->loc("You already own this ticket") );
3093     }
3094     else {
3095         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3096
3097     }
3098
3099 }
3100
3101
3102
3103
3104
3105 =head2 ValidateStatus STATUS
3106
3107 Takes a string. Returns true if that status is a valid status for this ticket.
3108 Returns false otherwise.
3109
3110 =cut
3111
3112 sub ValidateStatus {
3113     my $self   = shift;
3114     my $status = shift;
3115
3116     #Make sure the status passed in is valid
3117     return 1 if $self->QueueObj->IsValidStatus($status);
3118
3119     my $i = 0;
3120     while ( my $caller = (caller($i++))[3] ) {
3121         return 1 if $caller eq 'RT::Ticket::SetQueue';
3122     }
3123
3124     return 0;
3125 }
3126
3127
3128
3129 =head2 SetStatus STATUS
3130
3131 Set this ticket's status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3132
3133 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3134 If FORCE is true, ignore unresolved dependencies and force a status change.
3135 if SETSTARTED is true( it's the default value), set Started to current datetime if Started 
3136 is not set and the status is changed from initial to not initial. 
3137
3138 =cut
3139
3140 sub SetStatus {
3141     my $self = shift;
3142     my %args;
3143     if (@_ == 1) {
3144         $args{Status} = shift;
3145     }
3146     else {
3147         %args = (@_);
3148     }
3149
3150     # this only allows us to SetStarted, not we must SetStarted.
3151     # this option was added for rtir initially
3152     $args{SetStarted} = 1 unless exists $args{SetStarted};
3153
3154
3155     my $lifecycle = $self->QueueObj->Lifecycle;
3156
3157     my $new = $args{'Status'};
3158     unless ( $lifecycle->IsValid( $new ) ) {
3159         return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3160     }
3161
3162     my $old = $self->__Value('Status');
3163     unless ( $lifecycle->IsTransition( $old => $new ) ) {
3164         return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3165     }
3166
3167     my $check_right = $lifecycle->CheckRight( $old => $new );
3168     unless ( $self->CurrentUserHasRight( $check_right ) ) {
3169         return ( 0, $self->loc('Permission Denied') );
3170     }
3171
3172     if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3173         return (0, $self->loc('That ticket has unresolved dependencies'));
3174     }
3175
3176     my $now = RT::Date->new( $self->CurrentUser );
3177     $now->SetToNow();
3178
3179     my $raw_started = RT::Date->new(RT->SystemUser);
3180     $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3181
3182     #If we're changing the status from new, record that we've started
3183     if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3184         #Set the Started time to "now"
3185         $self->_Set(
3186             Field             => 'Started',
3187             Value             => $now->ISO,
3188             RecordTransaction => 0
3189         );
3190     }
3191
3192     #When we close a ticket, set the 'Resolved' attribute to now.
3193     # It's misnamed, but that's just historical.
3194     if ( $lifecycle->IsInactive($new) ) {
3195         $self->_Set(
3196             Field             => 'Resolved',
3197             Value             => $now->ISO,
3198             RecordTransaction => 0,
3199         );
3200     }
3201
3202     #Actually update the status
3203     my ($val, $msg)= $self->_Set(
3204         Field           => 'Status',
3205         Value           => $args{Status},
3206         TimeTaken       => 0,
3207         CheckACL        => 0,
3208         TransactionType => 'Status',
3209     );
3210     return ($val, $msg);
3211 }
3212
3213
3214
3215 =head2 Delete
3216
3217 Takes no arguments. Marks this ticket for garbage collection
3218
3219 =cut
3220
3221 sub Delete {
3222     my $self = shift;
3223     unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3224         return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3225     }
3226     return ( $self->SetStatus('deleted') );
3227 }
3228
3229
3230 =head2 SetTold ISO  [TIMETAKEN]
3231
3232 Updates the told and records a transaction
3233
3234 =cut
3235
3236 sub SetTold {
3237     my $self = shift;
3238     my $told;
3239     $told = shift if (@_);
3240     my $timetaken = shift || 0;
3241
3242     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3243         return ( 0, $self->loc("Permission Denied") );
3244     }
3245
3246     my $datetold = RT::Date->new( $self->CurrentUser );
3247     if ($told) {
3248         $datetold->Set( Format => 'iso',
3249                         Value  => $told );
3250     }
3251     else {
3252         $datetold->SetToNow();
3253     }
3254
3255     return ( $self->_Set( Field           => 'Told',
3256                           Value           => $datetold->ISO,
3257                           TimeTaken       => $timetaken,
3258                           TransactionType => 'Told' ) );
3259 }
3260
3261 =head2 _SetTold
3262
3263 Updates the told without a transaction or acl check. Useful when we're sending replies.
3264
3265 =cut
3266
3267 sub _SetTold {
3268     my $self = shift;
3269
3270     my $now = RT::Date->new( $self->CurrentUser );
3271     $now->SetToNow();
3272
3273     #use __Set to get no ACLs ;)
3274     return ( $self->__Set( Field => 'Told',
3275                            Value => $now->ISO ) );
3276 }
3277
3278 =head2 SeenUpTo
3279
3280
3281 =cut
3282
3283 sub SeenUpTo {
3284     my $self = shift;
3285     my $uid = $self->CurrentUser->id;
3286     my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3287     return if $attr && $attr->Content gt $self->LastUpdated;
3288
3289     my $txns = $self->Transactions;
3290     $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3291     $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3292     $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3293     $txns->Limit(
3294         FIELD => 'Created',
3295         OPERATOR => '>',
3296         VALUE => $attr->Content
3297     ) if $attr;
3298     $txns->RowsPerPage(1);
3299     return $txns->First;
3300 }
3301
3302 =head2 RanTransactionBatch
3303
3304 Acts as a guard around running TransactionBatch scrips.
3305
3306 Should be false until you enter the code that runs TransactionBatch scrips
3307
3308 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3309
3310 =cut
3311
3312 sub RanTransactionBatch {
3313     my $self = shift;
3314     my $val = shift;
3315
3316     if ( defined $val ) {
3317         return $self->{_RanTransactionBatch} = $val;
3318     } else {
3319         return $self->{_RanTransactionBatch};
3320     }
3321
3322 }
3323
3324
3325 =head2 TransactionBatch
3326
3327 Returns an array reference of all transactions created on this ticket during
3328 this ticket object's lifetime or since last application of a batch, or undef
3329 if there were none.
3330
3331 Only works when the C<UseTransactionBatch> config option is set to true.
3332
3333 =cut
3334
3335 sub TransactionBatch {
3336     my $self = shift;
3337     return $self->{_TransactionBatch};
3338 }
3339
3340 =head2 ApplyTransactionBatch
3341
3342 Applies scrips on the current batch of transactions and shinks it. Usually
3343 batch is applied when object is destroyed, but in some cases it's too late.
3344
3345 =cut
3346
3347 sub ApplyTransactionBatch {
3348     my $self = shift;
3349
3350     my $batch = $self->TransactionBatch;
3351     return unless $batch && @$batch;
3352
3353     $self->_ApplyTransactionBatch;
3354
3355     $self->{_TransactionBatch} = [];
3356 }
3357
3358 sub _ApplyTransactionBatch {
3359     my $self = shift;
3360
3361     return if $self->RanTransactionBatch;
3362     $self->RanTransactionBatch(1);
3363
3364     my $still_exists = RT::Ticket->new( RT->SystemUser );
3365     $still_exists->Load( $self->Id );
3366     if (not $still_exists->Id) {
3367         # The ticket has been removed from the database, but we still
3368         # have pending TransactionBatch txns for it.  Unfortunately,
3369         # because it isn't in the DB anymore, attempting to run scrips
3370         # on it may produce unpredictable results; simply drop the
3371         # batched transactions.
3372         $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.");
3373         return;
3374     }
3375
3376     my $batch = $self->TransactionBatch;
3377
3378     my %seen;
3379     my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3380
3381     require RT::Scrips;
3382     RT::Scrips->new(RT->SystemUser)->Apply(
3383         Stage          => 'TransactionBatch',
3384         TicketObj      => $self,
3385         TransactionObj => $batch->[0],
3386         Type           => $types,
3387     );
3388
3389     # Entry point of the rule system
3390     my $rules = RT::Ruleset->FindAllRules(
3391         Stage          => 'TransactionBatch',
3392         TicketObj      => $self,
3393         TransactionObj => $batch->[0],
3394         Type           => $types,
3395     );
3396     RT::Ruleset->CommitRules($rules);
3397 }
3398
3399 sub DESTROY {
3400     my $self = shift;
3401
3402     # DESTROY methods need to localize $@, or it may unset it.  This
3403     # causes $m->abort to not bubble all of the way up.  See perlbug
3404     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3405     local $@;
3406
3407     # The following line eliminates reentrancy.
3408     # It protects against the fact that perl doesn't deal gracefully
3409     # when an object's refcount is changed in its destructor.
3410     return if $self->{_Destroyed}++;
3411
3412     if (in_global_destruction()) {
3413        unless ($ENV{'HARNESS_ACTIVE'}) {
3414             warn "Too late to safely run transaction-batch scrips!"
3415                 ." This is typically caused by using ticket objects"
3416                 ." at the top-level of a script which uses the RT API."
3417                ." Be sure to explicitly undef such ticket objects,"
3418                 ." or put them inside of a lexical scope.";
3419         }
3420         return;
3421     }
3422
3423     return $self->ApplyTransactionBatch;
3424 }
3425
3426
3427
3428
3429 sub _OverlayAccessible {
3430     {
3431         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3432           Queue           => { 'read' => 1,  'write' => 1 },
3433           Requestors      => { 'read' => 1,  'write' => 1 },
3434           Owner           => { 'read' => 1,  'write' => 1 },
3435           Subject         => { 'read' => 1,  'write' => 1 },
3436           InitialPriority => { 'read' => 1,  'write' => 1 },
3437           FinalPriority   => { 'read' => 1,  'write' => 1 },
3438           Priority        => { 'read' => 1,  'write' => 1 },
3439           Status          => { 'read' => 1,  'write' => 1 },
3440           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3441           TimeWorked      => { 'read' => 1,  'write' => 1 },
3442           TimeLeft        => { 'read' => 1,  'write' => 1 },
3443           Told            => { 'read' => 1,  'write' => 1 },
3444           Resolved        => { 'read' => 1 },
3445           Type            => { 'read' => 1 },
3446           Starts        => { 'read' => 1, 'write' => 1 },
3447           Started       => { 'read' => 1, 'write' => 1 },
3448           Due           => { 'read' => 1, 'write' => 1 },
3449           Creator       => { 'read' => 1, 'auto'  => 1 },
3450           Created       => { 'read' => 1, 'auto'  => 1 },
3451           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3452           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3453     };
3454
3455 }
3456
3457
3458
3459 sub _Set {
3460     my $self = shift;
3461
3462     my %args = ( Field             => undef,
3463                  Value             => undef,
3464                  TimeTaken         => 0,
3465                  RecordTransaction => 1,
3466                  UpdateTicket      => 1,
3467                  CheckACL          => 1,
3468                  TransactionType   => 'Set',
3469                  @_ );
3470
3471     if ($args{'CheckACL'}) {
3472       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3473           return ( 0, $self->loc("Permission Denied"));
3474       }
3475    }
3476
3477     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3478         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3479         return(0, $self->loc("Internal Error"));
3480     }
3481
3482     #if the user is trying to modify the record
3483
3484     #Take care of the old value we really don't want to get in an ACL loop.
3485     # so ask the super::_Value
3486     my $Old = $self->SUPER::_Value("$args{'Field'}");
3487     
3488     my ($ret, $msg);
3489     if ( $args{'UpdateTicket'}  ) {
3490
3491         #Set the new value
3492         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3493                                                 Value => $args{'Value'} );
3494     
3495         #If we can't actually set the field to the value, don't record
3496         # a transaction. instead, get out of here.
3497         return ( 0, $msg ) unless $ret;
3498     }
3499
3500     if ( $args{'RecordTransaction'} == 1 ) {
3501
3502         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3503                                                Type => $args{'TransactionType'},
3504                                                Field     => $args{'Field'},
3505                                                NewValue  => $args{'Value'},
3506                                                OldValue  => $Old,
3507                                                TimeTaken => $args{'TimeTaken'},
3508         );
3509         # Ensure that we can read the transaction, even if the change
3510         # just made the ticket unreadable to us
3511         $TransObj->{ _object_is_readable } = 1;
3512         return ( $Trans, scalar $TransObj->BriefDescription );
3513     }
3514     else {
3515         return ( $ret, $msg );
3516     }
3517 }
3518
3519
3520
3521 =head2 _Value
3522
3523 Takes the name of a table column.
3524 Returns its value as a string, if the user passes an ACL check
3525
3526 =cut
3527
3528 sub _Value {
3529
3530     my $self  = shift;
3531     my $field = shift;
3532
3533     #if the field is public, return it.
3534     if ( $self->_Accessible( $field, 'public' ) ) {
3535
3536         #$RT::Logger->debug("Skipping ACL check for $field");
3537         return ( $self->SUPER::_Value($field) );
3538
3539     }
3540
3541     #If the current user doesn't have ACLs, don't let em at it.  
3542
3543     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3544         return (undef);
3545     }
3546     return ( $self->SUPER::_Value($field) );
3547
3548 }
3549
3550
3551
3552 =head2 _UpdateTimeTaken
3553
3554 This routine will increment the timeworked counter. it should
3555 only be called from _NewTransaction 
3556
3557 =cut
3558
3559 sub _UpdateTimeTaken {
3560     my $self    = shift;
3561     my $Minutes = shift;
3562     my ($Total);
3563
3564     $Total = $self->SUPER::_Value("TimeWorked");
3565     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3566     $self->SUPER::_Set(
3567         Field => "TimeWorked",
3568         Value => $Total
3569     );
3570
3571     return ($Total);
3572 }
3573
3574
3575
3576
3577
3578 =head2 CurrentUserHasRight
3579
3580   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3581 1 if the user has that right. It returns 0 if the user doesn't have that right.
3582
3583 =cut
3584
3585 sub CurrentUserHasRight {
3586     my $self  = shift;
3587     my $right = shift;
3588
3589     return $self->CurrentUser->PrincipalObj->HasRight(
3590         Object => $self,
3591         Right  => $right,
3592     )
3593 }
3594
3595
3596 =head2 CurrentUserCanSee
3597
3598 Returns true if the current user can see the ticket, using ShowTicket
3599
3600 =cut
3601
3602 sub CurrentUserCanSee {
3603     my $self = shift;
3604     return $self->CurrentUserHasRight('ShowTicket');
3605 }
3606
3607 =head2 HasRight
3608
3609  Takes a paramhash with the attributes 'Right' and 'Principal'
3610   'Right' is a ticket-scoped textual right from RT::ACE 
3611   'Principal' is an RT::User object
3612
3613   Returns 1 if the principal has the right. Returns undef if not.
3614
3615 =cut
3616
3617 sub HasRight {
3618     my $self = shift;
3619     my %args = (
3620         Right     => undef,
3621         Principal => undef,
3622         @_
3623     );
3624
3625     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3626     {
3627         Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3628         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3629         return(undef);
3630     }
3631
3632     return (
3633         $args{'Principal'}->HasRight(
3634             Object => $self,
3635             Right     => $args{'Right'}
3636           )
3637     );
3638 }
3639
3640
3641
3642 =head2 Reminders
3643
3644 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3645 It isn't acutally a searchbuilder collection itself.
3646
3647 =cut
3648
3649 sub Reminders {
3650     my $self = shift;
3651     
3652     unless ($self->{'__reminders'}) {
3653         $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3654         $self->{'__reminders'}->Ticket($self->id);
3655     }
3656     return $self->{'__reminders'};
3657
3658 }
3659
3660
3661
3662
3663 =head2 Transactions
3664
3665   Returns an RT::Transactions object of all transactions on this ticket
3666
3667 =cut
3668
3669 sub Transactions {
3670     my $self = shift;
3671
3672     my $transactions = RT::Transactions->new( $self->CurrentUser );
3673
3674     #If the user has no rights, return an empty object
3675     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3676         $transactions->LimitToTicket($self->id);
3677
3678         # if the user may not see comments do not return them
3679         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3680             $transactions->Limit(
3681                 SUBCLAUSE => 'acl',
3682                 FIELD    => 'Type',
3683                 OPERATOR => '!=',
3684                 VALUE    => "Comment"
3685             );
3686             $transactions->Limit(
3687                 SUBCLAUSE => 'acl',
3688                 FIELD    => 'Type',
3689                 OPERATOR => '!=',
3690                 VALUE    => "CommentEmailRecord",
3691                 ENTRYAGGREGATOR => 'AND'
3692             );
3693
3694         }
3695     } else {
3696         $transactions->Limit(
3697             SUBCLAUSE => 'acl',
3698             FIELD    => 'id',
3699             VALUE    => 0,
3700             ENTRYAGGREGATOR => 'AND'
3701         );
3702     }
3703
3704     return ($transactions);
3705 }
3706
3707
3708
3709
3710 =head2 TransactionCustomFields
3711
3712     Returns the custom fields that transactions on tickets will have.
3713
3714 =cut
3715
3716 sub TransactionCustomFields {
3717     my $self = shift;
3718     my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3719     $cfs->SetContextObject( $self );
3720     return $cfs;
3721 }
3722
3723
3724 =head2 LoadCustomFieldByIdentifier
3725
3726 Finds and returns the custom field of the given name for the ticket,
3727 overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3728 queue-specific CFs before global ones.
3729
3730 =cut
3731
3732 sub LoadCustomFieldByIdentifier {
3733     my $self  = shift;
3734     my $field = shift;
3735
3736     return $self->SUPER::LoadCustomFieldByIdentifier($field)
3737         if ref $field or $field =~ /^\d+$/;
3738
3739     my $cf = RT::CustomField->new( $self->CurrentUser );
3740     $cf->SetContextObject( $self );
3741     $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3742     $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ) unless $cf->id;
3743     return $cf;
3744 }
3745
3746
3747 =head2 CustomFieldLookupType
3748
3749 Returns the RT::Ticket lookup type, which can be passed to 
3750 RT::CustomField->Create() via the 'LookupType' hash key.
3751
3752 =cut
3753
3754
3755 sub CustomFieldLookupType {
3756     "RT::Queue-RT::Ticket";
3757 }
3758
3759 =head2 ACLEquivalenceObjects
3760
3761 This method returns a list of objects for which a user's rights also apply
3762 to this ticket. Generally, this is only the ticket's queue, but some RT 
3763 extensions may make other objects available too.
3764
3765 This method is called from L<RT::Principal/HasRight>.
3766
3767 =cut
3768
3769 sub ACLEquivalenceObjects {
3770     my $self = shift;
3771     return $self->QueueObj;
3772
3773 }
3774
3775