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