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