This commit was generated by cvs2svn to compensate for changes in r2523,
[freeside.git] / rt / lib / RT / Transaction.pm
1 # $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Transaction.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
2 # Copyright 1999-2001 Jesse Vincent <jesse@fsck.com>
3 # Released under the terms of the GNU Public License
4
5 =head1 NAME
6
7   RT::Transaction - RT\'s transaction object
8
9 =head1 SYNOPSIS
10
11   use RT::Transaction;
12
13
14 =head1 DESCRIPTION
15
16
17 Each RT::Transaction describes an atomic change to a ticket object 
18 or an update to an RT::Ticket object.
19 It can have arbitrary MIME attachments.
20
21
22 =head1 METHODS
23
24 =begin testing
25
26 ok(require RT::TestHarness);
27 ok(require RT::Transaction);
28
29 =end testing
30
31 =cut
32
33 package RT::Transaction;
34
35 use RT::Record;
36 @ISA= qw(RT::Record);
37     
38 use RT::Attachments;
39
40 # {{{ sub _Init 
41 sub _Init  {
42     my $self = shift;
43   $self->{'table'} = "Transactions";
44   return ($self->SUPER::_Init(@_));
45
46 }
47 # }}}
48
49 # {{{ sub Create 
50
51 =head2 Create
52
53 Create a new transaction.
54
55 This routine should _never_ be called anything other Than RT::Ticket. It should not be called 
56 from client code. Ever. Not ever.  If you do this, we will hunt you down. and break your kneecaps.
57 Then the unpleasant stuff will start.
58
59 TODO: Document what gets passed to this
60
61 =cut
62
63 sub Create  {
64     my $self = shift;
65     my %args = ( id => undef,
66                  TimeTaken => 0,
67                  Ticket => 0 ,
68                  Type => 'undefined',
69                  Data => '',
70                  Field => undef,
71                  OldValue => undef,
72                  NewValue => undef,
73                  MIMEObj => undef,
74                  ActivateScrips => 1,
75                  @_
76                );
77     
78     #if we didn't specify a ticket, we need to bail
79     unless ( $args{'Ticket'} ) {
80         return(0, "RT::Transaction->Create couldn't, as you didn't specify a ticket id");
81     }
82         
83     #lets create our transaction
84     my $id = $self->SUPER::Create(Ticket => $args{'Ticket'},
85                                   TimeTaken => $args{'TimeTaken'},
86                                   Type => $args{'Type'},
87                                   Data => $args{'Data'},
88                                   Field => $args{'Field'},
89                                   OldValue => $args{'OldValue'},
90                                   NewValue => $args{'NewValue'},
91                                   Created => $args{'Created'}
92                                  );
93     $self->Load($id);
94     $self->_Attach($args{'MIMEObj'})
95       if defined $args{'MIMEObj'};
96     
97     #Provide a way to turn off scrips if we need to
98     if ($args{'ActivateScrips'}) {
99
100         #We're really going to need a non-acled ticket for the scrips to work
101         my $TicketAsSystem = RT::Ticket->new($RT::SystemUser);
102         $TicketAsSystem->Load($args{'Ticket'}) || 
103           $RT::Logger->err("$self couldn't load ticket $args{'Ticket'}\n");
104         
105         my $TransAsSystem = RT::Transaction->new($RT::SystemUser);
106         $TransAsSystem->Load($self->id) ||
107           $RT::Logger->err("$self couldn't load a copy of itself as superuser\n");
108         
109         # {{{ Deal with Scrips
110     
111     #Load a scripscopes object
112     use RT::Scrips;
113     my $PossibleScrips = RT::Scrips->new($RT::SystemUser);
114     
115     $PossibleScrips->LimitToQueue($TicketAsSystem->QueueObj->Id); #Limit it to  $Ticket->QueueObj->Id
116     $PossibleScrips->LimitToGlobal(); # or to "global"
117     my $ConditionsAlias = $PossibleScrips->NewAlias('ScripConditions');
118     
119     $PossibleScrips->Join(ALIAS1 => 'main',  FIELD1 => 'ScripCondition',
120                           ALIAS2 => $ConditionsAlias, FIELD2=> 'id');
121     
122     
123     #We only want things where the scrip applies to this sort of transaction
124     $PossibleScrips->Limit(ALIAS=> $ConditionsAlias,
125                            FIELD=>'ApplicableTransTypes',
126                            OPERATOR => 'LIKE',
127                            VALUE => $args{'Type'},
128                            ENTRYAGGREGATOR => 'OR',
129                           );
130     
131     # Or where the scrip applies to any transaction
132     $PossibleScrips->Limit(ALIAS=> $ConditionsAlias,
133                            FIELD=>'ApplicableTransTypes',
134                            OPERATOR => 'LIKE',
135                            VALUE => "Any",
136                            ENTRYAGGREGATOR => 'OR',
137                           );                        
138     
139     #Iterate through each script and check it's applicability.
140     
141     while (my $Scrip = $PossibleScrips->Next()) {
142       
143       #TODO: properly deal with errors raised in this scrip loop
144         
145       #$RT::Logger->debug("$self now dealing with ".$Scrip->Id. "\n");      
146         eval {
147           local $SIG{__DIE__} = sub { $RT::Logger->error($_[0])};
148           
149           
150           #Load the scrip's Condition object
151           $Scrip->ConditionObj->LoadCondition(TicketObj => $TicketAsSystem, 
152                                               TransactionObj => $TransAsSystem);          
153           
154           
155           #If it's applicable, prepare and commit it
156           
157         $RT::Logger->debug ("$self: Checking condition ".$Scrip->ConditionObj->Name. "...\n");
158           
159           if ( $Scrip->IsApplicable() ) {
160               
161                 $RT::Logger->debug ("$self: Matches condition ".$Scrip->ConditionObj->Name. "...\n");
162               #TODO: handle some errors here
163               
164               $Scrip->ActionObj->LoadAction(TicketObj => $TicketAsSystem, 
165                                            TransactionObj => $TransAsSystem);
166           
167               
168               if ($Scrip->Prepare()) {
169                   $RT::Logger->debug("$self: Prepared " .
170                                    $Scrip->ActionObj->Name . "\n");
171                   if ($Scrip->Commit()) {
172                         $RT::Logger->debug("$self: Committed " .
173                                            $Scrip->ActionObj->Name . "\n");
174                   }
175                   else {
176                         $RT::Logger->info("$self: Failed to commit ".
177                                            $Scrip->ActionObj->Name . "\n");
178                   } 
179               }
180               else {
181                   $RT::Logger->info("$self: Failed to prepare " .
182                                      $Scrip->ActionObj->Name . "\n");
183               }
184
185               #We're done with it. lets clean up.
186               #TODO: something else isn't letting these get garbage collected. check em out.
187               $Scrip->ActionObj->DESTROY();
188               $Scrip->ConditionObj->DESTROY;
189           }
190           
191           
192         else {
193             $RT::Logger->debug ("$self: Doesn't match condition ".$Scrip->ConditionObj->Name. "...\n");
194
195             # TODO: why doesn't this catch all the ScripObjs we create. 
196             # and why do we explictly need to destroy them?
197             $Scrip->ConditionObj->DESTROY;
198         }
199       } 
200     }
201
202     # }}}
203         
204     }
205
206     return ($id, "Transaction Created");
207 }
208
209 # }}}
210
211 # {{{ sub Delete
212
213 sub Delete {
214     my $self = shift;
215     return (0, 'Deleting this object could break referential integrity');
216 }
217
218 # }}}
219
220 # {{{ Routines dealing with Attachments
221
222 # {{{ sub Message 
223
224 =head2 Message
225
226   Returns the RT::Attachments Object which contains the "top-level" object
227   attachment for this transaction
228
229 =cut
230
231 sub Message  {
232
233     my $self = shift;
234     
235     if (!defined ($self->{'message'}) ){
236         
237         $self->{'message'} = new RT::Attachments($self->CurrentUser);
238         $self->{'message'}->Limit(FIELD => 'TransactionId',
239                                   VALUE => $self->Id);
240         
241         $self->{'message'}->ChildrenOf(0);
242     } 
243     return($self->{'message'});
244 }
245 # }}}
246
247 # {{{ sub Content
248
249 =head2 Content PARAMHASH
250
251 If this transaction has attached mime objects, returns the first text/ part.
252 Otherwise, returns undef.
253
254 Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message 
255 at $args{'Wrap'}.  $args{'Wrap'} defaults to 70.
256
257
258 =cut
259
260 sub Content {
261     my $self = shift;
262     my %args = ( Quote => 0,
263                  Wrap => 70,
264                  @_ );
265
266     my $content = undef;
267
268     # If we don\'t have any content, return undef now.
269     unless ($self->Message->First) {
270         return (undef);
271     }   
272     
273     # Get the set of toplevel attachments to this transaction.
274     my $MIMEObj = $self->Message->First();
275     
276     # If it's a message or a plain part, just return the
277     # body. 
278     if ($MIMEObj->ContentType() =~ '^(text|message)(/|$)') {
279         $content = $MIMEObj->Content();
280     }
281     
282     # If it's a multipart object, first try returning the first 
283     # text/plain part. 
284     
285     elsif ($MIMEObj->ContentType() =~ '^multipart/') {
286         my $plain_parts = $MIMEObj->Children();
287         $plain_parts->ContentType(VALUE => 'text/plain');
288         
289         # If we actully found a part, return its content
290         if ($plain_parts->First && 
291         $plain_parts->First->Content ne '') {
292             $content = $plain_parts->First->Content;            
293         }       
294         
295         # If that fails, return the  first text/ or message/ part 
296         # which has some content.
297     
298         else {
299             my $all_parts = $MIMEObj->Children();
300             while (($content == undef) && 
301                    (my $part = $all_parts->Next)) {
302                 if (($part->ContentType() =~ '^(text|message)(/|$)') and
303                     ($part->Content())) {
304                     $content = $part->Content;
305                 }       
306             }
307         }       
308
309     }
310     # If all else fails, return a message that we couldn't find
311     # any content
312     else { 
313         $content = 'This transaction appears to have no content';
314     }   
315
316     if ($args{'Quote'}) {
317         # Remove quoted signature.
318         $content =~ s/\n-- \n(.*)$//s;
319
320         # What's the longest line like?
321         foreach (split (/\n/,$content)) {
322             $max=length if ( length > $max);
323         }
324
325         if ($max>76) {
326             require Text::Wrapper;
327             my $wrapper=new Text::Wrapper
328                 (
329                  columns => $args{'Wrap'}, 
330                  body_start => ($max > 70*3 ? '   ' : ''),
331                  par_start => ''
332                  );
333             $content=$wrapper->wrap($content);
334         }
335
336         $content =~ s/^/> /gm;
337         $content = '[' . $self->CreatorObj->Name() . ' - ' . $self->CreatedAsString()
338                     . "]:\n\n"
339                 . $content . "\n\n";
340
341     }
342
343     return ($content); 
344 }
345 # }}}
346
347 # {{{ sub Subject
348
349 =head2 Subject
350
351 If this transaction has attached mime objects, returns the first one's subject
352 Otherwise, returns null
353   
354 =cut
355
356 sub Subject {
357     my $self = shift;
358     if ($self->Message->First) {
359         return ($self->Message->First->Subject);
360     }
361     else {
362         return (undef);
363     }
364 }
365 # }}}
366
367 # {{{ sub Attachments 
368
369 =head2 Attachments
370
371   Returns all the RT::Attachment objects which are attached
372 to this transaction. Takes an optional parameter, which is
373 a ContentType that Attachments should be restricted to.
374
375 =cut
376
377
378 sub Attachments  {
379     my $self = shift;
380     my $Types = '';
381     $Types = shift if (@_);
382
383     my $Attachments = new RT::Attachments($self->CurrentUser);
384     
385     #If it's a comment, return an empty object if they don't have the right to see it
386     if ($self->Type eq 'Comment') {
387         unless ($self->CurrentUserHasRight('ShowTicketComments')) {
388             return ($Attachments);
389         }
390     }   
391     #if they ain't got rights to see, return an empty object
392     else {
393         unless ($self->CurrentUserHasRight('ShowTicket')) {
394             return ($Attachments);
395         }
396     }
397     
398     $Attachments->Limit(FIELD => 'TransactionId',
399                         VALUE => $self->Id);
400
401     # Get the attachments in the order they're put into
402     # the database.  Arguably, we should be returning a tree
403     # of attachments, not a set...but no current app seems to need
404     # it. 
405
406     $Attachments->OrderBy(ALIAS => 'main', 
407                           FIELD => 'Id',
408                           ORDER => 'asc');
409
410     if ($Types) {
411         $Attachments->ContentType( VALUE => "$Types",
412                                    OPERATOR => "LIKE");
413     }
414     
415     
416     return($Attachments);
417     
418 }
419
420 # }}}
421
422 # {{{ sub _Attach 
423
424 =head2 _Attach
425
426 A private method used to attach a mime object to this transaction.
427
428 =cut
429
430 sub _Attach  {
431     my $self = shift;
432     my $MIMEObject = shift;
433     
434     if (!defined($MIMEObject)) {
435         $RT::Logger->error("$self _Attach: We can't attach a mime object if you don't give us one.\n");
436         return(0, "$self: no attachment specified");
437     }
438     
439   
440     use RT::Attachment;
441     my $Attachment = new RT::Attachment ($self->CurrentUser);
442     $Attachment->Create(TransactionId => $self->Id,
443                         Attachment => $MIMEObject);
444     return ($Attachment, "Attachment created");
445     
446 }
447
448 # }}}
449
450 # }}}
451
452 # {{{ Routines dealing with Transaction Attributes
453
454 # {{{ sub TicketObj
455
456 =head2 TicketObj
457
458 Returns this transaction's ticket object.
459
460 =cut
461
462 sub TicketObj {
463     my $self = shift;
464     if (! exists $self->{'TicketObj'}) {
465         $self->{'TicketObj'} = new RT::Ticket($self->CurrentUser);
466         $self->{'TicketObj'}->Load($self->Ticket);
467     }
468     
469     return $self->{'TicketObj'};
470 }
471 # }}}
472
473 # {{{ sub Description 
474
475 =head2 Description
476
477 Returns a text string which describes this transaction
478
479 =cut
480
481
482 sub Description  {
483     my $self = shift;
484
485     #Check those ACLs
486     #If it's a comment, we need to be extra special careful
487     if ($self->__Value('Type') eq 'Comment') {
488         unless ($self->CurrentUserHasRight('ShowTicketComments')) {
489             return (0, "Permission Denied");
490         }
491     }   
492
493     #if they ain't got rights to see, don't let em
494     else {
495         unless ($self->CurrentUserHasRight('ShowTicket')) {
496             return (0, "Permission Denied");
497         }
498     }
499
500     if (!defined($self->Type)) {
501         return("No transaction type specified");
502     }
503     
504     return ($self->BriefDescription . " by " . $self->CreatorObj->Name);
505 }
506
507 # }}}
508
509 # {{{ sub BriefDescription 
510
511 =head2 BriefDescription
512
513 Returns a text string which briefly describes this transaction
514
515 =cut
516
517
518 sub BriefDescription  {
519     my $self = shift;
520
521     #Check those ACLs
522     #If it's a comment, we need to be extra special careful
523     if ($self->__Value('Type') eq 'Comment') {
524         unless ($self->CurrentUserHasRight('ShowTicketComments')) {
525             return (0, "Permission Denied");
526         }
527     }   
528
529     #if they ain't got rights to see, don't let em
530     else {
531         unless ($self->CurrentUserHasRight('ShowTicket')) {
532             return (0, "Permission Denied");
533         }
534     }
535
536     if (!defined($self->Type)) {
537         return("No transaction type specified");
538     }
539     
540     if ($self->Type eq 'Create'){
541         return("Ticket created");
542     }
543     elsif ($self->Type =~ /Status/) {
544         if ($self->Field eq 'Status') {
545             if ($self->NewValue eq 'dead') {
546                 return ("Ticket killed");
547       }
548             else {
549                 return( "Status changed from ".  $self->OldValue . 
550                         " to ". $self->NewValue);
551
552             }
553         }
554         # Generic:
555         return ($self->Field." changed from ".($self->OldValue||"(empty value)").
556           " to ".$self->NewValue );
557       }
558     
559     if ($self->Type eq 'Correspond')    {
560         return("Correspondence added");
561     }
562     
563     elsif ($self->Type eq 'Comment')  {
564         return( "Comments added");
565     }
566     
567     elsif ($self->Type eq 'Keyword') {
568
569         my $field = 'Keyword';
570
571         if ($self->Field) {
572             my $keywordsel = new RT::KeywordSelect ($self->CurrentUser);
573             $keywordsel->Load($self->Field);
574             $field = $keywordsel->Name();
575         }
576
577         if ($self->OldValue eq '') {
578             return ($field." ".$self->NewValue." added");
579         }
580         elsif ($self->NewValue eq '') {
581             return ($field." ".$self->OldValue." deleted"); 
582             
583         }
584         else {
585             return ($field." ".$self->OldValue . " changed to ". 
586                      $self->NewValue);
587         }       
588     }
589     
590     elsif ($self->Type eq 'Untake'){
591             return( "Untaken");
592         }
593     
594     elsif ($self->Type eq "Take") {
595         return( "Taken");
596     }
597     
598     elsif ($self->Type eq "Force") {
599         my $Old = RT::User->new($self->CurrentUser);
600         $Old->Load($self->OldValue);
601         my $New = RT::User->new($self->CurrentUser);
602         $New->Load($self->NewValue);
603         return "Owner forcibly changed from ".$Old->Name . " to ". $New->Name;
604     }
605     elsif ($self->Type eq "Steal") {
606         my $Old = RT::User->new($self->CurrentUser);
607         $Old->Load($self->OldValue);
608         return "Stolen from ".$Old->Name;
609     }
610     
611     elsif ($self->Type eq "Give") {
612         my $New = RT::User->new($self->CurrentUser);
613         $New->Load($self->NewValue);
614         return( "Given to ".$New->Name);
615     }
616     
617     elsif ($self->Type eq 'AddWatcher'){
618         return( $self->Field." ". $self->NewValue ." added");
619     }
620     
621     elsif ($self->Type eq 'DelWatcher'){
622         return( $self->Field." ".$self->OldValue ." deleted");
623     }
624     
625     elsif ($self->Type eq 'Subject') {
626         return( "Subject changed to ".$self->Data);
627     }
628     elsif ($self->Type eq 'Told') {
629         return( "User notified");
630     }
631     
632     elsif ($self->Type eq 'AddLink') {
633         return ($self->Data);
634     }
635     elsif ($self->Type eq 'DeleteLink') {
636         return ($self->Data);
637     }
638     elsif ($self->Type eq 'Set') {
639         if ($self->Field eq 'Queue') {
640             my $q1 = new RT::Queue($self->CurrentUser);
641             $q1->Load($self->OldValue);
642             my $q2 = new RT::Queue($self->CurrentUser);
643             $q2->Load($self->NewValue);
644             return ($self->Field . " changed from " . $q1->Name . " to ".
645                     $q2->Name);
646         }
647
648         # Write the date/time change at local time:                    
649     elsif ($self->Field =~  /Due|Starts|Started|Told/) {           
650         my $t1 = new RT::Date($self->CurrentUser);                 
651         $t1->Set(Format => 'ISO', Value => $self->NewValue);       
652         my $t2 = new RT::Date($self->CurrentUser);                 
653         $t2->Set(Format => 'ISO', Value => $self->OldValue);       
654         return ($self->Field . " changed from " . $t2->AsString .  
655                     " to ".$t1->AsString);      
656     }                
657         else {
658             return ($self->Field . " changed from " . $self->OldValue . 
659                     " to ".$self->NewValue);
660         }       
661     }
662     elsif ($self->Type eq 'PurgeTransaction') {
663         return ("Transaction ".$self->Data. " purged");
664     }
665     else {
666         return ("Default: ". $self->Type ."/". $self->Field . 
667                 " changed from " . $self->OldValue . 
668                 " to ".$self->NewValue);
669         
670     }
671 }
672
673 # }}}
674
675 # {{{ Utility methods
676
677 # {{{ sub IsInbound
678
679 =head2 IsInbound
680
681 Returns true if the creator of the transaction is a requestor of the ticket.
682 Returns false otherwise
683
684 =cut
685
686 sub IsInbound {
687     my $self=shift;
688     return ($self->TicketObj->IsRequestor($self->CreatorObj));
689 }
690
691 # }}}
692
693 # }}}
694
695 # {{{ sub _Accessible 
696
697 sub _Accessible  {
698   my $self = shift;
699   my %Cols = (
700               TimeTaken => 'read',
701               Ticket => 'read/public',
702               Type=> 'read',
703               Field => 'read',
704               Data => 'read',
705               NewValue => 'read',
706               OldValue => 'read',
707               Creator => 'read/auto',
708               Created => 'read/auto',
709              );
710   return $self->SUPER::_Accessible(@_, %Cols);
711 }
712
713 # }}}
714
715 # }}}
716
717 # {{{ sub _Set
718
719 sub _Set {
720     my $self = shift;
721     return(0, 'Transactions are immutable');
722 }
723
724 # }}}
725
726 # {{{ sub _Value 
727
728 =head2 _Value
729
730 Takes the name of a table column.
731 Returns its value as a string, if the user passes an ACL check
732
733 =cut
734
735 sub _Value  {
736
737     my $self = shift;
738     my $field = shift;
739     
740     
741     #if the field is public, return it.
742     if ($self->_Accessible($field, 'public')) {
743         return($self->__Value($field));
744         
745     }
746     #If it's a comment, we need to be extra special careful
747     if ($self->__Value('Type') eq 'Comment') {
748         unless ($self->CurrentUserHasRight('ShowTicketComments')) {
749             return (undef);
750         }
751     }   
752     #if they ain't got rights to see, don't let em
753     else {
754         unless ($self->CurrentUserHasRight('ShowTicket')) {
755             return (undef);
756         }
757     }   
758     
759     return($self->__Value($field));
760     
761 }
762
763 # }}}
764
765 # {{{ sub CurrentUserHasRight
766
767 =head2 CurrentUserHasRight RIGHT
768
769 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
770 passed in here.
771
772 =cut
773
774 sub CurrentUserHasRight {
775     my $self = shift;
776     my $right = shift;
777     return ($self->CurrentUser->HasQueueRight(Right => "$right", 
778                                               TicketObj => $self->TicketObj));            
779 }
780
781 # }}}
782
783 1;