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