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