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