c9e13dd775b25e639c2ab47b471166c077930ed6
[freeside.git] / rt / lib / RT / Transaction_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
6 #                                          <sales@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/licenses/old-licenses/gpl-2.0.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
49 =head1 NAME
50
51   RT::Transaction - RT\'s transaction object
52
53 =head1 SYNOPSIS
54
55   use RT::Transaction;
56
57
58 =head1 DESCRIPTION
59
60
61 Each RT::Transaction describes an atomic change to a ticket object 
62 or an update to an RT::Ticket object.
63 It can have arbitrary MIME attachments.
64
65
66 =head1 METHODS
67
68
69 =cut
70
71
72 package RT::Transaction;
73
74 use strict;
75 no warnings qw(redefine);
76
77 use vars qw( %_BriefDescriptions $PreferredContentType );
78
79 use RT::Attachments;
80 use RT::Scrips;
81 use RT::Ruleset;
82
83 use HTML::FormatText;
84 use HTML::TreeBuilder;
85
86 # {{{ sub Create 
87
88 =head2 Create
89
90 Create a new transaction.
91
92 This routine should _never_ be called by anything other than RT::Ticket. 
93 It should not be called 
94 from client code. Ever. Not ever.  If you do this, we will hunt you down and break your kneecaps.
95 Then the unpleasant stuff will start.
96
97 TODO: Document what gets passed to this
98
99 =cut
100
101 sub Create {
102     my $self = shift;
103     my %args = (
104         id             => undef,
105         TimeTaken      => 0,
106         Type           => 'undefined',
107         Data           => '',
108         Field          => undef,
109         OldValue       => undef,
110         NewValue       => undef,
111         MIMEObj        => undef,
112         ActivateScrips => 1,
113         CommitScrips => 1,
114         ObjectType => 'RT::Ticket',
115         ObjectId => 0,
116         ReferenceType => undef,
117         OldReference       => undef,
118         NewReference       => undef,
119         @_
120     );
121
122     $args{ObjectId} ||= $args{Ticket};
123
124     #if we didn't specify a ticket, we need to bail
125     unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
126         return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
127     }
128
129
130
131     #lets create our transaction
132     my %params = (
133         Type      => $args{'Type'},
134         Data      => $args{'Data'},
135         Field     => $args{'Field'},
136         OldValue  => $args{'OldValue'},
137         NewValue  => $args{'NewValue'},
138         Created   => $args{'Created'},
139         ObjectType => $args{'ObjectType'},
140         ObjectId => $args{'ObjectId'},
141         ReferenceType => $args{'ReferenceType'},
142         OldReference => $args{'OldReference'},
143         NewReference => $args{'NewReference'},
144     );
145
146     # Parameters passed in during an import that we probably don't want to touch, otherwise
147     foreach my $attr qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy) {
148         $params{$attr} = $args{$attr} if ($args{$attr});
149     }
150  
151     my $id = $self->SUPER::Create(%params);
152     $self->Load($id);
153     if ( defined $args{'MIMEObj'} ) {
154         my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
155         unless ( $id ) {
156             $RT::Logger->error("Couldn't add attachment: $msg");
157             return ( 0, $self->loc("Couldn't add attachment") );
158         }
159     }
160
161
162     #Provide a way to turn off scrips if we need to
163         $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
164     if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
165        $self->{'scrips'} = RT::Scrips->new($RT::SystemUser);
166
167         $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id); 
168
169         $self->{'scrips'}->Prepare(
170             Stage       => 'TransactionCreate',
171             Type        => $args{'Type'},
172             Ticket      => $args{'ObjectId'},
173             Transaction => $self->id,
174         );
175
176        # Entry point of the rule system
177        my $ticket = RT::Ticket->new($RT::SystemUser);
178        $ticket->Load($args{'ObjectId'});
179        my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
180             Stage       => 'TransactionCreate',
181             Type        => $args{'Type'},
182             TicketObj   => $ticket,
183             TransactionObj => $self,
184        );
185
186         if ($args{'CommitScrips'} ) {
187             $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
188             $self->{'scrips'}->Commit();
189             RT::Ruleset->CommitRules($rules);
190         }
191     }
192
193     return ( $id, $self->loc("Transaction Created") );
194 }
195
196 # }}}
197
198 =head2 Scrips
199
200 Returns the Scrips object for this transaction.
201 This routine is only useful on a freshly created transaction object.
202 Scrips do not get persisted to the database with transactions.
203
204
205 =cut
206
207
208 sub Scrips {
209     my $self = shift;
210     return($self->{'scrips'});
211 }
212
213
214 =head2 Rules
215
216 Returns the array of Rule objects for this transaction.
217 This routine is only useful on a freshly created transaction object.
218 Rules do not get persisted to the database with transactions.
219
220
221 =cut
222
223
224 sub Rules {
225     my $self = shift;
226     return($self->{'rules'});
227 }
228
229
230 # {{{ sub Delete
231
232 =head2 Delete
233
234 Delete this transaction. Currently DOES NOT CHECK ACLS
235
236 =cut
237
238 sub Delete {
239     my $self = shift;
240
241
242     $RT::Handle->BeginTransaction();
243
244     my $attachments = $self->Attachments;
245
246     while (my $attachment = $attachments->Next) {
247         my ($id, $msg) = $attachment->Delete();
248         unless ($id) {
249             $RT::Handle->Rollback();
250             return($id, $self->loc("System Error: [_1]", $msg));
251         }
252     }
253     my ($id,$msg) = $self->SUPER::Delete();
254         unless ($id) {
255             $RT::Handle->Rollback();
256             return($id, $self->loc("System Error: [_1]", $msg));
257         }
258     $RT::Handle->Commit();
259     return ($id,$msg);
260 }
261
262 # }}}
263
264 # {{{ Routines dealing with Attachments
265
266 # {{{ sub Message 
267
268 =head2 Message
269
270 Returns the L<RT::Attachments> object which contains the "top-level" object
271 attachment for this transaction.
272
273 =cut
274
275 sub Message {
276     my $self = shift;
277
278     # XXX: Where is ACL check?
279     
280     unless ( defined $self->{'message'} ) {
281
282         $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
283         $self->{'message'}->Limit(
284             FIELD => 'TransactionId',
285             VALUE => $self->Id
286         );
287         $self->{'message'}->ChildrenOf(0);
288     } else {
289         $self->{'message'}->GotoFirstItem;
290     }
291     return $self->{'message'};
292 }
293
294 # }}}
295
296 # {{{ sub Content
297
298 =head2 Content PARAMHASH
299
300 If this transaction has attached mime objects, returns the body of the first
301 textual part (as defined in RT::I18N::IsTextualContentType).  Otherwise,
302 returns undef.
303
304 Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message 
305 at $args{'Wrap'}.  $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
306
307 If $args{'Type'} is set to C<text/html>, this will return an HTML 
308 part of the message, if available.  Otherwise it looks for a text/plain
309 part. If $args{'Type'} is missing, it defaults to the value of 
310 C<$RT::Transaction::PreferredContentType>, if that's missing too, 
311 defaults to textual.
312
313 =cut
314
315 sub Content {
316     my $self = shift;
317     my %args = (
318         Type => $PreferredContentType || '',
319         Quote => 0,
320         Wrap  => 70,
321         Wrap  => ( $RT::MessageBoxWidth || 72 ) - 2,
322         @_
323     );
324
325     my $content;
326     if ( my $content_obj =
327         $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
328     {
329         $content = $content_obj->Content ||'';
330
331         if ( lc $content_obj->ContentType eq 'text/html' ) {
332             $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
333
334             if ($args{Type} ne 'text/html') {
335                 my $tree = HTML::TreeBuilder->new_from_content( $content );
336                 $content = HTML::FormatText->new(
337                     leftmargin  => 0,
338                     rightmargin => 78,
339                 )->format( $tree);
340                 $tree->delete;
341             }
342         }
343         else {
344             $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
345             if ($args{Type} eq 'text/html') {
346                 # Extremely simple text->html converter
347                 $content =~ s/&/&#38;/g;
348                 $content =~ s/</&lt;/g;
349                 $content =~ s/>/&gt;/g;
350                 $content = "<pre>$content</pre>";
351             }
352         }
353     }
354
355     # If all else fails, return a message that we couldn't find any content
356     else {
357         $content = $self->loc('This transaction appears to have no content');
358     }
359
360     if ( $args{'Quote'} ) {
361
362         # What's the longest line like?
363         my $max = 0;
364         foreach ( split ( /\n/, $content ) ) {
365             $max = length if length > $max;
366         }
367
368         if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
369             require Text::Wrapper;
370             my $wrapper = new Text::Wrapper(
371                 columns    => $args{'Wrap'},
372                 body_start => ( $max > 70 * 3 ? '   ' : '' ),
373                 par_start  => ''
374             );
375             $content = $wrapper->wrap($content);
376         }
377
378         $content =~ s/^/> /gm;
379         $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name)
380           . "\n$content\n\n";
381     }
382
383     return ($content);
384 }
385
386 # }}}
387
388
389 =head2 Addresses
390
391 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
392
393 =cut
394
395 sub Addresses {
396         my $self = shift;
397
398         if (my $attach = $self->Attachments->First) {   
399                 return $attach->Addresses;
400         }
401         else {
402                 return {};
403         }
404
405 }
406
407
408 # {{{ ContentObj
409
410 =head2 ContentObj 
411
412 Returns the RT::Attachment object which contains the content for this Transaction
413
414 =cut
415
416
417 sub ContentObj {
418     my $self = shift;
419     my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
420
421     # If we don't have any content, return undef now.
422     # Get the set of toplevel attachments to this transaction.
423
424     my $Attachment = $args{'Attachment'};
425
426     $Attachment ||= $self->Attachments->First;
427
428     return undef unless ($Attachment);
429
430     # If it's a textual part, just return the body.
431     if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
432         return ($Attachment);
433     }
434
435     # If it's a multipart object, first try returning the first part with preferred
436     # MIME type ('text/plain' by default).
437
438     elsif ( $Attachment->ContentType =~ qr|^multipart/mixed|i ) {
439         my $kids = $Attachment->Children;
440         while (my $child = $kids->Next) {
441             my $ret =  $self->ContentObj(%args, Attachment => $child);
442             return $ret if ($ret);
443         }
444     }
445     elsif ( $Attachment->ContentType =~ qr|^multipart/|i ) {
446         if ( $args{Type} ) {
447             my $plain_parts = $Attachment->Children;
448             $plain_parts->ContentType( VALUE => $args{Type} );
449             $plain_parts->LimitNotEmpty;
450
451             # If we actully found a part, return its content
452             if ( my $first = $plain_parts->First ) {
453                 return $first;
454             }
455         }
456
457         # If that fails, return the first textual part which has some content.
458         my $all_parts = $self->Attachments;
459         while ( my $part = $all_parts->Next ) {
460             next unless RT::I18N::IsTextualContentType($part->ContentType)
461                         && $part->Content;
462             return $part;
463         }
464     }
465
466     # We found no content. suck
467     return (undef);
468 }
469
470 # }}}
471
472 # {{{ sub Subject
473
474 =head2 Subject
475
476 If this transaction has attached mime objects, returns the first one's subject
477 Otherwise, returns null
478   
479 =cut
480
481 sub Subject {
482     my $self = shift;
483     return undef unless my $first = $self->Attachments->First;
484     return $first->Subject;
485 }
486
487 # }}}
488
489 # {{{ sub Attachments 
490
491 =head2 Attachments
492
493 Returns all the RT::Attachment objects which are attached
494 to this transaction. Takes an optional parameter, which is
495 a ContentType that Attachments should be restricted to.
496
497 =cut
498
499 sub Attachments {
500     my $self = shift;
501
502     if ( $self->{'attachments'} ) {
503         $self->{'attachments'}->GotoFirstItem;
504         return $self->{'attachments'};
505     }
506
507     $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
508
509     unless ( $self->CurrentUserCanSee ) {
510         $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0');
511         return $self->{'attachments'};
512     }
513
514     $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
515
516     # Get the self->{'attachments'} in the order they're put into
517     # the database.  Arguably, we should be returning a tree
518     # of self->{'attachments'}, not a set...but no current app seems to need
519     # it.
520
521     $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
522
523     return $self->{'attachments'};
524 }
525
526 # }}}
527
528 # {{{ sub _Attach 
529
530 =head2 _Attach
531
532 A private method used to attach a mime object to this transaction.
533
534 =cut
535
536 sub _Attach {
537     my $self       = shift;
538     my $MIMEObject = shift;
539
540     unless ( defined $MIMEObject ) {
541         $RT::Logger->error("We can't attach a mime object if you don't give us one.");
542         return ( 0, $self->loc("[_1]: no attachment specified", $self) );
543     }
544
545     my $Attachment = RT::Attachment->new( $self->CurrentUser );
546     my ($id, $msg) = $Attachment->Create(
547         TransactionId => $self->Id,
548         Attachment    => $MIMEObject
549     );
550     return ( $Attachment, $msg || $self->loc("Attachment created") );
551 }
552
553 # }}}
554
555 # }}}
556
557 sub ContentAsMIME {
558     my $self = shift;
559
560     my $main_content = $self->ContentObj;
561     return unless $main_content;
562
563     my $entity = $main_content->ContentAsMIME;
564
565     if ( $main_content->Parent ) {
566         # main content is not top most entity, we shouldn't loose
567         # From/To/Cc headers that are on a top part
568         my $attachments = RT::Attachments->new( $self->CurrentUser );
569         $attachments->Columns(qw(id Parent TransactionId Headers));
570         $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
571         $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
572         $attachments->Limit( FIELD => 'Parent', OPERATOR => 'IS', VALUE => 'NULL', QUOTEVALUE => 0 );
573         $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
574         my $tmp = $attachments->First;
575         if ( $tmp && $tmp->id ne $main_content->id ) {
576             $entity->make_multipart;
577             $entity->head->add( split /:/, $_, 2 ) foreach $tmp->SplitHeaders;
578             $entity->make_singlepart;
579         }
580     }
581
582     my $attachments = RT::Attachments->new( $self->CurrentUser );
583     $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
584     $attachments->Limit(
585         FIELD => 'id',
586         OPERATOR => '!=',
587         VALUE => $main_content->id,
588     );
589     $attachments->Limit(
590         FIELD => 'ContentType',
591         OPERATOR => 'NOT STARTSWITH',
592         VALUE => 'multipart/',
593     );
594     $attachments->LimitNotEmpty;
595     while ( my $a = $attachments->Next ) {
596         $entity->make_multipart unless $entity->is_multipart;
597         $entity->add_part( $a->ContentAsMIME );
598     }
599     return $entity;
600 }
601
602 # {{{ Routines dealing with Transaction Attributes
603
604 # {{{ sub Description 
605
606 =head2 Description
607
608 Returns a text string which describes this transaction
609
610 =cut
611
612 sub Description {
613     my $self = shift;
614
615     unless ( $self->CurrentUserCanSee ) {
616         return ( $self->loc("Permission Denied") );
617     }
618
619     unless ( defined $self->Type ) {
620         return ( $self->loc("No transaction type specified"));
621     }
622
623     return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
624 }
625
626 # }}}
627
628 # {{{ sub BriefDescription 
629
630 =head2 BriefDescription
631
632 Returns a text string which briefly describes this transaction
633
634 =cut
635
636 sub BriefDescription {
637     my $self = shift;
638
639     unless ( $self->CurrentUserCanSee ) {
640         return ( $self->loc("Permission Denied") );
641     }
642
643     my $type = $self->Type;    #cache this, rather than calling it 30 times
644
645     unless ( defined $type ) {
646         return $self->loc("No transaction type specified");
647     }
648
649     my $obj_type = $self->FriendlyObjectType;
650
651     if ( $type eq 'Create' ) {
652         return ( $self->loc( "[_1] created", $obj_type ) );
653     }
654     elsif ( $type eq 'Enabled' ) {
655         return ( $self->loc( "[_1] enabled", $obj_type ) );
656     }
657     elsif ( $type eq 'Disabled' ) {
658         return ( $self->loc( "[_1] disabled", $obj_type ) );
659     }
660     elsif ( $type =~ /Status/ ) {
661         if ( $self->Field eq 'Status' ) {
662             if ( $self->NewValue eq 'deleted' ) {
663                 return ( $self->loc( "[_1] deleted", $obj_type ) );
664             }
665             else {
666                 return (
667                     $self->loc(
668                         "Status changed from [_1] to [_2]",
669                         "'" . $self->loc( $self->OldValue ) . "'",
670                         "'" . $self->loc( $self->NewValue ) . "'"
671                     )
672                 );
673
674             }
675         }
676
677         # Generic:
678         my $no_value = $self->loc("(no value)");
679         return (
680             $self->loc(
681                 "[_1] changed from [_2] to [_3]",
682                 $self->Field,
683                 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
684                 "'" . $self->NewValue . "'"
685             )
686         );
687     }
688     elsif ( $type =~ /SystemError/ ) {
689         return $self->loc("System error");
690     }
691
692     if ( my $code = $_BriefDescriptions{$type} ) {
693         return $code->($self);
694     }
695
696     return $self->loc(
697         "Default: [_1]/[_2] changed from [_3] to [_4]",
698         $type,
699         $self->Field,
700         (
701             $self->OldValue
702             ? "'" . $self->OldValue . "'"
703             : $self->loc("(no value)")
704         ),
705         "'" . $self->NewValue . "'"
706     );
707 }
708
709 %_BriefDescriptions = (
710     CommentEmailRecord => sub {
711         my $self = shift;
712         return $self->loc("Outgoing email about a comment recorded");
713     },
714     EmailRecord => sub {
715         my $self = shift;
716         return $self->loc("Outgoing email recorded");
717     },
718     Correspond => sub {
719         my $self = shift;
720         return $self->loc("Correspondence added");
721     },
722     Comment => sub {
723         my $self = shift;
724         return $self->loc("Comments added");
725     },
726     CustomField => sub {
727         my $self = shift;
728         my $field = $self->loc('CustomField');
729
730         if ( $self->Field ) {
731             my $cf = RT::CustomField->new( $self->CurrentUser );
732             $cf->Load( $self->Field );
733             $field = $cf->Name();
734         }
735
736         if ( ! defined $self->OldValue || $self->OldValue eq '' ) {
737             return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
738         }
739         elsif ( !defined $self->NewValue || $self->NewValue eq '' ) {
740             return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
741
742         }
743         else {
744             return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
745         }
746     },
747     Untake => sub {
748         my $self = shift;
749         return $self->loc("Untaken");
750     },
751     Take => sub {
752         my $self = shift;
753         return $self->loc("Taken");
754     },
755     Force => sub {
756         my $self = shift;
757         my $Old = RT::User->new( $self->CurrentUser );
758         $Old->Load( $self->OldValue );
759         my $New = RT::User->new( $self->CurrentUser );
760         $New->Load( $self->NewValue );
761
762         return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
763     },
764     Steal => sub {
765         my $self = shift;
766         my $Old = RT::User->new( $self->CurrentUser );
767         $Old->Load( $self->OldValue );
768         return $self->loc("Stolen from [_1]",  $Old->Name);
769     },
770     Give => sub {
771         my $self = shift;
772         my $New = RT::User->new( $self->CurrentUser );
773         $New->Load( $self->NewValue );
774         return $self->loc( "Given to [_1]",  $New->Name );
775     },
776     AddWatcher => sub {
777         my $self = shift;
778         my $principal = RT::Principal->new($self->CurrentUser);
779         $principal->Load($self->NewValue);
780         return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
781     },
782     DelWatcher => sub {
783         my $self = shift;
784         my $principal = RT::Principal->new($self->CurrentUser);
785         $principal->Load($self->OldValue);
786         return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
787     },
788     Subject => sub {
789         my $self = shift;
790         return $self->loc( "Subject changed to [_1]", $self->Data );
791     },
792     AddLink => sub {
793         my $self = shift;
794         my $value;
795         if ( $self->NewValue ) {
796             my $URI = RT::URI->new( $self->CurrentUser );
797             $URI->FromURI( $self->NewValue );
798             if ( $URI->Resolver ) {
799                 $value = $URI->Resolver->AsString;
800             }
801             else {
802                 $value = $self->NewValue;
803             }
804             if ( $self->Field eq 'DependsOn' ) {
805                 return $self->loc( "Dependency on [_1] added", $value );
806             }
807             elsif ( $self->Field eq 'DependedOnBy' ) {
808                 return $self->loc( "Dependency by [_1] added", $value );
809
810             }
811             elsif ( $self->Field eq 'RefersTo' ) {
812                 return $self->loc( "Reference to [_1] added", $value );
813             }
814             elsif ( $self->Field eq 'ReferredToBy' ) {
815                 return $self->loc( "Reference by [_1] added", $value );
816             }
817             elsif ( $self->Field eq 'MemberOf' ) {
818                 return $self->loc( "Membership in [_1] added", $value );
819             }
820             elsif ( $self->Field eq 'HasMember' ) {
821                 return $self->loc( "Member [_1] added", $value );
822             }
823             elsif ( $self->Field eq 'MergedInto' ) {
824                 return $self->loc( "Merged into [_1]", $value );
825             }
826         }
827         else {
828             return ( $self->Data );
829         }
830     },
831     DeleteLink => sub {
832         my $self = shift;
833         my $value;
834         if ( $self->OldValue ) {
835             my $URI = RT::URI->new( $self->CurrentUser );
836             $URI->FromURI( $self->OldValue );
837             if ( $URI->Resolver ) {
838                 $value = $URI->Resolver->AsString;
839             }
840             else {
841                 $value = $self->OldValue;
842             }
843
844             if ( $self->Field eq 'DependsOn' ) {
845                 return $self->loc( "Dependency on [_1] deleted", $value );
846             }
847             elsif ( $self->Field eq 'DependedOnBy' ) {
848                 return $self->loc( "Dependency by [_1] deleted", $value );
849
850             }
851             elsif ( $self->Field eq 'RefersTo' ) {
852                 return $self->loc( "Reference to [_1] deleted", $value );
853             }
854             elsif ( $self->Field eq 'ReferredToBy' ) {
855                 return $self->loc( "Reference by [_1] deleted", $value );
856             }
857             elsif ( $self->Field eq 'MemberOf' ) {
858                 return $self->loc( "Membership in [_1] deleted", $value );
859             }
860             elsif ( $self->Field eq 'HasMember' ) {
861                 return $self->loc( "Member [_1] deleted", $value );
862             }
863         }
864         else {
865             return ( $self->Data );
866         }
867     },
868     Told => sub {
869         my $self = shift;
870         if ( $self->Field eq 'Told' ) {
871             my $t1 = new RT::Date($self->CurrentUser);
872             $t1->Set(Format => 'ISO', Value => $self->NewValue);
873             my $t2 = new RT::Date($self->CurrentUser);
874             $t2->Set(Format => 'ISO', Value => $self->OldValue);
875             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
876         }
877         else {
878             return $self->loc( "[_1] changed from [_2] to [_3]",
879                                $self->loc($self->Field),
880                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
881         }
882     },
883     Set => sub {
884         my $self = shift;
885         if ( $self->Field eq 'Password' ) {
886             return $self->loc('Password changed');
887         }
888         elsif ( $self->Field eq 'Queue' ) {
889             my $q1 = new RT::Queue( $self->CurrentUser );
890             $q1->Load( $self->OldValue );
891             my $q2 = new RT::Queue( $self->CurrentUser );
892             $q2->Load( $self->NewValue );
893             return $self->loc("[_1] changed from [_2] to [_3]",
894                               $self->loc($self->Field) , $q1->Name , $q2->Name);
895         }
896
897         # Write the date/time change at local time:
898         elsif ($self->Field =~  /Due|Starts|Started|Told/) {
899             my $t1 = new RT::Date($self->CurrentUser);
900             $t1->Set(Format => 'ISO', Value => $self->NewValue);
901             my $t2 = new RT::Date($self->CurrentUser);
902             $t2->Set(Format => 'ISO', Value => $self->OldValue);
903             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
904         }
905         else {
906             return $self->loc( "[_1] changed from [_2] to [_3]",
907                                $self->loc($self->Field),
908                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
909         }
910     },
911     PurgeTransaction => sub {
912         my $self = shift;
913         return $self->loc("Transaction [_1] purged", $self->Data);
914     },
915     AddReminder => sub {
916         my $self = shift;
917         my $ticket = RT::Ticket->new($self->CurrentUser);
918         $ticket->Load($self->NewValue);
919         return $self->loc("Reminder '[_1]' added", $ticket->Subject);
920     },
921     OpenReminder => sub {
922         my $self = shift;
923         my $ticket = RT::Ticket->new($self->CurrentUser);
924         $ticket->Load($self->NewValue);
925         return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
926     
927     },
928     ResolveReminder => sub {
929         my $self = shift;
930         my $ticket = RT::Ticket->new($self->CurrentUser);
931         $ticket->Load($self->NewValue);
932         return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
933     
934     
935     }
936 );
937
938 # }}}
939
940 # {{{ Utility methods
941
942 # {{{ sub IsInbound
943
944 =head2 IsInbound
945
946 Returns true if the creator of the transaction is a requestor of the ticket.
947 Returns false otherwise
948
949 =cut
950
951 sub IsInbound {
952     my $self = shift;
953     $self->ObjectType eq 'RT::Ticket' or return undef;
954     return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
955 }
956
957 # }}}
958
959 # }}}
960
961 sub _OverlayAccessible {
962     {
963
964           ObjectType => { public => 1},
965           ObjectId => { public => 1},
966
967     }
968 };
969
970 # }}}
971
972 # }}}
973
974 # {{{ sub _Set
975
976 sub _Set {
977     my $self = shift;
978     return ( 0, $self->loc('Transactions are immutable') );
979 }
980
981 # }}}
982
983 # {{{ sub _Value 
984
985 =head2 _Value
986
987 Takes the name of a table column.
988 Returns its value as a string, if the user passes an ACL check
989
990 =cut
991
992 sub _Value {
993     my $self  = shift;
994     my $field = shift;
995
996     #if the field is public, return it.
997     if ( $self->_Accessible( $field, 'public' ) ) {
998         return $self->SUPER::_Value( $field );
999     }
1000
1001     unless ( $self->CurrentUserCanSee ) {
1002         return undef;
1003     }
1004
1005     return $self->SUPER::_Value( $field );
1006 }
1007
1008 # }}}
1009
1010 # {{{ sub CurrentUserHasRight
1011
1012 =head2 CurrentUserHasRight RIGHT
1013
1014 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1015 passed in here.
1016
1017 =cut
1018
1019 sub CurrentUserHasRight {
1020     my $self  = shift;
1021     my $right = shift;
1022     return $self->CurrentUser->HasRight(
1023         Right  => $right,
1024         Object => $self->Object
1025     );
1026 }
1027
1028 =head2 CurrentUserCanSee
1029
1030 Returns true if current user has rights to see this particular transaction.
1031
1032 This fact depends on type of the transaction, type of an object the transaction
1033 is attached to and may be other conditions, so this method is prefered over
1034 custom implementations.
1035
1036 =cut
1037
1038 sub CurrentUserCanSee {
1039     my $self = shift;
1040
1041     # If it's a comment, we need to be extra special careful
1042     my $type = $self->__Value('Type');
1043     if ( $type eq 'Comment' ) {
1044         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1045             return 0;
1046         }
1047     }
1048     elsif ( $type eq 'CommentEmailRecord' ) {
1049         unless ( $self->CurrentUserHasRight('ShowTicketComments')
1050             && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1051             return 0;
1052         }
1053     }
1054     elsif ( $type eq 'EmailRecord' ) {
1055         unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1056             return 0;
1057         }
1058     }
1059     # Make sure the user can see the custom field before showing that it changed
1060     elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1061         my $cf = RT::CustomField->new( $self->CurrentUser );
1062         $cf->SetContextObject( $self->Object );
1063         $cf->Load( $cf_id );
1064         return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1065     }
1066     #if they ain't got rights to see, don't let em
1067     elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
1068         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1069             return 0;
1070         }
1071     }
1072
1073     return 1;
1074 }
1075
1076 # }}}
1077
1078 sub Ticket {
1079     my $self = shift;
1080     return $self->ObjectId;
1081 }
1082
1083 sub TicketObj {
1084     my $self = shift;
1085     return $self->Object;
1086 }
1087
1088 sub OldValue {
1089     my $self = shift;
1090     if ( my $type = $self->__Value('ReferenceType')
1091          and my $id = $self->__Value('OldReference') )
1092     {
1093         my $Object = $type->new($self->CurrentUser);
1094         $Object->Load( $id );
1095         return $Object->Content;
1096     }
1097     else {
1098         return $self->__Value('OldValue');
1099     }
1100 }
1101
1102 sub NewValue {
1103     my $self = shift;
1104     if ( my $type = $self->__Value('ReferenceType')
1105          and my $id = $self->__Value('NewReference') )
1106     {
1107         my $Object = $type->new($self->CurrentUser);
1108         $Object->Load( $id );
1109         return $Object->Content;
1110     }
1111     else {
1112         return $self->__Value('NewValue');
1113     }
1114 }
1115
1116 sub Object {
1117     my $self  = shift;
1118     my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1119     $Object->Load($self->__Value('ObjectId'));
1120     return $Object;
1121 }
1122
1123 sub FriendlyObjectType {
1124     my $self = shift;
1125     my $type = $self->ObjectType or return undef;
1126     $type =~ s/^RT:://;
1127     return $self->loc($type);
1128 }
1129
1130 =head2 UpdateCustomFields
1131     
1132     Takes a hash of 
1133
1134     CustomField-<<Id>> => Value
1135         or 
1136
1137     Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1138     this transaction's custom fields
1139
1140 =cut
1141
1142 sub UpdateCustomFields {
1143     my $self = shift;
1144     my %args = (@_);
1145
1146     # This method used to have an API that took a hash of a single
1147     # value "ARGSRef", which was a reference to a hash of arguments.
1148     # This was insane. The next few lines of code preserve that API
1149     # while giving us something saner.
1150
1151     # TODO: 3.6: DEPRECATE OLD API
1152
1153     my $args; 
1154
1155     if ($args{'ARGSRef'}) { 
1156         $args = $args{ARGSRef};
1157     } else {
1158         $args = \%args;
1159     }
1160
1161     foreach my $arg ( keys %$args ) {
1162         next
1163           unless ( $arg =~
1164             /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1165         next if $arg =~ /-Magic$/;
1166         my $cfid   = $1;
1167         my $values = $args->{$arg};
1168         foreach
1169           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1170         {
1171             next unless length($value);
1172             $self->_AddCustomFieldValue(
1173                 Field             => $cfid,
1174                 Value             => $value,
1175                 RecordTransaction => 0,
1176             );
1177         }
1178     }
1179 }
1180
1181
1182
1183 =head2 CustomFieldValues
1184
1185  Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1186
1187  See L<RT::Record>
1188
1189 =cut
1190
1191 sub CustomFieldValues {
1192     my $self  = shift;
1193     my $field = shift;
1194
1195     if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1196
1197         # XXX: $field could be undef when we want fetch values for all CFs
1198         #      do we want to cover this situation somehow here?
1199         unless ( defined $field && $field =~ /^\d+$/o ) {
1200             my $CFs = RT::CustomFields->new( $self->CurrentUser );
1201             $CFs->Limit( FIELD => 'Name', VALUE => $field );
1202             $CFs->LimitToLookupType($self->CustomFieldLookupType);
1203             $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1204             $field = $CFs->First->id if $CFs->First;
1205         }
1206     }
1207     return $self->SUPER::CustomFieldValues($field);
1208 }
1209
1210 # }}}
1211
1212 # {{{ sub CustomFieldLookupType
1213
1214 =head2 CustomFieldLookupType
1215
1216 Returns the RT::Transaction lookup type, which can 
1217 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1218
1219 =cut
1220
1221 # }}}
1222
1223 sub CustomFieldLookupType {
1224     "RT::Queue-RT::Ticket-RT::Transaction";
1225 }
1226
1227
1228 =head2 DeferredRecipients($freq, $include_sent )
1229
1230 Takes the following arguments:
1231
1232 =over
1233
1234 =item * a string to indicate the frequency of digest delivery.  Valid values are "daily", "weekly", or "susp".
1235
1236 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1237
1238 =back
1239
1240 Returns an array of users who should now receive the notification that
1241 was recorded in this transaction.  Returns an empty array if there were
1242 no deferred users, or if $include_sent was not specified and the deferred
1243 notifications have been sent.
1244
1245 =cut
1246
1247 sub DeferredRecipients {
1248     my $self = shift;
1249     my $freq = shift;
1250     my $include_sent = @_? shift : 0;
1251
1252     my $attr = $self->FirstAttribute('DeferredRecipients');
1253
1254     return () unless ($attr);
1255
1256     my $deferred = $attr->Content;
1257
1258     return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1259
1260     # Skip it.
1261    
1262     for my $user (keys %{$deferred->{$freq}}) {
1263         if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) { 
1264             delete $deferred->{$freq}->{$user} 
1265         }
1266     }
1267     # Now get our users.  Easy.
1268     
1269     return keys %{ $deferred->{$freq} };
1270 }
1271
1272
1273
1274 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1275 sub _CacheConfig {
1276   {
1277      'cache_p'        => 1,
1278      'fast_update_p'  => 1,
1279      'cache_for_sec'  => 6000,
1280   }
1281 }
1282
1283
1284 =head2 ACLEquivalenceObjects
1285
1286 This method returns a list of objects for which a user's rights also apply
1287 to this Transaction.
1288
1289 This currently only applies to Transaction Custom Fields on Tickets, so we return
1290 the Ticket's Queue and the Ticket.
1291
1292 This method is called from L<RT::Principal/HasRight>.
1293
1294 =cut
1295
1296 sub ACLEquivalenceObjects {
1297     my $self = shift;
1298
1299     return unless $self->ObjectType eq 'RT::Ticket';
1300     my $object = $self->Object;
1301     return $object,$object->QueueObj;
1302
1303 }
1304
1305 1;