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