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