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