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