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