Upgrade to 4.2.2
[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 = "<pre>$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 ("System error"); #loc()
843     },
844     "Forward Transaction" => sub {
845         my $self = shift;
846         my $recipients = join ", ", map {
847             RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
848         } RT::EmailParser->ParseEmailAddress($self->Data);
849
850         return ( "Forwarded [_3]Transaction #[_1][_4] to [_2]",
851             $self->Field, $recipients,
852             [\'<a href="#txn-', $self->Field, \'">'], \'</a>'); #loc()
853     },
854     "Forward Ticket" => sub {
855         my $self = shift;
856         my $recipients = join ", ", map {
857             RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
858         } RT::EmailParser->ParseEmailAddress($self->Data);
859
860         return ( "Forwarded Ticket to [_1]", $recipients ); #loc()
861     },
862     CommentEmailRecord => sub {
863         my $self = shift;
864         return ("Outgoing email about a comment recorded"); #loc()
865     },
866     EmailRecord => sub {
867         my $self = shift;
868         return ("Outgoing email recorded"); #loc()
869     },
870     Correspond => sub {
871         my $self = shift;
872         return ("Correspondence added");    #loc()
873     },
874     Comment => sub {
875         my $self = shift;
876         return ("Comments added");          #loc()
877     },
878     CustomField => sub {
879         my $self = shift;
880         my $field = $self->loc('CustomField');
881
882         my $cf;
883         if ( $self->Field ) {
884             $cf = RT::CustomField->new( $self->CurrentUser );
885             $cf->SetContextObject( $self->Object );
886             $cf->Load( $self->Field );
887             $field = $cf->Name();
888             $field = $self->loc('a custom field') if !defined($field);
889         }
890
891         my $new = $self->NewValue;
892         my $old = $self->OldValue;
893
894         if ( $cf ) {
895
896             if ( $cf->Type eq 'DateTime' ) {
897                 if ($old) {
898                     my $date = RT::Date->new( $self->CurrentUser );
899                     $date->Set( Format => 'ISO', Value => $old );
900                     $old = $date->AsString;
901                 }
902
903                 if ($new) {
904                     my $date = RT::Date->new( $self->CurrentUser );
905                     $date->Set( Format => 'ISO', Value => $new );
906                     $new = $date->AsString;
907                 }
908             }
909             elsif ( $cf->Type eq 'Date' ) {
910                 if ($old) {
911                     my $date = RT::Date->new( $self->CurrentUser );
912                     $date->Set(
913                         Format   => 'unknown',
914                         Value    => $old,
915                         Timezone => 'UTC',
916                     );
917                     $old = $date->AsString( Time => 0, Timezone => 'UTC' );
918                 }
919
920                 if ($new) {
921                     my $date = RT::Date->new( $self->CurrentUser );
922                     $date->Set(
923                         Format   => 'unknown',
924                         Value    => $new,
925                         Timezone => 'UTC',
926                     );
927                     $new = $date->AsString( Time => 0, Timezone => 'UTC' );
928                 }
929             }
930         }
931
932         if ( !defined($old) || $old eq '' ) {
933             return ("[_1] [_2] added", $field, $new);   #loc()
934         }
935         elsif ( !defined($new) || $new eq '' ) {
936             return ("[_1] [_2] deleted", $field, $old); #loc()
937         }
938         else {
939             return ("[_1] [_2] changed to [_3]", $field, $old, $new);   #loc()
940         }
941     },
942     Untake => sub {
943         my $self = shift;
944         return ("Untaken"); #loc()
945     },
946     Take => sub {
947         my $self = shift;
948         return ("Taken"); #loc()
949     },
950     Force => sub {
951         my $self = shift;
952         my $Old = RT::User->new( $self->CurrentUser );
953         $Old->Load( $self->OldValue );
954         my $New = RT::User->new( $self->CurrentUser );
955         $New->Load( $self->NewValue );
956
957         return ("Owner forcibly changed from [_1] to [_2]",
958                 map { $self->_FormatUser($_) } $Old, $New);  #loc()
959     },
960     Steal => sub {
961         my $self = shift;
962         my $Old = RT::User->new( $self->CurrentUser );
963         $Old->Load( $self->OldValue );
964         return ("Stolen from [_1]", $self->_FormatUser($Old));   #loc()
965     },
966     Give => sub {
967         my $self = shift;
968         my $New = RT::User->new( $self->CurrentUser );
969         $New->Load( $self->NewValue );
970         return ( "Given to [_1]", $self->_FormatUser($New));    #loc()
971     },
972     AddWatcher => sub {
973         my $self = shift;
974         my $principal = RT::Principal->new($self->CurrentUser);
975         $principal->Load($self->NewValue);
976         return ( "[_1] [_2] added", $self->loc($self->Field), $self->_FormatPrincipal($principal));    #loc()
977     },
978     DelWatcher => sub {
979         my $self = shift;
980         my $principal = RT::Principal->new($self->CurrentUser);
981         $principal->Load($self->OldValue);
982         return ( "[_1] [_2] deleted", $self->loc($self->Field), $self->_FormatPrincipal($principal));  #loc()
983     },
984     SetWatcher => sub {
985         my $self = shift;
986         my $principal = RT::Principal->new($self->CurrentUser);
987         $principal->Load($self->NewValue);
988         return ( "[_1] set to [_2]", $self->loc($self->Field), $self->_FormatPrincipal($principal));  #loc()
989     },
990     Subject => sub {
991         my $self = shift;
992         return ( "Subject changed to [_1]", $self->Data );  #loc()
993     },
994     AddLink => sub {
995         my $self = shift;
996         my $value;
997         if ( $self->NewValue ) {
998             my $URI = RT::URI->new( $self->CurrentUser );
999             if ( $URI->FromURI( $self->NewValue ) ) {
1000                 $value = [
1001                     \'<a href="', $URI->AsHREF, \'">',
1002                     $URI->AsString,
1003                     \'</a>'
1004                 ];
1005             }
1006             else {
1007                 $value = $self->NewValue;
1008             }
1009
1010             if ( $self->Field eq 'DependsOn' ) {
1011                 return ( "Dependency on [_1] added", $value );  #loc()
1012             }
1013             elsif ( $self->Field eq 'DependedOnBy' ) {
1014                 return ( "Dependency by [_1] added", $value );  #loc()
1015             }
1016             elsif ( $self->Field eq 'RefersTo' ) {
1017                 return ( "Reference to [_1] added", $value );   #loc()
1018             }
1019             elsif ( $self->Field eq 'ReferredToBy' ) {
1020                 return ( "Reference by [_1] added", $value );   #loc()
1021             }
1022             elsif ( $self->Field eq 'MemberOf' ) {
1023                 return ( "Membership in [_1] added", $value );  #loc()
1024             }
1025             elsif ( $self->Field eq 'HasMember' ) {
1026                 return ( "Member [_1] added", $value );         #loc()
1027             }
1028             elsif ( $self->Field eq 'MergedInto' ) {
1029                 return ( "Merged into [_1]", $value );          #loc()
1030             }
1031         }
1032         else {
1033             return ( "[_1]", $self->Data ); #loc()
1034         }
1035     },
1036     DeleteLink => sub {
1037         my $self = shift;
1038         my $value;
1039         if ( $self->OldValue ) {
1040             my $URI = RT::URI->new( $self->CurrentUser );
1041             if ( $URI->FromURI( $self->OldValue ) ) {
1042                 $value = [
1043                     \'<a href="', $URI->AsHREF, \'">',
1044                     $URI->AsString,
1045                     \'</a>'
1046                 ];
1047             }
1048             else {
1049                 $value = $self->OldValue;
1050             }
1051
1052             if ( $self->Field eq 'DependsOn' ) {
1053                 return ( "Dependency on [_1] deleted", $value );    #loc()
1054             }
1055             elsif ( $self->Field eq 'DependedOnBy' ) {
1056                 return ( "Dependency by [_1] deleted", $value );    #loc()
1057             }
1058             elsif ( $self->Field eq 'RefersTo' ) {
1059                 return ( "Reference to [_1] deleted", $value );     #loc()
1060             }
1061             elsif ( $self->Field eq 'ReferredToBy' ) {
1062                 return ( "Reference by [_1] deleted", $value );     #loc()
1063             }
1064             elsif ( $self->Field eq 'MemberOf' ) {
1065                 return ( "Membership in [_1] deleted", $value );    #loc()
1066             }
1067             elsif ( $self->Field eq 'HasMember' ) {
1068                 return ( "Member [_1] deleted", $value );           #loc()
1069             }
1070         }
1071         else {
1072             return ( "[_1]", $self->Data ); #loc()
1073         }
1074     },
1075     Told => sub {
1076         my $self = shift;
1077         if ( $self->Field eq 'Told' ) {
1078             my $t1 = RT::Date->new($self->CurrentUser);
1079             $t1->Set(Format => 'ISO', Value => $self->NewValue);
1080             my $t2 = RT::Date->new($self->CurrentUser);
1081             $t2->Set(Format => 'ISO', Value => $self->OldValue);
1082             return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );    #loc()
1083         }
1084         else {
1085             return ( "[_1] changed from [_2] to [_3]",
1086                     $self->loc($self->Field),
1087                     ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );  #loc()
1088         }
1089     },
1090     Set => sub {
1091         my $self = shift;
1092         if ( $self->Field eq 'Password' ) {
1093             return ('Password changed');    #loc()
1094         }
1095         elsif ( $self->Field eq 'Queue' ) {
1096             my $q1 = RT::Queue->new( $self->CurrentUser );
1097             $q1->Load( $self->OldValue );
1098             my $q2 = RT::Queue->new( $self->CurrentUser );
1099             $q2->Load( $self->NewValue );
1100             return ("[_1] changed from [_2] to [_3]",
1101                     $self->loc($self->Field) , $q1->Name , $q2->Name);  #loc()
1102         }
1103
1104         # Write the date/time change at local time:
1105         elsif ($self->Field =~  /Due|Starts|Started|Told/) {
1106             my $t1 = RT::Date->new($self->CurrentUser);
1107             $t1->Set(Format => 'ISO', Value => $self->NewValue);
1108             my $t2 = RT::Date->new($self->CurrentUser);
1109             $t2->Set(Format => 'ISO', Value => $self->OldValue);
1110             return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );    #loc()
1111         }
1112         elsif ( $self->Field eq 'Owner' ) {
1113             my $Old = RT::User->new( $self->CurrentUser );
1114             $Old->Load( $self->OldValue );
1115             my $New = RT::User->new( $self->CurrentUser );
1116             $New->Load( $self->NewValue );
1117
1118             if ( $Old->id == RT->Nobody->id ) {
1119                 if ( $New->id == $self->Creator ) {
1120                     return ("Taken");   #loc()
1121                 }
1122                 else {
1123                     return ( "Given to [_1]", $self->_FormatUser($New) );    #loc()
1124                 }
1125             }
1126             else {
1127                 if ( $New->id == $self->Creator ) {
1128                     return ("Stolen from [_1]",  $self->_FormatUser($Old) );   #loc()
1129                 }
1130                 elsif ( $Old->id == $self->Creator ) {
1131                     if ( $New->id == RT->Nobody->id ) {
1132                         return ("Untaken"); #loc()
1133                     }
1134                     else {
1135                         return ( "Given to [_1]", $self->_FormatUser($New) ); #loc()
1136                     }
1137                 }
1138                 else {
1139                     return (
1140                         "Owner forcibly changed from [_1] to [_2]",
1141                         map { $self->_FormatUser($_) } $Old, $New
1142                     );   #loc()
1143                 }
1144             }
1145         }
1146         else {
1147             return ( "[_1] changed from [_2] to [_3]",
1148                     $self->loc($self->Field),
1149                     ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")),
1150                     ($self->NewValue? "'".$self->NewValue ."'" : $self->loc("(no value)")));  #loc()
1151         }
1152     },
1153     "Set-TimeWorked" => sub {
1154         my $self = shift;
1155         my $old  = $self->OldValue || 0;
1156         my $new  = $self->NewValue || 0;
1157         my $duration = $new - $old;
1158         if ($duration < 0) {
1159             return ("Adjusted time worked by [quant,_1,minute,minutes]", $duration);
1160         }
1161         elsif ($duration < 60) {
1162             return ("Worked [quant,_1,minute,minutes]", $duration);
1163         } else {
1164             return ("Worked [quant,_1,hour,hours] ([numf,_2] minutes)", sprintf("%.1f", $duration / 60), $duration);
1165         }
1166     },
1167     PurgeTransaction => sub {
1168         my $self = shift;
1169         return ("Transaction [_1] purged", $self->Data);    #loc()
1170     },
1171     AddReminder => sub {
1172         my $self = shift;
1173         my $ticket = RT::Ticket->new($self->CurrentUser);
1174         $ticket->Load($self->NewValue);
1175         my $subject = [
1176             \'<a href="', RT->Config->Get('WebPath'),
1177             "/Ticket/Reminders.html?id=", $self->ObjectId,
1178             "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1179         ];
1180         return ("Reminder '[_1]' added", $subject); #loc()
1181     },
1182     OpenReminder => sub {
1183         my $self = shift;
1184         my $ticket = RT::Ticket->new($self->CurrentUser);
1185         $ticket->Load($self->NewValue);
1186         my $subject = [
1187             \'<a href="', RT->Config->Get('WebPath'),
1188             "/Ticket/Reminders.html?id=", $self->ObjectId,
1189             "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1190         ];
1191         return ("Reminder '[_1]' reopened", $subject);  #loc()
1192     },
1193     ResolveReminder => 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]' completed", $subject); #loc()
1203     }
1204 );
1205
1206
1207
1208
1209 =head2 IsInbound
1210
1211 Returns true if the creator of the transaction is a requestor of the ticket.
1212 Returns false otherwise
1213
1214 =cut
1215
1216 sub IsInbound {
1217     my $self = shift;
1218     $self->ObjectType eq 'RT::Ticket' or return undef;
1219     return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
1220 }
1221
1222
1223
1224 sub _OverlayAccessible {
1225     {
1226
1227           ObjectType => { public => 1},
1228           ObjectId => { public => 1},
1229
1230     }
1231 };
1232
1233
1234
1235
1236 sub _Set {
1237     my $self = shift;
1238     return ( 0, $self->loc('Transactions are immutable') );
1239 }
1240
1241
1242
1243 =head2 _Value
1244
1245 Takes the name of a table column.
1246 Returns its value as a string, if the user passes an ACL check
1247
1248 =cut
1249
1250 sub _Value {
1251     my $self  = shift;
1252     my $field = shift;
1253
1254     #if the field is public, return it.
1255     if ( $self->_Accessible( $field, 'public' ) ) {
1256         return $self->SUPER::_Value( $field );
1257     }
1258
1259     unless ( $self->CurrentUserCanSee ) {
1260         return undef;
1261     }
1262
1263     return $self->SUPER::_Value( $field );
1264 }
1265
1266
1267 =head2 CurrentUserCanSee
1268
1269 Returns true if current user has rights to see this particular transaction.
1270
1271 This fact depends on type of the transaction, type of an object the transaction
1272 is attached to and may be other conditions, so this method is prefered over
1273 custom implementations.
1274
1275 =cut
1276
1277 sub CurrentUserCanSee {
1278     my $self = shift;
1279
1280     # Make sure the user can see the custom field before showing that it changed
1281     my $type = $self->__Value('Type');
1282     if ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1283         my $cf = RT::CustomField->new( $self->CurrentUser );
1284         $cf->SetContextObject( $self->Object );
1285         $cf->Load( $cf_id );
1286         return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1287     }
1288
1289     # Transactions that might have changed the ->Object's visibility to
1290     # the current user are marked readable
1291     return 1 if $self->{ _object_is_readable };
1292
1293     # Defer to the object in question
1294     return $self->Object->CurrentUserCanSee("Transaction", $self);
1295 }
1296
1297
1298 sub Ticket {
1299     my $self = shift;
1300     return $self->ObjectId;
1301 }
1302
1303 sub TicketObj {
1304     my $self = shift;
1305     return $self->Object;
1306 }
1307
1308 sub OldValue {
1309     my $self = shift;
1310     if ( my $Object = $self->OldReferenceObject ) {
1311         return $Object->Content;
1312     }
1313     else {
1314         return $self->_Value('OldValue');
1315     }
1316 }
1317
1318 sub NewValue {
1319     my $self = shift;
1320     if ( my $Object = $self->NewReferenceObject ) {
1321         return $Object->Content;
1322     }
1323     else {
1324         return $self->_Value('NewValue');
1325     }
1326 }
1327
1328 sub Object {
1329     my $self  = shift;
1330     my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1331     $Object->Load($self->__Value('ObjectId'));
1332     return $Object;
1333 }
1334
1335 =head2 NewReferenceObject
1336
1337 =head2 OldReferenceObject
1338
1339 Returns an object of the class specified by the column C<ReferenceType> and
1340 loaded with the id specified by the column C<NewReference> or C<OldReference>.
1341 C<ReferenceType> is assumed to be an L<RT::Record> subclass.
1342
1343 The object may be unloaded (check C<< $object->id >>) if the reference is
1344 corrupt (such as if the referenced record was improperly deleted).
1345
1346 Returns undef if either C<ReferenceType> or C<NewReference>/C<OldReference> is
1347 false.
1348
1349 =cut
1350
1351 sub NewReferenceObject { $_[0]->_ReferenceObject("New") }
1352 sub OldReferenceObject { $_[0]->_ReferenceObject("Old") }
1353
1354 sub _ReferenceObject {
1355     my $self  = shift;
1356     my $which = shift;
1357     my $type  = $self->__Value("ReferenceType");
1358     my $id    = $self->__Value("${which}Reference");
1359     return unless $type and $id;
1360
1361     my $object = $type->new($self->CurrentUser);
1362     $object->Load( $id );
1363     return $object;
1364 }
1365
1366 sub FriendlyObjectType {
1367     my $self = shift;
1368     return $self->loc( $self->Object->RecordType );
1369 }
1370
1371 =head2 UpdateCustomFields
1372
1373 Takes a hash of:
1374
1375     CustomField-C<Id> => Value
1376
1377 or:
1378
1379     Object-RT::Transaction-CustomField-C<Id> => Value
1380
1381 parameters to update this transaction's custom fields.
1382
1383 =cut
1384
1385 sub UpdateCustomFields {
1386     my $self = shift;
1387     my %args = (@_);
1388
1389     # This method used to have an API that took a hash of a single
1390     # value "ARGSRef", which was a reference to a hash of arguments.
1391     # This was insane. The next few lines of code preserve that API
1392     # while giving us something saner.
1393     my $args;
1394     if ($args{'ARGSRef'}) {
1395         RT->Deprecated( Arguments => "ARGSRef", Remove => "4.4" );
1396         $args = $args{ARGSRef};
1397     } else {
1398         $args = \%args;
1399     }
1400
1401     foreach my $arg ( keys %$args ) {
1402         next
1403           unless ( $arg =~
1404             /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1405         next if $arg =~ /-Magic$/;
1406         my $cfid   = $1;
1407         my $values = $args->{$arg};
1408         foreach
1409           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1410         {
1411             next unless (defined($value) && length($value));
1412             $self->_AddCustomFieldValue(
1413                 Field             => $cfid,
1414                 Value             => $value,
1415                 RecordTransaction => 0,
1416             );
1417         }
1418     }
1419 }
1420
1421 =head2 LoadCustomFieldByIdentifier
1422
1423 Finds and returns the custom field of the given name for the
1424 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1425 look for queue-specific CFs before global ones.
1426
1427 =cut
1428
1429 sub LoadCustomFieldByIdentifier {
1430     my $self  = shift;
1431     my $field = shift;
1432
1433     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1434         if ref $field or $field =~ /^\d+$/;
1435
1436     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1437         unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1438
1439     my $CFs = RT::CustomFields->new( $self->CurrentUser );
1440     $CFs->SetContextObject( $self->Object );
1441     $CFs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
1442     $CFs->LimitToLookupType($self->CustomFieldLookupType);
1443     $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1444     return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1445 }
1446
1447 =head2 CustomFieldLookupType
1448
1449 Returns the RT::Transaction lookup type, which can 
1450 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1451
1452 =cut
1453
1454
1455 sub CustomFieldLookupType {
1456     "RT::Queue-RT::Ticket-RT::Transaction";
1457 }
1458
1459
1460 =head2 SquelchMailTo
1461
1462 Similar to Ticket class SquelchMailTo method - returns a list of
1463 transaction's squelched addresses.  As transactions are immutable, the
1464 list of squelched recipients cannot be modified after creation.
1465
1466 =cut
1467
1468 sub SquelchMailTo {
1469     my $self = shift;
1470     return () unless $self->CurrentUserCanSee;
1471     return $self->Attributes->Named('SquelchMailTo');
1472 }
1473
1474 =head2 Recipients
1475
1476 Returns the list of email addresses (as L<Email::Address> objects)
1477 that this transaction would send mail to.  There may be duplicates.
1478
1479 =cut
1480
1481 sub Recipients {
1482     my $self = shift;
1483     my @recipients;
1484     foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1485         my $action = $scrip->ActionObj->Action;
1486         next unless $action->isa('RT::Action::SendEmail');
1487
1488         foreach my $type (qw(To Cc Bcc)) {
1489             push @recipients, $action->$type();
1490         }
1491     }
1492
1493     if ( $self->Rules ) {
1494         for my $rule (@{$self->Rules}) {
1495             next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1496             my $data = $rule->{hints}{recipients};
1497             foreach my $type (qw(To Cc Bcc)) {
1498                 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1499             }
1500         }
1501     }
1502     return @recipients;
1503 }
1504
1505 =head2 DeferredRecipients($freq, $include_sent )
1506
1507 Takes the following arguments:
1508
1509 =over
1510
1511 =item * a string to indicate the frequency of digest delivery.  Valid values are "daily", "weekly", or "susp".
1512
1513 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1514
1515 =back
1516
1517 Returns an array of users who should now receive the notification that
1518 was recorded in this transaction.  Returns an empty array if there were
1519 no deferred users, or if $include_sent was not specified and the deferred
1520 notifications have been sent.
1521
1522 =cut
1523
1524 sub DeferredRecipients {
1525     my $self = shift;
1526     my $freq = shift;
1527     my $include_sent = @_? shift : 0;
1528
1529     my $attr = $self->FirstAttribute('DeferredRecipients');
1530
1531     return () unless ($attr);
1532
1533     my $deferred = $attr->Content;
1534
1535     return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1536
1537     # Skip it.
1538    
1539     for my $user (keys %{$deferred->{$freq}}) {
1540         if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) { 
1541             delete $deferred->{$freq}->{$user} 
1542         }
1543     }
1544     # Now get our users.  Easy.
1545     
1546     return keys %{ $deferred->{$freq} };
1547 }
1548
1549
1550
1551 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1552 sub _CacheConfig {
1553   {
1554      'cache_p'        => 1,
1555      'fast_update_p'  => 1,
1556      'cache_for_sec'  => 6000,
1557   }
1558 }
1559
1560
1561 =head2 ACLEquivalenceObjects
1562
1563 This method returns a list of objects for which a user's rights also apply
1564 to this Transaction.
1565
1566 This currently only applies to Transaction Custom Fields on Tickets, so we return
1567 the Ticket's Queue and the Ticket.
1568
1569 This method is called from L<RT::Principal/HasRight>.
1570
1571 =cut
1572
1573 sub ACLEquivalenceObjects {
1574     my $self = shift;
1575
1576     return unless $self->ObjectType eq 'RT::Ticket';
1577     my $object = $self->Object;
1578     return $object,$object->QueueObj;
1579
1580 }
1581
1582
1583
1584
1585
1586 =head2 id
1587
1588 Returns the current value of id.
1589 (In the database, id is stored as int(11).)
1590
1591
1592 =cut
1593
1594
1595 =head2 ObjectType
1596
1597 Returns the current value of ObjectType.
1598 (In the database, ObjectType is stored as varchar(64).)
1599
1600
1601
1602 =head2 SetObjectType VALUE
1603
1604
1605 Set ObjectType to VALUE.
1606 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1607 (In the database, ObjectType will be stored as a varchar(64).)
1608
1609
1610 =cut
1611
1612
1613 =head2 ObjectId
1614
1615 Returns the current value of ObjectId.
1616 (In the database, ObjectId is stored as int(11).)
1617
1618
1619
1620 =head2 SetObjectId VALUE
1621
1622
1623 Set ObjectId to VALUE.
1624 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1625 (In the database, ObjectId will be stored as a int(11).)
1626
1627
1628 =cut
1629
1630
1631 =head2 TimeTaken
1632
1633 Returns the current value of TimeTaken.
1634 (In the database, TimeTaken is stored as int(11).)
1635
1636
1637
1638 =head2 SetTimeTaken VALUE
1639
1640
1641 Set TimeTaken to VALUE.
1642 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1643 (In the database, TimeTaken will be stored as a int(11).)
1644
1645
1646 =cut
1647
1648
1649 =head2 Type
1650
1651 Returns the current value of Type.
1652 (In the database, Type is stored as varchar(20).)
1653
1654
1655
1656 =head2 SetType VALUE
1657
1658
1659 Set Type to VALUE.
1660 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1661 (In the database, Type will be stored as a varchar(20).)
1662
1663
1664 =cut
1665
1666
1667 =head2 Field
1668
1669 Returns the current value of Field.
1670 (In the database, Field is stored as varchar(40).)
1671
1672
1673
1674 =head2 SetField VALUE
1675
1676
1677 Set Field to VALUE.
1678 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1679 (In the database, Field will be stored as a varchar(40).)
1680
1681
1682 =cut
1683
1684
1685 =head2 OldValue
1686
1687 Returns the current value of OldValue.
1688 (In the database, OldValue is stored as varchar(255).)
1689
1690
1691
1692 =head2 SetOldValue VALUE
1693
1694
1695 Set OldValue to VALUE.
1696 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1697 (In the database, OldValue will be stored as a varchar(255).)
1698
1699
1700 =cut
1701
1702
1703 =head2 NewValue
1704
1705 Returns the current value of NewValue.
1706 (In the database, NewValue is stored as varchar(255).)
1707
1708
1709
1710 =head2 SetNewValue VALUE
1711
1712
1713 Set NewValue to VALUE.
1714 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1715 (In the database, NewValue will be stored as a varchar(255).)
1716
1717
1718 =cut
1719
1720
1721 =head2 ReferenceType
1722
1723 Returns the current value of ReferenceType.
1724 (In the database, ReferenceType is stored as varchar(255).)
1725
1726
1727
1728 =head2 SetReferenceType VALUE
1729
1730
1731 Set ReferenceType to VALUE.
1732 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1733 (In the database, ReferenceType will be stored as a varchar(255).)
1734
1735
1736 =cut
1737
1738
1739 =head2 OldReference
1740
1741 Returns the current value of OldReference.
1742 (In the database, OldReference is stored as int(11).)
1743
1744
1745
1746 =head2 SetOldReference VALUE
1747
1748
1749 Set OldReference to VALUE.
1750 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1751 (In the database, OldReference will be stored as a int(11).)
1752
1753
1754 =cut
1755
1756
1757 =head2 NewReference
1758
1759 Returns the current value of NewReference.
1760 (In the database, NewReference is stored as int(11).)
1761
1762
1763
1764 =head2 SetNewReference VALUE
1765
1766
1767 Set NewReference to VALUE.
1768 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1769 (In the database, NewReference will be stored as a int(11).)
1770
1771
1772 =cut
1773
1774
1775 =head2 Data
1776
1777 Returns the current value of Data.
1778 (In the database, Data is stored as varchar(255).)
1779
1780
1781
1782 =head2 SetData VALUE
1783
1784
1785 Set Data to VALUE.
1786 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1787 (In the database, Data will be stored as a varchar(255).)
1788
1789
1790 =cut
1791
1792
1793 =head2 Creator
1794
1795 Returns the current value of Creator.
1796 (In the database, Creator is stored as int(11).)
1797
1798
1799 =cut
1800
1801
1802 =head2 Created
1803
1804 Returns the current value of Created.
1805 (In the database, Created is stored as datetime.)
1806
1807
1808 =cut
1809
1810
1811
1812 sub _CoreAccessible {
1813     {
1814
1815         id =>
1816                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1817         ObjectType =>
1818                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1819         ObjectId =>
1820                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1821         TimeTaken =>
1822                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1823         Type =>
1824                 {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
1825         Field =>
1826                 {read => 1, write => 1, sql_type => 12, length => 40,  is_blob => 0,  is_numeric => 0,  type => 'varchar(40)', default => ''},
1827         OldValue =>
1828                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1829         NewValue =>
1830                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1831         ReferenceType =>
1832                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1833         OldReference =>
1834                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1835         NewReference =>
1836                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1837         Data =>
1838                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1839         Creator =>
1840                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1841         Created =>
1842                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1843
1844  }
1845 };
1846
1847 sub FindDependencies {
1848     my $self = shift;
1849     my ($walker, $deps) = @_;
1850
1851     $self->SUPER::FindDependencies($walker, $deps);
1852
1853     $deps->Add( out => $self->Object );
1854     $deps->Add( in => $self->Attachments );
1855
1856     my $type = $self->Type;
1857     if ($type eq "CustomField") {
1858         my $cf = RT::CustomField->new( RT->SystemUser );
1859         $cf->Load( $self->Field );
1860         $deps->Add( out => $cf );
1861     } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
1862         for my $field (qw/OldValue NewValue/) {
1863             my $user = RT::User->new( RT->SystemUser );
1864             $user->Load( $self->$field );
1865             $deps->Add( out => $user );
1866         }
1867     } elsif ($type eq "DelWatcher") {
1868         my $principal = RT::Principal->new( RT->SystemUser );
1869         $principal->Load( $self->OldValue );
1870         $deps->Add( out => $principal->Object );
1871     } elsif ($type eq "AddWatcher") {
1872         my $principal = RT::Principal->new( RT->SystemUser );
1873         $principal->Load( $self->NewValue );
1874         $deps->Add( out => $principal->Object );
1875     } elsif ($type eq "DeleteLink") {
1876         if ($self->OldValue) {
1877             my $base = RT::URI->new( $self->CurrentUser );
1878             $base->FromURI( $self->OldValue );
1879             $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
1880         }
1881     } elsif ($type eq "AddLink") {
1882         if ($self->NewValue) {
1883             my $base = RT::URI->new( $self->CurrentUser );
1884             $base->FromURI( $self->NewValue );
1885             $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
1886         }
1887     } elsif ($type eq "Set" and $self->Field eq "Queue") {
1888         for my $field (qw/OldValue NewValue/) {
1889             my $queue = RT::Queue->new( RT->SystemUser );
1890             $queue->Load( $self->$field );
1891             $deps->Add( out => $queue );
1892         }
1893     } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
1894         my $ticket = RT::Ticket->new( RT->SystemUser );
1895         $ticket->Load( $self->NewValue );
1896         $deps->Add( out => $ticket );
1897     }
1898 }
1899
1900 sub Serialize {
1901     my $self = shift;
1902     my %args = (@_);
1903     my %store = $self->SUPER::Serialize(@_);
1904
1905     my $type = $store{Type};
1906     if ($type eq "CustomField") {
1907         my $cf = RT::CustomField->new( RT->SystemUser );
1908         $cf->Load( $store{Field} );
1909         $store{Field} = \($cf->UID);
1910     } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
1911         for my $field (qw/OldValue NewValue/) {
1912             my $user = RT::User->new( RT->SystemUser );
1913             $user->Load( $store{$field} );
1914             $store{$field} = \($user->UID);
1915         }
1916     } elsif ($type eq "DelWatcher") {
1917         my $principal = RT::Principal->new( RT->SystemUser );
1918         $principal->Load( $store{OldValue} );
1919         $store{OldValue} = \($principal->UID);
1920     } elsif ($type eq "AddWatcher") {
1921         my $principal = RT::Principal->new( RT->SystemUser );
1922         $principal->Load( $store{NewValue} );
1923         $store{NewValue} = \($principal->UID);
1924     } elsif ($type eq "DeleteLink") {
1925         if ($store{OldValue}) {
1926             my $base = RT::URI->new( $self->CurrentUser );
1927             $base->FromURI( $store{OldValue} );
1928             $store{OldValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
1929         }
1930     } elsif ($type eq "AddLink") {
1931         if ($store{NewValue}) {
1932             my $base = RT::URI->new( $self->CurrentUser );
1933             $base->FromURI( $store{NewValue} );
1934             $store{NewValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
1935         }
1936     } elsif ($type eq "Set" and $store{Field} eq "Queue") {
1937         for my $field (qw/OldValue NewValue/) {
1938             my $queue = RT::Queue->new( RT->SystemUser );
1939             $queue->Load( $store{$field} );
1940             $store{$field} = \($queue->UID);
1941         }
1942     } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
1943         my $ticket = RT::Ticket->new( RT->SystemUser );
1944         $ticket->Load( $store{NewValue} );
1945         $store{NewValue} = \($ticket->UID);
1946     }
1947
1948     return %store;
1949 }
1950
1951 sub PreInflate {
1952     my $class = shift;
1953     my ($importer, $uid, $data) = @_;
1954
1955     if ($data->{Object} and ref $data->{Object}) {
1956         my $on_uid = ${ $data->{Object} };
1957         return if $importer->ShouldSkipTransaction($on_uid);
1958     }
1959
1960     if ($data->{Type} eq "DeleteLink" and ref $data->{OldValue}) {
1961         my $uid = ${ $data->{OldValue} };
1962         my $obj = $importer->LookupObj( $uid );
1963         $data->{OldValue} = $obj->URI;
1964     } elsif ($data->{Type} eq "AddLink" and ref $data->{NewValue}) {
1965         my $uid = ${ $data->{NewValue} };
1966         my $obj = $importer->LookupObj( $uid );
1967         $data->{NewValue} = $obj->URI;
1968     }
1969
1970     return $class->SUPER::PreInflate( $importer, $uid, $data );
1971 }
1972
1973 RT::Base->_ImportOverlays();
1974
1975 1;