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