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