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