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