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