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