Merge branch 'master' of git.uio.no:usit-rt
[usit-rt.git] / lib / RT / Transaction.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 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 base 'RT::Record';
75 use strict;
76 use warnings;
77
78
79 use vars qw( %_BriefDescriptions $PreferredContentType );
80
81 use RT::Attachments;
82 use RT::Scrips;
83 use RT::Ruleset;
84
85 use HTML::FormatText::WithLinks::AndTables;
86 use HTML::Scrubber;
87
88 # For EscapeHTML() and decode_entities()
89 require RT::Interface::Web;
90 require HTML::Entities;
91
92 sub Table {'Transactions'}
93
94 # {{{ sub Create 
95
96 =head2 Create
97
98 Create a new transaction.
99
100 This routine should _never_ be called by anything other than RT::Ticket. 
101 It should not be called 
102 from client code. Ever. Not ever.  If you do this, we will hunt you down and break your kneecaps.
103 Then the unpleasant stuff will start.
104
105 TODO: Document what gets passed to this
106
107 =cut
108
109 sub Create {
110     my $self = shift;
111     my %args = (
112         id             => undef,
113         TimeTaken      => 0,
114         Type           => 'undefined',
115         Data           => '',
116         Field          => undef,
117         OldValue       => undef,
118         NewValue       => undef,
119         MIMEObj        => undef,
120         ActivateScrips => 1,
121         CommitScrips   => 1,
122         ObjectType     => 'RT::Ticket',
123         ObjectId       => 0,
124         ReferenceType  => undef,
125         OldReference   => undef,
126         NewReference   => undef,
127         SquelchMailTo  => undef,
128         @_
129     );
130
131     $args{ObjectId} ||= $args{Ticket};
132
133     #if we didn't specify a ticket, we need to bail
134     unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
135         return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
136     }
137
138
139
140     #lets create our transaction
141     my %params = (
142         Type      => $args{'Type'},
143         Data      => $args{'Data'},
144         Field     => $args{'Field'},
145         OldValue  => $args{'OldValue'},
146         NewValue  => $args{'NewValue'},
147         Created   => $args{'Created'},
148         ObjectType => $args{'ObjectType'},
149         ObjectId => $args{'ObjectId'},
150         ReferenceType => $args{'ReferenceType'},
151         OldReference => $args{'OldReference'},
152         NewReference => $args{'NewReference'},
153     );
154
155     # Parameters passed in during an import that we probably don't want to touch, otherwise
156     foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
157         $params{$attr} = $args{$attr} if ($args{$attr});
158     }
159  
160     my $id = $self->SUPER::Create(%params);
161     $self->Load($id);
162     if ( defined $args{'MIMEObj'} ) {
163         my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
164         unless ( $id ) {
165             $RT::Logger->error("Couldn't add attachment: $msg");
166             return ( 0, $self->loc("Couldn't add attachment") );
167         }
168     }
169
170     $self->AddAttribute(
171         Name    => 'SquelchMailTo',
172         Content => RT::User->CanonicalizeEmailAddress($_)
173     ) for @{$args{'SquelchMailTo'} || []};
174
175     my @return = ( $id, $self->loc("Transaction Created") );
176
177     return @return unless $args{'ObjectType'} eq 'RT::Ticket';
178
179     # Provide a way to turn off scrips if we need to
180     unless ( $args{'ActivateScrips'} ) {
181         $RT::Logger->debug('Skipping scrips for transaction #' .$self->Id);
182         return @return;
183     }
184
185     $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
186
187     $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id); 
188
189     $self->{'scrips'}->Prepare(
190         Stage       => 'TransactionCreate',
191         Type        => $args{'Type'},
192         Ticket      => $args{'ObjectId'},
193         Transaction => $self->id,
194     );
195
196    # Entry point of the rule system
197    my $ticket = RT::Ticket->new(RT->SystemUser);
198    $ticket->Load($args{'ObjectId'});
199    my $txn = RT::Transaction->new($RT::SystemUser);
200    $txn->Load($self->id);
201
202    my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
203         Stage       => 'TransactionCreate',
204         Type        => $args{'Type'},
205         TicketObj   => $ticket,
206         TransactionObj => $txn,
207    );
208
209     if ($args{'CommitScrips'} ) {
210         $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
211         $self->{'scrips'}->Commit();
212         RT::Ruleset->CommitRules($rules);
213     }
214
215     return @return;
216 }
217
218
219 =head2 Scrips
220
221 Returns the Scrips object for this transaction.
222 This routine is only useful on a freshly created transaction object.
223 Scrips do not get persisted to the database with transactions.
224
225
226 =cut
227
228
229 sub Scrips {
230     my $self = shift;
231     return($self->{'scrips'});
232 }
233
234
235 =head2 Rules
236
237 Returns the array of Rule objects for this transaction.
238 This routine is only useful on a freshly created transaction object.
239 Rules do not get persisted to the database with transactions.
240
241
242 =cut
243
244
245 sub Rules {
246     my $self = shift;
247     return($self->{'rules'});
248 }
249
250
251
252 =head2 Delete
253
254 Delete this transaction. Currently DOES NOT CHECK ACLS
255
256 =cut
257
258 sub Delete {
259     my $self = shift;
260
261
262     $RT::Handle->BeginTransaction();
263
264     my $attachments = $self->Attachments;
265
266     while (my $attachment = $attachments->Next) {
267         my ($id, $msg) = $attachment->Delete();
268         unless ($id) {
269             $RT::Handle->Rollback();
270             return($id, $self->loc("System Error: [_1]", $msg));
271         }
272     }
273     my ($id,$msg) = $self->SUPER::Delete();
274         unless ($id) {
275             $RT::Handle->Rollback();
276             return($id, $self->loc("System Error: [_1]", $msg));
277         }
278     $RT::Handle->Commit();
279     return ($id,$msg);
280 }
281
282
283
284
285 =head2 Message
286
287 Returns the L<RT::Attachments> object which contains the "top-level" object
288 attachment for this transaction.
289
290 =cut
291
292 sub Message {
293     my $self = shift;
294
295     # XXX: Where is ACL check?
296     
297     unless ( defined $self->{'message'} ) {
298
299         $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
300         $self->{'message'}->Limit(
301             FIELD => 'TransactionId',
302             VALUE => $self->Id
303         );
304         $self->{'message'}->ChildrenOf(0);
305     } else {
306         $self->{'message'}->GotoFirstItem;
307     }
308     return $self->{'message'};
309 }
310
311
312
313 =head2 Content PARAMHASH
314
315 If this transaction has attached mime objects, returns the body of the first
316 textual part (as defined in RT::I18N::IsTextualContentType).  Otherwise,
317 returns undef.
318
319 Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message 
320 at $args{'Wrap'}.  $args{'Wrap'} defaults to 70.
321
322 If $args{'Type'} is set to C<text/html>, this will return an HTML 
323 part of the message, if available.  Otherwise it looks for a text/plain
324 part. If $args{'Type'} is missing, it defaults to the value of 
325 C<$RT::Transaction::PreferredContentType>, if that's missing too, 
326 defaults to textual.
327
328 =cut
329
330 sub Content {
331     my $self = shift;
332     my %args = (
333         Type => $PreferredContentType || '',
334         Quote => 0,
335         Wrap  => 70,
336         @_
337     );
338
339     my $content;
340     if ( my $content_obj =
341         $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
342     {
343         $content = $content_obj->Content ||'';
344
345         if ( lc $content_obj->ContentType eq 'text/html' ) {
346             $content =~ s/(?:(<\/div>)|<p>|<br\s*\/?>|<div(\s+class="[^"]+")?>)\s*--\s+<br\s*\/?>.*?$/$1/s if $args{'Quote'};
347
348             if ($args{Type} ne 'text/html') {
349                 $content = RT::Interface::Email::ConvertHTMLToText($content);
350             } else {
351                 # Scrub out <html>, <head>, <meta>, and <body>, and
352                 # leave all else untouched.
353                 my $scrubber = HTML::Scrubber->new();
354                 $scrubber->rules(
355                     html => 0,
356                     head => 0,
357                     meta => 0,
358                     body => 0,
359                 );
360                 $scrubber->default( 1 => { '*' => 1 } );
361                 $content = $scrubber->scrub( $content );
362             }
363         }
364         else {
365             $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
366             if ($args{Type} eq 'text/html') {
367                 # Extremely simple text->html converter
368                 $content =~ s/&/&#38;/g;
369                 $content =~ s/</&lt;/g;
370                 $content =~ s/>/&gt;/g;
371                 $content = qq|<pre style="white-space: pre-wrap; font-family: monospace;">$content</pre>|;
372             }
373         }
374     }
375
376     # If all else fails, return a message that we couldn't find any content
377     else {
378         $content = $self->loc('This transaction appears to have no content');
379     }
380
381     if ( $args{'Quote'} ) {
382         if ($args{Type} eq 'text/html') {
383             $content = '<div class="gmail_quote">'
384                 . $self->QuoteHeader
385                 . '<br /><blockquote class="gmail_quote" type="cite">'
386                 . $content
387                 . '</blockquote></div><br /><br />';
388         } else {
389             $content = $self->ApplyQuoteWrap(content => $content,
390                                              cols    => $args{'Wrap'} );
391
392             $content = $self->QuoteHeader . "\n$content\n\n";
393         }
394     }
395
396     return ($content);
397 }
398
399 =head2 QuoteHeader
400
401 Returns text prepended to content when transaction is quoted
402 (see C<Quote> argument in L</Content>). By default returns
403 localized "On <date> <user name> wrote:\n".
404
405 =cut
406
407 sub QuoteHeader {
408     my $self = shift;
409     return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
410 }
411
412 =head2 ApplyQuoteWrap PARAMHASH
413
414 Wrapper to calculate wrap criteria and apply quote wrapping if needed.
415
416 =cut
417
418 sub ApplyQuoteWrap {
419     my $self = shift;
420     my %args = @_;
421     my $content = $args{content};
422
423     # What's the longest line like?
424     my $max = 0;
425     foreach ( split ( /\n/, $args{content} ) ) {
426         $max = length if length > $max;
427     }
428
429     if ( $max > 76 ) {
430         require Text::Quoted;
431         require Text::Wrapper;
432
433         my $structure = Text::Quoted::extract($args{content});
434         $content = $self->QuoteWrap(content_ref => $structure,
435                                     cols        => $args{cols},
436                                     max         => $max );
437     }
438
439     $content =~ s/^/> /gm;  # use regex since string might be multi-line
440     return $content;
441 }
442
443 =head2 QuoteWrap PARAMHASH
444
445 Wrap the contents of transactions based on Wrap settings, maintaining
446 the quote character from the original.
447
448 =cut
449
450 sub QuoteWrap {
451     my $self = shift;
452     my %args = @_;
453     my $ref = $args{content_ref};
454     my $final_string;
455
456     if ( ref $ref eq 'ARRAY' ){
457         foreach my $array (@$ref){
458             $final_string .= $self->QuoteWrap(content_ref => $array,
459                                               cols        => $args{cols},
460                                               max         => $args{max} );
461         }
462     }
463     elsif ( ref $ref eq 'HASH' ){
464         return $ref->{quoter} . "\n" if $ref->{empty}; # Blank line
465
466         my $col = $args{cols} - (length $ref->{quoter});
467         my $wrapper = Text::Wrapper->new( columns => $col );
468
469         # Wrap on individual lines to honor incoming line breaks
470         # Otherwise deliberate separate lines (like a list or a sig)
471         # all get combined incorrectly into single paragraphs.
472
473         my @lines = split /\n/, $ref->{text};
474         my $wrap = join '', map { $wrapper->wrap($_) } @lines;
475         my $quoter = $ref->{quoter};
476
477         # Only add the space if actually quoting
478         $quoter .= ' ' if length $quoter;
479         $wrap =~ s/^/$quoter/mg;  # use regex since string might be multi-line
480
481         return $wrap;
482     }
483     else{
484         $RT::Logger->warning("Can't apply quoting with $ref");
485         return;
486     }
487     return $final_string;
488 }
489
490
491 =head2 Addresses
492
493 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
494
495 =cut
496
497 sub Addresses {
498     my $self = shift;
499
500     if (my $attach = $self->Attachments->First) {
501         return $attach->Addresses;
502     }
503     else {
504         return {};
505     }
506
507 }
508
509
510
511 =head2 ContentObj 
512
513 Returns the RT::Attachment object which contains the content for this Transaction
514
515 =cut
516
517
518 sub ContentObj {
519     my $self = shift;
520     my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
521
522     # If we don't have any content, return undef now.
523     # Get the set of toplevel attachments to this transaction.
524
525     my $Attachment = $args{'Attachment'};
526
527     $Attachment ||= $self->Attachments->First;
528
529     return undef unless ($Attachment);
530
531     # If it's a textual part, just return the body.
532     if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
533         return ($Attachment);
534     }
535
536     # If it's a multipart object, first try returning the first part with preferred
537     # MIME type ('text/plain' by default).
538
539     elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
540         my $kids = $Attachment->Children;
541         while (my $child = $kids->Next) {
542             my $ret =  $self->ContentObj(%args, Attachment => $child);
543             return $ret if ($ret);
544         }
545     }
546     elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
547         if ( $args{Type} ) {
548             my $plain_parts = $Attachment->Children;
549             $plain_parts->ContentType( VALUE => $args{Type} );
550             $plain_parts->LimitNotEmpty;
551
552             # If we actully found a part, return its content
553             if ( my $first = $plain_parts->First ) {
554                 return $first;
555             }
556         }
557
558         # If that fails, return the first textual part which has some content.
559         my $all_parts = $self->Attachments;
560         while ( my $part = $all_parts->Next ) {
561             next unless RT::I18N::IsTextualContentType($part->ContentType)
562                         && $part->Content;
563             return $part;
564         }
565     }
566
567     # We found no content. suck
568     return (undef);
569 }
570
571
572
573 =head2 Subject
574
575 If this transaction has attached mime objects, returns the first one's subject
576 Otherwise, returns null
577   
578 =cut
579
580 sub Subject {
581     my $self = shift;
582     return undef unless my $first = $self->Attachments->First;
583     return $first->Subject;
584 }
585
586
587
588 =head2 Attachments
589
590 Returns all the RT::Attachment objects which are attached
591 to this transaction. Takes an optional parameter, which is
592 a ContentType that Attachments should be restricted to.
593
594 =cut
595
596 sub Attachments {
597     my $self = shift;
598
599     if ( $self->{'attachments'} ) {
600         $self->{'attachments'}->GotoFirstItem;
601         return $self->{'attachments'};
602     }
603
604     $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
605
606     unless ( $self->CurrentUserCanSee ) {
607         $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
608         return $self->{'attachments'};
609     }
610
611     $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
612
613     # Get the self->{'attachments'} in the order they're put into
614     # the database.  Arguably, we should be returning a tree
615     # of self->{'attachments'}, not a set...but no current app seems to need
616     # it.
617
618     $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
619
620     return $self->{'attachments'};
621 }
622
623
624
625 =head2 _Attach
626
627 A private method used to attach a mime object to this transaction.
628
629 =cut
630
631 sub _Attach {
632     my $self       = shift;
633     my $MIMEObject = shift;
634
635     unless ( defined $MIMEObject ) {
636         $RT::Logger->error("We can't attach a mime object if you don't give us one.");
637         return ( 0, $self->loc("[_1]: no attachment specified", $self) );
638     }
639
640     my $Attachment = RT::Attachment->new( $self->CurrentUser );
641     my ($id, $msg) = $Attachment->Create(
642         TransactionId => $self->Id,
643         Attachment    => $MIMEObject
644     );
645     return ( $Attachment, $msg || $self->loc("Attachment created") );
646 }
647
648
649
650 sub ContentAsMIME {
651     my $self = shift;
652
653     # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
654     # since it has less information available without looking to it's parent
655     # transaction.  Check ACLs here before we go any further.
656     return unless $self->CurrentUserCanSee;
657
658     my $attachments = RT::Attachments->new( $self->CurrentUser );
659     $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
660     $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
661     $attachments->Limit( FIELD => 'Parent',        VALUE => 0 );
662     $attachments->RowsPerPage(1);
663
664     my $top = $attachments->First;
665     return unless $top;
666
667     my $entity = MIME::Entity->build(
668         Type        => 'message/rfc822',
669         Description => 'transaction ' . $self->id,
670         Data        => $top->ContentAsMIME(Children => 1)->as_string,
671     );
672
673     return $entity;
674 }
675
676
677
678 =head2 Description
679
680 Returns a text string which describes this transaction
681
682 =cut
683
684 sub Description {
685     my $self = shift;
686
687     unless ( $self->CurrentUserCanSee ) {
688         return ( $self->loc("Permission Denied") );
689     }
690
691     unless ( defined $self->Type ) {
692         return ( $self->loc("No transaction type specified"));
693     }
694
695     return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
696 }
697
698
699
700 =head2 BriefDescription
701
702 Returns a text string which briefly describes this transaction
703
704 =cut
705
706 {
707     my $scrubber = HTML::Scrubber->new(default => 0); # deny everything
708
709     sub BriefDescription {
710         my $self = shift;
711         my $desc = $self->BriefDescriptionAsHTML;
712            $desc = $scrubber->scrub($desc);
713            $desc = HTML::Entities::decode_entities($desc);
714         return $desc;
715     }
716 }
717
718 =head2 BriefDescriptionAsHTML
719
720 Returns an HTML string which briefly describes this transaction.
721
722 =cut
723
724 sub BriefDescriptionAsHTML {
725     my $self = shift;
726
727     unless ( $self->CurrentUserCanSee ) {
728         return ( $self->loc("Permission Denied") );
729     }
730
731     my ($objecttype, $type, $field) = ($self->ObjectType, $self->Type, $self->Field);
732
733     unless ( defined $type ) {
734         return $self->loc("No transaction type specified");
735     }
736
737     my ($template, @params);
738
739     my @code = grep { ref eq 'CODE' } map { $_BriefDescriptions{$_} }
740         ( $field
741             ? ("$objecttype-$type-$field", "$type-$field")
742             : () ),
743         "$objecttype-$type", $type;
744
745     if (@code) {
746         ($template, @params) = $code[0]->($self);
747     }
748
749     unless ($template) {
750         ($template, @params) = (
751             "Default: [_1]/[_2] changed from [_3] to [_4]", #loc
752             $type,
753             $field,
754             (
755                 $self->OldValue
756                 ? "'" . $self->OldValue . "'"
757                 : $self->loc("(no value)")
758             ),
759             (
760                 $self->NewValue
761                 ? "'" . $self->NewValue . "'"
762                 : $self->loc("(no value)")
763             ),
764         );
765     }
766     return $self->loc($template, $self->_ProcessReturnValues(@params));
767 }
768
769 sub _ProcessReturnValues {
770     my $self   = shift;
771     my @values = @_;
772     return map {
773         if    (ref eq 'ARRAY')  { $_ = join "", $self->_ProcessReturnValues(@$_) }
774         elsif (ref eq 'SCALAR') { $_ = $$_ }
775         else                    { RT::Interface::Web::EscapeHTML(\$_) }
776         $_
777     } @values;
778 }
779
780 sub _FormatPrincipal {
781     my $self = shift;
782     my $principal = shift;
783     if ($principal->IsUser) {
784         return $self->_FormatUser( $principal->Object );
785     } else {
786         return $self->loc("group [_1]", $principal->Object->Name);
787     }
788 }
789
790 sub _FormatUser {
791     my $self = shift;
792     my $user = shift;
793     return [
794         \'<span class="user" data-replace="user" data-user-id="', $user->id, \'">',
795         $user->Format,
796         \'</span>'
797     ];
798 }
799
800 %_BriefDescriptions = (
801     Create => sub {
802         my $self = shift;
803         return ( "[_1] created", $self->FriendlyObjectType );   #loc()
804     },
805     Enabled => sub {
806         my $self = shift;
807         return ( "[_1] enabled", $self->FriendlyObjectType );   #loc()
808     },
809     Disabled => sub {
810         my $self = shift;
811         return ( "[_1] disabled", $self->FriendlyObjectType );  #loc()
812     },
813     Status => sub {
814         my $self = shift;
815         if ( $self->Field eq 'Status' ) {
816             if ( $self->NewValue eq 'deleted' ) {
817                 return ( "[_1] deleted", $self->FriendlyObjectType );   #loc()
818             }
819             else {
820                 my $canon = $self->Object->DOES("RT::Record::Role::Status")
821                     ? sub { $self->Object->LifecycleObj->CanonicalCase(@_) }
822                     : sub { return $_[0] };
823                 return (
824                     "Status changed from [_1] to [_2]",
825                     "'" . $self->loc( $canon->($self->OldValue) ) . "'",
826                     "'" . $self->loc( $canon->($self->NewValue) ) . "'"
827                 );   # loc()
828             }
829         }
830
831         # Generic:
832         my $no_value = $self->loc("(no value)");
833         return (
834             "[_1] changed from [_2] to [_3]",
835             $self->Field,
836             ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
837             "'" . $self->NewValue . "'"
838         ); #loc()
839     },
840     SystemError => sub {
841         my $self = shift;
842         return $self->Data // ("System error"); #loc()
843     },
844     AttachmentTruncate => sub {
845         my $self = shift;
846         if ( defined $self->Data ) {
847             return ( "File '[_1]' truncated because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).",
848                 $self->Data, $self->OldValue, $self->NewValue ); #loc()
849         }
850         else {
851             return ( "Content truncated because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).",
852                 $self->OldValue, $self->NewValue ); #loc()
853         }
854     },
855     AttachmentDrop => sub {
856         my $self = shift;
857         if ( defined $self->Data ) {
858             return ( "File '[_1]' dropped because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).",
859                 $self->Data, $self->OldValue, $self->NewValue ); #loc()
860         }
861         else {
862             return ( "Content dropped because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).",
863                 $self->OldValue, $self->NewValue ); #loc()
864         }
865     },
866     "Forward Transaction" => sub {
867         my $self = shift;
868         my $recipients = join ", ", map {
869             RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
870         } RT::EmailParser->ParseEmailAddress($self->Data);
871
872         return ( "Forwarded [_3]Transaction #[_1][_4] to [_2]",
873             $self->Field, $recipients,
874             [\'<a href="#txn-', $self->Field, \'">'], \'</a>'); #loc()
875     },
876     "Forward Ticket" => sub {
877         my $self = shift;
878         my $recipients = join ", ", map {
879             RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
880         } RT::EmailParser->ParseEmailAddress($self->Data);
881
882         return ( "Forwarded Ticket to [_1]", $recipients ); #loc()
883     },
884     CommentEmailRecord => sub {
885         my $self = shift;
886         return ("Outgoing email about a comment recorded"); #loc()
887     },
888     EmailRecord => sub {
889         my $self = shift;
890         return ("Outgoing email recorded"); #loc()
891     },
892     Correspond => sub {
893         my $self = shift;
894         return ("Correspondence added");    #loc()
895     },
896     Comment => sub {
897         my $self = shift;
898         return ("Comments added");          #loc()
899     },
900     CustomField => sub {
901         my $self = shift;
902         my $field = $self->loc('CustomField');
903
904         my $cf;
905         if ( $self->Field ) {
906             $cf = RT::CustomField->new( $self->CurrentUser );
907             $cf->SetContextObject( $self->Object );
908             $cf->Load( $self->Field );
909             $field = $cf->Name();
910             $field = $self->loc('a custom field') if !defined($field);
911         }
912
913         my $new = $self->NewValue;
914         my $old = $self->OldValue;
915
916         if ( $cf ) {
917
918             if ( $cf->Type eq 'DateTime' ) {
919                 if ($old) {
920                     my $date = RT::Date->new( $self->CurrentUser );
921                     $date->Set( Format => 'ISO', Value => $old );
922                     $old = $date->AsString;
923                 }
924
925                 if ($new) {
926                     my $date = RT::Date->new( $self->CurrentUser );
927                     $date->Set( Format => 'ISO', Value => $new );
928                     $new = $date->AsString;
929                 }
930             }
931             elsif ( $cf->Type eq 'Date' ) {
932                 if ($old) {
933                     my $date = RT::Date->new( $self->CurrentUser );
934                     $date->Set(
935                         Format   => 'unknown',
936                         Value    => $old,
937                         Timezone => 'UTC',
938                     );
939                     $old = $date->AsString( Time => 0, Timezone => 'UTC' );
940                 }
941
942                 if ($new) {
943                     my $date = RT::Date->new( $self->CurrentUser );
944                     $date->Set(
945                         Format   => 'unknown',
946                         Value    => $new,
947                         Timezone => 'UTC',
948                     );
949                     $new = $date->AsString( Time => 0, Timezone => 'UTC' );
950                 }
951             }
952         }
953
954         if ( !defined($old) || $old eq '' ) {
955             return ("[_1] [_2] added", $field, $new);   #loc()
956         }
957         elsif ( !defined($new) || $new eq '' ) {
958             return ("[_1] [_2] deleted", $field, $old); #loc()
959         }
960         else {
961             return ("[_1] [_2] changed to [_3]", $field, $old, $new);   #loc()
962         }
963     },
964     Untake => sub {
965         my $self = shift;
966         return ("Untaken"); #loc()
967     },
968     Take => sub {
969         my $self = shift;
970         return ("Taken"); #loc()
971     },
972     Force => sub {
973         my $self = shift;
974         my $Old = RT::User->new( $self->CurrentUser );
975         $Old->Load( $self->OldValue );
976         my $New = RT::User->new( $self->CurrentUser );
977         $New->Load( $self->NewValue );
978
979         return ("Owner forcibly changed from [_1] to [_2]",
980                 map { $self->_FormatUser($_) } $Old, $New);  #loc()
981     },
982     Steal => sub {
983         my $self = shift;
984         my $Old = RT::User->new( $self->CurrentUser );
985         $Old->Load( $self->OldValue );
986         return ("Stolen from [_1]", $self->_FormatUser($Old));   #loc()
987     },
988     Give => sub {
989         my $self = shift;
990         my $New = RT::User->new( $self->CurrentUser );
991         $New->Load( $self->NewValue );
992         return ( "Given to [_1]", $self->_FormatUser($New));    #loc()
993     },
994     AddWatcher => sub {
995         my $self = shift;
996         my $principal = RT::Principal->new($self->CurrentUser);
997         $principal->Load($self->NewValue);
998         return ( "[_1] [_2] added", $self->loc($self->Field), $self->_FormatPrincipal($principal));    #loc()
999     },
1000     DelWatcher => sub {
1001         my $self = shift;
1002         my $principal = RT::Principal->new($self->CurrentUser);
1003         $principal->Load($self->OldValue);
1004         return ( "[_1] [_2] deleted", $self->loc($self->Field), $self->_FormatPrincipal($principal));  #loc()
1005     },
1006     SetWatcher => sub {
1007         my $self = shift;
1008         my $principal = RT::Principal->new($self->CurrentUser);
1009         $principal->Load($self->NewValue);
1010         return ( "[_1] set to [_2]", $self->loc($self->Field), $self->_FormatPrincipal($principal));  #loc()
1011     },
1012     Subject => sub {
1013         my $self = shift;
1014         return ( "Subject changed to [_1]", $self->Data );  #loc()
1015     },
1016     AddLink => sub {
1017         my $self = shift;
1018         my $value;
1019         if ( $self->NewValue ) {
1020             my $URI = RT::URI->new( $self->CurrentUser );
1021             if ( $URI->FromURI( $self->NewValue ) ) {
1022                 $value = [
1023                     \'<a href="', $URI->AsHREF, \'">',
1024                     $URI->AsString,
1025                     \'</a>'
1026                 ];
1027             }
1028             else {
1029                 $value = $self->NewValue;
1030             }
1031
1032             if ( $self->Field eq 'DependsOn' ) {
1033                 return ( "Dependency on [_1] added", $value );  #loc()
1034             }
1035             elsif ( $self->Field eq 'DependedOnBy' ) {
1036                 return ( "Dependency by [_1] added", $value );  #loc()
1037             }
1038             elsif ( $self->Field eq 'RefersTo' ) {
1039                 return ( "Reference to [_1] added", $value );   #loc()
1040             }
1041             elsif ( $self->Field eq 'ReferredToBy' ) {
1042                 return ( "Reference by [_1] added", $value );   #loc()
1043             }
1044             elsif ( $self->Field eq 'MemberOf' ) {
1045                 return ( "Membership in [_1] added", $value );  #loc()
1046             }
1047             elsif ( $self->Field eq 'HasMember' ) {
1048                 return ( "Member [_1] added", $value );         #loc()
1049             }
1050             elsif ( $self->Field eq 'MergedInto' ) {
1051                 return ( "Merged into [_1]", $value );          #loc()
1052             }
1053         }
1054         else {
1055             return ( "[_1]", $self->Data ); #loc()
1056         }
1057     },
1058     DeleteLink => sub {
1059         my $self = shift;
1060         my $value;
1061         if ( $self->OldValue ) {
1062             my $URI = RT::URI->new( $self->CurrentUser );
1063             if ( $URI->FromURI( $self->OldValue ) ) {
1064                 $value = [
1065                     \'<a href="', $URI->AsHREF, \'">',
1066                     $URI->AsString,
1067                     \'</a>'
1068                 ];
1069             }
1070             else {
1071                 $value = $self->OldValue;
1072             }
1073
1074             if ( $self->Field eq 'DependsOn' ) {
1075                 return ( "Dependency on [_1] deleted", $value );    #loc()
1076             }
1077             elsif ( $self->Field eq 'DependedOnBy' ) {
1078                 return ( "Dependency by [_1] deleted", $value );    #loc()
1079             }
1080             elsif ( $self->Field eq 'RefersTo' ) {
1081                 return ( "Reference to [_1] deleted", $value );     #loc()
1082             }
1083             elsif ( $self->Field eq 'ReferredToBy' ) {
1084                 return ( "Reference by [_1] deleted", $value );     #loc()
1085             }
1086             elsif ( $self->Field eq 'MemberOf' ) {
1087                 return ( "Membership in [_1] deleted", $value );    #loc()
1088             }
1089             elsif ( $self->Field eq 'HasMember' ) {
1090                 return ( "Member [_1] deleted", $value );           #loc()
1091             }
1092         }
1093         else {
1094             return ( "[_1]", $self->Data ); #loc()
1095         }
1096     },
1097     Told => sub {
1098         my $self = shift;
1099         if ( $self->Field eq 'Told' ) {
1100             my $t1 = RT::Date->new($self->CurrentUser);
1101             $t1->Set(Format => 'ISO', Value => $self->NewValue);
1102             my $t2 = RT::Date->new($self->CurrentUser);
1103             $t2->Set(Format => 'ISO', Value => $self->OldValue);
1104             return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );    #loc()
1105         }
1106         else {
1107             return ( "[_1] changed from [_2] to [_3]",
1108                     $self->loc($self->Field),
1109                     ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );  #loc()
1110         }
1111     },
1112     Set => sub {
1113         my $self = shift;
1114         if ( $self->Field eq 'Password' ) {
1115             return ('Password changed');    #loc()
1116         }
1117         elsif ( $self->Field eq 'Queue' ) {
1118             my $q1 = RT::Queue->new( $self->CurrentUser );
1119             $q1->Load( $self->OldValue );
1120             my $q2 = RT::Queue->new( $self->CurrentUser );
1121             $q2->Load( $self->NewValue );
1122             return ("[_1] changed from [_2] to [_3]",
1123                     $self->loc($self->Field) , $q1->Name , $q2->Name);  #loc()
1124         }
1125
1126         # Write the date/time change at local time:
1127         elsif ($self->Field =~  /Due|Starts|Started|Told/) {
1128             my $t1 = RT::Date->new($self->CurrentUser);
1129             $t1->Set(Format => 'ISO', Value => $self->NewValue);
1130             my $t2 = RT::Date->new($self->CurrentUser);
1131             $t2->Set(Format => 'ISO', Value => $self->OldValue);
1132             return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );    #loc()
1133         }
1134         elsif ( $self->Field eq 'Owner' ) {
1135             my $Old = RT::User->new( $self->CurrentUser );
1136             $Old->Load( $self->OldValue );
1137             my $New = RT::User->new( $self->CurrentUser );
1138             $New->Load( $self->NewValue );
1139
1140             if ( $Old->id == RT->Nobody->id ) {
1141                 if ( $New->id == $self->Creator ) {
1142                     return ("Taken");   #loc()
1143                 }
1144                 else {
1145                     return ( "Given to [_1]", $self->_FormatUser($New) );    #loc()
1146                 }
1147             }
1148             else {
1149                 if ( $New->id == $self->Creator ) {
1150                     return ("Stolen from [_1]",  $self->_FormatUser($Old) );   #loc()
1151                 }
1152                 elsif ( $Old->id == $self->Creator ) {
1153                     if ( $New->id == RT->Nobody->id ) {
1154                         return ("Untaken"); #loc()
1155                     }
1156                     else {
1157                         return ( "Given to [_1]", $self->_FormatUser($New) ); #loc()
1158                     }
1159                 }
1160                 else {
1161                     return (
1162                         "Owner forcibly changed from [_1] to [_2]",
1163                         map { $self->_FormatUser($_) } $Old, $New
1164                     );   #loc()
1165                 }
1166             }
1167         }
1168         else {
1169             return ( "[_1] changed from [_2] to [_3]",
1170                     $self->loc($self->Field),
1171                     ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")),
1172                     ($self->NewValue? "'".$self->NewValue ."'" : $self->loc("(no value)")));  #loc()
1173         }
1174     },
1175     "Set-TimeWorked" => sub {
1176         my $self = shift;
1177         my $old  = $self->OldValue || 0;
1178         my $new  = $self->NewValue || 0;
1179         my $duration = $new - $old;
1180         if ($duration < 0) {
1181             return ("Adjusted time worked by [quant,_1,minute,minutes]", $duration); # loc()
1182         }
1183         elsif ($duration < 60) {
1184             return ("Worked [quant,_1,minute,minutes]", $duration); # loc()
1185         } else {
1186             return ("Worked [quant,_1,hour,hours] ([quant,_2,minute,minutes])", sprintf("%.1f", $duration / 60), $duration); # loc()
1187         }
1188     },
1189     PurgeTransaction => sub {
1190         my $self = shift;
1191         return ("Transaction [_1] purged", $self->Data);    #loc()
1192     },
1193     AddReminder => sub {
1194         my $self = shift;
1195         my $ticket = RT::Ticket->new($self->CurrentUser);
1196         $ticket->Load($self->NewValue);
1197         my $subject = [
1198             \'<a href="', RT->Config->Get('WebPath'),
1199             "/Ticket/Reminders.html?id=", $self->ObjectId,
1200             "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1201         ];
1202         return ("Reminder '[_1]' added", $subject); #loc()
1203     },
1204     OpenReminder => sub {
1205         my $self = shift;
1206         my $ticket = RT::Ticket->new($self->CurrentUser);
1207         $ticket->Load($self->NewValue);
1208         my $subject = [
1209             \'<a href="', RT->Config->Get('WebPath'),
1210             "/Ticket/Reminders.html?id=", $self->ObjectId,
1211             "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1212         ];
1213         return ("Reminder '[_1]' reopened", $subject);  #loc()
1214     },
1215     ResolveReminder => sub {
1216         my $self = shift;
1217         my $ticket = RT::Ticket->new($self->CurrentUser);
1218         $ticket->Load($self->NewValue);
1219         my $subject = [
1220             \'<a href="', RT->Config->Get('WebPath'),
1221             "/Ticket/Reminders.html?id=", $self->ObjectId,
1222             "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1223         ];
1224         return ("Reminder '[_1]' completed", $subject); #loc()
1225     }
1226 );
1227
1228
1229
1230
1231 =head2 IsInbound
1232
1233 Returns true if the creator of the transaction is a requestor of the ticket.
1234 Returns false otherwise
1235
1236 =cut
1237
1238 sub IsInbound {
1239     my $self = shift;
1240     $self->ObjectType eq 'RT::Ticket' or return undef;
1241     return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
1242 }
1243
1244
1245
1246 sub _OverlayAccessible {
1247     {
1248
1249           ObjectType => { public => 1},
1250           ObjectId => { public => 1},
1251
1252     }
1253 };
1254
1255
1256
1257
1258 sub _Set {
1259     my $self = shift;
1260     return ( 0, $self->loc('Transactions are immutable') );
1261 }
1262
1263
1264
1265 =head2 _Value
1266
1267 Takes the name of a table column.
1268 Returns its value as a string, if the user passes an ACL check
1269
1270 =cut
1271
1272 sub _Value {
1273     my $self  = shift;
1274     my $field = shift;
1275
1276     #if the field is public, return it.
1277     if ( $self->_Accessible( $field, 'public' ) ) {
1278         return $self->SUPER::_Value( $field );
1279     }
1280
1281     unless ( $self->CurrentUserCanSee ) {
1282         return undef;
1283     }
1284
1285     return $self->SUPER::_Value( $field );
1286 }
1287
1288
1289 =head2 CurrentUserCanSee
1290
1291 Returns true if current user has rights to see this particular transaction.
1292
1293 This fact depends on type of the transaction, type of an object the transaction
1294 is attached to and may be other conditions, so this method is prefered over
1295 custom implementations.
1296
1297 =cut
1298
1299 sub CurrentUserCanSee {
1300     my $self = shift;
1301
1302     # Make sure the user can see the custom field before showing that it changed
1303     my $type = $self->__Value('Type');
1304     if ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1305         my $cf = RT::CustomField->new( $self->CurrentUser );
1306         $cf->SetContextObject( $self->Object );
1307         $cf->Load( $cf_id );
1308         return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1309     }
1310
1311     # Transactions that might have changed the ->Object's visibility to
1312     # the current user are marked readable
1313     return 1 if $self->{ _object_is_readable };
1314
1315     # Defer to the object in question
1316     return $self->Object->CurrentUserCanSee("Transaction", $self);
1317 }
1318
1319
1320 sub Ticket {
1321     my $self = shift;
1322     return $self->ObjectId;
1323 }
1324
1325 sub TicketObj {
1326     my $self = shift;
1327     return $self->Object;
1328 }
1329
1330 sub OldValue {
1331     my $self = shift;
1332     if ( my $Object = $self->OldReferenceObject ) {
1333         return $Object->Content;
1334     }
1335     else {
1336         return $self->_Value('OldValue');
1337     }
1338 }
1339
1340 sub NewValue {
1341     my $self = shift;
1342     if ( my $Object = $self->NewReferenceObject ) {
1343         return $Object->Content;
1344     }
1345     else {
1346         return $self->_Value('NewValue');
1347     }
1348 }
1349
1350 sub Object {
1351     my $self  = shift;
1352     my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1353     $Object->Load($self->__Value('ObjectId'));
1354     return $Object;
1355 }
1356
1357 =head2 NewReferenceObject
1358
1359 =head2 OldReferenceObject
1360
1361 Returns an object of the class specified by the column C<ReferenceType> and
1362 loaded with the id specified by the column C<NewReference> or C<OldReference>.
1363 C<ReferenceType> is assumed to be an L<RT::Record> subclass.
1364
1365 The object may be unloaded (check C<< $object->id >>) if the reference is
1366 corrupt (such as if the referenced record was improperly deleted).
1367
1368 Returns undef if either C<ReferenceType> or C<NewReference>/C<OldReference> is
1369 false.
1370
1371 =cut
1372
1373 sub NewReferenceObject { $_[0]->_ReferenceObject("New") }
1374 sub OldReferenceObject { $_[0]->_ReferenceObject("Old") }
1375
1376 sub _ReferenceObject {
1377     my $self  = shift;
1378     my $which = shift;
1379     my $type  = $self->__Value("ReferenceType");
1380     my $id    = $self->__Value("${which}Reference");
1381     return unless $type and $id;
1382
1383     my $object = $type->new($self->CurrentUser);
1384     $object->Load( $id );
1385     return $object;
1386 }
1387
1388 sub FriendlyObjectType {
1389     my $self = shift;
1390     return $self->loc( $self->Object->RecordType );
1391 }
1392
1393 =head2 UpdateCustomFields
1394
1395 Takes a hash of:
1396
1397     CustomField-C<Id> => Value
1398
1399 or:
1400
1401     Object-RT::Transaction-CustomField-C<Id> => Value
1402
1403 parameters to update this transaction's custom fields.
1404
1405 =cut
1406
1407 sub UpdateCustomFields {
1408     my $self = shift;
1409     my %args = (@_);
1410
1411     # This method used to have an API that took a hash of a single
1412     # value "ARGSRef", which was a reference to a hash of arguments.
1413     # This was insane. The next few lines of code preserve that API
1414     # while giving us something saner.
1415     my $args;
1416     if ($args{'ARGSRef'}) {
1417         RT->Deprecated( Arguments => "ARGSRef", Remove => "4.4" );
1418         $args = $args{ARGSRef};
1419     } else {
1420         $args = \%args;
1421     }
1422
1423     foreach my $arg ( keys %$args ) {
1424         next
1425           unless ( $arg =~
1426             /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1427         next if $arg =~ /-Magic$/;
1428         my $cfid   = $1;
1429         my $values = $args->{$arg};
1430         my $cf = $self->LoadCustomFieldByIdentifier($cfid);
1431         next unless $cf->ObjectTypeFromLookupType->isa(ref $self);
1432         foreach
1433           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1434         {
1435             next unless (defined($value) && length($value));
1436             $self->_AddCustomFieldValue(
1437                 Field             => $cfid,
1438                 Value             => $value,
1439                 RecordTransaction => 0,
1440             );
1441         }
1442     }
1443 }
1444
1445 =head2 LoadCustomFieldByIdentifier
1446
1447 Finds and returns the custom field of the given name for the
1448 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1449 look for queue-specific CFs before global ones.
1450
1451 =cut
1452
1453 sub LoadCustomFieldByIdentifier {
1454     my $self  = shift;
1455     my $field = shift;
1456
1457     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1458         if ref $field or $field =~ /^\d+$/;
1459
1460     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1461         unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1462
1463     my $CFs = RT::CustomFields->new( $self->CurrentUser );
1464     $CFs->SetContextObject( $self->Object );
1465     $CFs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
1466     $CFs->LimitToLookupType($self->CustomFieldLookupType);
1467     $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1468     return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1469 }
1470
1471 =head2 CustomFieldLookupType
1472
1473 Returns the RT::Transaction lookup type, which can 
1474 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1475
1476 =cut
1477
1478
1479 sub CustomFieldLookupType {
1480     "RT::Queue-RT::Ticket-RT::Transaction";
1481 }
1482
1483
1484 =head2 SquelchMailTo
1485
1486 Similar to Ticket class SquelchMailTo method - returns a list of
1487 transaction's squelched addresses.  As transactions are immutable, the
1488 list of squelched recipients cannot be modified after creation.
1489
1490 =cut
1491
1492 sub SquelchMailTo {
1493     my $self = shift;
1494     return () unless $self->CurrentUserCanSee;
1495     return $self->Attributes->Named('SquelchMailTo');
1496 }
1497
1498 =head2 Recipients
1499
1500 Returns the list of email addresses (as L<Email::Address> objects)
1501 that this transaction would send mail to.  There may be duplicates.
1502
1503 =cut
1504
1505 sub Recipients {
1506     my $self = shift;
1507     my @recipients;
1508     foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1509         my $action = $scrip->ActionObj->Action;
1510         next unless $action->isa('RT::Action::SendEmail');
1511
1512         foreach my $type (qw(To Cc Bcc)) {
1513             push @recipients, $action->$type();
1514         }
1515     }
1516
1517     if ( $self->Rules ) {
1518         for my $rule (@{$self->Rules}) {
1519             next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1520             my $data = $rule->{hints}{recipients};
1521             foreach my $type (qw(To Cc Bcc)) {
1522                 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1523             }
1524         }
1525     }
1526     return @recipients;
1527 }
1528
1529 =head2 DeferredRecipients($freq, $include_sent )
1530
1531 Takes the following arguments:
1532
1533 =over
1534
1535 =item * a string to indicate the frequency of digest delivery.  Valid values are "daily", "weekly", or "susp".
1536
1537 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1538
1539 =back
1540
1541 Returns an array of users who should now receive the notification that
1542 was recorded in this transaction.  Returns an empty array if there were
1543 no deferred users, or if $include_sent was not specified and the deferred
1544 notifications have been sent.
1545
1546 =cut
1547
1548 sub DeferredRecipients {
1549     my $self = shift;
1550     my $freq = shift;
1551     my $include_sent = @_? shift : 0;
1552
1553     my $attr = $self->FirstAttribute('DeferredRecipients');
1554
1555     return () unless ($attr);
1556
1557     my $deferred = $attr->Content;
1558
1559     return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1560
1561     # Skip it.
1562    
1563     for my $user (keys %{$deferred->{$freq}}) {
1564         if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) { 
1565             delete $deferred->{$freq}->{$user} 
1566         }
1567     }
1568     # Now get our users.  Easy.
1569     
1570     return keys %{ $deferred->{$freq} };
1571 }
1572
1573
1574
1575 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1576 sub _CacheConfig {
1577   {
1578      'cache_p'        => 1,
1579      'fast_update_p'  => 1,
1580      'cache_for_sec'  => 6000,
1581   }
1582 }
1583
1584
1585 =head2 ACLEquivalenceObjects
1586
1587 This method returns a list of objects for which a user's rights also apply
1588 to this Transaction.
1589
1590 This currently only applies to Transaction Custom Fields on Tickets, so we return
1591 the Ticket's Queue and the Ticket.
1592
1593 This method is called from L<RT::Principal/HasRight>.
1594
1595 =cut
1596
1597 sub ACLEquivalenceObjects {
1598     my $self = shift;
1599
1600     return unless $self->ObjectType eq 'RT::Ticket';
1601     my $object = $self->Object;
1602     return $object,$object->QueueObj;
1603
1604 }
1605
1606
1607
1608
1609
1610 =head2 id
1611
1612 Returns the current value of id.
1613 (In the database, id is stored as int(11).)
1614
1615
1616 =cut
1617
1618
1619 =head2 ObjectType
1620
1621 Returns the current value of ObjectType.
1622 (In the database, ObjectType is stored as varchar(64).)
1623
1624
1625
1626 =head2 SetObjectType VALUE
1627
1628
1629 Set ObjectType to VALUE.
1630 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1631 (In the database, ObjectType will be stored as a varchar(64).)
1632
1633
1634 =cut
1635
1636
1637 =head2 ObjectId
1638
1639 Returns the current value of ObjectId.
1640 (In the database, ObjectId is stored as int(11).)
1641
1642
1643
1644 =head2 SetObjectId VALUE
1645
1646
1647 Set ObjectId to VALUE.
1648 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1649 (In the database, ObjectId will be stored as a int(11).)
1650
1651
1652 =cut
1653
1654
1655 =head2 TimeTaken
1656
1657 Returns the current value of TimeTaken.
1658 (In the database, TimeTaken is stored as int(11).)
1659
1660
1661
1662 =head2 SetTimeTaken VALUE
1663
1664
1665 Set TimeTaken to VALUE.
1666 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1667 (In the database, TimeTaken will be stored as a int(11).)
1668
1669
1670 =cut
1671
1672
1673 =head2 Type
1674
1675 Returns the current value of Type.
1676 (In the database, Type is stored as varchar(20).)
1677
1678
1679
1680 =head2 SetType VALUE
1681
1682
1683 Set Type to VALUE.
1684 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1685 (In the database, Type will be stored as a varchar(20).)
1686
1687
1688 =cut
1689
1690
1691 =head2 Field
1692
1693 Returns the current value of Field.
1694 (In the database, Field is stored as varchar(40).)
1695
1696
1697
1698 =head2 SetField VALUE
1699
1700
1701 Set Field to VALUE.
1702 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1703 (In the database, Field will be stored as a varchar(40).)
1704
1705
1706 =cut
1707
1708
1709 =head2 OldValue
1710
1711 Returns the current value of OldValue.
1712 (In the database, OldValue is stored as varchar(255).)
1713
1714
1715
1716 =head2 SetOldValue VALUE
1717
1718
1719 Set OldValue to VALUE.
1720 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1721 (In the database, OldValue will be stored as a varchar(255).)
1722
1723
1724 =cut
1725
1726
1727 =head2 NewValue
1728
1729 Returns the current value of NewValue.
1730 (In the database, NewValue is stored as varchar(255).)
1731
1732
1733
1734 =head2 SetNewValue VALUE
1735
1736
1737 Set NewValue to VALUE.
1738 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1739 (In the database, NewValue will be stored as a varchar(255).)
1740
1741
1742 =cut
1743
1744
1745 =head2 ReferenceType
1746
1747 Returns the current value of ReferenceType.
1748 (In the database, ReferenceType is stored as varchar(255).)
1749
1750
1751
1752 =head2 SetReferenceType VALUE
1753
1754
1755 Set ReferenceType to VALUE.
1756 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1757 (In the database, ReferenceType will be stored as a varchar(255).)
1758
1759
1760 =cut
1761
1762
1763 =head2 OldReference
1764
1765 Returns the current value of OldReference.
1766 (In the database, OldReference is stored as int(11).)
1767
1768
1769
1770 =head2 SetOldReference VALUE
1771
1772
1773 Set OldReference to VALUE.
1774 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1775 (In the database, OldReference will be stored as a int(11).)
1776
1777
1778 =cut
1779
1780
1781 =head2 NewReference
1782
1783 Returns the current value of NewReference.
1784 (In the database, NewReference is stored as int(11).)
1785
1786
1787
1788 =head2 SetNewReference VALUE
1789
1790
1791 Set NewReference to VALUE.
1792 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1793 (In the database, NewReference will be stored as a int(11).)
1794
1795
1796 =cut
1797
1798
1799 =head2 Data
1800
1801 Returns the current value of Data.
1802 (In the database, Data is stored as varchar(255).)
1803
1804
1805
1806 =head2 SetData VALUE
1807
1808
1809 Set Data to VALUE.
1810 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1811 (In the database, Data will be stored as a varchar(255).)
1812
1813
1814 =cut
1815
1816
1817 =head2 Creator
1818
1819 Returns the current value of Creator.
1820 (In the database, Creator is stored as int(11).)
1821
1822
1823 =cut
1824
1825
1826 =head2 Created
1827
1828 Returns the current value of Created.
1829 (In the database, Created is stored as datetime.)
1830
1831
1832 =cut
1833
1834
1835
1836 sub _CoreAccessible {
1837     {
1838
1839         id =>
1840                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1841         ObjectType =>
1842                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1843         ObjectId =>
1844                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1845         TimeTaken =>
1846                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1847         Type =>
1848                 {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
1849         Field =>
1850                 {read => 1, write => 1, sql_type => 12, length => 40,  is_blob => 0,  is_numeric => 0,  type => 'varchar(40)', default => ''},
1851         OldValue =>
1852                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1853         NewValue =>
1854                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1855         ReferenceType =>
1856                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1857         OldReference =>
1858                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1859         NewReference =>
1860                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1861         Data =>
1862                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1863         Creator =>
1864                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1865         Created =>
1866                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1867
1868  }
1869 };
1870
1871 sub FindDependencies {
1872     my $self = shift;
1873     my ($walker, $deps) = @_;
1874
1875     $self->SUPER::FindDependencies($walker, $deps);
1876
1877     $deps->Add( out => $self->Object );
1878     $deps->Add( in => $self->Attachments );
1879
1880     my $type = $self->Type;
1881     if ($type eq "CustomField") {
1882         my $cf = RT::CustomField->new( RT->SystemUser );
1883         $cf->Load( $self->Field );
1884         $deps->Add( out => $cf );
1885     } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
1886         for my $field (qw/OldValue NewValue/) {
1887             my $user = RT::User->new( RT->SystemUser );
1888             $user->Load( $self->$field );
1889             $deps->Add( out => $user );
1890         }
1891     } elsif ($type eq "DelWatcher") {
1892         my $principal = RT::Principal->new( RT->SystemUser );
1893         $principal->Load( $self->OldValue );
1894         $deps->Add( out => $principal->Object );
1895     } elsif ($type eq "AddWatcher") {
1896         my $principal = RT::Principal->new( RT->SystemUser );
1897         $principal->Load( $self->NewValue );
1898         $deps->Add( out => $principal->Object );
1899     } elsif ($type eq "DeleteLink") {
1900         if ($self->OldValue) {
1901             my $base = RT::URI->new( $self->CurrentUser );
1902             $base->FromURI( $self->OldValue );
1903             $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
1904         }
1905     } elsif ($type eq "AddLink") {
1906         if ($self->NewValue) {
1907             my $base = RT::URI->new( $self->CurrentUser );
1908             $base->FromURI( $self->NewValue );
1909             $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
1910         }
1911     } elsif ($type eq "Set" and $self->Field eq "Queue") {
1912         for my $field (qw/OldValue NewValue/) {
1913             my $queue = RT::Queue->new( RT->SystemUser );
1914             $queue->Load( $self->$field );
1915             $deps->Add( out => $queue );
1916         }
1917     } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
1918         my $ticket = RT::Ticket->new( RT->SystemUser );
1919         $ticket->Load( $self->NewValue );
1920         $deps->Add( out => $ticket );
1921     }
1922 }
1923
1924 sub Serialize {
1925     my $self = shift;
1926     my %args = (@_);
1927     my %store = $self->SUPER::Serialize(@_);
1928
1929     my $type = $store{Type};
1930     if ($type eq "CustomField") {
1931         my $cf = RT::CustomField->new( RT->SystemUser );
1932         $cf->Load( $store{Field} );
1933         $store{Field} = \($cf->UID);
1934     } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
1935         for my $field (qw/OldValue NewValue/) {
1936             my $user = RT::User->new( RT->SystemUser );
1937             $user->Load( $store{$field} );
1938             $store{$field} = \($user->UID);
1939         }
1940     } elsif ($type eq "DelWatcher") {
1941         my $principal = RT::Principal->new( RT->SystemUser );
1942         $principal->Load( $store{OldValue} );
1943         $store{OldValue} = \($principal->UID);
1944     } elsif ($type eq "AddWatcher") {
1945         my $principal = RT::Principal->new( RT->SystemUser );
1946         $principal->Load( $store{NewValue} );
1947         $store{NewValue} = \($principal->UID);
1948     } elsif ($type eq "DeleteLink") {
1949         if ($store{OldValue}) {
1950             my $base = RT::URI->new( $self->CurrentUser );
1951             $base->FromURI( $store{OldValue} );
1952             $store{OldValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
1953         }
1954     } elsif ($type eq "AddLink") {
1955         if ($store{NewValue}) {
1956             my $base = RT::URI->new( $self->CurrentUser );
1957             $base->FromURI( $store{NewValue} );
1958             $store{NewValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
1959         }
1960     } elsif ($type eq "Set" and $store{Field} eq "Queue") {
1961         for my $field (qw/OldValue NewValue/) {
1962             my $queue = RT::Queue->new( RT->SystemUser );
1963             $queue->Load( $store{$field} );
1964             $store{$field} = \($queue->UID);
1965         }
1966     } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
1967         my $ticket = RT::Ticket->new( RT->SystemUser );
1968         $ticket->Load( $store{NewValue} );
1969         $store{NewValue} = \($ticket->UID);
1970     }
1971
1972     return %store;
1973 }
1974
1975 sub PreInflate {
1976     my $class = shift;
1977     my ($importer, $uid, $data) = @_;
1978
1979     if ($data->{Object} and ref $data->{Object}) {
1980         my $on_uid = ${ $data->{Object} };
1981         return if $importer->ShouldSkipTransaction($on_uid);
1982     }
1983
1984     if ($data->{Type} eq "DeleteLink" and ref $data->{OldValue}) {
1985         my $uid = ${ $data->{OldValue} };
1986         my $obj = $importer->LookupObj( $uid );
1987         $data->{OldValue} = $obj->URI;
1988     } elsif ($data->{Type} eq "AddLink" and ref $data->{NewValue}) {
1989         my $uid = ${ $data->{NewValue} };
1990         my $obj = $importer->LookupObj( $uid );
1991         $data->{NewValue} = $obj->URI;
1992     }
1993
1994     return $class->SUPER::PreInflate( $importer, $uid, $data );
1995 }
1996
1997 RT::Base->_ImportOverlays();
1998
1999 1;