hopefully, finally fix ticket auto-association not getting along with RT ACLs
[freeside.git] / rt / lib / RT / Ticket_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2
3 # COPYRIGHT:
4 #  
5 # This software is Copyright (c) 1996-2007 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/copyleft/gpl.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     # {{{ Deal with auto-customer association
720
721     #unless we already have (a) customer(s)...
722     unless ( $self->Customers->Count ) {
723
724       #first find any requestors with emails but *without* customer targets
725       my @NoCust_Requestors =
726         grep { $_->EmailAddress && ! $_->Customers->Count }
727              @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
728
729       for my $Requestor (@NoCust_Requestors) {
730
731          #perhaps the stuff in here should be in a User method??
732          my @Customers =
733            &RT::URI::freeside::email_search( email=>$Requestor->EmailAddress );
734
735          foreach my $custnum ( map $_->{'custnum'}, @Customers ) {
736
737            ## false laziness w/RT/Interface/Web_Vendor.pm
738            my @link = ( 'Type'   => 'MemberOf',
739                         'Target' => "freeside://freeside/cust_main/$custnum",
740                       );
741
742            my( $val, $msg ) = $Requestor->_AddLink(@link);
743            #XXX should do something with $msg# push @non_fatal_errors, $msg;
744
745          }
746
747       }
748
749       #find any requestors with customer targets
750   
751       my %cust_target = ();
752   
753       my @Requestors =
754         grep { $_->Customers->Count }
755              @{ $self->_Requestors->UserMembersObj->ItemsArrayRef };
756   
757       foreach my $Requestor ( @Requestors ) {
758         foreach my $cust_link ( @{ $Requestor->Customers->ItemsArrayRef } ) {
759           $cust_target{ $cust_link->Target } = 1;
760         }
761       }
762   
763       #and then auto-associate this ticket with those customers
764   
765       foreach my $cust_target ( keys %cust_target ) {
766   
767         my @link = ( 'Type'   => 'MemberOf',
768                      #'Target' => "freeside://freeside/cust_main/$custnum",
769                      'Target' => $cust_target,
770                    );
771   
772         my( $val, $msg ) = $self->_AddLink(@link);
773         push @non_fatal_errors, $msg;
774   
775       }
776
777     }
778
779     # }}}
780
781     # {{{ Add all the custom fields
782
783     foreach my $arg ( keys %args ) {
784         next unless ( $arg =~ /^CustomField-(\d+)$/i );
785         my $cfid = $1;
786         foreach
787           my $value ( UNIVERSAL::isa( $args{$arg} => 'ARRAY' ) ? @{ $args{$arg} } : ( $args{$arg} ) )
788         {
789             next unless ( length($value) );
790
791             # Allow passing in uploaded LargeContent etc by hash reference
792             $self->_AddCustomFieldValue(
793                 (UNIVERSAL::isa( $value => 'HASH' )
794                     ? %$value
795                     : (Value => $value)
796                 ),
797                 Field             => $cfid,
798                 RecordTransaction => 0,
799             );
800         }
801     }
802
803     # }}}
804
805     if ( $args{'_RecordTransaction'} ) {
806
807         # {{{ Add a transaction for the create
808         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
809                                                      Type      => "Create",
810                                                      TimeTaken => $args{'TimeWorked'},
811                                                      MIMEObj => $args{'MIMEObj'}
812         );
813
814         if ( $self->Id && $Trans ) {
815
816             $TransObj->UpdateCustomFields(ARGSRef => \%args);
817
818             $RT::Logger->info( "Ticket " . $self->Id . " created in queue '" . $QueueObj->Name . "' by " . $self->CurrentUser->Name );
819             $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
820             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
821         }
822         else {
823             $RT::Handle->Rollback();
824
825             $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
826             $RT::Logger->error("Ticket couldn't be created: $ErrStr");
827             return ( 0, 0, $self->loc( "Ticket could not be created due to an internal error"));
828         }
829
830         $RT::Handle->Commit();
831         return ( $self->Id, $TransObj->Id, $ErrStr );
832
833         # }}}
834     }
835     else {
836
837         # Not going to record a transaction
838         $RT::Handle->Commit();
839         $ErrStr = $self->loc( "Ticket [_1] created in queue '[_2]'", $self->Id, $QueueObj->Name );
840         $ErrStr = join( "\n", $ErrStr, @non_fatal_errors );
841         return ( $self->Id, 0, $ErrStr );
842
843     }
844 }
845
846
847 # }}}
848
849
850 # {{{ UpdateFrom822 
851
852 =head2 UpdateFrom822 $MESSAGE
853
854 Takes an RFC822 format message as a string and uses it to make a bunch of changes to a ticket.
855 Returns an um. ask me again when the code exists
856
857
858 =begin testing
859
860 my $simple_update = <<EOF;
861 Subject: target
862 AddRequestor: jesse\@example.com
863 EOF
864
865 my $ticket = RT::Ticket->new($RT::SystemUser);
866 my ($id,$msg) =$ticket->Create(Subject => 'first', Queue => 'general');
867 ok($ticket->Id, "Created the test ticket - ".$id ." - ".$msg);
868 $ticket->UpdateFrom822($simple_update);
869 is($ticket->Subject, 'target', "changed the subject");
870 my $jesse = RT::User->new($RT::SystemUser);
871 $jesse->LoadByEmail('jesse@example.com');
872 ok ($jesse->Id, "There's a user for jesse");
873 ok($ticket->Requestors->HasMember( $jesse->PrincipalObj), "It has the jesse principal object as a requestor ");
874
875 =end testing
876
877
878 =cut
879
880 sub UpdateFrom822 {
881         my $self = shift;
882         my $content = shift;
883         my %args = $self->_Parse822HeadersForAttributes($content);
884
885         
886     my %ticketargs = (
887         Queue           => $args{'queue'},
888         Subject         => $args{'subject'},
889         Status          => $args{'status'},
890         Due             => $args{'due'},
891         Starts          => $args{'starts'},
892         Started         => $args{'started'},
893         Resolved        => $args{'resolved'},
894         Owner           => $args{'owner'},
895         Requestor       => $args{'requestor'},
896         Cc              => $args{'cc'},
897         AdminCc         => $args{'admincc'},
898         TimeWorked      => $args{'timeworked'},
899         TimeEstimated   => $args{'timeestimated'},
900         TimeLeft        => $args{'timeleft'},
901         InitialPriority => $args{'initialpriority'},
902         Priority => $args{'priority'},
903         FinalPriority   => $args{'finalpriority'},
904         Type            => $args{'type'},
905         DependsOn       => $args{'dependson'},
906         DependedOnBy    => $args{'dependedonby'},
907         RefersTo        => $args{'refersto'},
908         ReferredToBy    => $args{'referredtoby'},
909         Members         => $args{'members'},
910         MemberOf        => $args{'memberof'},
911         MIMEObj         => $args{'mimeobj'}
912     );
913
914     foreach my $type qw(Requestor Cc Admincc) {
915
916         foreach my $action ( 'Add', 'Del', '' ) {
917
918             my $lctag = lc($action) . lc($type);
919             foreach my $list ( $args{$lctag}, $args{ $lctag . 's' } ) {
920
921                 foreach my $entry ( ref($list) ? @{$list} : ($list) ) {
922                     push @{$ticketargs{ $action . $type }} , split ( /\s*,\s*/, $entry );
923                 }
924
925             }
926
927             # Todo: if we're given an explicit list, transmute it into a list of adds/deletes
928
929         }
930     }
931
932     # Add custom field entries to %ticketargs.
933     # TODO: allow named custom fields
934     map {
935         /^customfield-(\d+)$/
936           && ( $ticketargs{ "CustomField-" . $1 } = $args{$_} );
937     } keys(%args);
938
939 # for each ticket we've been told to update, iterate through the set of
940 # rfc822 headers and perform that update to the ticket.
941
942
943     # {{{ Set basic fields 
944     my @attribs = qw(
945       Subject
946       FinalPriority
947       Priority
948       TimeEstimated
949       TimeWorked
950       TimeLeft
951       Status
952       Queue
953       Type
954     );
955
956
957     # Resolve the queue from a name to a numeric id.
958     if ( $ticketargs{'Queue'} and ( $ticketargs{'Queue'} !~ /^(\d+)$/ ) ) {
959         my $tempqueue = RT::Queue->new($RT::SystemUser);
960         $tempqueue->Load( $ticketargs{'Queue'} );
961         $ticketargs{'Queue'} = $tempqueue->Id() if ( $tempqueue->id );
962     }
963
964     my @results;
965
966     foreach my $attribute (@attribs) {
967         my $value = $ticketargs{$attribute};
968
969         if ( $value ne $self->$attribute() ) {
970
971             my $method = "Set$attribute";
972             my ( $code, $msg ) = $self->$method($value);
973
974             push @results, $self->loc($attribute) . ': ' . $msg;
975
976         }
977     }
978
979     # We special case owner changing, so we can use ForceOwnerChange
980     if ( $ticketargs{'Owner'} && ( $self->Owner != $ticketargs{'Owner'} ) ) {
981         my $ChownType = "Give";
982         $ChownType = "Force" if ( $ticketargs{'ForceOwnerChange'} );
983
984         my ( $val, $msg ) = $self->SetOwner( $ticketargs{'Owner'}, $ChownType );
985         push ( @results, $msg );
986     }
987
988     # }}}
989 # Deal with setting watchers
990
991
992 # Acceptable arguments:
993 #  Requestor
994 #  Requestors
995 #  AddRequestor
996 #  AddRequestors
997 #  DelRequestor
998  
999  foreach my $type qw(Requestor Cc AdminCc) {
1000
1001         # If we've been given a number of delresses to del, do it.
1002                 foreach my $address (@{$ticketargs{'Del'.$type}}) {
1003                 my ($id, $msg) = $self->DeleteWatcher( Type => $type, Email => $address);
1004                 push (@results, $msg) ;
1005                 }
1006
1007         # If we've been given a number of addresses to add, do it.
1008                 foreach my $address (@{$ticketargs{'Add'.$type}}) {
1009                 $RT::Logger->debug("Adding $address as a $type");
1010                 my ($id, $msg) = $self->AddWatcher( Type => $type, Email => $address);
1011                 push (@results, $msg) ;
1012
1013         }
1014
1015
1016 }
1017
1018
1019 }
1020 # }}}
1021
1022 # {{{ _Parse822HeadersForAttributes Content
1023
1024 =head2 _Parse822HeadersForAttributes Content
1025
1026 Takes an RFC822 style message and parses its attributes into a hash.
1027
1028 =cut
1029
1030 sub _Parse822HeadersForAttributes {
1031     my $self    = shift;
1032     my $content = shift;
1033     my %args;
1034
1035     my @lines = ( split ( /\n/, $content ) );
1036     while ( defined( my $line = shift @lines ) ) {
1037         if ( $line =~ /^(.*?):(?:\s+(.*))?$/ ) {
1038             my $value = $2;
1039             my $tag   = lc($1);
1040
1041             $tag =~ s/-//g;
1042             if ( defined( $args{$tag} ) )
1043             {    #if we're about to get a second value, make it an array
1044                 $args{$tag} = [ $args{$tag} ];
1045             }
1046             if ( ref( $args{$tag} ) )
1047             {    #If it's an array, we want to push the value
1048                 push @{ $args{$tag} }, $value;
1049             }
1050             else {    #if there's nothing there, just set the value
1051                 $args{$tag} = $value;
1052             }
1053         } elsif ($line =~ /^$/) {
1054
1055             #TODO: this won't work, since "" isn't of the form "foo:value"
1056
1057                 while ( defined( my $l = shift @lines ) ) {
1058                     push @{ $args{'content'} }, $l;
1059                 }
1060             }
1061         
1062     }
1063
1064     foreach my $date qw(due starts started resolved) {
1065         my $dateobj = RT::Date->new($RT::SystemUser);
1066         if ( $args{$date} =~ /^\d+$/ ) {
1067             $dateobj->Set( Format => 'unix', Value => $args{$date} );
1068         }
1069         else {
1070             $dateobj->Set( Format => 'unknown', Value => $args{$date} );
1071         }
1072         $args{$date} = $dateobj->ISO;
1073     }
1074     $args{'mimeobj'} = MIME::Entity->new();
1075     $args{'mimeobj'}->build(
1076         Type => ( $args{'contenttype'} || 'text/plain' ),
1077         Data => ($args{'content'} || '')
1078     );
1079
1080     return (%args);
1081 }
1082
1083 # }}}
1084
1085 # {{{ sub Import
1086
1087 =head2 Import PARAMHASH
1088
1089 Import a ticket. 
1090 Doesn\'t create a transaction. 
1091 Doesn\'t supply queue defaults, etc.
1092
1093 Returns: TICKETID
1094
1095 =cut
1096
1097 sub Import {
1098     my $self = shift;
1099     my ( $ErrStr, $QueueObj, $Owner );
1100
1101     my %args = (
1102         id              => undef,
1103         EffectiveId     => undef,
1104         Queue           => undef,
1105         Requestor       => undef,
1106         Type            => 'ticket',
1107         Owner           => $RT::Nobody->Id,
1108         Subject         => '[no subject]',
1109         InitialPriority => undef,
1110         FinalPriority   => undef,
1111         Status          => 'new',
1112         TimeWorked      => "0",
1113         Due             => undef,
1114         Created         => undef,
1115         Updated         => undef,
1116         Resolved        => undef,
1117         Told            => undef,
1118         @_
1119     );
1120
1121     if ( ( defined( $args{'Queue'} ) ) && ( !ref( $args{'Queue'} ) ) ) {
1122         $QueueObj = RT::Queue->new($RT::SystemUser);
1123         $QueueObj->Load( $args{'Queue'} );
1124
1125         #TODO error check this and return 0 if it\'s not loading properly +++
1126     }
1127     elsif ( ref( $args{'Queue'} ) eq 'RT::Queue' ) {
1128         $QueueObj = RT::Queue->new($RT::SystemUser);
1129         $QueueObj->Load( $args{'Queue'}->Id );
1130     }
1131     else {
1132         $RT::Logger->debug(
1133             "$self " . $args{'Queue'} . " not a recognised queue object." );
1134     }
1135
1136     #Can't create a ticket without a queue.
1137     unless ( defined($QueueObj) and $QueueObj->Id ) {
1138         $RT::Logger->debug("$self No queue given for ticket creation.");
1139         return ( 0, $self->loc('Could not create ticket. Queue not set') );
1140     }
1141
1142     #Now that we have a queue, Check the ACLS
1143     unless (
1144         $self->CurrentUser->HasRight(
1145             Right    => 'CreateTicket',
1146             Object => $QueueObj
1147         )
1148       )
1149     {
1150         return ( 0,
1151             $self->loc("No permission to create tickets in the queue '[_1]'"
1152               , $QueueObj->Name));
1153     }
1154
1155     # {{{ Deal with setting the owner
1156
1157     # Attempt to take user object, user name or user id.
1158     # Assign to nobody if lookup fails.
1159     if ( defined( $args{'Owner'} ) ) {
1160         if ( ref( $args{'Owner'} ) ) {
1161             $Owner = $args{'Owner'};
1162         }
1163         else {
1164             $Owner = new RT::User( $self->CurrentUser );
1165             $Owner->Load( $args{'Owner'} );
1166             if ( !defined( $Owner->id ) ) {
1167                 $Owner->Load( $RT::Nobody->id );
1168             }
1169         }
1170     }
1171
1172     #If we have a proposed owner and they don't have the right 
1173     #to own a ticket, scream about it and make them not the owner
1174     if (
1175         ( defined($Owner) )
1176         and ( $Owner->Id != $RT::Nobody->Id )
1177         and (
1178             !$Owner->HasRight(
1179                 Object => $QueueObj,
1180                 Right    => 'OwnTicket'
1181             )
1182         )
1183       )
1184     {
1185
1186         $RT::Logger->warning( "$self user "
1187               . $Owner->Name . "("
1188               . $Owner->id
1189               . ") was proposed "
1190               . "as a ticket owner but has no rights to own "
1191               . "tickets in '"
1192               . $QueueObj->Name . "'\n" );
1193
1194         $Owner = undef;
1195     }
1196
1197     #If we haven't been handed a valid owner, make it nobody.
1198     unless ( defined($Owner) ) {
1199         $Owner = new RT::User( $self->CurrentUser );
1200         $Owner->Load( $RT::Nobody->UserObj->Id );
1201     }
1202
1203     # }}}
1204
1205     unless ( $self->ValidateStatus( $args{'Status'} ) ) {
1206         return ( 0, $self->loc("'[_1]' is an invalid value for status", $args{'Status'}) );
1207     }
1208
1209     $self->{'_AccessibleCache'}{Created}       = { 'read' => 1, 'write' => 1 };
1210     $self->{'_AccessibleCache'}{Creator}       = { 'read' => 1, 'auto'  => 1 };
1211     $self->{'_AccessibleCache'}{LastUpdated}   = { 'read' => 1, 'write' => 1 };
1212     $self->{'_AccessibleCache'}{LastUpdatedBy} = { 'read' => 1, 'auto'  => 1 };
1213
1214     # If we're coming in with an id, set that now.
1215     my $EffectiveId = undef;
1216     if ( $args{'id'} ) {
1217         $EffectiveId = $args{'id'};
1218
1219     }
1220
1221     my $id = $self->SUPER::Create(
1222         id              => $args{'id'},
1223         EffectiveId     => $EffectiveId,
1224         Queue           => $QueueObj->Id,
1225         Owner           => $Owner->Id,
1226         Subject         => $args{'Subject'},            # loc
1227         InitialPriority => $args{'InitialPriority'},    # loc
1228         FinalPriority   => $args{'FinalPriority'},      # loc
1229         Priority        => $args{'InitialPriority'},    # loc
1230         Status          => $args{'Status'},             # loc
1231         TimeWorked      => $args{'TimeWorked'},         # loc
1232         Type            => $args{'Type'},               # loc
1233         Created         => $args{'Created'},            # loc
1234         Told            => $args{'Told'},               # loc
1235         LastUpdated     => $args{'Updated'},            # loc
1236         Resolved        => $args{'Resolved'},           # loc
1237         Due             => $args{'Due'},                # loc
1238     );
1239
1240     # If the ticket didn't have an id
1241     # Set the ticket's effective ID now that we've created it.
1242     if ( $args{'id'} ) {
1243         $self->Load( $args{'id'} );
1244     }
1245     else {
1246         my ( $val, $msg ) =
1247           $self->__Set( Field => 'EffectiveId', Value => $id );
1248
1249         unless ($val) {
1250             $RT::Logger->err(
1251                 $self . "->Import couldn't set EffectiveId: $msg\n" );
1252         }
1253     }
1254
1255     my $create_groups_ret = $self->_CreateTicketGroups();
1256     unless ($create_groups_ret) {
1257         $RT::Logger->crit(
1258             "Couldn't create ticket groups for ticket " . $self->Id );
1259     }
1260
1261     $self->OwnerGroup->_AddMember( PrincipalId => $Owner->PrincipalId );
1262
1263     my $watcher;
1264     foreach $watcher ( @{ $args{'Cc'} } ) {
1265         $self->_AddWatcher( Type => 'Cc', Email => $watcher, Silent => 1 );
1266     }
1267     foreach $watcher ( @{ $args{'AdminCc'} } ) {
1268         $self->_AddWatcher( Type => 'AdminCc', Email => $watcher,
1269             Silent => 1 );
1270     }
1271     foreach $watcher ( @{ $args{'Requestor'} } ) {
1272         $self->_AddWatcher( Type => 'Requestor', Email => $watcher,
1273             Silent => 1 );
1274     }
1275
1276     return ( $self->Id, $ErrStr );
1277 }
1278
1279 # }}}
1280
1281 # {{{ Routines dealing with watchers.
1282
1283 # {{{ _CreateTicketGroups 
1284
1285 =head2 _CreateTicketGroups
1286
1287 Create the ticket groups and links for this ticket. 
1288 This routine expects to be called from Ticket->Create _inside of a transaction_
1289
1290 It will create four groups for this ticket: Requestor, Cc, AdminCc and Owner.
1291
1292 It will return true on success and undef on failure.
1293
1294 =begin testing
1295
1296 my $ticket = RT::Ticket->new($RT::SystemUser);
1297 my ($id, $msg) = $ticket->Create(Subject => "Foo",
1298                 Owner => $RT::SystemUser->Id,
1299                 Status => 'open',
1300                 Requestor => ['jesse@example.com'],
1301                 Queue => '1'
1302                 );
1303 ok ($id, "Ticket $id was created");
1304 ok(my $group = RT::Group->new($RT::SystemUser));
1305 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Requestor'));
1306 ok ($group->Id, "Found the requestors object for this ticket");
1307
1308 ok(my $jesse = RT::User->new($RT::SystemUser), "Creating a jesse rt::user");
1309 $jesse->LoadByEmail('jesse@example.com');
1310 ok($jesse->Id,  "Found the jesse rt user");
1311
1312
1313 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $jesse->PrincipalId), "The ticket actually has jesse at fsck.com as a requestor");
1314 ok ((my $add_id, $add_msg) = $ticket->AddWatcher(Type => 'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1315 ok ($add_id, "Add succeeded: ($add_msg)");
1316 ok(my $bob = RT::User->new($RT::SystemUser), "Creating a bob rt::user");
1317 $bob->LoadByEmail('bob@fsck.com');
1318 ok($bob->Id,  "Found the bob rt user");
1319 ok ($ticket->IsWatcher(Type => 'Requestor', PrincipalId => $bob->PrincipalId), "The ticket actually has bob at fsck.com as a requestor");;
1320 ok ((my $add_id, $add_msg) = $ticket->DeleteWatcher(Type =>'Requestor', Email => 'bob@fsck.com'), "Added bob at fsck.com as a requestor");
1321 ok (!$ticket->IsWatcher(Type => 'Requestor', Principal => $bob->PrincipalId), "The ticket no longer has bob at fsck.com as a requestor");;
1322
1323
1324 $group = RT::Group->new($RT::SystemUser);
1325 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Cc'));
1326 ok ($group->Id, "Found the cc object for this ticket");
1327 $group = RT::Group->new($RT::SystemUser);
1328 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'AdminCc'));
1329 ok ($group->Id, "Found the AdminCc object for this ticket");
1330 $group = RT::Group->new($RT::SystemUser);
1331 ok($group->LoadTicketRoleGroup(Ticket => $id, Type=> 'Owner'));
1332 ok ($group->Id, "Found the Owner object for this ticket");
1333 ok($group->HasMember($RT::SystemUser->UserObj->PrincipalObj), "the owner group has the member 'RT_System'");
1334
1335 =end testing
1336
1337 =cut
1338
1339
1340 sub _CreateTicketGroups {
1341     my $self = shift;
1342     
1343     my @types = qw(Requestor Owner Cc AdminCc);
1344
1345     foreach my $type (@types) {
1346         my $type_obj = RT::Group->new($self->CurrentUser);
1347         my ($id, $msg) = $type_obj->CreateRoleGroup(Domain => 'RT::Ticket-Role',
1348                                                        Instance => $self->Id, 
1349                                                        Type => $type);
1350         unless ($id) {
1351             $RT::Logger->error("Couldn't create a ticket group of type '$type' for ticket ".
1352                                $self->Id.": ".$msg);     
1353             return(undef);
1354         }
1355      }
1356     return(1);
1357     
1358 }
1359
1360 # }}}
1361
1362 # {{{ sub OwnerGroup
1363
1364 =head2 OwnerGroup
1365
1366 A constructor which returns an RT::Group object containing the owner of this ticket.
1367
1368 =cut
1369
1370 sub OwnerGroup {
1371     my $self = shift;
1372     my $owner_obj = RT::Group->new($self->CurrentUser);
1373     $owner_obj->LoadTicketRoleGroup( Ticket => $self->Id,  Type => 'Owner');
1374     return ($owner_obj);
1375 }
1376
1377 # }}}
1378
1379
1380 # {{{ sub AddWatcher
1381
1382 =head2 AddWatcher
1383
1384 AddWatcher takes a parameter hash. The keys are as follows:
1385
1386 Type        One of Requestor, Cc, AdminCc
1387
1388 PrinicpalId The RT::Principal id of the user or group that's being added as a watcher
1389
1390 Email       The email address of the new watcher. If a user with this 
1391             email address can't be found, a new nonprivileged user will be created.
1392
1393 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.
1394
1395 =cut
1396
1397 sub AddWatcher {
1398     my $self = shift;
1399     my %args = (
1400         Type  => undef,
1401         PrincipalId => undef,
1402         Email => undef,
1403         @_
1404     );
1405
1406     # XXX, FIXME, BUG: if only email is provided then we only check
1407     # for ModifyTicket right, but must try to get PrincipalId and
1408     # check Watch* rights too if user exist
1409
1410     # {{{ Check ACLS
1411     #If the watcher we're trying to add is for the current user
1412     if ( $self->CurrentUser->PrincipalId == ($args{'PrincipalId'} || 0)
1413        or    lc( $self->CurrentUser->UserObj->EmailAddress )
1414           eq lc( RT::User->CanonicalizeEmailAddress( $args{'Email'} ) || '' ) )
1415     {
1416         #  If it's an AdminCc and they don't have 
1417         #   'WatchAsAdminCc' or 'ModifyTicket', bail
1418         if ( $args{'Type'} eq 'AdminCc' ) {
1419             unless ( $self->CurrentUserHasRight('ModifyTicket')
1420                 or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1421                 return ( 0, $self->loc('Permission Denied'))
1422             }
1423         }
1424
1425         #  If it's a Requestor or Cc and they don't have
1426         #   'Watch' or 'ModifyTicket', bail
1427         elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) ) {
1428
1429             unless ( $self->CurrentUserHasRight('ModifyTicket')
1430                 or $self->CurrentUserHasRight('Watch') ) {
1431                 return ( 0, $self->loc('Permission Denied'))
1432             }
1433         }
1434         else {
1435             $RT::Logger->warning( "$self -> AddWatcher got passed a bogus type");
1436             return ( 0, $self->loc('Error in parameters to Ticket->AddWatcher') );
1437         }
1438     }
1439
1440     # If the watcher isn't the current user 
1441     # and the current user  doesn't have 'ModifyTicket'
1442     # bail
1443     else {
1444         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1445             return ( 0, $self->loc("Permission Denied") );
1446         }
1447     }
1448
1449     # }}}
1450
1451     return ( $self->_AddWatcher(%args) );
1452 }
1453
1454 #This contains the meat of AddWatcher. but can be called from a routine like
1455 # Create, which doesn't need the additional acl check
1456 sub _AddWatcher {
1457     my $self = shift;
1458     my %args = (
1459         Type   => undef,
1460         Silent => undef,
1461         PrincipalId => undef,
1462         Email => undef,
1463         @_
1464     );
1465
1466
1467     my $principal = RT::Principal->new($self->CurrentUser);
1468     if ($args{'Email'}) {
1469         my $user = RT::User->new($RT::SystemUser);
1470         my ($pid, $msg) = $user->LoadOrCreateByEmail($args{'Email'});
1471         # If we can't load the user by email address, let's try to load by username     
1472         unless ($pid) { 
1473                 ($pid,$msg) = $user->Load($args{'Email'})
1474         }
1475         if ($pid) {
1476             $args{'PrincipalId'} = $pid; 
1477         }
1478     }
1479     if ($args{'PrincipalId'}) {
1480         $principal->Load($args{'PrincipalId'});
1481     } 
1482
1483  
1484     # If we can't find this watcher, we need to bail.
1485     unless ($principal->Id) {
1486             $RT::Logger->error("Could not load create a user with the email address '".$args{'Email'}. "' to add as a watcher for ticket ".$self->Id);
1487         return(0, $self->loc("Could not find or create that user"));
1488     }
1489
1490
1491     my $group = RT::Group->new($self->CurrentUser);
1492     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->Id);
1493     unless ($group->id) {
1494         return(0,$self->loc("Group not found"));
1495     }
1496
1497     if ( $group->HasMember( $principal)) {
1498
1499         return ( 0, $self->loc('That principal is already a [_1] for this ticket', $self->loc($args{'Type'})) );
1500     }
1501
1502
1503     my ( $m_id, $m_msg ) = $group->_AddMember( PrincipalId => $principal->Id,
1504                                                InsideTransaction => 1 );
1505     unless ($m_id) {
1506         $RT::Logger->error("Failed to add ".$principal->Id." as a member of group ".$group->Id."\n".$m_msg);
1507
1508         return ( 0, $self->loc('Could not make that principal a [_1] for this ticket', $self->loc($args{'Type'})) );
1509     }
1510
1511     unless ( $args{'Silent'} ) {
1512         $self->_NewTransaction(
1513             Type     => 'AddWatcher',
1514             NewValue => $principal->Id,
1515             Field    => $args{'Type'}
1516         );
1517     }
1518
1519         return ( 1, $self->loc('Added principal as a [_1] for this ticket', $self->loc($args{'Type'})) );
1520 }
1521
1522 # }}}
1523
1524
1525 # {{{ sub DeleteWatcher
1526
1527 =head2 DeleteWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL_ADDRESS }
1528
1529
1530 Deletes a Ticket watcher.  Takes two arguments:
1531
1532 Type  (one of Requestor,Cc,AdminCc)
1533
1534 and one of
1535
1536 PrincipalId (an RT::Principal Id of the watcher you want to remove)
1537     OR
1538 Email (the email address of an existing wathcer)
1539
1540
1541 =cut
1542
1543
1544 sub DeleteWatcher {
1545     my $self = shift;
1546
1547     my %args = ( Type        => undef,
1548                  PrincipalId => undef,
1549                  Email       => undef,
1550                  @_ );
1551
1552     unless ( $args{'PrincipalId'} || $args{'Email'} ) {
1553         return ( 0, $self->loc("No principal specified") );
1554     }
1555     my $principal = RT::Principal->new( $self->CurrentUser );
1556     if ( $args{'PrincipalId'} ) {
1557
1558         $principal->Load( $args{'PrincipalId'} );
1559     }
1560     else {
1561         my $user = RT::User->new( $self->CurrentUser );
1562         $user->LoadByEmail( $args{'Email'} );
1563         $principal->Load( $user->Id );
1564     }
1565
1566     # If we can't find this watcher, we need to bail.
1567     unless ( $principal->Id ) {
1568         return ( 0, $self->loc("Could not find that principal") );
1569     }
1570
1571     my $group = RT::Group->new( $self->CurrentUser );
1572     $group->LoadTicketRoleGroup( Type => $args{'Type'}, Ticket => $self->Id );
1573     unless ( $group->id ) {
1574         return ( 0, $self->loc("Group not found") );
1575     }
1576
1577     # {{{ Check ACLS
1578     #If the watcher we're trying to add is for the current user
1579     if ( $self->CurrentUser->PrincipalId == $principal->id ) {
1580
1581         #  If it's an AdminCc and they don't have
1582         #   'WatchAsAdminCc' or 'ModifyTicket', bail
1583         if ( $args{'Type'} eq 'AdminCc' ) {
1584             unless (    $self->CurrentUserHasRight('ModifyTicket')
1585                      or $self->CurrentUserHasRight('WatchAsAdminCc') ) {
1586                 return ( 0, $self->loc('Permission Denied') );
1587             }
1588         }
1589
1590         #  If it's a Requestor or Cc and they don't have
1591         #   'Watch' or 'ModifyTicket', bail
1592         elsif ( ( $args{'Type'} eq 'Cc' ) or ( $args{'Type'} eq 'Requestor' ) )
1593         {
1594             unless (    $self->CurrentUserHasRight('ModifyTicket')
1595                      or $self->CurrentUserHasRight('Watch') ) {
1596                 return ( 0, $self->loc('Permission Denied') );
1597             }
1598         }
1599         else {
1600             $RT::Logger->warn("$self -> DeleteWatcher got passed a bogus type");
1601             return ( 0,
1602                      $self->loc('Error in parameters to Ticket->DeleteWatcher') );
1603         }
1604     }
1605
1606     # If the watcher isn't the current user
1607     # and the current user  doesn't have 'ModifyTicket' bail
1608     else {
1609         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1610             return ( 0, $self->loc("Permission Denied") );
1611         }
1612     }
1613
1614     # }}}
1615
1616     # see if this user is already a watcher.
1617
1618     unless ( $group->HasMember($principal) ) {
1619         return ( 0,
1620                  $self->loc( 'That principal is not a [_1] for this ticket',
1621                              $args{'Type'} ) );
1622     }
1623
1624     my ( $m_id, $m_msg ) = $group->_DeleteMember( $principal->Id );
1625     unless ($m_id) {
1626         $RT::Logger->error( "Failed to delete "
1627                             . $principal->Id
1628                             . " as a member of group "
1629                             . $group->Id . "\n"
1630                             . $m_msg );
1631
1632         return (0,
1633                 $self->loc(
1634                     'Could not remove that principal as a [_1] for this ticket',
1635                     $args{'Type'} ) );
1636     }
1637
1638     unless ( $args{'Silent'} ) {
1639         $self->_NewTransaction( Type     => 'DelWatcher',
1640                                 OldValue => $principal->Id,
1641                                 Field    => $args{'Type'} );
1642     }
1643
1644     return ( 1,
1645              $self->loc( "[_1] is no longer a [_2] for this ticket.",
1646                          $principal->Object->Name,
1647                          $args{'Type'} ) );
1648 }
1649
1650
1651
1652 # }}}
1653
1654
1655 =head2 SquelchMailTo [EMAIL]
1656
1657 Takes an optional email address to never email about updates to this ticket.
1658
1659
1660 Returns an array of the RT::Attribute objects for this ticket's 'SquelchMailTo' attributes.
1661
1662 =begin testing
1663
1664 my $t = RT::Ticket->new($RT::SystemUser);
1665 ok($t->Create(Queue => 'general', Subject => 'SquelchTest'));
1666
1667 is($#{$t->SquelchMailTo}, -1, "The ticket has no squelched recipients");
1668
1669 my @returned = $t->SquelchMailTo('nobody@example.com');
1670
1671 is($#returned, 0, "The ticket has one squelched recipients");
1672
1673 my @names = $t->Attributes->Names;
1674 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1675 @returned = $t->SquelchMailTo('nobody@example.com');
1676
1677
1678 is($#returned, 0, "The ticket has one squelched recipients");
1679
1680 @names = $t->Attributes->Names;
1681 is(shift @names, 'SquelchMailTo', "The attribute we have is SquelchMailTo");
1682
1683
1684 my ($ret, $msg) = $t->UnsquelchMailTo('nobody@example.com');
1685 ok($ret, "Removed nobody as a squelched recipient - ".$msg);
1686 @returned = $t->SquelchMailTo();
1687 is($#returned, -1, "The ticket has no squelched recipients". join(',',@returned));
1688
1689
1690 =end testing
1691
1692 =cut
1693
1694 sub SquelchMailTo {
1695     my $self = shift;
1696     if (@_) {
1697         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1698             return undef;
1699         }
1700         my $attr = shift;
1701         $self->AddAttribute( Name => 'SquelchMailTo', Content => $attr )
1702           unless grep { $_->Content eq $attr }
1703           $self->Attributes->Named('SquelchMailTo');
1704
1705     }
1706     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1707         return undef;
1708     }
1709     my @attributes = $self->Attributes->Named('SquelchMailTo');
1710     return (@attributes);
1711 }
1712
1713
1714 =head2 UnsquelchMailTo ADDRESS
1715
1716 Takes an address and removes it from this ticket's "SquelchMailTo" list. If an address appears multiple times, each instance is removed.
1717
1718 Returns a tuple of (status, message)
1719
1720 =cut
1721
1722 sub UnsquelchMailTo {
1723     my $self = shift;
1724
1725     my $address = shift;
1726     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
1727         return ( 0, $self->loc("Permission Denied") );
1728     }
1729
1730     my ($val, $msg) = $self->Attributes->DeleteEntry ( Name => 'SquelchMailTo', Content => $address);
1731     return ($val, $msg);
1732 }
1733
1734
1735 # {{{ a set of  [foo]AsString subs that will return the various sorts of watchers for a ticket/queue as a comma delineated string
1736
1737 =head2 RequestorAddresses
1738
1739  B<Returns> String: All Ticket Requestor email addresses as a string.
1740
1741 =cut
1742
1743 sub RequestorAddresses {
1744     my $self = shift;
1745
1746     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1747         return undef;
1748     }
1749
1750     return ( $self->Requestors->MemberEmailAddressesAsString );
1751 }
1752
1753
1754 =head2 AdminCcAddresses
1755
1756 returns String: All Ticket AdminCc email addresses as a string
1757
1758 =cut
1759
1760 sub AdminCcAddresses {
1761     my $self = shift;
1762
1763     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1764         return undef;
1765     }
1766
1767     return ( $self->AdminCc->MemberEmailAddressesAsString )
1768
1769 }
1770
1771 =head2 CcAddresses
1772
1773 returns String: All Ticket Ccs as a string of email addresses
1774
1775 =cut
1776
1777 sub CcAddresses {
1778     my $self = shift;
1779
1780     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1781         return undef;
1782     }
1783
1784     return ( $self->Cc->MemberEmailAddressesAsString);
1785
1786 }
1787
1788 # }}}
1789
1790 # {{{ Routines that return RT::Watchers objects of Requestors, Ccs and AdminCcs
1791
1792 # {{{ sub Requestors
1793
1794 =head2 Requestors
1795
1796 Takes nothing.
1797 Returns this ticket's Requestors as an RT::Group object
1798
1799 =cut
1800
1801 sub Requestors {
1802     my $self = shift;
1803
1804     my $group = RT::Group->new($self->CurrentUser);
1805     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1806         $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1807     }
1808     return ($group);
1809
1810 }
1811
1812 # }}}
1813
1814 # {{{ sub _Requestors
1815
1816 =head2 _Requestors
1817
1818 Private non-ACLed variant of Reqeustors so that we can look them up for the
1819 purposes of customer auto-association during create.
1820
1821 =cut
1822
1823 sub _Requestors {
1824     my $self = shift:
1825
1826     my $group = RT::Group->new($RT::SystemUser);
1827     $group->LoadTicketRoleGroup(Type => 'Requestor', Ticket => $self->Id);
1828     return ($group);
1829 }
1830
1831 % }}}
1832
1833 # {{{ sub Cc
1834
1835 =head2 Cc
1836
1837 Takes nothing.
1838 Returns an RT::Group object which contains this ticket's Ccs.
1839 If the user doesn't have "ShowTicket" permission, returns an empty group
1840
1841 =cut
1842
1843 sub Cc {
1844     my $self = shift;
1845
1846     my $group = RT::Group->new($self->CurrentUser);
1847     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1848         $group->LoadTicketRoleGroup(Type => 'Cc', Ticket => $self->Id);
1849     }
1850     return ($group);
1851
1852 }
1853
1854 # }}}
1855
1856 # {{{ sub AdminCc
1857
1858 =head2 AdminCc
1859
1860 Takes nothing.
1861 Returns an RT::Group object which contains this ticket's AdminCcs.
1862 If the user doesn't have "ShowTicket" permission, returns an empty group
1863
1864 =cut
1865
1866 sub AdminCc {
1867     my $self = shift;
1868
1869     my $group = RT::Group->new($self->CurrentUser);
1870     if ( $self->CurrentUserHasRight('ShowTicket') ) {
1871         $group->LoadTicketRoleGroup(Type => 'AdminCc', Ticket => $self->Id);
1872     }
1873     return ($group);
1874
1875 }
1876
1877 # }}}
1878
1879 # }}}
1880
1881 # {{{ IsWatcher,IsRequestor,IsCc, IsAdminCc
1882
1883 # {{{ sub IsWatcher
1884 # a generic routine to be called by IsRequestor, IsCc and IsAdminCc
1885
1886 =head2 IsWatcher { Type => TYPE, PrincipalId => PRINCIPAL_ID, Email => EMAIL }
1887
1888 Takes a param hash with the attributes Type and either PrincipalId or Email
1889
1890 Type is one of Requestor, Cc, AdminCc and Owner
1891
1892 PrincipalId is an RT::Principal id, and Email is an email address.
1893
1894 Returns true if the specified principal (or the one corresponding to the
1895 specified address) is a member of the group Type for this ticket.
1896
1897 XX TODO: This should be Memoized. 
1898
1899 =cut
1900
1901 sub IsWatcher {
1902     my $self = shift;
1903
1904     my %args = ( Type  => 'Requestor',
1905         PrincipalId    => undef,
1906         Email          => undef,
1907         @_
1908     );
1909
1910     # Load the relevant group. 
1911     my $group = RT::Group->new($self->CurrentUser);
1912     $group->LoadTicketRoleGroup(Type => $args{'Type'}, Ticket => $self->id);
1913
1914     # Find the relevant principal.
1915     my $principal = RT::Principal->new($self->CurrentUser);
1916     if (!$args{PrincipalId} && $args{Email}) {
1917         # Look up the specified user.
1918         my $user = RT::User->new($self->CurrentUser);
1919         $user->LoadByEmail($args{Email});
1920         if ($user->Id) {
1921             $args{PrincipalId} = $user->PrincipalId;
1922         }
1923         else {
1924             # A non-existent user can't be a group member.
1925             return 0;
1926         }
1927     }
1928     $principal->Load($args{'PrincipalId'});
1929
1930     # Ask if it has the member in question
1931     return ($group->HasMember($principal));
1932 }
1933
1934 # }}}
1935
1936 # {{{ sub IsRequestor
1937
1938 =head2 IsRequestor PRINCIPAL_ID
1939   
1940   Takes an RT::Principal id
1941   Returns true if the principal is a requestor of the current ticket.
1942
1943
1944 =cut
1945
1946 sub IsRequestor {
1947     my $self   = shift;
1948     my $person = shift;
1949
1950     return ( $self->IsWatcher( Type => 'Requestor', PrincipalId => $person ) );
1951
1952 };
1953
1954 # }}}
1955
1956 # {{{ sub IsCc
1957
1958 =head2 IsCc PRINCIPAL_ID
1959
1960   Takes an RT::Principal id.
1961   Returns true if the principal is a requestor of the current ticket.
1962
1963
1964 =cut
1965
1966 sub IsCc {
1967     my $self = shift;
1968     my $cc   = shift;
1969
1970     return ( $self->IsWatcher( Type => 'Cc', PrincipalId => $cc ) );
1971
1972 }
1973
1974 # }}}
1975
1976 # {{{ sub IsAdminCc
1977
1978 =head2 IsAdminCc PRINCIPAL_ID
1979
1980   Takes an RT::Principal id.
1981   Returns true if the principal is a requestor of the current ticket.
1982
1983 =cut
1984
1985 sub IsAdminCc {
1986     my $self   = shift;
1987     my $person = shift;
1988
1989     return ( $self->IsWatcher( Type => 'AdminCc', PrincipalId => $person ) );
1990
1991 }
1992
1993 # }}}
1994
1995 # {{{ sub IsOwner
1996
1997 =head2 IsOwner
1998
1999   Takes an RT::User object. Returns true if that user is this ticket's owner.
2000 returns undef otherwise
2001
2002 =cut
2003
2004 sub IsOwner {
2005     my $self   = shift;
2006     my $person = shift;
2007
2008     # no ACL check since this is used in acl decisions
2009     # unless ($self->CurrentUserHasRight('ShowTicket')) {
2010     #   return(undef);
2011     #   }       
2012
2013     #Tickets won't yet have owners when they're being created.
2014     unless ( $self->OwnerObj->id ) {
2015         return (undef);
2016     }
2017
2018     if ( $person->id == $self->OwnerObj->id ) {
2019         return (1);
2020     }
2021     else {
2022         return (undef);
2023     }
2024 }
2025
2026 # }}}
2027
2028 # }}}
2029
2030 # }}}
2031
2032 # {{{ Routines dealing with queues 
2033
2034 # {{{ sub ValidateQueue
2035
2036 sub ValidateQueue {
2037     my $self  = shift;
2038     my $Value = shift;
2039
2040     if ( !$Value ) {
2041         $RT::Logger->warning( " RT:::Queue::ValidateQueue called with a null value. this isn't ok.");
2042         return (1);
2043     }
2044
2045     my $QueueObj = RT::Queue->new( $self->CurrentUser );
2046     my $id       = $QueueObj->Load($Value);
2047
2048     if ($id) {
2049         return (1);
2050     }
2051     else {
2052         return (undef);
2053     }
2054 }
2055
2056 # }}}
2057
2058 # {{{ sub SetQueue  
2059
2060 sub SetQueue {
2061     my $self     = shift;
2062     my $NewQueue = shift;
2063
2064     #Redundant. ACL gets checked in _Set;
2065     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2066         return ( 0, $self->loc("Permission Denied") );
2067     }
2068
2069     my $NewQueueObj = RT::Queue->new( $self->CurrentUser );
2070     $NewQueueObj->Load($NewQueue);
2071
2072     unless ( $NewQueueObj->Id() ) {
2073         return ( 0, $self->loc("That queue does not exist") );
2074     }
2075
2076     if ( $NewQueueObj->Id == $self->QueueObj->Id ) {
2077         return ( 0, $self->loc('That is the same value') );
2078     }
2079     unless (
2080         $self->CurrentUser->HasRight(
2081             Right    => 'CreateTicket',
2082             Object => $NewQueueObj
2083         )
2084       )
2085     {
2086         return ( 0, $self->loc("You may not create requests in that queue.") );
2087     }
2088
2089     unless (
2090         $self->OwnerObj->HasRight(
2091             Right    => 'OwnTicket',
2092             Object => $NewQueueObj
2093         )
2094       )
2095     {
2096         my $clone = RT::Ticket->new( $RT::SystemUser );
2097         $clone->Load( $self->Id );
2098         unless ( $clone->Id ) {
2099             return ( 0, $self->loc("Couldn't load copy of ticket #[_1].", $self->Id) );
2100         }
2101         my ($status, $msg) = $clone->SetOwner( $RT::Nobody->Id, 'Force' );
2102         $RT::Logger->error("Couldn't set owner on queue change: $msg") unless $status;
2103     }
2104
2105     return ( $self->_Set( Field => 'Queue', Value => $NewQueueObj->Id() ) );
2106 }
2107
2108 # }}}
2109
2110 # {{{ sub QueueObj
2111
2112 =head2 QueueObj
2113
2114 Takes nothing. returns this ticket's queue object
2115
2116 =cut
2117
2118 sub QueueObj {
2119     my $self = shift;
2120
2121     my $queue_obj = RT::Queue->new( $self->CurrentUser );
2122
2123     #We call __Value so that we can avoid the ACL decision and some deep recursion
2124     my ($result) = $queue_obj->Load( $self->__Value('Queue') );
2125     return ($queue_obj);
2126 }
2127
2128 # }}}
2129
2130 # }}}
2131
2132 # {{{ Date printing routines
2133
2134 # {{{ sub DueObj
2135
2136 =head2 DueObj
2137
2138   Returns an RT::Date object containing this ticket's due date
2139
2140 =cut
2141
2142 sub DueObj {
2143     my $self = shift;
2144
2145     my $time = new RT::Date( $self->CurrentUser );
2146
2147     # -1 is RT::Date slang for never
2148     if ( $self->Due ) {
2149         $time->Set( Format => 'sql', Value => $self->Due );
2150     }
2151     else {
2152         $time->Set( Format => 'unix', Value => -1 );
2153     }
2154
2155     return $time;
2156 }
2157
2158 # }}}
2159
2160 # {{{ sub DueAsString 
2161
2162 =head2 DueAsString
2163
2164 Returns this ticket's due date as a human readable string
2165
2166 =cut
2167
2168 sub DueAsString {
2169     my $self = shift;
2170     return $self->DueObj->AsString();
2171 }
2172
2173 # }}}
2174
2175 # {{{ sub ResolvedObj
2176
2177 =head2 ResolvedObj
2178
2179   Returns an RT::Date object of this ticket's 'resolved' time.
2180
2181 =cut
2182
2183 sub ResolvedObj {
2184     my $self = shift;
2185
2186     my $time = new RT::Date( $self->CurrentUser );
2187     $time->Set( Format => 'sql', Value => $self->Resolved );
2188     return $time;
2189 }
2190
2191 # }}}
2192
2193 # {{{ sub SetStarted
2194
2195 =head2 SetStarted
2196
2197 Takes a date in ISO format or undef
2198 Returns a transaction id and a message
2199 The client calls "Start" to note that the project was started on the date in $date.
2200 A null date means "now"
2201
2202 =cut
2203
2204 sub SetStarted {
2205     my $self = shift;
2206     my $time = shift || 0;
2207
2208     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2209         return ( 0, $self->loc("Permission Denied") );
2210     }
2211
2212     #We create a date object to catch date weirdness
2213     my $time_obj = new RT::Date( $self->CurrentUser() );
2214     if ( $time ) {
2215         $time_obj->Set( Format => 'ISO', Value => $time );
2216     }
2217     else {
2218         $time_obj->SetToNow();
2219     }
2220
2221     #Now that we're starting, open this ticket
2222     #TODO do we really want to force this as policy? it should be a scrip
2223
2224     #We need $TicketAsSystem, in case the current user doesn't have
2225     #ShowTicket
2226     #
2227     my $TicketAsSystem = new RT::Ticket($RT::SystemUser);
2228     $TicketAsSystem->Load( $self->Id );
2229     if ( $TicketAsSystem->Status eq 'new' ) {
2230         $TicketAsSystem->Open();
2231     }
2232
2233     return ( $self->_Set( Field => 'Started', Value => $time_obj->ISO ) );
2234
2235 }
2236
2237 # }}}
2238
2239 # {{{ sub StartedObj
2240
2241 =head2 StartedObj
2242
2243   Returns an RT::Date object which contains this ticket's 
2244 'Started' time.
2245
2246 =cut
2247
2248 sub StartedObj {
2249     my $self = shift;
2250
2251     my $time = new RT::Date( $self->CurrentUser );
2252     $time->Set( Format => 'sql', Value => $self->Started );
2253     return $time;
2254 }
2255
2256 # }}}
2257
2258 # {{{ sub StartsObj
2259
2260 =head2 StartsObj
2261
2262   Returns an RT::Date object which contains this ticket's 
2263 'Starts' time.
2264
2265 =cut
2266
2267 sub StartsObj {
2268     my $self = shift;
2269
2270     my $time = new RT::Date( $self->CurrentUser );
2271     $time->Set( Format => 'sql', Value => $self->Starts );
2272     return $time;
2273 }
2274
2275 # }}}
2276
2277 # {{{ sub ToldObj
2278
2279 =head2 ToldObj
2280
2281   Returns an RT::Date object which contains this ticket's 
2282 'Told' time.
2283
2284 =cut
2285
2286 sub ToldObj {
2287     my $self = shift;
2288
2289     my $time = new RT::Date( $self->CurrentUser );
2290     $time->Set( Format => 'sql', Value => $self->Told );
2291     return $time;
2292 }
2293
2294 # }}}
2295
2296 # {{{ sub ToldAsString
2297
2298 =head2 ToldAsString
2299
2300 A convenience method that returns ToldObj->AsString
2301
2302 TODO: This should be deprecated
2303
2304 =cut
2305
2306 sub ToldAsString {
2307     my $self = shift;
2308     if ( $self->Told ) {
2309         return $self->ToldObj->AsString();
2310     }
2311     else {
2312         return ("Never");
2313     }
2314 }
2315
2316 # }}}
2317
2318 # {{{ sub TimeWorkedAsString
2319
2320 =head2 TimeWorkedAsString
2321
2322 Returns the amount of time worked on this ticket as a Text String
2323
2324 =cut
2325
2326 sub TimeWorkedAsString {
2327     my $self = shift;
2328     return "0" unless $self->TimeWorked;
2329
2330     #This is not really a date object, but if we diff a number of seconds 
2331     #vs the epoch, we'll get a nice description of time worked.
2332
2333     my $worked = new RT::Date( $self->CurrentUser );
2334
2335     #return the  #of minutes worked turned into seconds and written as
2336     # a simple text string
2337
2338     return ( $worked->DurationAsString( $self->TimeWorked * 60 ) );
2339 }
2340
2341 # }}}
2342
2343 # }}}
2344
2345 # {{{ Routines dealing with correspondence/comments
2346
2347 # {{{ sub Comment
2348
2349 =head2 Comment
2350
2351 Comment on this ticket.
2352 Takes a hashref with the following attributes:
2353 If MIMEObj is undefined, Content will be used to build a MIME::Entity for this
2354 commentl
2355
2356 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2357
2358 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2359 They will, however, be prepared and you'll be able to access them through the TransactionObj
2360
2361 Returns: Transaction id, Error Message, Transaction Object
2362 (note the different order from Create()!)
2363
2364 =cut
2365
2366 sub Comment {
2367     my $self = shift;
2368
2369     my %args = ( CcMessageTo  => undef,
2370                  BccMessageTo => undef,
2371                  MIMEObj      => undef,
2372                  Content      => undef,
2373                  TimeTaken => 0,
2374                  DryRun     => 0, 
2375                  @_ );
2376
2377     unless (    ( $self->CurrentUserHasRight('CommentOnTicket') )
2378              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2379         return ( 0, $self->loc("Permission Denied"), undef );
2380     }
2381     $args{'NoteType'} = 'Comment';
2382
2383     if ($args{'DryRun'}) {
2384         $RT::Handle->BeginTransaction();
2385         $args{'CommitScrips'} = 0;
2386     }
2387
2388     my @results = $self->_RecordNote(%args);
2389     if ($args{'DryRun'}) {
2390         $RT::Handle->Rollback();
2391     }
2392
2393     return(@results);
2394 }
2395 # }}}
2396
2397 # {{{ sub Correspond
2398
2399 =head2 Correspond
2400
2401 Correspond on this ticket.
2402 Takes a hashref with the following attributes:
2403
2404
2405 MIMEObj, TimeTaken, CcMessageTo, BccMessageTo, Content, DryRun
2406
2407 if there's no MIMEObj, Content is used to build a MIME::Entity object
2408
2409 If DryRun is defined, this update WILL NOT BE RECORDED. Scrips will not be committed.
2410 They will, however, be prepared and you'll be able to access them through the TransactionObj
2411
2412 Returns: Transaction id, Error Message, Transaction Object
2413 (note the different order from Create()!)
2414
2415
2416 =cut
2417
2418 sub Correspond {
2419     my $self = shift;
2420     my %args = ( CcMessageTo  => undef,
2421                  BccMessageTo => undef,
2422                  MIMEObj      => undef,
2423                  Content      => undef,
2424                  TimeTaken    => 0,
2425                  @_ );
2426
2427     unless (    ( $self->CurrentUserHasRight('ReplyToTicket') )
2428              or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
2429         return ( 0, $self->loc("Permission Denied"), undef );
2430     }
2431
2432     $args{'NoteType'} = 'Correspond'; 
2433     if ($args{'DryRun'}) {
2434         $RT::Handle->BeginTransaction();
2435         $args{'CommitScrips'} = 0;
2436     }
2437
2438     my @results = $self->_RecordNote(%args);
2439
2440     #Set the last told date to now if this isn't mail from the requestor.
2441     #TODO: Note that this will wrongly ack mail from any non-requestor as a "told"
2442     $self->_SetTold unless ( $self->IsRequestor($self->CurrentUser->id));
2443
2444     if ($args{'DryRun'}) {
2445         $RT::Handle->Rollback();
2446     }
2447
2448     return (@results);
2449
2450 }
2451
2452 # }}}
2453
2454 # {{{ sub _RecordNote
2455
2456 =head2 _RecordNote
2457
2458 the meat of both comment and correspond. 
2459
2460 Performs no access control checks. hence, dangerous.
2461
2462 =cut
2463
2464 sub _RecordNote {
2465
2466     my $self = shift;
2467     my %args = ( CcMessageTo  => undef,
2468                  BccMessageTo => undef,
2469                  MIMEObj      => undef,
2470                  Content      => undef,
2471                  TimeTaken    => 0,
2472                  CommitScrips => 1,
2473                  @_ );
2474
2475     unless ( $args{'MIMEObj'} || $args{'Content'} ) {
2476             return ( 0, $self->loc("No message attached"), undef );
2477     }
2478     unless ( $args{'MIMEObj'} ) {
2479             $args{'MIMEObj'} = MIME::Entity->build( Data => (
2480                                                           ref $args{'Content'}
2481                                                           ? $args{'Content'}
2482                                                           : [ $args{'Content'} ]
2483                                                     ) );
2484         }
2485
2486     # convert text parts into utf-8
2487     RT::I18N::SetMIMEEntityToUTF8( $args{'MIMEObj'} );
2488
2489 # If we've been passed in CcMessageTo and BccMessageTo fields,
2490 # add them to the mime object for passing on to the transaction handler
2491 # The "NotifyOtherRecipients" scripAction will look for RT-Send-Cc: and RT-Send-Bcc:
2492 # headers
2493
2494
2495     foreach my $type (qw/Cc Bcc/) {
2496         if ( defined $args{ $type . 'MessageTo' } ) {
2497
2498             my $addresses = join ', ', (
2499                 map { RT::User->CanonicalizeEmailAddress( $_->address ) }
2500                     Mail::Address->parse( $args{ $type . 'MessageTo' } ) );
2501             $args{'MIMEObj'}->head->add( 'RT-Send-' . $type, $addresses );
2502         }
2503     }
2504
2505     # If this is from an external source, we need to come up with its
2506     # internal Message-ID now, so all emails sent because of this
2507     # message have a common Message-ID
2508     unless ( ($args{'MIMEObj'}->head->get('Message-ID') || '')
2509             =~ /<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$RT::Organization>/ )
2510     {
2511         $args{'MIMEObj'}->head->set( 'RT-Message-ID',
2512             "<rt-"
2513             . $RT::VERSION . "-"
2514             . $$ . "-"
2515             . CORE::time() . "-"
2516             . int(rand(2000)) . '.'
2517             . $self->id . "-"
2518             . "0" . "-"  # Scrip
2519             . "0" . "@"  # Email sent
2520             . $RT::Organization
2521             . ">" );
2522     }
2523
2524     #Record the correspondence (write the transaction)
2525     my ( $Trans, $msg, $TransObj ) = $self->_NewTransaction(
2526              Type => $args{'NoteType'},
2527              Data => ( $args{'MIMEObj'}->head->get('subject') || 'No Subject' ),
2528              TimeTaken => $args{'TimeTaken'},
2529              MIMEObj   => $args{'MIMEObj'}, 
2530              CommitScrips => $args{'CommitScrips'},
2531     );
2532
2533     unless ($Trans) {
2534         $RT::Logger->err("$self couldn't init a transaction $msg");
2535         return ( $Trans, $self->loc("Message could not be recorded"), undef );
2536     }
2537
2538     return ( $Trans, $self->loc("Message recorded"), $TransObj );
2539 }
2540
2541 # }}}
2542
2543 # }}}
2544
2545 # {{{ sub _Links 
2546
2547 sub _Links {
2548     my $self = shift;
2549
2550     #TODO: Field isn't the right thing here. but I ahave no idea what mnemonic ---
2551     #tobias meant by $f
2552     my $field = shift;
2553     my $type  = shift || "";
2554
2555     unless ( $self->{"$field$type"} ) {
2556         $self->{"$field$type"} = new RT::Links( $self->CurrentUser );
2557
2558         #not sure what this ACL was supposed to do... but returning the
2559         # bare (unlimited) RT::Links certainly seems wrong, it causes the
2560         # $Ticket->Customers method during creation to return results for every
2561         # ticket...
2562         #if ( $self->CurrentUserHasRight('ShowTicket') ) {
2563
2564             # Maybe this ticket is a merged ticket
2565             my $Tickets = new RT::Tickets( $self->CurrentUser );
2566             # at least to myself
2567             $self->{"$field$type"}->Limit( FIELD => $field,
2568                                            VALUE => $self->URI,
2569                                            ENTRYAGGREGATOR => 'OR' );
2570             $Tickets->Limit( FIELD => 'EffectiveId',
2571                              VALUE => $self->EffectiveId );
2572             while (my $Ticket = $Tickets->Next) {
2573                 $self->{"$field$type"}->Limit( FIELD => $field,
2574                                                VALUE => $Ticket->URI,
2575                                                ENTRYAGGREGATOR => 'OR' );
2576             }
2577             $self->{"$field$type"}->Limit( FIELD => 'Type',
2578                                            VALUE => $type )
2579               if ($type);
2580         #}
2581     }
2582     return ( $self->{"$field$type"} );
2583 }
2584
2585 # }}}
2586
2587 # {{{ sub DeleteLink 
2588
2589 =head2 DeleteLink
2590
2591 Delete a link. takes a paramhash of Base, Target and Type.
2592 Either Base or Target must be null. The null value will 
2593 be replaced with this ticket\'s id
2594
2595 =cut 
2596
2597 sub DeleteLink {
2598     my $self = shift;
2599     my %args = (
2600         Base   => undef,
2601         Target => undef,
2602         Type   => undef,
2603         @_
2604     );
2605
2606     unless ( $args{'Target'} || $args{'Base'} ) {
2607         $RT::Logger->error("Base or Target must be specified\n");
2608         return ( 0, $self->loc('Either base or target must be specified') );
2609     }
2610
2611     #check acls
2612     my $right = 0;
2613     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2614     if ( !$right && $RT::StrictLinkACL ) {
2615         return ( 0, $self->loc("Permission Denied") );
2616     }
2617
2618     # If the other URI is an RT::Ticket, we want to make sure the user
2619     # can modify it too...
2620     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2621     return (0, $msg) unless $status;
2622     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2623         $right++;
2624     }
2625     if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2626          ( $RT::StrictLinkACL && $right < 2 ) )
2627     {
2628         return ( 0, $self->loc("Permission Denied") );
2629     }
2630
2631     my ($val, $Msg) = $self->SUPER::_DeleteLink(%args);
2632
2633     if ( !$val ) {
2634         $RT::Logger->debug("Couldn't find that link\n");
2635         return ( 0, $Msg );
2636     }
2637
2638     my ($direction, $remote_link);
2639
2640     if ( $args{'Base'} ) {
2641         $remote_link = $args{'Base'};
2642         $direction = 'Target';
2643     }
2644     elsif ( $args{'Target'} ) {
2645         $remote_link = $args{'Target'};
2646         $direction='Base';
2647     }
2648
2649     if ( $args{'Silent'} ) {
2650         return ( $val, $Msg );
2651     }
2652     else {
2653         my $remote_uri = RT::URI->new( $self->CurrentUser );
2654         $remote_uri->FromURI( $remote_link );
2655
2656         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
2657             Type      => 'DeleteLink',
2658             Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2659             OldValue =>  $remote_uri->URI || $remote_link,
2660             TimeTaken => 0
2661         );
2662
2663         if ( $remote_uri->IsLocal ) {
2664
2665             my $OtherObj = $remote_uri->Object;
2666             my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'DeleteLink',
2667                                                            Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base}
2668                                                                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
2669                                                            OldValue => $self->URI,
2670                                                            ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2671                                                            TimeTaken => 0 );
2672         }
2673
2674         return ( $Trans, $Msg );
2675     }
2676 }
2677
2678 # }}}
2679
2680 # {{{ sub AddLink
2681
2682 =head2 AddLink
2683
2684 Takes a paramhash of Type and one of Base or Target. Adds that link to this ticket.
2685
2686 =cut
2687
2688 sub AddLink {
2689     my $self = shift;
2690     my %args = ( Target => '',
2691                  Base   => '',
2692                  Type   => '',
2693                  Silent => undef,
2694                  @_ );
2695
2696     unless ( $args{'Target'} || $args{'Base'} ) {
2697         $RT::Logger->error("Base or Target must be specified\n");
2698         return ( 0, $self->loc('Either base or target must be specified') );
2699     }
2700
2701     my $right = 0;
2702     $right++ if $self->CurrentUserHasRight('ModifyTicket');
2703     if ( !$right && $RT::StrictLinkACL ) {
2704         return ( 0, $self->loc("Permission Denied") );
2705     }
2706
2707     # If the other URI is an RT::Ticket, we want to make sure the user
2708     # can modify it too...
2709     my ($status, $msg, $other_ticket) = $self->__GetTicketFromURI( URI => $args{'Target'} || $args{'Base'} );
2710     return (0, $msg) unless $status;
2711     if ( !$other_ticket || $other_ticket->CurrentUserHasRight('ModifyTicket') ) {
2712         $right++;
2713     }
2714     if ( ( !$RT::StrictLinkACL && $right == 0 ) ||
2715          ( $RT::StrictLinkACL && $right < 2 ) )
2716     {
2717         return ( 0, $self->loc("Permission Denied") );
2718     }
2719
2720     return $self->_AddLink(%args);
2721 }
2722
2723 sub __GetTicketFromURI {
2724     my $self = shift;
2725     my %args = ( URI => '', @_ );
2726
2727     # If the other URI is an RT::Ticket, we want to make sure the user
2728     # can modify it too...
2729     my $uri_obj = RT::URI->new( $self->CurrentUser );
2730     $uri_obj->FromURI( $args{'URI'} );
2731
2732     unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
2733             my $msg = $self->loc( "Couldn't resolve '[_1]' into a URI.", $args{'URI'} );
2734         $RT::Logger->warning( "$msg\n" );
2735         return( 0, $msg );
2736     }
2737     my $obj = $uri_obj->Resolver->Object;
2738     unless ( UNIVERSAL::isa($obj, 'RT::Ticket') && $obj->id ) {
2739         return (1, 'Found not a ticket', undef);
2740     }
2741     return (1, 'Found ticket', $obj);
2742 }
2743
2744 =head2 _AddLink  
2745
2746 Private non-acled variant of AddLink so that links can be added during create.
2747
2748 =cut
2749
2750 sub _AddLink {
2751     my $self = shift;
2752     my %args = ( Target => '',
2753                  Base   => '',
2754                  Type   => '',
2755                  Silent => undef,
2756                  @_ );
2757
2758     my ($val, $msg, $exist) = $self->SUPER::_AddLink(%args);
2759     return ($val, $msg) if !$val || $exist;
2760
2761     my ($direction, $remote_link);
2762     if ( $args{'Target'} ) {
2763         $remote_link  = $args{'Target'};
2764         $direction    = 'Base';
2765     } elsif ( $args{'Base'} ) {
2766         $remote_link  = $args{'Base'};
2767         $direction    = 'Target';
2768     }
2769
2770     # Don't write the transaction if we're doing this on create
2771     if ( $args{'Silent'} ) {
2772         return ( $val, $msg );
2773     }
2774     else {
2775         my $remote_uri = RT::URI->new( $self->CurrentUser );
2776         $remote_uri->FromURI( $remote_link );
2777
2778         #Write the transaction
2779         my ( $Trans, $Msg, $TransObj ) = 
2780             $self->_NewTransaction(Type  => 'AddLink',
2781                                    Field => $LINKDIRMAP{$args{'Type'}}->{$direction},
2782                                    NewValue =>  $remote_uri->URI || $remote_link,
2783                                    TimeTaken => 0 );
2784
2785         if ( $remote_uri->IsLocal ) {
2786
2787             my $OtherObj = $remote_uri->Object;
2788             my ( $val, $Msg ) = $OtherObj->_NewTransaction(Type  => 'AddLink',
2789                                                            Field => $direction eq 'Target' ? $LINKDIRMAP{$args{'Type'}}->{Base} 
2790                                                                                            : $LINKDIRMAP{$args{'Type'}}->{Target},
2791                                                            NewValue => $self->URI,
2792                                                            ActivateScrips => ! $RT::LinkTransactionsRun1Scrip,
2793                                                            TimeTaken => 0 );
2794         }
2795         return ( $val, $Msg );
2796     }
2797
2798 }
2799
2800 # }}}
2801
2802
2803 # {{{ sub MergeInto
2804
2805 =head2 MergeInto
2806
2807 MergeInto take the id of the ticket to merge this ticket into.
2808
2809
2810 =begin testing
2811
2812 my $t1 = RT::Ticket->new($RT::SystemUser);
2813 $t1->Create ( Subject => 'Merge test 1', Queue => 'general', Requestor => 'merge1@example.com');
2814 my $t1id = $t1->id;
2815 my $t2 = RT::Ticket->new($RT::SystemUser);
2816 $t2->Create ( Subject => 'Merge test 2', Queue => 'general', Requestor => 'merge2@example.com');
2817 my $t2id = $t2->id;
2818 my ($msg, $val) = $t1->MergeInto($t2->id);
2819 ok ($msg,$val);
2820 $t1 = RT::Ticket->new($RT::SystemUser);
2821 is ($t1->id, undef, "ok. we've got a blank ticket1");
2822 $t1->Load($t1id);
2823
2824 is ($t1->id, $t2->id);
2825
2826 is ($t1->Requestors->MembersObj->Count, 2);
2827
2828
2829 =end testing
2830
2831 =cut
2832
2833 sub MergeInto {
2834     my $self      = shift;
2835     my $ticket_id = shift;
2836
2837     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
2838         return ( 0, $self->loc("Permission Denied") );
2839     }
2840
2841     # Load up the new ticket.
2842     my $MergeInto = RT::Ticket->new($RT::SystemUser);
2843     $MergeInto->Load($ticket_id);
2844
2845     # make sure it exists.
2846     unless ( $MergeInto->Id ) {
2847         return ( 0, $self->loc("New ticket doesn't exist") );
2848     }
2849
2850     # Make sure the current user can modify the new ticket.
2851     unless ( $MergeInto->CurrentUserHasRight('ModifyTicket') ) {
2852         return ( 0, $self->loc("Permission Denied") );
2853     }
2854
2855     $RT::Handle->BeginTransaction();
2856
2857     # We use EffectiveId here even though it duplicates information from
2858     # the links table becasue of the massive performance hit we'd take
2859     # by trying to do a separate database query for merge info everytime 
2860     # loaded a ticket. 
2861
2862     #update this ticket's effective id to the new ticket's id.
2863     my ( $id_val, $id_msg ) = $self->__Set(
2864         Field => 'EffectiveId',
2865         Value => $MergeInto->Id()
2866     );
2867
2868     unless ($id_val) {
2869         $RT::Handle->Rollback();
2870         return ( 0, $self->loc("Merge failed. Couldn't set EffectiveId") );
2871     }
2872
2873
2874     if ( $self->__Value('Status') ne 'resolved' ) {
2875
2876         my ( $status_val, $status_msg )
2877             = $self->__Set( Field => 'Status', Value => 'resolved' );
2878
2879         unless ($status_val) {
2880             $RT::Handle->Rollback();
2881             $RT::Logger->error(
2882                 $self->loc(
2883                     "[_1] couldn't set status to resolved. RT's Database may be inconsistent.",
2884                     $self
2885                 )
2886             );
2887             return ( 0, $self->loc("Merge failed. Couldn't set Status") );
2888         }
2889     }
2890
2891     # update all the links that point to that old ticket
2892     my $old_links_to = RT::Links->new($self->CurrentUser);
2893     $old_links_to->Limit(FIELD => 'Target', VALUE => $self->URI);
2894
2895     my %old_seen;
2896     while (my $link = $old_links_to->Next) {
2897         if (exists $old_seen{$link->Base."-".$link->Type}) {
2898             $link->Delete;
2899         }   
2900         elsif ($link->Base eq $MergeInto->URI) {
2901             $link->Delete;
2902         } else {
2903             # First, make sure the link doesn't already exist. then move it over.
2904             my $tmp = RT::Link->new($RT::SystemUser);
2905             $tmp->LoadByCols(Base => $link->Base, Type => $link->Type, LocalTarget => $MergeInto->id);
2906             if ($tmp->id)   {
2907                     $link->Delete;
2908             } else { 
2909                 $link->SetTarget($MergeInto->URI);
2910                 $link->SetLocalTarget($MergeInto->id);
2911             }
2912             $old_seen{$link->Base."-".$link->Type} =1;
2913         }
2914
2915     }
2916
2917     my $old_links_from = RT::Links->new($self->CurrentUser);
2918     $old_links_from->Limit(FIELD => 'Base', VALUE => $self->URI);
2919
2920     while (my $link = $old_links_from->Next) {
2921         if (exists $old_seen{$link->Type."-".$link->Target}) {
2922             $link->Delete;
2923         }   
2924         if ($link->Target eq $MergeInto->URI) {
2925             $link->Delete;
2926         } else {
2927             # First, make sure the link doesn't already exist. then move it over.
2928             my $tmp = RT::Link->new($RT::SystemUser);
2929             $tmp->LoadByCols(Target => $link->Target, Type => $link->Type, LocalBase => $MergeInto->id);
2930             if ($tmp->id)   {
2931                     $link->Delete;
2932             } else { 
2933                 $link->SetBase($MergeInto->URI);
2934                 $link->SetLocalBase($MergeInto->id);
2935                 $old_seen{$link->Type."-".$link->Target} =1;
2936             }
2937         }
2938
2939     }
2940
2941     # Update time fields
2942     foreach my $type qw(TimeEstimated TimeWorked TimeLeft) {
2943
2944         my $mutator = "Set$type";
2945         $MergeInto->$mutator(
2946             ( $MergeInto->$type() || 0 ) + ( $self->$type() || 0 ) );
2947
2948     }
2949 #add all of this ticket's watchers to that ticket.
2950     foreach my $watcher_type qw(Requestors Cc AdminCc) {
2951
2952         my $people = $self->$watcher_type->MembersObj;
2953         my $addwatcher_type =  $watcher_type;
2954         $addwatcher_type  =~ s/s$//;
2955
2956         while ( my $watcher = $people->Next ) {
2957             
2958            my ($val, $msg) =  $MergeInto->_AddWatcher(
2959                 Type        => $addwatcher_type,
2960                 Silent => 1,
2961                 PrincipalId => $watcher->MemberId
2962             );
2963             unless ($val) {
2964                 $RT::Logger->warning($msg);
2965             }
2966     }
2967
2968     }
2969
2970     #find all of the tickets that were merged into this ticket. 
2971     my $old_mergees = new RT::Tickets( $self->CurrentUser );
2972     $old_mergees->Limit(
2973         FIELD    => 'EffectiveId',
2974         OPERATOR => '=',
2975         VALUE    => $self->Id
2976     );
2977
2978     #   update their EffectiveId fields to the new ticket's id
2979     while ( my $ticket = $old_mergees->Next() ) {
2980         my ( $val, $msg ) = $ticket->__Set(
2981             Field => 'EffectiveId',
2982             Value => $MergeInto->Id()
2983         );
2984     }
2985
2986     #make a new link: this ticket is merged into that other ticket.
2987     $self->AddLink( Type   => 'MergedInto', Target => $MergeInto->Id());
2988
2989     $MergeInto->_SetLastUpdated;    
2990
2991     $RT::Handle->Commit();
2992     return ( 1, $self->loc("Merge Successful") );
2993 }
2994
2995 # }}}
2996
2997 # }}}
2998
2999 # {{{ Routines dealing with ownership
3000
3001 # {{{ sub OwnerObj
3002
3003 =head2 OwnerObj
3004
3005 Takes nothing and returns an RT::User object of 
3006 this ticket's owner
3007
3008 =cut
3009
3010 sub OwnerObj {
3011     my $self = shift;
3012
3013     #If this gets ACLed, we lose on a rights check in User.pm and
3014     #get deep recursion. if we need ACLs here, we need
3015     #an equiv without ACLs
3016
3017     my $owner = new RT::User( $self->CurrentUser );
3018     $owner->Load( $self->__Value('Owner') );
3019
3020     #Return the owner object
3021     return ($owner);
3022 }
3023
3024 # }}}
3025
3026 # {{{ sub OwnerAsString 
3027
3028 =head2 OwnerAsString
3029
3030 Returns the owner's email address
3031
3032 =cut
3033
3034 sub OwnerAsString {
3035     my $self = shift;
3036     return ( $self->OwnerObj->EmailAddress );
3037
3038 }
3039
3040 # }}}
3041
3042 # {{{ sub SetOwner
3043
3044 =head2 SetOwner
3045
3046 Takes two arguments:
3047      the Id or Name of the owner 
3048 and  (optionally) the type of the SetOwner Transaction. It defaults
3049 to 'Give'.  'Steal' is also a valid option.
3050
3051 =begin testing
3052
3053 my $root = RT::User->new($RT::SystemUser);
3054 $root->Load('root');
3055 ok ($root->Id, "Loaded the root user");
3056 my $t = RT::Ticket->new($RT::SystemUser);
3057 $t->Load(1);
3058 $t->SetOwner('root');
3059 is ($t->OwnerObj->Name, 'root' , "Root owns the ticket");
3060 $t->Steal();
3061 is ($t->OwnerObj->id, $RT::SystemUser->id , "SystemUser owns the ticket");
3062 my $txns = RT::Transactions->new($RT::SystemUser);
3063 $txns->OrderBy(FIELD => 'id', ORDER => 'DESC');
3064 $txns->Limit(FIELD => 'ObjectId', VALUE => '1');
3065 $txns->Limit(FIELD => 'ObjectType', VALUE => 'RT::Ticket');
3066 $txns->Limit(FIELD => 'Type', OPERATOR => '!=',  VALUE => 'EmailRecord');
3067
3068 my $steal  = $txns->First;
3069 ok($steal->OldValue == $root->Id , "Stolen from root");
3070 ok($steal->NewValue == $RT::SystemUser->Id , "Stolen by the systemuser");
3071
3072 =end testing
3073
3074 =cut
3075
3076 sub SetOwner {
3077     my $self     = shift;
3078     my $NewOwner = shift;
3079     my $Type     = shift || "Give";
3080
3081     $RT::Handle->BeginTransaction();
3082
3083     $self->_SetLastUpdated(); # lock the ticket
3084     $self->Load( $self->id ); # in case $self changed while waiting for lock
3085
3086     my $OldOwnerObj = $self->OwnerObj;
3087
3088     my $NewOwnerObj = RT::User->new( $self->CurrentUser );
3089     $NewOwnerObj->Load( $NewOwner );
3090     unless ( $NewOwnerObj->Id ) {
3091         $RT::Handle->Rollback();
3092         return ( 0, $self->loc("That user does not exist") );
3093     }
3094
3095
3096     # must have ModifyTicket rights
3097     # or TakeTicket/StealTicket and $NewOwner is self
3098     # see if it's a take
3099     if ( $OldOwnerObj->Id == $RT::Nobody->Id ) {
3100         unless (    $self->CurrentUserHasRight('ModifyTicket')
3101                  || $self->CurrentUserHasRight('TakeTicket') ) {
3102             $RT::Handle->Rollback();
3103             return ( 0, $self->loc("Permission Denied") );
3104         }
3105     }
3106
3107     # see if it's a steal
3108     elsif (    $OldOwnerObj->Id != $RT::Nobody->Id
3109             && $OldOwnerObj->Id != $self->CurrentUser->id ) {
3110
3111         unless (    $self->CurrentUserHasRight('ModifyTicket')
3112                  || $self->CurrentUserHasRight('StealTicket') ) {
3113             $RT::Handle->Rollback();
3114             return ( 0, $self->loc("Permission Denied") );
3115         }
3116     }
3117     else {
3118         unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3119             $RT::Handle->Rollback();
3120             return ( 0, $self->loc("Permission Denied") );
3121         }
3122     }
3123
3124     # If we're not stealing and the ticket has an owner and it's not
3125     # the current user
3126     if ( $Type ne 'Steal' and $Type ne 'Force'
3127          and $OldOwnerObj->Id != $RT::Nobody->Id
3128          and $OldOwnerObj->Id != $self->CurrentUser->Id )
3129     {
3130         $RT::Handle->Rollback();
3131         return ( 0, $self->loc("You can only take tickets that are unowned") )
3132             if $NewOwnerObj->id == $self->CurrentUser->id;
3133         return (
3134             0,
3135             $self->loc("You can only reassign tickets that you own or that are unowned" )
3136         );
3137     }
3138
3139     #If we've specified a new owner and that user can't modify the ticket
3140     elsif ( !$NewOwnerObj->HasRight( Right => 'OwnTicket', Object => $self ) ) {
3141         $RT::Handle->Rollback();
3142         return ( 0, $self->loc("That user may not own tickets in that queue") );
3143     }
3144
3145     # If the ticket has an owner and it's the new owner, we don't need
3146     # To do anything
3147     elsif ( $NewOwnerObj->Id == $OldOwnerObj->Id ) {
3148         $RT::Handle->Rollback();
3149         return ( 0, $self->loc("That user already owns that ticket") );
3150     }
3151
3152     # Delete the owner in the owner group, then add a new one
3153     # TODO: is this safe? it's not how we really want the API to work
3154     # for most things, but it's fast.
3155     my ( $del_id, $del_msg ) = $self->OwnerGroup->MembersObj->First->Delete();
3156     unless ($del_id) {
3157         $RT::Handle->Rollback();
3158         return ( 0, $self->loc("Could not change owner. ") . $del_msg );
3159     }
3160
3161     my ( $add_id, $add_msg ) = $self->OwnerGroup->_AddMember(
3162                                        PrincipalId => $NewOwnerObj->PrincipalId,
3163                                        InsideTransaction => 1 );
3164     unless ($add_id) {
3165         $RT::Handle->Rollback();
3166         return ( 0, $self->loc("Could not change owner. ") . $add_msg );
3167     }
3168
3169     # We call set twice with slightly different arguments, so
3170     # as to not have an SQL transaction span two RT transactions
3171
3172     my ( $val, $msg ) = $self->_Set(
3173                       Field             => 'Owner',
3174                       RecordTransaction => 0,
3175                       Value             => $NewOwnerObj->Id,
3176                       TimeTaken         => 0,
3177                       TransactionType   => $Type,
3178                       CheckACL          => 0,                  # don't check acl
3179     );
3180
3181     unless ($val) {
3182         $RT::Handle->Rollback;
3183         return ( 0, $self->loc("Could not change owner. ") . $msg );
3184     }
3185
3186     ($val, $msg) = $self->_NewTransaction(
3187         Type      => $Type,
3188         Field     => 'Owner',
3189         NewValue  => $NewOwnerObj->Id,
3190         OldValue  => $OldOwnerObj->Id,
3191         TimeTaken => 0,
3192     );
3193
3194     if ( $val ) {
3195         $msg = $self->loc( "Owner changed from [_1] to [_2]",
3196                            $OldOwnerObj->Name, $NewOwnerObj->Name );
3197     }
3198     else {
3199         $RT::Handle->Rollback();
3200         return ( 0, $msg );
3201     }
3202
3203     $RT::Handle->Commit();
3204
3205     return ( $val, $msg );
3206 }
3207
3208 # }}}
3209
3210 # {{{ sub Take
3211
3212 =head2 Take
3213
3214 A convenince method to set the ticket's owner to the current user
3215
3216 =cut
3217
3218 sub Take {
3219     my $self = shift;
3220     return ( $self->SetOwner( $self->CurrentUser->Id, 'Take' ) );
3221 }
3222
3223 # }}}
3224
3225 # {{{ sub Untake
3226
3227 =head2 Untake
3228
3229 Convenience method to set the owner to 'nobody' if the current user is the owner.
3230
3231 =cut
3232
3233 sub Untake {
3234     my $self = shift;
3235     return ( $self->SetOwner( $RT::Nobody->UserObj->Id, 'Untake' ) );
3236 }
3237
3238 # }}}
3239
3240 # {{{ sub Steal 
3241
3242 =head2 Steal
3243
3244 A convenience method to change the owner of the current ticket to the
3245 current user. Even if it's owned by another user.
3246
3247 =cut
3248
3249 sub Steal {
3250     my $self = shift;
3251
3252     if ( $self->IsOwner( $self->CurrentUser ) ) {
3253         return ( 0, $self->loc("You already own this ticket") );
3254     }
3255     else {
3256         return ( $self->SetOwner( $self->CurrentUser->Id, 'Steal' ) );
3257
3258     }
3259
3260 }
3261
3262 # }}}
3263
3264 # }}}
3265
3266 # {{{ Routines dealing with status
3267
3268 # {{{ sub ValidateStatus 
3269
3270 =head2 ValidateStatus STATUS
3271
3272 Takes a string. Returns true if that status is a valid status for this ticket.
3273 Returns false otherwise.
3274
3275 =cut
3276
3277 sub ValidateStatus {
3278     my $self   = shift;
3279     my $status = shift;
3280
3281     #Make sure the status passed in is valid
3282     unless ( $self->QueueObj->IsValidStatus($status) ) {
3283         return (undef);
3284     }
3285
3286     return (1);
3287
3288 }
3289
3290 # }}}
3291
3292 # {{{ sub SetStatus
3293
3294 =head2 SetStatus STATUS
3295
3296 Set this ticket\'s status. STATUS can be one of: new, open, stalled, resolved, rejected or deleted.
3297
3298 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.
3299
3300 =begin testing
3301
3302 my $tt = RT::Ticket->new($RT::SystemUser);
3303 my ($id, $tid, $msg)= $tt->Create(Queue => 'general',
3304             Subject => 'test');
3305 ok($id, $msg);
3306 is($tt->Status, 'new', "New ticket is created as new");
3307
3308 ($id, $msg) = $tt->SetStatus('open');
3309 ok($id, $msg);
3310 like($msg, qr/open/i, "Status message is correct");
3311 ($id, $msg) = $tt->SetStatus('resolved');
3312 ok($id, $msg);
3313 like($msg, qr/resolved/i, "Status message is correct");
3314 ($id, $msg) = $tt->SetStatus('resolved');
3315 ok(!$id,$msg);
3316
3317
3318 =end testing
3319
3320
3321 =cut
3322
3323 sub SetStatus {
3324     my $self   = shift;
3325     my %args;
3326
3327     if (@_ == 1) {
3328         $args{Status} = shift;
3329     }
3330     else {
3331         %args = (@_);
3332     }
3333
3334     #Check ACL
3335     if ( $args{Status} eq 'deleted') {
3336             unless ($self->CurrentUserHasRight('DeleteTicket')) {
3337             return ( 0, $self->loc('Permission Denied') );
3338        }
3339     } else {
3340             unless ($self->CurrentUserHasRight('ModifyTicket')) {
3341             return ( 0, $self->loc('Permission Denied') );
3342        }
3343     }
3344
3345     if (!$args{Force} && ($args{'Status'} eq 'resolved') && $self->HasUnresolvedDependencies) {
3346         return (0, $self->loc('That ticket has unresolved dependencies'));
3347     }
3348
3349     my $now = RT::Date->new( $self->CurrentUser );
3350     $now->SetToNow();
3351
3352     #If we're changing the status from new, record that we've started
3353     if ( ( $self->Status =~ /new/ ) && ( $args{Status} ne 'new' ) ) {
3354
3355         #Set the Started time to "now"
3356         $self->_Set( Field             => 'Started',
3357                      Value             => $now->ISO,
3358                      RecordTransaction => 0 );
3359     }
3360
3361     #When we close a ticket, set the 'Resolved' attribute to now.
3362     # It's misnamed, but that's just historical.
3363     if ( $self->QueueObj->IsInactiveStatus($args{Status}) ) {
3364         $self->_Set( Field             => 'Resolved',
3365                      Value             => $now->ISO,
3366                      RecordTransaction => 0 );
3367     }
3368
3369     #Actually update the status
3370    my ($val, $msg)= $self->_Set( Field           => 'Status',
3371                           Value           => $args{Status},
3372                           TimeTaken       => 0,
3373                           CheckACL      => 0,
3374                           TransactionType => 'Status'  );
3375
3376     return($val,$msg);
3377 }
3378
3379 # }}}
3380
3381 # {{{ sub Kill
3382
3383 =head2 Kill
3384
3385 Takes no arguments. Marks this ticket for garbage collection
3386
3387 =cut
3388
3389 sub Kill {
3390     my $self = shift;
3391     $RT::Logger->crit("'Kill' is deprecated. use 'Delete' instead at (". join(":",caller).").");
3392     return $self->Delete;
3393 }
3394
3395 sub Delete {
3396     my $self = shift;
3397     return ( $self->SetStatus('deleted') );
3398
3399     # TODO: garbage collection
3400 }
3401
3402 # }}}
3403
3404 # {{{ sub Stall
3405
3406 =head2 Stall
3407
3408 Sets this ticket's status to stalled
3409
3410 =cut
3411
3412 sub Stall {
3413     my $self = shift;
3414     return ( $self->SetStatus('stalled') );
3415 }
3416
3417 # }}}
3418
3419 # {{{ sub Reject
3420
3421 =head2 Reject
3422
3423 Sets this ticket's status to rejected
3424
3425 =cut
3426
3427 sub Reject {
3428     my $self = shift;
3429     return ( $self->SetStatus('rejected') );
3430 }
3431
3432 # }}}
3433
3434 # {{{ sub Open
3435
3436 =head2 Open
3437
3438 Sets this ticket\'s status to Open
3439
3440 =cut
3441
3442 sub Open {
3443     my $self = shift;
3444     return ( $self->SetStatus('open') );
3445 }
3446
3447 # }}}
3448
3449 # {{{ sub Resolve
3450
3451 =head2 Resolve
3452
3453 Sets this ticket\'s status to Resolved
3454
3455 =cut
3456
3457 sub Resolve {
3458     my $self = shift;
3459     return ( $self->SetStatus('resolved') );
3460 }
3461
3462 # }}}
3463
3464 # }}}
3465
3466         
3467 # {{{ Actions + Routines dealing with transactions
3468
3469 # {{{ sub SetTold and _SetTold
3470
3471 =head2 SetTold ISO  [TIMETAKEN]
3472
3473 Updates the told and records a transaction
3474
3475 =cut
3476
3477 sub SetTold {
3478     my $self = shift;
3479     my $told;
3480     $told = shift if (@_);
3481     my $timetaken = shift || 0;
3482
3483     unless ( $self->CurrentUserHasRight('ModifyTicket') ) {
3484         return ( 0, $self->loc("Permission Denied") );
3485     }
3486
3487     my $datetold = new RT::Date( $self->CurrentUser );
3488     if ($told) {
3489         $datetold->Set( Format => 'iso',
3490                         Value  => $told );
3491     }
3492     else {
3493         $datetold->SetToNow();
3494     }
3495
3496     return ( $self->_Set( Field           => 'Told',
3497                           Value           => $datetold->ISO,
3498                           TimeTaken       => $timetaken,
3499                           TransactionType => 'Told' ) );
3500 }
3501
3502 =head2 _SetTold
3503
3504 Updates the told without a transaction or acl check. Useful when we're sending replies.
3505
3506 =cut
3507
3508 sub _SetTold {
3509     my $self = shift;
3510
3511     my $now = new RT::Date( $self->CurrentUser );
3512     $now->SetToNow();
3513
3514     #use __Set to get no ACLs ;)
3515     return ( $self->__Set( Field => 'Told',
3516                            Value => $now->ISO ) );
3517 }
3518
3519 # }}}
3520
3521 =head2 TransactionBatch
3522
3523   Returns an array reference of all transactions created on this ticket during
3524   this ticket object's lifetime, or undef if there were none.
3525
3526   Only works when the $RT::UseTransactionBatch config variable is set to true.
3527
3528 =cut
3529
3530 sub TransactionBatch {
3531     my $self = shift;
3532     return $self->{_TransactionBatch};
3533 }
3534
3535 sub DESTROY {
3536     my $self = shift;
3537
3538     # DESTROY methods need to localize $@, or it may unset it.  This
3539     # causes $m->abort to not bubble all of the way up.  See perlbug
3540     # http://rt.perl.org/rt3/Ticket/Display.html?id=17650
3541     local $@;
3542
3543     # The following line eliminates reentrancy.
3544     # It protects against the fact that perl doesn't deal gracefully
3545     # when an object's refcount is changed in its destructor.
3546     return if $self->{_Destroyed}++;
3547
3548     my $batch = $self->TransactionBatch or return;
3549     return unless @$batch;
3550
3551     require RT::Scrips;
3552     RT::Scrips->new($RT::SystemUser)->Apply(
3553         Stage           => 'TransactionBatch',
3554         TicketObj       => $self,
3555         TransactionObj  => $batch->[0],
3556         Type            => join(',', (map { $_->Type } @{$batch}) )
3557     );
3558 }
3559
3560 # }}}
3561
3562 # {{{ PRIVATE UTILITY METHODS. Mostly needed so Ticket can be a DBIx::Record
3563
3564 # {{{ sub _OverlayAccessible
3565
3566 sub _OverlayAccessible {
3567     {
3568         EffectiveId       => { 'read' => 1,  'write' => 1,  'public' => 1 },
3569           Queue           => { 'read' => 1,  'write' => 1 },
3570           Requestors      => { 'read' => 1,  'write' => 1 },
3571           Owner           => { 'read' => 1,  'write' => 1 },
3572           Subject         => { 'read' => 1,  'write' => 1 },
3573           InitialPriority => { 'read' => 1,  'write' => 1 },
3574           FinalPriority   => { 'read' => 1,  'write' => 1 },
3575           Priority        => { 'read' => 1,  'write' => 1 },
3576           Status          => { 'read' => 1,  'write' => 1 },
3577           TimeEstimated      => { 'read' => 1,  'write' => 1 },
3578           TimeWorked      => { 'read' => 1,  'write' => 1 },
3579           TimeLeft        => { 'read' => 1,  'write' => 1 },
3580           Told            => { 'read' => 1,  'write' => 1 },
3581           Resolved        => { 'read' => 1 },
3582           Type            => { 'read' => 1 },
3583           Starts        => { 'read' => 1, 'write' => 1 },
3584           Started       => { 'read' => 1, 'write' => 1 },
3585           Due           => { 'read' => 1, 'write' => 1 },
3586           Creator       => { 'read' => 1, 'auto'  => 1 },
3587           Created       => { 'read' => 1, 'auto'  => 1 },
3588           LastUpdatedBy => { 'read' => 1, 'auto'  => 1 },
3589           LastUpdated   => { 'read' => 1, 'auto'  => 1 }
3590     };
3591
3592 }
3593
3594 # }}}
3595
3596 # {{{ sub _Set
3597
3598 sub _Set {
3599     my $self = shift;
3600
3601     my %args = ( Field             => undef,
3602                  Value             => undef,
3603                  TimeTaken         => 0,
3604                  RecordTransaction => 1,
3605                  UpdateTicket      => 1,
3606                  CheckACL          => 1,
3607                  TransactionType   => 'Set',
3608                  @_ );
3609
3610     if ($args{'CheckACL'}) {
3611       unless ( $self->CurrentUserHasRight('ModifyTicket')) {
3612           return ( 0, $self->loc("Permission Denied"));
3613       }
3614    }
3615
3616     unless ($args{'UpdateTicket'} || $args{'RecordTransaction'}) {
3617         $RT::Logger->error("Ticket->_Set called without a mandate to record an update or update the ticket");
3618         return(0, $self->loc("Internal Error"));
3619     }
3620
3621     #if the user is trying to modify the record
3622
3623     #Take care of the old value we really don't want to get in an ACL loop.
3624     # so ask the super::_Value
3625     my $Old = $self->SUPER::_Value("$args{'Field'}");
3626     
3627     my ($ret, $msg);
3628     if ( $args{'UpdateTicket'}  ) {
3629
3630         #Set the new value
3631         ( $ret, $msg ) = $self->SUPER::_Set( Field => $args{'Field'},
3632                                                 Value => $args{'Value'} );
3633     
3634         #If we can't actually set the field to the value, don't record
3635         # a transaction. instead, get out of here.
3636         return ( 0, $msg ) unless $ret;
3637     }
3638
3639     if ( $args{'RecordTransaction'} == 1 ) {
3640
3641         my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction(
3642                                                Type => $args{'TransactionType'},
3643                                                Field     => $args{'Field'},
3644                                                NewValue  => $args{'Value'},
3645                                                OldValue  => $Old,
3646                                                TimeTaken => $args{'TimeTaken'},
3647         );
3648         return ( $Trans, scalar $TransObj->BriefDescription );
3649     }
3650     else {
3651         return ( $ret, $msg );
3652     }
3653 }
3654
3655 # }}}
3656
3657 # {{{ sub _Value 
3658
3659 =head2 _Value
3660
3661 Takes the name of a table column.
3662 Returns its value as a string, if the user passes an ACL check
3663
3664 =cut
3665
3666 sub _Value {
3667
3668     my $self  = shift;
3669     my $field = shift;
3670
3671     #if the field is public, return it.
3672     if ( $self->_Accessible( $field, 'public' ) ) {
3673
3674         #$RT::Logger->debug("Skipping ACL check for $field\n");
3675         return ( $self->SUPER::_Value($field) );
3676
3677     }
3678
3679     #If the current user doesn't have ACLs, don't let em at it.  
3680
3681     unless ( $self->CurrentUserHasRight('ShowTicket') ) {
3682         return (undef);
3683     }
3684     return ( $self->SUPER::_Value($field) );
3685
3686 }
3687
3688 # }}}
3689
3690 # {{{ sub _UpdateTimeTaken
3691
3692 =head2 _UpdateTimeTaken
3693
3694 This routine will increment the timeworked counter. it should
3695 only be called from _NewTransaction 
3696
3697 =cut
3698
3699 sub _UpdateTimeTaken {
3700     my $self    = shift;
3701     my $Minutes = shift;
3702     my ($Total);
3703
3704     $Total = $self->SUPER::_Value("TimeWorked");
3705     $Total = ( $Total || 0 ) + ( $Minutes || 0 );
3706     $self->SUPER::_Set(
3707         Field => "TimeWorked",
3708         Value => $Total
3709     );
3710
3711     return ($Total);
3712 }
3713
3714 # }}}
3715
3716 # }}}
3717
3718 # {{{ Routines dealing with ACCESS CONTROL
3719
3720 # {{{ sub CurrentUserHasRight 
3721
3722 =head2 CurrentUserHasRight
3723
3724   Takes the textual name of a Ticket scoped right (from RT::ACE) and returns
3725 1 if the user has that right. It returns 0 if the user doesn't have that right.
3726
3727 =cut
3728
3729 sub CurrentUserHasRight {
3730     my $self  = shift;
3731     my $right = shift;
3732
3733     return (
3734         $self->HasRight(
3735             Principal => $self->CurrentUser->UserObj(),
3736             Right     => "$right"
3737           )
3738     );
3739
3740 }
3741
3742 # }}}
3743
3744 # {{{ sub HasRight 
3745
3746 =head2 HasRight
3747
3748  Takes a paramhash with the attributes 'Right' and 'Principal'
3749   'Right' is a ticket-scoped textual right from RT::ACE 
3750   'Principal' is an RT::User object
3751
3752   Returns 1 if the principal has the right. Returns undef if not.
3753
3754 =cut
3755
3756 sub HasRight {
3757     my $self = shift;
3758     my %args = (
3759         Right     => undef,
3760         Principal => undef,
3761         @_
3762     );
3763
3764     unless ( ( defined $args{'Principal'} ) and ( ref( $args{'Principal'} ) ) )
3765     {
3766         Carp::cluck;
3767         $RT::Logger->crit("Principal attrib undefined for Ticket::HasRight");
3768         return(undef);
3769     }
3770
3771     return (
3772         $args{'Principal'}->HasRight(
3773             Object => $self,
3774             Right     => $args{'Right'}
3775           )
3776     );
3777 }
3778
3779 # }}}
3780
3781 # }}}
3782
3783 =head2 Reminders
3784
3785 Return the Reminders object for this ticket. (It's an RT::Reminders object.)
3786 It isn't acutally a searchbuilder collection itself.
3787
3788 =cut
3789
3790 sub Reminders {
3791     my $self = shift;
3792     
3793     unless ($self->{'__reminders'}) {
3794         $self->{'__reminders'} = RT::Reminders->new($self->CurrentUser);
3795         $self->{'__reminders'}->Ticket($self->id);
3796     }
3797     return $self->{'__reminders'};
3798
3799 }
3800
3801
3802
3803 # {{{ sub Transactions 
3804
3805 =head2 Transactions
3806
3807   Returns an RT::Transactions object of all transactions on this ticket
3808
3809 =cut
3810
3811 sub Transactions {
3812     my $self = shift;
3813
3814     my $transactions = RT::Transactions->new( $self->CurrentUser );
3815
3816     #If the user has no rights, return an empty object
3817     if ( $self->CurrentUserHasRight('ShowTicket') ) {
3818         $transactions->LimitToTicket($self->id);
3819
3820         # if the user may not see comments do not return them
3821         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
3822             $transactions->Limit(
3823                 FIELD    => 'Type',
3824                 OPERATOR => '!=',
3825                 VALUE    => "Comment"
3826             );
3827             $transactions->Limit(
3828                 FIELD    => 'Type',
3829                 OPERATOR => '!=',
3830                 VALUE    => "CommentEmailRecord",
3831                 ENTRYAGGREGATOR => 'AND'
3832             );
3833
3834         }
3835     }
3836
3837     return ($transactions);
3838 }
3839
3840 # }}}
3841
3842
3843 # {{{ TransactionCustomFields
3844
3845 =head2 TransactionCustomFields
3846
3847     Returns the custom fields that transactions on tickets will have.
3848
3849 =cut
3850
3851 sub TransactionCustomFields {
3852     my $self = shift;
3853     return $self->QueueObj->TicketTransactionCustomFields;
3854 }
3855
3856 # }}}
3857
3858 # {{{ sub CustomFieldValues
3859
3860 =head2 CustomFieldValues
3861
3862 # Do name => id mapping (if needed) before falling back to
3863 # RT::Record's CustomFieldValues
3864
3865 See L<RT::Record>
3866
3867 =cut
3868
3869 sub CustomFieldValues {
3870     my $self  = shift;
3871     my $field = shift;
3872     if ( $field and $field !~ /^\d+$/ ) {
3873         my $cf = RT::CustomField->new( $self->CurrentUser );
3874         $cf->LoadByNameAndQueue( Name => $field, Queue => $self->Queue );
3875         unless ( $cf->id ) {
3876             $cf->LoadByNameAndQueue( Name => $field, Queue => 0 );
3877         }
3878         unless ( $cf->id ) {
3879             # If we didn't find a valid cfid, give up.
3880             return RT::CustomFieldValues->new($self->CurrentUser);
3881         }
3882     }
3883     return $self->SUPER::CustomFieldValues($field);
3884 }
3885
3886 # }}}
3887
3888 # {{{ sub CustomFieldLookupType
3889
3890 =head2 CustomFieldLookupType
3891
3892 Returns the RT::Ticket lookup type, which can be passed to 
3893 RT::CustomField->Create() via the 'LookupType' hash key.
3894
3895 =cut
3896
3897 # }}}
3898
3899 sub CustomFieldLookupType {
3900     "RT::Queue-RT::Ticket";
3901 }
3902
3903 1;
3904
3905 =head1 AUTHOR
3906
3907 Jesse Vincent, jesse@bestpractical.com
3908
3909 =head1 SEE ALSO
3910
3911 RT
3912
3913 =cut
3914