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