b8ea9389c6bce3da6ca3e6b1482c90727a640a88
[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 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         @_
322     );
323
324     my $content;
325     if ( my $content_obj =
326         $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
327     {
328         $content = $content_obj->Content ||'';
329
330         if ( lc $content_obj->ContentType eq 'text/html' ) {
331             $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
332
333             if ($args{Type} ne 'text/html') {
334                 my $tree = HTML::TreeBuilder->new_from_content( $content );
335                 $content = HTML::FormatText->new(
336                     leftmargin  => 0,
337                     rightmargin => 78,
338                 )->format( $tree);
339                 $tree->delete;
340             }
341         }
342         else {
343             $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
344             if ($args{Type} eq 'text/html') {
345                 # Extremely simple text->html converter
346                 $content =~ s/&/&#38;/g;
347                 $content =~ s/</&lt;/g;
348                 $content =~ s/>/&gt;/g;
349                 $content = "<pre>$content</pre>";
350             }
351         }
352     }
353
354     # If all else fails, return a message that we couldn't find any content
355     else {
356         $content = $self->loc('This transaction appears to have no content');
357     }
358
359     if ( $args{'Quote'} ) {
360
361         # What's the longest line like?
362         my $max = 0;
363         foreach ( split ( /\n/, $content ) ) {
364             $max = length if length > $max;
365         }
366
367         if ( $max > 76 ) {
368             require Text::Wrapper;
369             my $wrapper = new Text::Wrapper(
370                 columns    => $args{'Wrap'},
371                 body_start => ( $max > 70 * 3 ? '   ' : '' ),
372                 par_start  => ''
373             );
374             $content = $wrapper->wrap($content);
375         }
376
377         $content =~ s/^/> /gm;
378         $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name)
379           . "\n$content\n\n";
380     }
381
382     return ($content);
383 }
384
385 # }}}
386
387
388 =head2 Addresses
389
390 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
391
392 =cut
393
394 sub Addresses {
395         my $self = shift;
396
397         if (my $attach = $self->Attachments->First) {   
398                 return $attach->Addresses;
399         }
400         else {
401                 return {};
402         }
403
404 }
405
406
407 # {{{ ContentObj
408
409 =head2 ContentObj 
410
411 Returns the RT::Attachment object which contains the content for this Transaction
412
413 =cut
414
415
416 sub ContentObj {
417     my $self = shift;
418     my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
419
420     # If we don't have any content, return undef now.
421     # Get the set of toplevel attachments to this transaction.
422
423     my $Attachment = $args{'Attachment'};
424
425     $Attachment ||= $self->Attachments->First;
426
427     return undef unless ($Attachment);
428
429     # If it's a textual part, just return the body.
430     if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
431         return ($Attachment);
432     }
433
434     # If it's a multipart object, first try returning the first part with preferred
435     # MIME type ('text/plain' by default).
436
437     elsif ( $Attachment->ContentType =~ qr|^multipart/mixed|i ) {
438         my $kids = $Attachment->Children;
439         while (my $child = $kids->Next) {
440             my $ret =  $self->ContentObj(%args, Attachment => $child);
441             return $ret if ($ret);
442         }
443     }
444     elsif ( $Attachment->ContentType =~ qr|^multipart/|i ) {
445         if ( $args{Type} ) {
446             my $plain_parts = $Attachment->Children;
447             $plain_parts->ContentType( VALUE => $args{Type} );
448             $plain_parts->LimitNotEmpty;
449
450             # If we actully found a part, return its content
451             if ( my $first = $plain_parts->First ) {
452                 return $first;
453             }
454         }
455
456         # If that fails, return the first textual part which has some content.
457         my $all_parts = $self->Attachments;
458         while ( my $part = $all_parts->Next ) {
459             next unless RT::I18N::IsTextualContentType($part->ContentType)
460                         && $part->Content;
461             return $part;
462         }
463     }
464
465     # We found no content. suck
466     return (undef);
467 }
468
469 # }}}
470
471 # {{{ sub Subject
472
473 =head2 Subject
474
475 If this transaction has attached mime objects, returns the first one's subject
476 Otherwise, returns null
477   
478 =cut
479
480 sub Subject {
481     my $self = shift;
482     return undef unless my $first = $self->Attachments->First;
483     return $first->Subject;
484 }
485
486 # }}}
487
488 # {{{ sub Attachments 
489
490 =head2 Attachments
491
492 Returns all the RT::Attachment objects which are attached
493 to this transaction. Takes an optional parameter, which is
494 a ContentType that Attachments should be restricted to.
495
496 =cut
497
498 sub Attachments {
499     my $self = shift;
500
501     if ( $self->{'attachments'} ) {
502         $self->{'attachments'}->GotoFirstItem;
503         return $self->{'attachments'};
504     }
505
506     $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
507
508     unless ( $self->CurrentUserCanSee ) {
509         $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0');
510         return $self->{'attachments'};
511     }
512
513     $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
514
515     # Get the self->{'attachments'} in the order they're put into
516     # the database.  Arguably, we should be returning a tree
517     # of self->{'attachments'}, not a set...but no current app seems to need
518     # it.
519
520     $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
521
522     return $self->{'attachments'};
523 }
524
525 # }}}
526
527 # {{{ sub _Attach 
528
529 =head2 _Attach
530
531 A private method used to attach a mime object to this transaction.
532
533 =cut
534
535 sub _Attach {
536     my $self       = shift;
537     my $MIMEObject = shift;
538
539     unless ( defined $MIMEObject ) {
540         $RT::Logger->error("We can't attach a mime object if you don't give us one.");
541         return ( 0, $self->loc("[_1]: no attachment specified", $self) );
542     }
543
544     my $Attachment = RT::Attachment->new( $self->CurrentUser );
545     my ($id, $msg) = $Attachment->Create(
546         TransactionId => $self->Id,
547         Attachment    => $MIMEObject
548     );
549     return ( $Attachment, $msg || $self->loc("Attachment created") );
550 }
551
552 # }}}
553
554 # }}}
555
556 sub ContentAsMIME {
557     my $self = shift;
558
559     my $main_content = $self->ContentObj;
560     return unless $main_content;
561
562     my $entity = $main_content->ContentAsMIME;
563
564     if ( $main_content->Parent ) {
565         # main content is not top most entity, we shouldn't loose
566         # From/To/Cc headers that are on a top part
567         my $attachments = RT::Attachments->new( $self->CurrentUser );
568         $attachments->Columns(qw(id Parent TransactionId Headers));
569         $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
570         $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
571         $attachments->Limit( FIELD => 'Parent', OPERATOR => 'IS', VALUE => 'NULL', QUOTEVALUE => 0 );
572         $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
573         my $tmp = $attachments->First;
574         if ( $tmp && $tmp->id ne $main_content->id ) {
575             $entity->make_multipart;
576             $entity->head->add( split /:/, $_, 2 ) foreach $tmp->SplitHeaders;
577             $entity->make_singlepart;
578         }
579     }
580
581     my $attachments = RT::Attachments->new( $self->CurrentUser );
582     $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
583     $attachments->Limit(
584         FIELD => 'id',
585         OPERATOR => '!=',
586         VALUE => $main_content->id,
587     );
588     $attachments->Limit(
589         FIELD => 'ContentType',
590         OPERATOR => 'NOT STARTSWITH',
591         VALUE => 'multipart/',
592     );
593     $attachments->LimitNotEmpty;
594     while ( my $a = $attachments->Next ) {
595         $entity->make_multipart unless $entity->is_multipart;
596         $entity->add_part( $a->ContentAsMIME );
597     }
598     return $entity;
599 }
600
601 # {{{ Routines dealing with Transaction Attributes
602
603 # {{{ sub Description 
604
605 =head2 Description
606
607 Returns a text string which describes this transaction
608
609 =cut
610
611 sub Description {
612     my $self = shift;
613
614     unless ( $self->CurrentUserCanSee ) {
615         return ( $self->loc("Permission Denied") );
616     }
617
618     unless ( defined $self->Type ) {
619         return ( $self->loc("No transaction type specified"));
620     }
621
622     return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
623 }
624
625 # }}}
626
627 # {{{ sub BriefDescription 
628
629 =head2 BriefDescription
630
631 Returns a text string which briefly describes this transaction
632
633 =cut
634
635 sub BriefDescription {
636     my $self = shift;
637
638     unless ( $self->CurrentUserCanSee ) {
639         return ( $self->loc("Permission Denied") );
640     }
641
642     my $type = $self->Type;    #cache this, rather than calling it 30 times
643
644     unless ( defined $type ) {
645         return $self->loc("No transaction type specified");
646     }
647
648     my $obj_type = $self->FriendlyObjectType;
649
650     if ( $type eq 'Create' ) {
651         return ( $self->loc( "[_1] created", $obj_type ) );
652     }
653     elsif ( $type eq 'Enabled' ) {
654         return ( $self->loc( "[_1] enabled", $obj_type ) );
655     }
656     elsif ( $type eq 'Disabled' ) {
657         return ( $self->loc( "[_1] disabled", $obj_type ) );
658     }
659     elsif ( $type =~ /Status/ ) {
660         if ( $self->Field eq 'Status' ) {
661             if ( $self->NewValue eq 'deleted' ) {
662                 return ( $self->loc( "[_1] deleted", $obj_type ) );
663             }
664             else {
665                 return (
666                     $self->loc(
667                         "Status changed from [_1] to [_2]",
668                         "'" . $self->loc( $self->OldValue ) . "'",
669                         "'" . $self->loc( $self->NewValue ) . "'"
670                     )
671                 );
672
673             }
674         }
675
676         # Generic:
677         my $no_value = $self->loc("(no value)");
678         return (
679             $self->loc(
680                 "[_1] changed from [_2] to [_3]",
681                 $self->Field,
682                 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
683                 "'" . $self->NewValue . "'"
684             )
685         );
686     }
687     elsif ( $type =~ /SystemError/ ) {
688         return $self->loc("System error");
689     }
690
691     if ( my $code = $_BriefDescriptions{$type} ) {
692         return $code->($self);
693     }
694
695     return $self->loc(
696         "Default: [_1]/[_2] changed from [_3] to [_4]",
697         $type,
698         $self->Field,
699         (
700             $self->OldValue
701             ? "'" . $self->OldValue . "'"
702             : $self->loc("(no value)")
703         ),
704         "'" . $self->NewValue . "'"
705     );
706 }
707
708 %_BriefDescriptions = (
709     CommentEmailRecord => sub {
710         my $self = shift;
711         return $self->loc("Outgoing email about a comment recorded");
712     },
713     EmailRecord => sub {
714         my $self = shift;
715         return $self->loc("Outgoing email recorded");
716     },
717     Correspond => sub {
718         my $self = shift;
719         return $self->loc("Correspondence added");
720     },
721     Comment => sub {
722         my $self = shift;
723         return $self->loc("Comments added");
724     },
725     CustomField => sub {
726         my $self = shift;
727         my $field = $self->loc('CustomField');
728
729         if ( $self->Field ) {
730             my $cf = RT::CustomField->new( $self->CurrentUser );
731             $cf->Load( $self->Field );
732             $field = $cf->Name();
733         }
734
735         if ( ! defined $self->OldValue || $self->OldValue eq '' ) {
736             return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
737         }
738         elsif ( !defined $self->NewValue || $self->NewValue eq '' ) {
739             return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
740
741         }
742         else {
743             return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
744         }
745     },
746     Untake => sub {
747         my $self = shift;
748         return $self->loc("Untaken");
749     },
750     Take => sub {
751         my $self = shift;
752         return $self->loc("Taken");
753     },
754     Force => sub {
755         my $self = shift;
756         my $Old = RT::User->new( $self->CurrentUser );
757         $Old->Load( $self->OldValue );
758         my $New = RT::User->new( $self->CurrentUser );
759         $New->Load( $self->NewValue );
760
761         return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
762     },
763     Steal => sub {
764         my $self = shift;
765         my $Old = RT::User->new( $self->CurrentUser );
766         $Old->Load( $self->OldValue );
767         return $self->loc("Stolen from [_1]",  $Old->Name);
768     },
769     Give => sub {
770         my $self = shift;
771         my $New = RT::User->new( $self->CurrentUser );
772         $New->Load( $self->NewValue );
773         return $self->loc( "Given to [_1]",  $New->Name );
774     },
775     AddWatcher => sub {
776         my $self = shift;
777         my $principal = RT::Principal->new($self->CurrentUser);
778         $principal->Load($self->NewValue);
779         return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
780     },
781     DelWatcher => sub {
782         my $self = shift;
783         my $principal = RT::Principal->new($self->CurrentUser);
784         $principal->Load($self->OldValue);
785         return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
786     },
787     Subject => sub {
788         my $self = shift;
789         return $self->loc( "Subject changed to [_1]", $self->Data );
790     },
791     AddLink => sub {
792         my $self = shift;
793         my $value;
794         if ( $self->NewValue ) {
795             my $URI = RT::URI->new( $self->CurrentUser );
796             $URI->FromURI( $self->NewValue );
797             if ( $URI->Resolver ) {
798                 $value = $URI->Resolver->AsString;
799             }
800             else {
801                 $value = $self->NewValue;
802             }
803             if ( $self->Field eq 'DependsOn' ) {
804                 return $self->loc( "Dependency on [_1] added", $value );
805             }
806             elsif ( $self->Field eq 'DependedOnBy' ) {
807                 return $self->loc( "Dependency by [_1] added", $value );
808
809             }
810             elsif ( $self->Field eq 'RefersTo' ) {
811                 return $self->loc( "Reference to [_1] added", $value );
812             }
813             elsif ( $self->Field eq 'ReferredToBy' ) {
814                 return $self->loc( "Reference by [_1] added", $value );
815             }
816             elsif ( $self->Field eq 'MemberOf' ) {
817                 return $self->loc( "Membership in [_1] added", $value );
818             }
819             elsif ( $self->Field eq 'HasMember' ) {
820                 return $self->loc( "Member [_1] added", $value );
821             }
822             elsif ( $self->Field eq 'MergedInto' ) {
823                 return $self->loc( "Merged into [_1]", $value );
824             }
825         }
826         else {
827             return ( $self->Data );
828         }
829     },
830     DeleteLink => sub {
831         my $self = shift;
832         my $value;
833         if ( $self->OldValue ) {
834             my $URI = RT::URI->new( $self->CurrentUser );
835             $URI->FromURI( $self->OldValue );
836             if ( $URI->Resolver ) {
837                 $value = $URI->Resolver->AsString;
838             }
839             else {
840                 $value = $self->OldValue;
841             }
842
843             if ( $self->Field eq 'DependsOn' ) {
844                 return $self->loc( "Dependency on [_1] deleted", $value );
845             }
846             elsif ( $self->Field eq 'DependedOnBy' ) {
847                 return $self->loc( "Dependency by [_1] deleted", $value );
848
849             }
850             elsif ( $self->Field eq 'RefersTo' ) {
851                 return $self->loc( "Reference to [_1] deleted", $value );
852             }
853             elsif ( $self->Field eq 'ReferredToBy' ) {
854                 return $self->loc( "Reference by [_1] deleted", $value );
855             }
856             elsif ( $self->Field eq 'MemberOf' ) {
857                 return $self->loc( "Membership in [_1] deleted", $value );
858             }
859             elsif ( $self->Field eq 'HasMember' ) {
860                 return $self->loc( "Member [_1] deleted", $value );
861             }
862         }
863         else {
864             return ( $self->Data );
865         }
866     },
867     Told => sub {
868         my $self = shift;
869         if ( $self->Field eq 'Told' ) {
870             my $t1 = new RT::Date($self->CurrentUser);
871             $t1->Set(Format => 'ISO', Value => $self->NewValue);
872             my $t2 = new RT::Date($self->CurrentUser);
873             $t2->Set(Format => 'ISO', Value => $self->OldValue);
874             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
875         }
876         else {
877             return $self->loc( "[_1] changed from [_2] to [_3]",
878                                $self->loc($self->Field),
879                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
880         }
881     },
882     Set => sub {
883         my $self = shift;
884         if ( $self->Field eq 'Password' ) {
885             return $self->loc('Password changed');
886         }
887         elsif ( $self->Field eq 'Queue' ) {
888             my $q1 = new RT::Queue( $self->CurrentUser );
889             $q1->Load( $self->OldValue );
890             my $q2 = new RT::Queue( $self->CurrentUser );
891             $q2->Load( $self->NewValue );
892             return $self->loc("[_1] changed from [_2] to [_3]",
893                               $self->loc($self->Field) , $q1->Name , $q2->Name);
894         }
895
896         # Write the date/time change at local time:
897         elsif ($self->Field =~  /Due|Starts|Started|Told/) {
898             my $t1 = new RT::Date($self->CurrentUser);
899             $t1->Set(Format => 'ISO', Value => $self->NewValue);
900             my $t2 = new RT::Date($self->CurrentUser);
901             $t2->Set(Format => 'ISO', Value => $self->OldValue);
902             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
903         }
904         else {
905             return $self->loc( "[_1] changed from [_2] to [_3]",
906                                $self->loc($self->Field),
907                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
908         }
909     },
910     PurgeTransaction => sub {
911         my $self = shift;
912         return $self->loc("Transaction [_1] purged", $self->Data);
913     },
914     AddReminder => sub {
915         my $self = shift;
916         my $ticket = RT::Ticket->new($self->CurrentUser);
917         $ticket->Load($self->NewValue);
918         return $self->loc("Reminder '[_1]' added", $ticket->Subject);
919     },
920     OpenReminder => sub {
921         my $self = shift;
922         my $ticket = RT::Ticket->new($self->CurrentUser);
923         $ticket->Load($self->NewValue);
924         return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
925     
926     },
927     ResolveReminder => sub {
928         my $self = shift;
929         my $ticket = RT::Ticket->new($self->CurrentUser);
930         $ticket->Load($self->NewValue);
931         return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
932     
933     
934     }
935 );
936
937 # }}}
938
939 # {{{ Utility methods
940
941 # {{{ sub IsInbound
942
943 =head2 IsInbound
944
945 Returns true if the creator of the transaction is a requestor of the ticket.
946 Returns false otherwise
947
948 =cut
949
950 sub IsInbound {
951     my $self = shift;
952     $self->ObjectType eq 'RT::Ticket' or return undef;
953     return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
954 }
955
956 # }}}
957
958 # }}}
959
960 sub _OverlayAccessible {
961     {
962
963           ObjectType => { public => 1},
964           ObjectId => { public => 1},
965
966     }
967 };
968
969 # }}}
970
971 # }}}
972
973 # {{{ sub _Set
974
975 sub _Set {
976     my $self = shift;
977     return ( 0, $self->loc('Transactions are immutable') );
978 }
979
980 # }}}
981
982 # {{{ sub _Value 
983
984 =head2 _Value
985
986 Takes the name of a table column.
987 Returns its value as a string, if the user passes an ACL check
988
989 =cut
990
991 sub _Value {
992     my $self  = shift;
993     my $field = shift;
994
995     #if the field is public, return it.
996     if ( $self->_Accessible( $field, 'public' ) ) {
997         return $self->SUPER::_Value( $field );
998     }
999
1000     unless ( $self->CurrentUserCanSee ) {
1001         return undef;
1002     }
1003
1004     return $self->SUPER::_Value( $field );
1005 }
1006
1007 # }}}
1008
1009 # {{{ sub CurrentUserHasRight
1010
1011 =head2 CurrentUserHasRight RIGHT
1012
1013 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1014 passed in here.
1015
1016 =cut
1017
1018 sub CurrentUserHasRight {
1019     my $self  = shift;
1020     my $right = shift;
1021     return $self->CurrentUser->HasRight(
1022         Right  => $right,
1023         Object => $self->Object
1024     );
1025 }
1026
1027 =head2 CurrentUserCanSee
1028
1029 Returns true if current user has rights to see this particular transaction.
1030
1031 This fact depends on type of the transaction, type of an object the transaction
1032 is attached to and may be other conditions, so this method is prefered over
1033 custom implementations.
1034
1035 =cut
1036
1037 sub CurrentUserCanSee {
1038     my $self = shift;
1039
1040     # If it's a comment, we need to be extra special careful
1041     my $type = $self->__Value('Type');
1042     if ( $type eq 'Comment' ) {
1043         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1044             return 0;
1045         }
1046     }
1047     elsif ( $type eq 'CommentEmailRecord' ) {
1048         unless ( $self->CurrentUserHasRight('ShowTicketComments')
1049             && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1050             return 0;
1051         }
1052     }
1053     elsif ( $type eq 'EmailRecord' ) {
1054         unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1055             return 0;
1056         }
1057     }
1058     # Make sure the user can see the custom field before showing that it changed
1059     elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1060         my $cf = RT::CustomField->new( $self->CurrentUser );
1061         $cf->SetContextObject( $self->Object );
1062         $cf->Load( $cf_id );
1063         return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1064     }
1065     #if they ain't got rights to see, don't let em
1066     elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
1067         unless ( $self->CurrentUserHasRight('ShowTicket') ) {
1068             return 0;
1069         }
1070     }
1071
1072     return 1;
1073 }
1074
1075 # }}}
1076
1077 sub Ticket {
1078     my $self = shift;
1079     return $self->ObjectId;
1080 }
1081
1082 sub TicketObj {
1083     my $self = shift;
1084     return $self->Object;
1085 }
1086
1087 sub OldValue {
1088     my $self = shift;
1089     if ( my $type = $self->__Value('ReferenceType')
1090          and my $id = $self->__Value('OldReference') )
1091     {
1092         my $Object = $type->new($self->CurrentUser);
1093         $Object->Load( $id );
1094         return $Object->Content;
1095     }
1096     else {
1097         return $self->__Value('OldValue');
1098     }
1099 }
1100
1101 sub NewValue {
1102     my $self = shift;
1103     if ( my $type = $self->__Value('ReferenceType')
1104          and my $id = $self->__Value('NewReference') )
1105     {
1106         my $Object = $type->new($self->CurrentUser);
1107         $Object->Load( $id );
1108         return $Object->Content;
1109     }
1110     else {
1111         return $self->__Value('NewValue');
1112     }
1113 }
1114
1115 sub Object {
1116     my $self  = shift;
1117     my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1118     $Object->Load($self->__Value('ObjectId'));
1119     return $Object;
1120 }
1121
1122 sub FriendlyObjectType {
1123     my $self = shift;
1124     my $type = $self->ObjectType or return undef;
1125     $type =~ s/^RT:://;
1126     return $self->loc($type);
1127 }
1128
1129 =head2 UpdateCustomFields
1130     
1131     Takes a hash of 
1132
1133     CustomField-<<Id>> => Value
1134         or 
1135
1136     Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1137     this transaction's custom fields
1138
1139 =cut
1140
1141 sub UpdateCustomFields {
1142     my $self = shift;
1143     my %args = (@_);
1144
1145     # This method used to have an API that took a hash of a single
1146     # value "ARGSRef", which was a reference to a hash of arguments.
1147     # This was insane. The next few lines of code preserve that API
1148     # while giving us something saner.
1149
1150     # TODO: 3.6: DEPRECATE OLD API
1151
1152     my $args; 
1153
1154     if ($args{'ARGSRef'}) { 
1155         $args = $args{ARGSRef};
1156     } else {
1157         $args = \%args;
1158     }
1159
1160     foreach my $arg ( keys %$args ) {
1161         next
1162           unless ( $arg =~
1163             /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1164         next if $arg =~ /-Magic$/;
1165         my $cfid   = $1;
1166         my $values = $args->{$arg};
1167         foreach
1168           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1169         {
1170             next unless length($value);
1171             $self->_AddCustomFieldValue(
1172                 Field             => $cfid,
1173                 Value             => $value,
1174                 RecordTransaction => 0,
1175             );
1176         }
1177     }
1178 }
1179
1180
1181
1182 =head2 CustomFieldValues
1183
1184  Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1185
1186  See L<RT::Record>
1187
1188 =cut
1189
1190 sub CustomFieldValues {
1191     my $self  = shift;
1192     my $field = shift;
1193
1194     if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1195
1196         # XXX: $field could be undef when we want fetch values for all CFs
1197         #      do we want to cover this situation somehow here?
1198         unless ( defined $field && $field =~ /^\d+$/o ) {
1199             my $CFs = RT::CustomFields->new( $self->CurrentUser );
1200             $CFs->Limit( FIELD => 'Name', VALUE => $field );
1201             $CFs->LimitToLookupType($self->CustomFieldLookupType);
1202             $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1203             $field = $CFs->First->id if $CFs->First;
1204         }
1205     }
1206     return $self->SUPER::CustomFieldValues($field);
1207 }
1208
1209 # }}}
1210
1211 # {{{ sub CustomFieldLookupType
1212
1213 =head2 CustomFieldLookupType
1214
1215 Returns the RT::Transaction lookup type, which can 
1216 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1217
1218 =cut
1219
1220 # }}}
1221
1222 sub CustomFieldLookupType {
1223     "RT::Queue-RT::Ticket-RT::Transaction";
1224 }
1225
1226
1227 =head2 DeferredRecipients($freq, $include_sent )
1228
1229 Takes the following arguments:
1230
1231 =over
1232
1233 =item * a string to indicate the frequency of digest delivery.  Valid values are "daily", "weekly", or "susp".
1234
1235 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1236
1237 =back
1238
1239 Returns an array of users who should now receive the notification that
1240 was recorded in this transaction.  Returns an empty array if there were
1241 no deferred users, or if $include_sent was not specified and the deferred
1242 notifications have been sent.
1243
1244 =cut
1245
1246 sub DeferredRecipients {
1247     my $self = shift;
1248     my $freq = shift;
1249     my $include_sent = @_? shift : 0;
1250
1251     my $attr = $self->FirstAttribute('DeferredRecipients');
1252
1253     return () unless ($attr);
1254
1255     my $deferred = $attr->Content;
1256
1257     return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1258
1259     # Skip it.
1260    
1261     for my $user (keys %{$deferred->{$freq}}) {
1262         if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) { 
1263             delete $deferred->{$freq}->{$user} 
1264         }
1265     }
1266     # Now get our users.  Easy.
1267     
1268     return keys %{ $deferred->{$freq} };
1269 }
1270
1271
1272
1273 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1274 sub _CacheConfig {
1275   {
1276      'cache_p'        => 1,
1277      'fast_update_p'  => 1,
1278      'cache_for_sec'  => 6000,
1279   }
1280 }
1281
1282
1283 =head2 ACLEquivalenceObjects
1284
1285 This method returns a list of objects for which a user's rights also apply
1286 to this Transaction.
1287
1288 This currently only applies to Transaction Custom Fields on Tickets, so we return
1289 the Ticket's Queue and the Ticket.
1290
1291 This method is called from L<RT::Principal/HasRight>.
1292
1293 =cut
1294
1295 sub ACLEquivalenceObjects {
1296     my $self = shift;
1297
1298     return unless $self->ObjectType eq 'RT::Ticket';
1299     my $object = $self->Object;
1300     return $object,$object->QueueObj;
1301
1302 }
1303
1304 1;