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