Merge branch 'master' of https://github.com/jgoodman/Freeside
[freeside.git] / rt / lib / RT / Ticket.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 SYNOPSIS
50
51   use RT::Ticket;
52   my $ticket = RT::Ticket->new($CurrentUser);
53   $ticket->Load($ticket_id);
54
55 =head1 DESCRIPTION
56
57 This module lets you manipulate RT's ticket object.
58
59
60 =head1 METHODS
61
62
63 =cut
64
65
66 package RT::Ticket;
67
68 use strict;
69 use warnings;
70
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 (Defined in RT::Queue)
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->new();
862     $args{'mimeobj'}->build(
863         Type => ( $args{'contenttype'} || 'text/plain' ),
864         Data => ($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         $args{'MIMEObj'} = MIME::Entity->build(
2348             Data => ( ref $args{'Content'}? $args{'Content'}: [ $args{'Content'} ] )
2349         );
2350     }
2351
2352     $args{'MIMEObj'}->head->replace('X-RT-Interface' => 'API')
2353         unless $args{'MIMEObj'}->head->get('X-RT-Interface');
2354
2355     # convert text parts into utf-8
2356     RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2357
2358     # If we've been passed in CcMessageTo and BccMessageTo fields,
2359     # add them to the mime object for passing on to the transaction handler
2360     # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and
2361     # RT-Send-Bcc: headers
2362
2363
2364     foreach my $type (qw/Cc Bcc/) {
2365         if ( defined $args{ $type . 'MessageTo' } ) {
2366
2367             my $addresses = join ', ', (
2368                 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2369                     Email::Address->parse( $args{ $type . 'MessageTo' } ) );
2370             $args{'MIMEObj'}->head->replace( 'RT-Send-' . $type, Encode::encode_utf8( $addresses ) );
2371         }
2372     }
2373
2374     foreach my $argument (qw(Encrypt Sign)) {
2375         $args{'MIMEObj'}->head->replace(
2376             "X-RT-$argument" => Encode::encode_utf8( $args{ $argument } )
2377         ) if defined $args{ $argument };
2378     }
2379
2380     # If this is from an external source, we need to come up with its
2381     # internal Message-ID now, so all emails sent because of this
2382     # message have a common Message-ID
2383     my $org = RT->Config->Get('Organization');
2384     my $msgid = $args{'MIMEObj'}->head->get('Message-ID');
2385     unless (defined $msgid && $msgid =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>/) {
2386         $args{'MIMEObj'}->head->set(
2387             'RT-Message-ID' => Encode::encode_utf8(
2388                 RT::Interface::Email::GenMessageId( Ticket => $self )
2389             )
2390         );
2391     }
2392
2393     #Record the correspondence (write the transaction)
2394     my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2395              Type => $args{'NoteType'},
2396              Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2397              TimeTaken => $args{'TimeTaken'},
2398              MIMEObj   => $args{'MIMEObj'}, 
2399              CommitScrips => $args{'CommitScrips'},
2400              SquelchMailTo => $args{'SquelchMailTo'},
2401              CustomFields => $args{'CustomFields'},
2402     );
2403
2404     unless ($Trans) {
2405         $RT::Logger->err("$self couldn't init a transaction $msg");
2406         return ( $Trans, $self->loc("Message could not be recorded"), undef );
2407     }
2408
2409     return ( $Trans, $self->loc("Message recorded"), $TransObj );
2410 }
2411
2412
2413 =head2 DryRun
2414
2415 Builds a MIME object from the given C<UpdateSubject> and
2416 C<UpdateContent>, then calls L</Comment> or L</Correspond> with
2417 C<< DryRun => 1 >>, and returns the transaction so produced.
2418
2419 =cut
2420
2421 sub DryRun {
2422     my $self = shift;
2423     my %args = @_;
2424     my $action;
2425     if (($args{'UpdateType'} || $args{Action}) =~ /^respon(d|se)$/i ) {
2426         $action = 'Correspond';
2427     } else {
2428         $action = 'Comment';
2429     }
2430
2431     my $Message = MIME::Entity->build(
2432         Type    => 'text/plain',
2433         Subject => defined $args{UpdateSubject} ? Encode::encode_utf8( $args{UpdateSubject} ) : "",
2434         Charset => 'UTF-8',
2435         Data    => $args{'UpdateContent'} || "",
2436     );
2437
2438     my ( $Transaction, $Description, $Object ) = $self->$action(
2439         CcMessageTo  => $args{'UpdateCc'},
2440         BccMessageTo => $args{'UpdateBcc'},
2441         MIMEObj      => $Message,
2442         TimeTaken    => $args{'UpdateTimeWorked'},
2443         DryRun       => 1,
2444     );
2445     unless ( $Transaction ) {
2446         $RT::Logger->error("Couldn't fire '$action' action: $Description");
2447     }
2448
2449     return $Object;
2450 }
2451
2452 =head2 DryRunCreate
2453
2454 Prepares a MIME mesage with the given C<Subject>, C<Cc>, and
2455 C<Content>, then calls L</Create> with C<< DryRun => 1 >> and returns
2456 the resulting L<RT::Transaction>.
2457
2458 =cut
2459
2460 sub DryRunCreate {
2461     my $self = shift;
2462     my %args = @_;
2463     my $Message = MIME::Entity->build(
2464         Type    => 'text/plain',
2465         Subject => defined $args{Subject} ? Encode::encode_utf8( $args{'Subject'} ) : "",
2466         (defined $args{'Cc'} ?
2467              ( Cc => Encode::encode_utf8( $args{'Cc'} ) ) : ()),
2468         Charset => 'UTF-8',
2469         Data    => $args{'Content'} || "",
2470     );
2471
2472     my ( $Transaction, $Object, $Description ) = $self->Create(
2473         Type            => $args{'Type'} || 'ticket',
2474         Queue           => $args{'Queue'},
2475         Owner           => $args{'Owner'},
2476         Requestor       => $args{'Requestors'},
2477         Cc              => $args{'Cc'},
2478         AdminCc         => $args{'AdminCc'},
2479         InitialPriority => $args{'InitialPriority'},
2480         FinalPriority   => $args{'FinalPriority'},
2481         TimeLeft        => $args{'TimeLeft'},
2482         TimeEstimated   => $args{'TimeEstimated'},
2483         TimeWorked      => $args{'TimeWorked'},
2484         Subject         => $args{'Subject'},
2485         Status          => $args{'Status'},
2486         MIMEObj         => $Message,
2487         DryRun          => 1,
2488     );
2489     unless ( $Transaction ) {
2490         $RT::Logger->error("Couldn't fire Create action: $Description");
2491     }
2492
2493     return $Object;
2494 }
2495
2496
2497
2498 sub _Links {
2499     my $self = shift;
2500
2501     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2502     #tobias meant by $f
2503     my $field = shift;
2504     my $type  = shift || "";
2505
2506     my $cache_key = "$field$type";
2507     return $self->{ $cache_key } if $self->{ $cache_key };
2508
2509     my $links = $self->{ $cache_key }
2510               = RT::Links->new( $self->CurrentUser );
2511     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
2512         $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
2513         return $links;
2514     }
2515
2516     # Maybe this ticket is a merge ticket
2517     #my $limit_on = 'Local'. $field;
2518     # at least to myself
2519     $links->Limit(
2520         FIELD           => $field, #$limit_on,
2521         OPERATOR        => 'MATCHES',
2522         VALUE           => 'fsck.com-rt://%/ticket/'. $self->id,
2523         ENTRYAGGREGATOR => 'OR',
2524     );
2525     $links->Limit(
2526         FIELD           => $field, #$limit_on,
2527         OPERATOR        => 'MATCHES',
2528         VALUE           => 'fsck.com-rt://%/ticket/'. $_,
2529         ENTRYAGGREGATOR => 'OR',
2530     ) foreach $self->Merged;
2531     $links->Limit(
2532         FIELD => 'Type',
2533         VALUE => $type,
2534     ) if $type;
2535
2536     return $links;
2537 }
2538
2539
2540
2541 =head2 DeleteLink
2542
2543 Delete a link. takes a paramhash of Base, Target, Type, Silent,
2544 SilentBase and SilentTarget. Either Base or Target must be null.
2545 The null value will be replaced with this ticket's id.
2546
2547 If Silent is true then no transaction would be recorded, in other
2548 case you can control creation of transactions on both base and
2549 target with SilentBase and SilentTarget respectively. By default
2550 both transactions are created.
2551
2552 =cut 
2553
2554 sub DeleteLink {
2555     my $self = shift;
2556     my %args = (
2557         Base   => undef,
2558         Target => undef,
2559         Type   => undef,
2560         Silent => undef,
2561         SilentBase   => undef,
2562         SilentTarget => undef,
2563         @_
2564     );
2565
2566     unless ( $args{'Target'} || $args{'Base'} ) {
2567         $RT::Logger->error("Base or Target must be specified");
2568         return ( 0, $self->loc('Either base or target must be specified') );
2569     }
2570
2571     #check acls
2572     my $right = 0;
2573     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2574     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2575         return ( 0, $self->loc("Permission Denied") );
2576     }
2577
2578     # If the other URI is an RT::Ticket, we want to make sure the user
2579     # can modify it too...
2580     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2581     return (0, $msg) unless $status;
2582     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2583         $right++;
2584     }
2585     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2586          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2587     {
2588         return ( 0, $self->loc("Permission Denied") );
2589     }
2590
2591     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2592     return ( 0, $Msg ) unless $val;
2593
2594     return ( $val, $Msg ) if $args{'Silent'};
2595
2596     my ($direction, $remote_link);
2597
2598     if ( $args{'Base'} ) {
2599         $remote_link = $args{'Base'};
2600         $direction = 'Target';
2601     }
2602     elsif ( $args{'Target'} ) {
2603         $remote_link = $args{'Target'};
2604         $direction = 'Base';
2605     } 
2606
2607     my $remote_uri = RT::URI->new( $self->CurrentUser );
2608     $remote_uri->FromURI( $remote_link );
2609
2610     unless ( $args{ 'Silent'. $direction } ) {
2611         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2612             Type      => 'DeleteLink',
2613             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2614             OldValue  => $remote_uri->URI || $remote_link,
2615             TimeTaken => 0
2616         );
2617         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2618     }
2619
2620     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2621         my $OtherObj = $remote_uri->Object;
2622         my ( $val, $Msg ) = $OtherObj->_NewTransaction(
2623             Type           => 'DeleteLink',
2624             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2625                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2626             OldValue       => $self->URI,
2627             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2628             TimeTaken      => 0,
2629         );
2630         $RT::Logger->error("Couldn't create transaction: $Msg") unless $val;
2631     }
2632
2633     return ( $val, $Msg );
2634 }
2635
2636
2637
2638 =head2 AddLink
2639
2640 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2641
2642 If Silent is true then no transaction would be recorded, in other
2643 case you can control creation of transactions on both base and
2644 target with SilentBase and SilentTarget respectively. By default
2645 both transactions are created.
2646
2647 =cut
2648
2649 sub AddLink {
2650     my $self = shift;
2651     my %args = ( Target       => '',
2652                  Base         => '',
2653                  Type         => '',
2654                  Silent       => undef,
2655                  SilentBase   => undef,
2656                  SilentTarget => undef,
2657                  @_ );
2658
2659     unless ( $args{'Target'} || $args{'Base'} ) {
2660         $RT::Logger->error("Base or Target must be specified");
2661         return ( 0, $self->loc('Either base or target must be specified') );
2662     }
2663
2664     my $right = 0;
2665     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2666     if ( !$right && RT->Config->Get( 'StrictLinkACL' ) ) {
2667         return ( 0, $self->loc("Permission Denied") );
2668     }
2669
2670     # If the other URI is an RT::Ticket, we want to make sure the user
2671     # can modify it too...
2672     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2673     return (0, $msg) unless $status;
2674     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2675         $right++;
2676     }
2677     if ( ( !RT->Config->Get( 'StrictLinkACL' ) && $right == 0 ) ||
2678          ( RT->Config->Get( 'StrictLinkACL' ) && $right < 2 ) )
2679     {
2680         return ( 0, $self->loc("Permission Denied") );
2681     }
2682
2683     return ( 0, "Can't link to a deleted ticket" )
2684       if $other_ticket && lc $other_ticket->Status eq 'deleted';
2685
2686     return $self->_AddLink(%args);
2687 }
2688
2689 sub __GetTicketFromURI {
2690     my $self = shift;
2691     my %args = ( URI => '', @_ );
2692
2693     # If the other URI is an RT::Ticket, we want to make sure the user
2694     # can modify it too...
2695     my $uri_obj = RT::URI->new( $self->CurrentUser );
2696     unless ($uri_obj->FromURI( $args{'URI'} )) {
2697         my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2698         $RT::Logger->warning( $msg );
2699         return( 0, $msg );
2700     }
2701     my $obj = $uri_obj->Resolver->Object;
2702     unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2703         return (1, 'Found not a ticket', undef);
2704     }
2705     return (1, 'Found ticket', $obj);
2706 }
2707
2708 =head2 _AddLink  
2709
2710 Private non-acled variant of AddLink so that links can be added during create.
2711
2712 =cut
2713
2714 sub _AddLink {
2715     my $self = shift;
2716     my %args = ( Target       => '',
2717                  Base         => '',
2718                  Type         => '',
2719                  Silent       => undef,
2720                  SilentBase   => undef,
2721                  SilentTarget => undef,
2722                  @_ );
2723
2724     my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2725     return ($val, $msg) if !$val || $exist;
2726     return ($val, $msg) if $args{'Silent'};
2727
2728     my ($direction, $remote_link);
2729     if ( $args{'Target'} ) {
2730         $remote_link  = $args{'Target'};
2731         $direction    = 'Base';
2732     } elsif ( $args{'Base'} ) {
2733         $remote_link  = $args{'Base'};
2734         $direction    = 'Target';
2735     }
2736
2737     my $remote_uri = RT::URI->new( $self->CurrentUser );
2738     $remote_uri->FromURI( $remote_link );
2739
2740     unless ( $args{ 'Silent'. $direction } ) {
2741         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2742             Type      => 'AddLink',
2743             Field     => $LINKDIRMAP{$args{'Type'}}->{$direction},
2744             NewValue  =>  $remote_uri->URI || $remote_link,
2745             TimeTaken => 0
2746         );
2747         $RT::Logger->error("Couldn't create transaction: $Msg") unless $Trans;
2748     }
2749
2750     if ( !$args{ 'Silent'. ( $direction eq 'Target'? 'Base': 'Target' ) } && $remote_uri->IsLocal ) {
2751         my $OtherObj = $remote_uri->Object;
2752         my ( $val, $msg ) = $OtherObj->_NewTransaction(
2753             Type           => 'AddLink',
2754             Field          => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2755                                             : $LINKDIRMAP{$args{'Type'}}->{Target},
2756             NewValue       => $self->URI,
2757             ActivateScrips => !RT->Config->Get('LinkTransactionsRun1Scrip'),
2758             TimeTaken      => 0,
2759         );
2760         $RT::Logger->error("Couldn't create transaction: $msg") unless $val;
2761     }
2762
2763     return ( $val, $msg );
2764 }
2765
2766
2767
2768
2769 =head2 MergeInto
2770
2771 MergeInto take the id of the ticket to merge this ticket into.
2772
2773 =cut
2774
2775 sub MergeInto {
2776     my $self      = shift;
2777     my $ticket_id = shift;
2778
2779     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2780         return ( 0, $self->loc("Permission Denied") );
2781     }
2782
2783     # Load up the new ticket.
2784     my $MergeInto = RT::Ticket->new($self->CurrentUser);
2785     $MergeInto->Load($ticket_id);
2786
2787     # make sure it exists.
2788     unless ( $MergeInto->Id ) {
2789         return ( 0, $self->loc("New ticket doesn't exist") );
2790     }
2791
2792     # Make sure the current user can modify the new ticket.
2793     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2794         return ( 0, $self->loc("Permission Denied") );
2795     }
2796
2797     delete $MERGE_CACHE{'effective'}{ $self->id };
2798     delete @{ $MERGE_CACHE{'merged'} }{
2799         $ticket_id, $MergeInto->id, $self->id
2800     };
2801
2802     $RT::Handle->BeginTransaction();
2803
2804     $self->_MergeInto( $MergeInto );
2805
2806     $RT::Handle->Commit();
2807
2808     return ( 1, $self->loc("Merge Successful") );
2809 }
2810
2811 sub _MergeInto {
2812     my $self      = shift;
2813     my $MergeInto = shift;
2814
2815
2816     # We use EffectiveId here even though it duplicates information from
2817     # the links table becasue of the massive performance hit we'd take
2818     # by trying to do a separate database query for merge info everytime 
2819     # loaded a ticket. 
2820
2821     #update this ticket's effective id to the new ticket's id.
2822     my ( $id_val, $id_msg ) = $self->__Set(
2823         Field => 'EffectiveId',
2824         Value => $MergeInto->Id()
2825     );
2826
2827     unless ($id_val) {
2828         $RT::Handle->Rollback();
2829         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2830     }
2831
2832
2833     my $force_status = $self->QueueObj->Lifecycle->DefaultOnMerge;
2834     if ( $force_status && $force_status ne $self->__Value('Status') ) {
2835         my ( $status_val, $status_msg )
2836             = $self->__Set( Field => 'Status', Value => $force_status );
2837
2838         unless ($status_val) {
2839             $RT::Handle->Rollback();
2840             $RT::Logger->error(
2841                 "Couldn't set status to $force_status. RT's Database may be inconsistent."
2842             );
2843             return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2844         }
2845     }
2846
2847     # update all the links that point to that old ticket
2848     my $old_links_to = RT::Links->new($self->CurrentUser);
2849     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2850
2851     my %old_seen;
2852     while (my $link = $old_links_to->Next) {
2853         if (exists $old_seen{$link->Base."-".$link->Type}) {
2854             $link->Delete;
2855         }   
2856         elsif ($link->Base eq $MergeInto->URI) {
2857             $link->Delete;
2858         } else {
2859             # First, make sure the link doesn't already exist. then move it over.
2860             my $tmp = RT::Link->new(RT->SystemUser);
2861             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2862             if ($tmp->id)   {
2863                     $link->Delete;
2864             } else { 
2865                 $link->SetTarget($MergeInto->URI);
2866                 $link->SetLocalTarget($MergeInto->id);
2867             }
2868             $old_seen{$link->Base."-".$link->Type} =1;
2869         }
2870
2871     }
2872
2873     my $old_links_from = RT::Links->new($self->CurrentUser);
2874     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2875
2876     while (my $link = $old_links_from->Next) {
2877         if (exists $old_seen{$link->Type."-".$link->Target}) {
2878             $link->Delete;
2879         }   
2880         if ($link->Target eq $MergeInto->URI) {
2881             $link->Delete;
2882         } else {
2883             # First, make sure the link doesn't already exist. then move it over.
2884             my $tmp = RT::Link->new(RT->SystemUser);
2885             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2886             if ($tmp->id)   {
2887                     $link->Delete;
2888             } else { 
2889                 $link->SetBase($MergeInto->URI);
2890                 $link->SetLocalBase($MergeInto->id);
2891                 $old_seen{$link->Type."-".$link->Target} =1;
2892             }
2893         }
2894
2895     }
2896
2897     # Update time fields
2898     foreach my $type (qw(TimeEstimated TimeWorked TimeLeft)) {
2899
2900         my $mutator = "Set$type";
2901         $MergeInto->$mutator(
2902             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2903
2904     }
2905 #add all of this ticket's watchers to that ticket.
2906     foreach my $watcher_type (qw(Requestors Cc AdminCc)) {
2907
2908         my $people = $self->$watcher_type->MembersObj;
2909         my $addwatcher_type =  $watcher_type;
2910         $addwatcher_type  =~ s/s$//;
2911
2912         while ( my $watcher = $people->Next ) {
2913             
2914            my ($val, $msg) =  $MergeInto->_AddWatcher(
2915                 Type        => $addwatcher_type,
2916                 Silent => 1,
2917                 PrincipalId => $watcher->MemberId
2918             );
2919             unless ($val) {
2920                 $RT::Logger->debug($msg);
2921             }
2922     }
2923
2924     }
2925
2926     #find all of the tickets that were merged into this ticket. 
2927     my $old_mergees = RT::Tickets->new( $self->CurrentUser );
2928     $old_mergees->Limit(
2929         FIELD    => 'EffectiveId',
2930         OPERATOR => '=',
2931         VALUE    => $self->Id
2932     );
2933
2934     #   update their EffectiveId fields to the new ticket's id
2935     while ( my $ticket = $old_mergees->Next() ) {
2936         my ( $val, $msg ) = $ticket->__Set(
2937             Field => 'EffectiveId',
2938             Value => $MergeInto->Id()
2939         );
2940     }
2941
2942     #make a new link: this ticket is merged into that other ticket.
2943     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2944
2945     $MergeInto->_SetLastUpdated;    
2946 }
2947
2948 =head2 Merged
2949
2950 Returns list of tickets' ids that's been merged into this ticket.
2951
2952 =cut
2953
2954 sub Merged {
2955     my $self = shift;
2956
2957     my $id = $self->id;
2958     return @{ $MERGE_CACHE{'merged'}{ $id } }
2959         if $MERGE_CACHE{'merged'}{ $id };
2960
2961     my $mergees = RT::Tickets->new( $self->CurrentUser );
2962     $mergees->Limit(
2963         FIELD    => 'EffectiveId',
2964         VALUE    => $id,
2965     );
2966     $mergees->Limit(
2967         FIELD    => 'id',
2968         OPERATOR => '!=',
2969         VALUE    => $id,
2970     );
2971     return @{ $MERGE_CACHE{'merged'}{ $id } ||= [] }
2972         = map $_->id, @{ $mergees->ItemsArrayRef || [] };
2973 }
2974
2975
2976
2977
2978
2979 =head2 OwnerObj
2980
2981 Takes nothing and returns an RT::User object of 
2982 this ticket's owner
2983
2984 =cut
2985
2986 sub OwnerObj {
2987     my $self = shift;
2988
2989     #If this gets ACLed, we lose on a rights check in User.pm and
2990     #get deep recursion. if we need ACLs here, we need
2991     #an equiv without ACLs
2992
2993     my $owner = RT::User->new( $self->CurrentUser );
2994     $owner->Load( $self->__Value('Owner') );
2995
2996     #Return the owner object
2997     return ($owner);
2998 }
2999
3000
3001
3002 =head2 OwnerAsString
3003
3004 Returns the owner's email address
3005
3006 =cut
3007
3008 sub OwnerAsString {
3009     my $self = shift;
3010     return ( $self->OwnerObj->EmailAddress );
3011
3012 }
3013
3014
3015
3016 =head2 SetOwner
3017
3018 Takes two arguments:
3019      the Id or Name of the owner 
3020 and  (optionally) the type of the SetOwner Transaction. It defaults
3021 to 'Set'.  'Steal' is also a valid option.
3022
3023
3024 =cut
3025
3026 sub SetOwner {
3027     my $self     = shift;
3028     my $NewOwner = shift;
3029     my $Type     = shift || "Set";
3030
3031     $RT::Handle->BeginTransaction();
3032
3033     $self->_SetLastUpdated(); # lock the ticket
3034     $self->Load( $self->id ); # in case $self changed while waiting for lock
3035
3036     my $OldOwnerObj = $self->OwnerObj;
3037
3038     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3039     $NewOwnerObj->Load( $NewOwner );
3040     unless ( $NewOwnerObj->Id ) {
3041         $RT::Handle->Rollback();
3042         return ( 0, $self->loc("That user does not exist") );
3043     }
3044
3045
3046     # must have ModifyTicket rights
3047     # or TakeTicket/StealTicket and $NewOwner is self
3048     # see if it's a take
3049     if ( $OldOwnerObj->Id == RT->Nobody->Id ) {
3050         unless (    $self->CurrentUserHasRight('ModifyTicket')
3051                  || $self->CurrentUserHasRight('TakeTicket') ) {
3052             $RT::Handle->Rollback();
3053             return ( 0, $self->loc("Permission Denied") );
3054         }
3055     }
3056
3057     # see if it's a steal
3058     elsif (    $OldOwnerObj->Id != RT->Nobody->Id
3059             && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3060
3061         unless (    $self->CurrentUserHasRight('ModifyTicket')
3062                  || $self->CurrentUserHasRight('StealTicket') ) {
3063             $RT::Handle->Rollback();
3064             return ( 0, $self->loc("Permission Denied") );
3065         }
3066     }
3067     else {
3068         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3069             $RT::Handle->Rollback();
3070             return ( 0, $self->loc("Permission Denied") );
3071         }
3072     }
3073
3074     # If we're not stealing and the ticket has an owner and it's not
3075     # the current user
3076     if ( $Type ne 'Steal' and $Type ne 'Force'
3077          and $OldOwnerObj->Id != RT->Nobody->Id
3078          and $OldOwnerObj->Id != $self->CurrentUser->Id )
3079     {
3080         $RT::Handle->Rollback();
3081         return ( 0, $self->loc("You can only take tickets that are unowned") )
3082             if $NewOwnerObj->id == $self->CurrentUser->id;
3083         return (
3084             0,
3085             $self->loc("You can only reassign tickets that you own or that are unowned" )
3086         );
3087     }
3088
3089     #If we've specified a new owner and that user can't modify the ticket
3090     elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3091         $RT::Handle->Rollback();
3092         return ( 0, $self->loc("That user may not own tickets in that queue") );
3093     }
3094
3095     # If the ticket has an owner and it's the new owner, we don't need
3096     # To do anything
3097     elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3098         $RT::Handle->Rollback();
3099         return ( 0, $self->loc("That user already owns that ticket") );
3100     }
3101
3102     # Delete the owner in the owner group, then add a new one
3103     # TODO: is this safe? it's not how we really want the API to work
3104     # for most things, but it's fast.
3105     my ( $del_id, $del_msg );
3106     for my $owner (@{$self->OwnerGroup->MembersObj->ItemsArrayRef}) {
3107         ($del_id, $del_msg) = $owner->Delete();
3108         last unless ($del_id);
3109     }
3110
3111     unless ($del_id) {
3112         $RT::Handle->Rollback();
3113         return ( 0, $self->loc("Could not change owner: [_1]", $del_msg) );
3114     }
3115
3116     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3117                                        PrincipalId => $NewOwnerObj->PrincipalId,
3118                                        InsideTransaction => 1 );
3119     unless ($add_id) {
3120         $RT::Handle->Rollback();
3121         return ( 0, $self->loc("Could not change owner: [_1]", $add_msg ) );
3122     }
3123
3124     # We call set twice with slightly different arguments, so
3125     # as to not have an SQL transaction span two RT transactions
3126
3127     my ( $val, $msg ) = $self->_Set(
3128                       Field             => 'Owner',
3129                       RecordTransaction => 0,
3130                       Value             => $NewOwnerObj->Id,
3131                       TimeTaken         => 0,
3132                       TransactionType   => 'Set',
3133                       CheckACL          => 0,                  # don't check acl
3134     );
3135
3136     unless ($val) {
3137         $RT::Handle->Rollback;
3138         return ( 0, $self->loc("Could not change owner: [_1]", $msg) );
3139     }
3140
3141     ($val, $msg) = $self->_NewTransaction(
3142         Type      => 'Set',
3143         Field     => 'Owner',
3144         NewValue  => $NewOwnerObj->Id,
3145         OldValue  => $OldOwnerObj->Id,
3146         TimeTaken => 0,
3147     );
3148
3149     if ( $val ) {
3150         $msg = $self->loc( "Owner changed from [_1] to [_2]",
3151                            $OldOwnerObj->Name, $NewOwnerObj->Name );
3152     }
3153     else {
3154         $RT::Handle->Rollback();
3155         return ( 0, $msg );
3156     }
3157
3158     $RT::Handle->Commit();
3159
3160     return ( $val, $msg );
3161 }
3162
3163
3164
3165 =head2 Take
3166
3167 A convenince method to set the ticket's owner to the current user
3168
3169 =cut
3170
3171 sub Take {
3172     my $self = shift;
3173     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3174 }
3175
3176
3177
3178 =head2 Untake
3179
3180 Convenience method to set the owner to 'nobody' if the current user is the owner.
3181
3182 =cut
3183
3184 sub Untake {
3185     my $self = shift;
3186     return ( $self->SetOwner( RT->Nobody->UserObj->Id, 'Untake' ) );
3187 }
3188
3189
3190
3191 =head2 Steal
3192
3193 A convenience method to change the owner of the current ticket to the
3194 current user. Even if it's owned by another user.
3195
3196 =cut
3197
3198 sub Steal {
3199     my $self = shift;
3200
3201     if ( $self->IsOwner( $self->CurrentUser ) ) {
3202         return ( 0, $self->loc("You already own this ticket") );
3203     }
3204     else {
3205         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3206
3207     }
3208
3209 }
3210
3211
3212
3213
3214
3215 =head2 ValidateStatus STATUS
3216
3217 Takes a string. Returns true if that status is a valid status for this ticket.
3218 Returns false otherwise.
3219
3220 =cut
3221
3222 sub ValidateStatus {
3223     my $self   = shift;
3224     my $status = shift;
3225
3226     #Make sure the status passed in is valid
3227     return 1 if $self->QueueObj->IsValidStatus($status);
3228
3229     my $i = 0;
3230     while ( my $caller = (caller($i++))[3] ) {
3231         return 1 if $caller eq 'RT::Ticket::SetQueue';
3232     }
3233
3234     return 0;
3235 }
3236
3237 sub Status {
3238     my $self = shift;
3239     my $value = $self->_Value( 'Status' );
3240     return $value unless $self->QueueObj;
3241     return $self->QueueObj->Lifecycle->CanonicalCase( $value );
3242 }
3243
3244 =head2 SetStatus STATUS
3245
3246 Set this ticket's status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3247
3248 Alternatively, you can pass in a list of named parameters (Status => STATUS, Force => FORCE, SetStarted => SETSTARTED ).
3249 If FORCE is true, ignore unresolved dependencies and force a status change.
3250 if SETSTARTED is true( it's the default value), set Started to current datetime if Started 
3251 is not set and the status is changed from initial to not initial. 
3252
3253 =cut
3254
3255 sub SetStatus {
3256     my $self = shift;
3257     my %args;
3258     if (@_ == 1) {
3259         $args{Status} = shift;
3260     }
3261     else {
3262         %args = (@_);
3263     }
3264
3265     # this only allows us to SetStarted, not we must SetStarted.
3266     # this option was added for rtir initially
3267     $args{SetStarted} = 1 unless exists $args{SetStarted};
3268
3269
3270     my $lifecycle = $self->QueueObj->Lifecycle;
3271
3272     my $new = lc $args{'Status'};
3273     unless ( $lifecycle->IsValid( $new ) ) {
3274         return (0, $self->loc("Status '[_1]' isn't a valid status for tickets in this queue.", $self->loc($new)));
3275     }
3276
3277     my $old = $self->__Value('Status');
3278     unless ( $lifecycle->IsTransition( $old => $new ) ) {
3279         return (0, $self->loc("You can't change status from '[_1]' to '[_2]'.", $self->loc($old), $self->loc($new)));
3280     }
3281
3282     my $check_right = $lifecycle->CheckRight( $old => $new );
3283     unless ( $self->CurrentUserHasRight( $check_right ) ) {
3284         return ( 0, $self->loc('Permission Denied') );
3285     }
3286
3287     if ( !$args{Force} && $lifecycle->IsInactive( $new ) && $self->HasUnresolvedDependencies) {
3288         return (0, $self->loc('That ticket has unresolved dependencies'));
3289     }
3290
3291     my $now = RT::Date->new( $self->CurrentUser );
3292     $now->SetToNow();
3293
3294     my $raw_started = RT::Date->new(RT->SystemUser);
3295     $raw_started->Set(Format => 'ISO', Value => $self->__Value('Started'));
3296
3297     #If we're changing the status from new, record that we've started
3298     if ( $args{SetStarted} && $lifecycle->IsInitial($old) && !$lifecycle->IsInitial($new) && !$raw_started->Unix) {
3299         #Set the Started time to "now"
3300         $self->_Set(
3301             Field             => 'Started',
3302             Value             => $now->ISO,
3303             RecordTransaction => 0
3304         );
3305     }
3306
3307     #When we close a ticket, set the 'Resolved' attribute to now.
3308     # It's misnamed, but that's just historical.
3309     if ( $lifecycle->IsInactive($new) ) {
3310         $self->_Set(
3311             Field             => 'Resolved',
3312             Value             => $now->ISO,
3313             RecordTransaction => 0,
3314         );
3315     }
3316
3317     #Actually update the status
3318     my ($val, $msg)= $self->_Set(
3319         Field           => 'Status',
3320         Value           => $new,
3321         TimeTaken       => 0,
3322         CheckACL        => 0,
3323         TransactionType => 'Status',
3324     );
3325     return ($val, $msg);
3326 }
3327
3328
3329
3330 =head2 Delete
3331
3332 Takes no arguments. Marks this ticket for garbage collection
3333
3334 =cut
3335
3336 sub Delete {
3337     my $self = shift;
3338     unless ( $self->QueueObj->Lifecycle->IsValid('deleted') ) {
3339         return (0, $self->loc('Delete operation is disabled by lifecycle configuration') ); #loc
3340     }
3341     return ( $self->SetStatus('deleted') );
3342 }
3343
3344
3345 =head2 SetTold ISO  [TIMETAKEN]
3346
3347 Updates the told and records a transaction
3348
3349 =cut
3350
3351 sub SetTold {
3352     my $self = shift;
3353     my $told;
3354     $told = shift if (@_);
3355     my $timetaken = shift || 0;
3356
3357     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3358         return ( 0, $self->loc("Permission Denied") );
3359     }
3360
3361     my $datetold = RT::Date->new( $self->CurrentUser );
3362     if ($told) {
3363         $datetold->Set( Format => 'iso',
3364                         Value  => $told );
3365     }
3366     else {
3367         $datetold->SetToNow();
3368     }
3369
3370     return ( $self->_Set( Field           => 'Told',
3371                           Value           => $datetold->ISO,
3372                           TimeTaken       => $timetaken,
3373                           TransactionType => 'Told' ) );
3374 }
3375
3376 =head2 _SetTold
3377
3378 Updates the told without a transaction or acl check. Useful when we're sending replies.
3379
3380 =cut
3381
3382 sub _SetTold {
3383     my $self = shift;
3384
3385     my $now = RT::Date->new( $self->CurrentUser );
3386     $now->SetToNow();
3387
3388     #use __Set to get no ACLs ;)
3389     return ( $self->__Set( Field => 'Told',
3390                            Value => $now->ISO ) );
3391 }
3392
3393 =head2 SeenUpTo
3394
3395
3396 =cut
3397
3398 sub SeenUpTo {
3399     my $self = shift;
3400     my $uid = $self->CurrentUser->id;
3401     my $attr = $self->FirstAttribute( "User-". $uid ."-SeenUpTo" );
3402     return if $attr && $attr->Content gt $self->LastUpdated;
3403
3404     my $txns = $self->Transactions;
3405     $txns->Limit( FIELD => 'Type', VALUE => 'Comment' );
3406     $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
3407     $txns->Limit( FIELD => 'Creator', OPERATOR => '!=', VALUE => $uid );
3408     $txns->Limit(
3409         FIELD => 'Created',
3410         OPERATOR => '>',
3411         VALUE => $attr->Content
3412     ) if $attr;
3413     $txns->RowsPerPage(1);
3414     return $txns->First;
3415 }
3416
3417 =head2 RanTransactionBatch
3418
3419 Acts as a guard around running TransactionBatch scrips.
3420
3421 Should be false until you enter the code that runs TransactionBatch scrips
3422
3423 Accepts an optional argument to indicate that TransactionBatch Scrips should no longer be run on this object.
3424
3425 =cut
3426
3427 sub RanTransactionBatch {
3428     my $self = shift;
3429     my $val = shift;
3430
3431     if ( defined $val ) {
3432         return $self->{_RanTransactionBatch} = $val;
3433     } else {
3434         return $self->{_RanTransactionBatch};
3435     }
3436
3437 }
3438
3439
3440 =head2 TransactionBatch
3441
3442 Returns an array reference of all transactions created on this ticket during
3443 this ticket object's lifetime or since last application of a batch, or undef
3444 if there were none.
3445
3446 Only works when the C<UseTransactionBatch> config option is set to true.
3447
3448 =cut
3449
3450 sub TransactionBatch {
3451     my $self = shift;
3452     return $self->{_TransactionBatch};
3453 }
3454
3455 =head2 ApplyTransactionBatch
3456
3457 Applies scrips on the current batch of transactions and shinks it. Usually
3458 batch is applied when object is destroyed, but in some cases it's too late.
3459
3460 =cut
3461
3462 sub ApplyTransactionBatch {
3463     my $self = shift;
3464
3465     my $batch = $self->TransactionBatch;
3466     return unless $batch && @$batch;
3467
3468     $self->_ApplyTransactionBatch;
3469
3470     $self->{_TransactionBatch} = [];
3471 }
3472
3473 sub _ApplyTransactionBatch {
3474     my $self = shift;
3475
3476     return if $self->RanTransactionBatch;
3477     $self->RanTransactionBatch(1);
3478
3479     my $still_exists = RT::Ticket->new( RT->SystemUser );
3480     $still_exists->Load( $self->Id );
3481     if (not $still_exists->Id) {
3482         # The ticket has been removed from the database, but we still
3483         # have pending TransactionBatch txns for it.  Unfortunately,
3484         # because it isn't in the DB anymore, attempting to run scrips
3485         # on it may produce unpredictable results; simply drop the
3486         # batched transactions.
3487         $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.");
3488         return;
3489     }
3490
3491     my $batch = $self->TransactionBatch;
3492
3493     my %seen;
3494     my $types = join ',', grep !$seen{$_}++, grep defined, map $_->__Value('Type'), grep defined, @{$batch};
3495
3496     require RT::Scrips;
3497     RT::Scrips->new(RT->SystemUser)->Apply(
3498         Stage          => 'TransactionBatch',
3499         TicketObj      => $self,
3500         TransactionObj => $batch->[0],
3501         Type           => $types,
3502     );
3503
3504     # Entry point of the rule system
3505     my $rules = RT::Ruleset->FindAllRules(
3506         Stage          => 'TransactionBatch',
3507         TicketObj      => $self,
3508         TransactionObj => $batch->[0],
3509         Type           => $types,
3510     );
3511     RT::Ruleset->CommitRules($rules);
3512 }
3513
3514 sub DESTROY {
3515     my $self = shift;
3516
3517     # DESTROY methods need to localize $@, or it may unset it.  This
3518     # causes $m->abort to not bubble all of the way up.  See perlbug
3519     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3520     local $@;
3521
3522     # The following line eliminates reentrancy.
3523     # It protects against the fact that perl doesn't deal gracefully
3524     # when an object's refcount is changed in its destructor.
3525     return if $self->{_Destroyed}++;
3526
3527     if (in_global_destruction()) {
3528        unless ($ENV{'HARNESS_ACTIVE'}) {
3529             warn "Too late to safely run transaction-batch scrips!"
3530                 ." This is typically caused by using ticket objects"
3531                 ." at the top-level of a script which uses the RT API."
3532                ." Be sure to explicitly undef such ticket objects,"
3533                 ." or put them inside of a lexical scope.";
3534         }
3535         return;
3536     }
3537
3538     return $self->ApplyTransactionBatch;
3539 }
3540
3541
3542
3543
3544 sub _OverlayAccessible {
3545     {
3546         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3547           Queue           => { 'read' => 1,  'write' => 1 },
3548           Requestors      => { 'read' => 1,  'write' => 1 },
3549           Owner           => { 'read' => 1,  'write' => 1 },
3550           Subject         => { 'read' => 1,  'write' => 1 },
3551           InitialPriority => { 'read' => 1,  'write' => 1 },
3552           FinalPriority   => { 'read' => 1,  'write' => 1 },
3553           Priority        => { 'read' => 1,  'write' => 1 },
3554           Status          => { 'read' => 1,  'write' => 1 },
3555           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3556           TimeWorked      => { 'read' => 1,  'write' => 1 },
3557           TimeLeft        => { 'read' => 1,  'write' => 1 },
3558           Told            => { 'read' => 1,  'write' => 1 },
3559           Resolved        => { 'read' => 1 },
3560           Type            => { 'read' => 1 },
3561           Starts        => { 'read' => 1, 'write' => 1 },
3562           Started       => { 'read' => 1, 'write' => 1 },
3563           Due           => { 'read' => 1, 'write' => 1 },
3564           Creator       => { 'read' => 1, 'auto'  => 1 },
3565           Created       => { 'read' => 1, 'auto'  => 1 },
3566           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3567           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3568     };
3569
3570 }
3571
3572
3573
3574 sub _Set {
3575     my $self = shift;
3576
3577     my %args = ( Field             => undef,
3578                  Value             => undef,
3579                  TimeTaken         => 0,
3580                  RecordTransaction => 1,
3581                  UpdateTicket      => 1,
3582                  CheckACL          => 1,
3583                  TransactionType   => 'Set',
3584                  @_ );
3585
3586     if ($args{'CheckACL'}) {
3587       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3588           return ( 0, $self->loc("Permission Denied"));
3589       }
3590    }
3591
3592     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3593         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3594         return(0, $self->loc("Internal Error"));
3595     }
3596
3597     #if the user is trying to modify the record
3598
3599     #Take care of the old value we really don't want to get in an ACL loop.
3600     # so ask the super::_Value
3601     my $Old = $self->SUPER::_Value("$args{'Field'}");
3602     
3603     my ($ret, $msg);
3604     if ( $args{'UpdateTicket'}  ) {
3605
3606         #Set the new value
3607         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3608                                                 Value => $args{'Value'} );
3609     
3610         #If we can't actually set the field to the value, don't record
3611         # a transaction. instead, get out of here.
3612         return ( 0, $msg ) unless $ret;
3613     }
3614
3615     if ( $args{'RecordTransaction'} == 1 ) {
3616
3617         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3618                                                Type => $args{'TransactionType'},
3619                                                Field     => $args{'Field'},
3620                                                NewValue  => $args{'Value'},
3621                                                OldValue  => $Old,
3622                                                TimeTaken => $args{'TimeTaken'},
3623         );
3624         # Ensure that we can read the transaction, even if the change
3625         # just made the ticket unreadable to us
3626         $TransObj->{ _object_is_readable } = 1;
3627         return ( $Trans, scalar $TransObj->BriefDescription );
3628     }
3629     else {
3630         return ( $ret, $msg );
3631     }
3632 }
3633
3634
3635
3636 =head2 _Value
3637
3638 Takes the name of a table column.
3639 Returns its value as a string, if the user passes an ACL check
3640
3641 =cut
3642
3643 sub _Value {
3644
3645     my $self  = shift;
3646     my $field = shift;
3647
3648     #if the field is public, return it.
3649     if ( $self->_Accessible( $field, 'public' ) ) {
3650
3651         #$RT::Logger->debug("Skipping ACL check for $field");
3652         return ( $self->SUPER::_Value($field) );
3653
3654     }
3655
3656     #If the current user doesn't have ACLs, don't let em at it.  
3657
3658     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3659         return (undef);
3660     }
3661     return ( $self->SUPER::_Value($field) );
3662
3663 }
3664
3665
3666
3667 =head2 _UpdateTimeTaken
3668
3669 This routine will increment the timeworked counter. it should
3670 only be called from _NewTransaction 
3671
3672 =cut
3673
3674 sub _UpdateTimeTaken {
3675     my $self    = shift;
3676     my $Minutes = shift;
3677     my ($Total);
3678
3679     $Total = $self->SUPER::_Value("TimeWorked");
3680     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3681     $self->SUPER::_Set(
3682         Field => "TimeWorked",
3683         Value => $Total
3684     );
3685
3686     return ($Total);
3687 }
3688
3689
3690
3691
3692
3693 =head2 CurrentUserHasRight
3694
3695   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3696 1 if the user has that right. It returns 0 if the user doesn't have that right.
3697
3698 =cut
3699
3700 sub CurrentUserHasRight {
3701     my $self  = shift;
3702     my $right = shift;
3703
3704     return $self->CurrentUser->PrincipalObj->HasRight(
3705         Object => $self,
3706         Right  => $right,
3707     )
3708 }
3709
3710
3711 =head2 CurrentUserCanSee
3712
3713 Returns true if the current user can see the ticket, using ShowTicket
3714
3715 =cut
3716
3717 sub CurrentUserCanSee {
3718     my $self = shift;
3719     return $self->CurrentUserHasRight('ShowTicket');
3720 }
3721
3722 =head2 HasRight
3723
3724  Takes a paramhash with the attributes 'Right' and 'Principal'
3725   'Right' is a ticket-scoped textual right from RT::ACE 
3726   'Principal' is an RT::User object
3727
3728   Returns 1 if the principal has the right. Returns undef if not.
3729
3730 =cut
3731
3732 sub HasRight {
3733     my $self = shift;
3734     my %args = (
3735         Right     => undef,
3736         Principal => undef,
3737         @_
3738     );
3739
3740     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3741     {
3742         Carp::cluck("Principal attrib undefined for Ticket::HasRight");
3743         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3744         return(undef);
3745     }
3746
3747     return (
3748         $args{'Principal'}->HasRight(
3749             Object => $self,
3750             Right     => $args{'Right'}
3751           )
3752     );
3753 }
3754
3755
3756
3757 =head2 Reminders
3758
3759 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3760 It isn't acutally a searchbuilder collection itself.
3761
3762 =cut
3763
3764 sub Reminders {
3765     my $self = shift;
3766     
3767     unless ($self->{'__reminders'}) {
3768         $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3769         $self->{'__reminders'}->Ticket($self->id);
3770     }
3771     return $self->{'__reminders'};
3772
3773 }
3774
3775
3776
3777
3778 =head2 Transactions
3779
3780   Returns an RT::Transactions object of all transactions on this ticket
3781
3782 =cut
3783
3784 sub Transactions {
3785     my $self = shift;
3786
3787     my $transactions = RT::Transactions->new( $self->CurrentUser );
3788
3789     #If the user has no rights, return an empty object
3790     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3791         $transactions->LimitToTicket($self->id);
3792
3793         # if the user may not see comments do not return them
3794         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3795             $transactions->Limit(
3796                 SUBCLAUSE => 'acl',
3797                 FIELD    => 'Type',
3798                 OPERATOR => '!=',
3799                 VALUE    => "Comment"
3800             );
3801             $transactions->Limit(
3802                 SUBCLAUSE => 'acl',
3803                 FIELD    => 'Type',
3804                 OPERATOR => '!=',
3805                 VALUE    => "CommentEmailRecord",
3806                 ENTRYAGGREGATOR => 'AND'
3807             );
3808
3809         }
3810     } else {
3811         $transactions->Limit(
3812             SUBCLAUSE => 'acl',
3813             FIELD    => 'id',
3814             VALUE    => 0,
3815             ENTRYAGGREGATOR => 'AND'
3816         );
3817     }
3818
3819     return ($transactions);
3820 }
3821
3822
3823
3824
3825 =head2 TransactionCustomFields
3826
3827     Returns the custom fields that transactions on tickets will have.
3828
3829 =cut
3830
3831 sub TransactionCustomFields {
3832     my $self = shift;
3833     my $cfs = $self->QueueObj->TicketTransactionCustomFields;
3834     $cfs->SetContextObject( $self );
3835     return $cfs;
3836 }
3837
3838
3839 =head2 LoadCustomFieldByIdentifier
3840
3841 Finds and returns the custom field of the given name for the ticket,
3842 overriding L<RT::Record/LoadCustomFieldByIdentifier> to look for
3843 queue-specific CFs before global ones.
3844
3845 =cut
3846
3847 sub LoadCustomFieldByIdentifier {
3848     my $self  = shift;
3849     my $field = shift;
3850
3851     return $self->SUPER::LoadCustomFieldByIdentifier($field)
3852         if ref $field or $field =~ /^\d+$/;
3853
3854     my $cf = RT::CustomField->new( $self->CurrentUser );
3855     $cf->SetContextObject( $self );
3856     $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3857     $cf->LoadByNameAndQueue( Name => $field, Queue => 0 ) unless $cf->id;
3858     return $cf;
3859 }
3860
3861
3862 =head2 CustomFieldLookupType
3863
3864 Returns the RT::Ticket lookup type, which can be passed to 
3865 RT::CustomField->Create() via the 'LookupType' hash key.
3866
3867 =cut
3868
3869
3870 sub CustomFieldLookupType {
3871     "RT::Queue-RT::Ticket";
3872 }
3873
3874 =head2 ACLEquivalenceObjects
3875
3876 This method returns a list of objects for which a user's rights also apply
3877 to this ticket. Generally, this is only the ticket's queue, but some RT 
3878 extensions may make other objects available too.
3879
3880 This method is called from L<RT::Principal/HasRight>.
3881
3882 =cut
3883
3884 sub ACLEquivalenceObjects {
3885     my $self = shift;
3886     return $self->QueueObj;
3887
3888 }
3889
3890
3891 1;
3892
3893 =head1 AUTHOR
3894
3895 Jesse Vincent, jesse@bestpractical.com
3896
3897 =head1 SEE ALSO
3898
3899 RT
3900
3901 =cut
3902
3903
3904 use RT::Queue;
3905 use base 'RT::Record';
3906
3907 sub Table {'Tickets'}
3908
3909
3910
3911
3912
3913
3914 =head2 id
3915
3916 Returns the current value of id.
3917 (In the database, id is stored as int(11).)
3918
3919
3920 =cut
3921
3922
3923 =head2 EffectiveId
3924
3925 Returns the current value of EffectiveId.
3926 (In the database, EffectiveId is stored as int(11).)
3927
3928
3929
3930 =head2 SetEffectiveId VALUE
3931
3932
3933 Set EffectiveId to VALUE.
3934 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3935 (In the database, EffectiveId will be stored as a int(11).)
3936
3937
3938 =cut
3939
3940
3941 =head2 Queue
3942
3943 Returns the current value of Queue.
3944 (In the database, Queue is stored as int(11).)
3945
3946
3947
3948 =head2 SetQueue VALUE
3949
3950
3951 Set Queue to VALUE.
3952 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3953 (In the database, Queue will be stored as a int(11).)
3954
3955
3956 =cut
3957
3958
3959 =head2 Type
3960
3961 Returns the current value of Type.
3962 (In the database, Type is stored as varchar(16).)
3963
3964
3965
3966 =head2 SetType VALUE
3967
3968
3969 Set Type to VALUE.
3970 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3971 (In the database, Type will be stored as a varchar(16).)
3972
3973
3974 =cut
3975
3976
3977 =head2 IssueStatement
3978
3979 Returns the current value of IssueStatement.
3980 (In the database, IssueStatement is stored as int(11).)
3981
3982
3983
3984 =head2 SetIssueStatement VALUE
3985
3986
3987 Set IssueStatement to VALUE.
3988 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
3989 (In the database, IssueStatement will be stored as a int(11).)
3990
3991
3992 =cut
3993
3994
3995 =head2 Resolution
3996
3997 Returns the current value of Resolution.
3998 (In the database, Resolution is stored as int(11).)
3999
4000
4001
4002 =head2 SetResolution VALUE
4003
4004
4005 Set Resolution to VALUE.
4006 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4007 (In the database, Resolution will be stored as a int(11).)
4008
4009
4010 =cut
4011
4012
4013 =head2 Owner
4014
4015 Returns the current value of Owner.
4016 (In the database, Owner is stored as int(11).)
4017
4018
4019
4020 =head2 SetOwner VALUE
4021
4022
4023 Set Owner to VALUE.
4024 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4025 (In the database, Owner will be stored as a int(11).)
4026
4027
4028 =cut
4029
4030
4031 =head2 Subject
4032
4033 Returns the current value of Subject.
4034 (In the database, Subject is stored as varchar(200).)
4035
4036
4037
4038 =head2 SetSubject VALUE
4039
4040
4041 Set Subject to VALUE.
4042 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4043 (In the database, Subject will be stored as a varchar(200).)
4044
4045
4046 =cut
4047
4048
4049 =head2 InitialPriority
4050
4051 Returns the current value of InitialPriority.
4052 (In the database, InitialPriority is stored as int(11).)
4053
4054
4055
4056 =head2 SetInitialPriority VALUE
4057
4058
4059 Set InitialPriority to VALUE.
4060 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4061 (In the database, InitialPriority will be stored as a int(11).)
4062
4063
4064 =cut
4065
4066
4067 =head2 FinalPriority
4068
4069 Returns the current value of FinalPriority.
4070 (In the database, FinalPriority is stored as int(11).)
4071
4072
4073
4074 =head2 SetFinalPriority VALUE
4075
4076
4077 Set FinalPriority to VALUE.
4078 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4079 (In the database, FinalPriority will be stored as a int(11).)
4080
4081
4082 =cut
4083
4084
4085 =head2 Priority
4086
4087 Returns the current value of Priority.
4088 (In the database, Priority is stored as int(11).)
4089
4090
4091
4092 =head2 SetPriority VALUE
4093
4094
4095 Set Priority to VALUE.
4096 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4097 (In the database, Priority will be stored as a int(11).)
4098
4099
4100 =cut
4101
4102
4103 =head2 TimeEstimated
4104
4105 Returns the current value of TimeEstimated.
4106 (In the database, TimeEstimated is stored as int(11).)
4107
4108
4109
4110 =head2 SetTimeEstimated VALUE
4111
4112
4113 Set TimeEstimated to VALUE.
4114 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4115 (In the database, TimeEstimated will be stored as a int(11).)
4116
4117
4118 =cut
4119
4120
4121 =head2 TimeWorked
4122
4123 Returns the current value of TimeWorked.
4124 (In the database, TimeWorked is stored as int(11).)
4125
4126
4127
4128 =head2 SetTimeWorked VALUE
4129
4130
4131 Set TimeWorked to VALUE.
4132 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4133 (In the database, TimeWorked will be stored as a int(11).)
4134
4135
4136 =cut
4137
4138
4139 =head2 Status
4140
4141 Returns the current value of Status.
4142 (In the database, Status is stored as varchar(64).)
4143
4144
4145
4146 =head2 SetStatus VALUE
4147
4148
4149 Set Status to VALUE.
4150 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4151 (In the database, Status will be stored as a varchar(64).)
4152
4153
4154 =cut
4155
4156
4157 =head2 TimeLeft
4158
4159 Returns the current value of TimeLeft.
4160 (In the database, TimeLeft is stored as int(11).)
4161
4162
4163
4164 =head2 SetTimeLeft VALUE
4165
4166
4167 Set TimeLeft to VALUE.
4168 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4169 (In the database, TimeLeft will be stored as a int(11).)
4170
4171
4172 =cut
4173
4174
4175 =head2 Told
4176
4177 Returns the current value of Told.
4178 (In the database, Told is stored as datetime.)
4179
4180
4181
4182 =head2 SetTold VALUE
4183
4184
4185 Set Told to VALUE.
4186 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4187 (In the database, Told will be stored as a datetime.)
4188
4189
4190 =cut
4191
4192
4193 =head2 Starts
4194
4195 Returns the current value of Starts.
4196 (In the database, Starts is stored as datetime.)
4197
4198
4199
4200 =head2 SetStarts VALUE
4201
4202
4203 Set Starts to VALUE.
4204 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4205 (In the database, Starts will be stored as a datetime.)
4206
4207
4208 =cut
4209
4210
4211 =head2 Started
4212
4213 Returns the current value of Started.
4214 (In the database, Started is stored as datetime.)
4215
4216
4217
4218 =head2 SetStarted VALUE
4219
4220
4221 Set Started to VALUE.
4222 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4223 (In the database, Started will be stored as a datetime.)
4224
4225
4226 =cut
4227
4228
4229 =head2 Due
4230
4231 Returns the current value of Due.
4232 (In the database, Due is stored as datetime.)
4233
4234
4235
4236 =head2 SetDue VALUE
4237
4238
4239 Set Due to VALUE.
4240 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4241 (In the database, Due will be stored as a datetime.)
4242
4243
4244 =cut
4245
4246
4247 =head2 Resolved
4248
4249 Returns the current value of Resolved.
4250 (In the database, Resolved is stored as datetime.)
4251
4252
4253
4254 =head2 SetResolved VALUE
4255
4256
4257 Set Resolved to VALUE.
4258 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4259 (In the database, Resolved will be stored as a datetime.)
4260
4261
4262 =cut
4263
4264
4265 =head2 LastUpdatedBy
4266
4267 Returns the current value of LastUpdatedBy.
4268 (In the database, LastUpdatedBy is stored as int(11).)
4269
4270
4271 =cut
4272
4273
4274 =head2 LastUpdated
4275
4276 Returns the current value of LastUpdated.
4277 (In the database, LastUpdated is stored as datetime.)
4278
4279
4280 =cut
4281
4282
4283 =head2 Creator
4284
4285 Returns the current value of Creator.
4286 (In the database, Creator is stored as int(11).)
4287
4288
4289 =cut
4290
4291
4292 =head2 Created
4293
4294 Returns the current value of Created.
4295 (In the database, Created is stored as datetime.)
4296
4297
4298 =cut
4299
4300
4301 =head2 Disabled
4302
4303 Returns the current value of Disabled.
4304 (In the database, Disabled is stored as smallint(6).)
4305
4306
4307
4308 =head2 SetDisabled VALUE
4309
4310
4311 Set Disabled to VALUE.
4312 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
4313 (In the database, Disabled will be stored as a smallint(6).)
4314
4315
4316 =cut
4317
4318
4319
4320 sub _CoreAccessible {
4321     {
4322
4323         id =>
4324                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
4325         EffectiveId =>
4326                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4327         Queue =>
4328                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4329         Type =>
4330                 {read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
4331         IssueStatement =>
4332                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4333         Resolution =>
4334                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4335         Owner =>
4336                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4337         Subject =>
4338                 {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => '[no subject]'},
4339         InitialPriority =>
4340                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4341         FinalPriority =>
4342                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4343         Priority =>
4344                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4345         TimeEstimated =>
4346                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4347         TimeWorked =>
4348                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4349         Status =>
4350                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
4351         TimeLeft =>
4352                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4353         Told =>
4354                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4355         Starts =>
4356                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4357         Started =>
4358                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4359         Due =>
4360                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4361         Resolved =>
4362                 {read => 1, write => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4363         LastUpdatedBy =>
4364                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4365         LastUpdated =>
4366                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4367         Creator =>
4368                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
4369         Created =>
4370                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
4371         Disabled =>
4372                 {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
4373
4374  }
4375 };
4376
4377 RT::Base->_ImportOverlays();
4378
4379 1;