48d4e8ce9d2c19db116585a84dd4189d055a4f15
[freeside.git] / rt / lib / RT / Transaction.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 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;
86 use HTML::TreeBuilder;
87
88
89 sub Table {'Transactions'}
90
91 # {{{ sub Create 
92
93 =head2 Create
94
95 Create a new transaction.
96
97 This routine should _never_ be called by anything other than RT::Ticket. 
98 It should not be called 
99 from client code. Ever. Not ever.  If you do this, we will hunt you down and break your kneecaps.
100 Then the unpleasant stuff will start.
101
102 TODO: Document what gets passed to this
103
104 =cut
105
106 sub Create {
107     my $self = shift;
108     my %args = (
109         id             => undef,
110         TimeTaken      => 0,
111         Type           => 'undefined',
112         Data           => '',
113         Field          => undef,
114         OldValue       => undef,
115         NewValue       => undef,
116         MIMEObj        => undef,
117         ActivateScrips => 1,
118         CommitScrips   => 1,
119         ObjectType     => 'RT::Ticket',
120         ObjectId       => 0,
121         ReferenceType  => undef,
122         OldReference   => undef,
123         NewReference   => undef,
124         SquelchMailTo  => undef,
125         CustomFields   => {},
126         @_
127     );
128
129     $args{ObjectId} ||= $args{Ticket};
130
131     #if we didn't specify a ticket, we need to bail
132     unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
133         return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
134     }
135
136     #lets create our transaction
137     my %params = (
138         Type      => $args{'Type'},
139         Data      => $args{'Data'},
140         Field     => $args{'Field'},
141         OldValue  => $args{'OldValue'},
142         NewValue  => $args{'NewValue'},
143         Created   => $args{'Created'},
144         ObjectType => $args{'ObjectType'},
145         ObjectId => $args{'ObjectId'},
146         ReferenceType => $args{'ReferenceType'},
147         OldReference => $args{'OldReference'},
148         NewReference => $args{'NewReference'},
149     );
150
151     # Parameters passed in during an import that we probably don't want to touch, otherwise
152     foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
153         $params{$attr} = $args{$attr} if ($args{$attr});
154     }
155  
156     my $id = $self->SUPER::Create(%params);
157     $self->Load($id);
158     if ( defined $args{'MIMEObj'} ) {
159         my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
160         unless ( $id ) {
161             $RT::Logger->error("Couldn't add attachment: $msg");
162             return ( 0, $self->loc("Couldn't add attachment") );
163         }
164     }
165
166     # Set up any custom fields passed at creation.  Has to happen 
167     # before scrips.
168     
169     $self->UpdateCustomFields(%{ $args{'CustomFields'} });
170
171     $self->AddAttribute(
172         Name    => 'SquelchMailTo',
173         Content => RT::User->CanonicalizeEmailAddress($_)
174     ) for @{$args{'SquelchMailTo'} || []};
175
176     #Provide a way to turn off scrips if we need to
177         $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
178     if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
179        $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
180
181         $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id); 
182
183         $self->{'scrips'}->Prepare(
184             Stage       => 'TransactionCreate',
185             Type        => $args{'Type'},
186             Ticket      => $args{'ObjectId'},
187             Transaction => $self->id,
188         );
189
190        # Entry point of the rule system
191        my $ticket = RT::Ticket->new(RT->SystemUser);
192        $ticket->Load($args{'ObjectId'});
193        my $txn = RT::Transaction->new($RT::SystemUser);
194        $txn->Load($self->id);
195
196        my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
197             Stage       => 'TransactionCreate',
198             Type        => $args{'Type'},
199             TicketObj   => $ticket,
200             TransactionObj => $txn,
201        );
202
203         if ($args{'CommitScrips'} ) {
204             $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
205             $self->{'scrips'}->Commit();
206             RT::Ruleset->CommitRules($rules);
207         }
208     }
209
210     return ( $id, $self->loc("Transaction Created") );
211 }
212
213
214 =head2 Scrips
215
216 Returns the Scrips object for this transaction.
217 This routine is only useful on a freshly created transaction object.
218 Scrips do not get persisted to the database with transactions.
219
220
221 =cut
222
223
224 sub Scrips {
225     my $self = shift;
226     return($self->{'scrips'});
227 }
228
229
230 =head2 Rules
231
232 Returns the array of Rule objects for this transaction.
233 This routine is only useful on a freshly created transaction object.
234 Rules do not get persisted to the database with transactions.
235
236
237 =cut
238
239
240 sub Rules {
241     my $self = shift;
242     return($self->{'rules'});
243 }
244
245
246
247 =head2 Delete
248
249 Delete this transaction. Currently DOES NOT CHECK ACLS
250
251 =cut
252
253 sub Delete {
254     my $self = shift;
255
256
257     $RT::Handle->BeginTransaction();
258
259     my $attachments = $self->Attachments;
260
261     while (my $attachment = $attachments->Next) {
262         my ($id, $msg) = $attachment->Delete();
263         unless ($id) {
264             $RT::Handle->Rollback();
265             return($id, $self->loc("System Error: [_1]", $msg));
266         }
267     }
268     my ($id,$msg) = $self->SUPER::Delete();
269         unless ($id) {
270             $RT::Handle->Rollback();
271             return($id, $self->loc("System Error: [_1]", $msg));
272         }
273     $RT::Handle->Commit();
274     return ($id,$msg);
275 }
276
277
278
279
280 =head2 Message
281
282 Returns the L<RT::Attachments> object which contains the "top-level" object
283 attachment for this transaction.
284
285 =cut
286
287 sub Message {
288     my $self = shift;
289
290     # XXX: Where is ACL check?
291     
292     unless ( defined $self->{'message'} ) {
293
294         $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
295         $self->{'message'}->Limit(
296             FIELD => 'TransactionId',
297             VALUE => $self->Id
298         );
299         $self->{'message'}->ChildrenOf(0);
300     } else {
301         $self->{'message'}->GotoFirstItem;
302     }
303     return $self->{'message'};
304 }
305
306
307
308 =head2 Content PARAMHASH
309
310 If this transaction has attached mime objects, returns the body of the first
311 textual part (as defined in RT::I18N::IsTextualContentType).  Otherwise,
312 returns undef.
313
314 Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message 
315 at $args{'Wrap'}.  $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
316
317 If $args{'Type'} is set to C<text/html>, this will return an HTML 
318 part of the message, if available.  Otherwise it looks for a text/plain
319 part. If $args{'Type'} is missing, it defaults to the value of 
320 C<$RT::Transaction::PreferredContentType>, if that's missing too, 
321 defaults to textual.
322
323 =cut
324
325 sub Content {
326     my $self = shift;
327     my %args = (
328         Type => $PreferredContentType || '',
329         Quote => 0,
330         Wrap  => 70,
331         Wrap  => ( $RT::MessageBoxWidth || 72 ) - 2,
332         @_
333     );
334
335     my $content;
336     if ( my $content_obj =
337         $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
338     {
339         $content = $content_obj->Content ||'';
340
341         if ( lc $content_obj->ContentType eq 'text/html' ) {
342             $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
343
344             if ($args{Type} ne 'text/html') {
345                 my $tree = HTML::TreeBuilder->new_from_content( $content );
346                 $content = HTML::FormatText->new(
347                     leftmargin  => 0,
348                     rightmargin => 78,
349                 )->format( $tree);
350                 $tree->delete;
351             }
352         }
353         else {
354             $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
355             if ($args{Type} eq 'text/html') {
356                 # Extremely simple text->html converter
357                 $content =~ s/&/&#38;/g;
358                 $content =~ s/</&lt;/g;
359                 $content =~ s/>/&gt;/g;
360                 $content = "<pre>$content</pre>";
361             }
362         }
363     }
364
365     # If all else fails, return a message that we couldn't find any content
366     else {
367         $content = $self->loc('This transaction appears to have no content');
368     }
369
370     if ( $args{'Quote'} ) {
371
372         # What's the longest line like?
373         my $max = 0;
374         foreach ( split ( /\n/, $content ) ) {
375             $max = length if length > $max;
376         }
377
378         if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
379             require Text::Wrapper;
380             my $wrapper = Text::Wrapper->new(
381                 columns    => $args{'Wrap'},
382                 body_start => ( $max > 70 * 3 ? '   ' : '' ),
383                 par_start  => ''
384             );
385             $content = $wrapper->wrap($content);
386         }
387
388         $content =~ s/^/> /gm;
389         $content = $self->QuoteHeader . "\n$content\n\n";
390     }
391
392     return ($content);
393 }
394
395 =head2 QuoteHeader
396
397 Returns text prepended to content when transaction is quoted
398 (see C<Quote> argument in L</Content>). By default returns
399 localized "On <date> <user name> wrote:\n".
400
401 =cut
402
403 sub QuoteHeader {
404     my $self = shift;
405     return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
406 }
407
408
409 =head2 Addresses
410
411 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
412
413 =cut
414
415 sub Addresses {
416         my $self = shift;
417
418         if (my $attach = $self->Attachments->First) {   
419                 return $attach->Addresses;
420         }
421         else {
422                 return {};
423         }
424
425 }
426
427
428
429 =head2 ContentObj 
430
431 Returns the RT::Attachment object which contains the content for this Transaction
432
433 =cut
434
435
436 sub ContentObj {
437     my $self = shift;
438     my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
439
440     # If we don't have any content, return undef now.
441     # Get the set of toplevel attachments to this transaction.
442
443     my $Attachment = $args{'Attachment'};
444
445     $Attachment ||= $self->Attachments->First;
446
447     return undef unless ($Attachment);
448
449     # If it's a textual part, just return the body.
450     if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
451         return ($Attachment);
452     }
453
454     # If it's a multipart object, first try returning the first part with preferred
455     # MIME type ('text/plain' by default).
456
457     elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
458         my $kids = $Attachment->Children;
459         while (my $child = $kids->Next) {
460             my $ret =  $self->ContentObj(%args, Attachment => $child);
461             return $ret if ($ret);
462         }
463     }
464     elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
465         if ( $args{Type} ) {
466             my $plain_parts = $Attachment->Children;
467             $plain_parts->ContentType( VALUE => $args{Type} );
468             $plain_parts->LimitNotEmpty;
469
470             # If we actully found a part, return its content
471             if ( my $first = $plain_parts->First ) {
472                 return $first;
473             }
474         }
475
476         # If that fails, return the first textual part which has some content.
477         my $all_parts = $self->Attachments;
478         while ( my $part = $all_parts->Next ) {
479             next unless RT::I18N::IsTextualContentType($part->ContentType)
480                         && $part->Content;
481             return $part;
482         }
483     }
484
485     # We found no content. suck
486     return (undef);
487 }
488
489
490
491 =head2 Subject
492
493 If this transaction has attached mime objects, returns the first one's subject
494 Otherwise, returns null
495   
496 =cut
497
498 sub Subject {
499     my $self = shift;
500     return undef unless my $first = $self->Attachments->First;
501     return $first->Subject;
502 }
503
504
505
506 =head2 Attachments
507
508 Returns all the RT::Attachment objects which are attached
509 to this transaction. Takes an optional parameter, which is
510 a ContentType that Attachments should be restricted to.
511
512 =cut
513
514 sub Attachments {
515     my $self = shift;
516
517     if ( $self->{'attachments'} ) {
518         $self->{'attachments'}->GotoFirstItem;
519         return $self->{'attachments'};
520     }
521
522     $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
523
524     unless ( $self->CurrentUserCanSee ) {
525         $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
526         return $self->{'attachments'};
527     }
528
529     $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
530
531     # Get the self->{'attachments'} in the order they're put into
532     # the database.  Arguably, we should be returning a tree
533     # of self->{'attachments'}, not a set...but no current app seems to need
534     # it.
535
536     $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
537
538     return $self->{'attachments'};
539 }
540
541
542
543 =head2 _Attach
544
545 A private method used to attach a mime object to this transaction.
546
547 =cut
548
549 sub _Attach {
550     my $self       = shift;
551     my $MIMEObject = shift;
552
553     unless ( defined $MIMEObject ) {
554         $RT::Logger->error("We can't attach a mime object if you don't give us one.");
555         return ( 0, $self->loc("[_1]: no attachment specified", $self) );
556     }
557
558     my $Attachment = RT::Attachment->new( $self->CurrentUser );
559     my ($id, $msg) = $Attachment->Create(
560         TransactionId => $self->Id,
561         Attachment    => $MIMEObject
562     );
563     return ( $Attachment, $msg || $self->loc("Attachment created") );
564 }
565
566
567
568 sub ContentAsMIME {
569     my $self = shift;
570
571     # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
572     # since it has less information available without looking to it's parent
573     # transaction.  Check ACLs here before we go any further.
574     return unless $self->CurrentUserCanSee;
575
576     my $attachments = RT::Attachments->new( $self->CurrentUser );
577     $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
578     $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
579     $attachments->Limit( FIELD => 'Parent',        VALUE => 0 );
580     $attachments->RowsPerPage(1);
581
582     my $top = $attachments->First;
583     return unless $top;
584
585     my $entity = MIME::Entity->build(
586         Type        => 'message/rfc822',
587         Description => 'transaction ' . $self->id,
588         Data        => $top->ContentAsMIME(Children => 1)->as_string,
589     );
590
591     return $entity;
592 }
593
594
595
596 =head2 Description
597
598 Returns a text string which describes this transaction
599
600 =cut
601
602 sub Description {
603     my $self = shift;
604
605     unless ( $self->CurrentUserCanSee ) {
606         return ( $self->loc("Permission Denied") );
607     }
608
609     unless ( defined $self->Type ) {
610         return ( $self->loc("No transaction type specified"));
611     }
612
613     return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
614 }
615
616
617
618 =head2 BriefDescription
619
620 Returns a text string which briefly describes this transaction
621
622 =cut
623
624 sub BriefDescription {
625     my $self = shift;
626
627     unless ( $self->CurrentUserCanSee ) {
628         return ( $self->loc("Permission Denied") );
629     }
630
631     my $type = $self->Type;    #cache this, rather than calling it 30 times
632
633     unless ( defined $type ) {
634         return $self->loc("No transaction type specified");
635     }
636
637     my $obj_type = $self->FriendlyObjectType;
638
639     if ( $type eq 'Create' ) {
640         return ( $self->loc( "[_1] created", $obj_type ) );
641     }
642     elsif ( $type eq 'Enabled' ) {
643         return ( $self->loc( "[_1] enabled", $obj_type ) );
644     }
645     elsif ( $type eq 'Disabled' ) {
646         return ( $self->loc( "[_1] disabled", $obj_type ) );
647     }
648     elsif ( $type =~ /Status/ ) {
649         if ( $self->Field eq 'Status' ) {
650             if ( $self->NewValue eq 'deleted' ) {
651                 return ( $self->loc( "[_1] deleted", $obj_type ) );
652             }
653             else {
654                 my $canon = $self->Object->can("QueueObj")
655                     ? sub { $self->Object->QueueObj->Lifecycle->CanonicalCase(@_) }
656                     : sub { return $_[0] };
657                 return (
658                     $self->loc(
659                         "Status changed from [_1] to [_2]",
660                         "'" . $self->loc( $canon->($self->OldValue) ) . "'",
661                         "'" . $self->loc( $canon->($self->NewValue) ) . "'"
662                     )
663                 );
664
665             }
666         }
667
668         # Generic:
669         my $no_value = $self->loc("(no value)");
670         return (
671             $self->loc(
672                 "[_1] changed from [_2] to [_3]",
673                 $self->Field,
674                 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
675                 "'" . $self->NewValue . "'"
676             )
677         );
678     }
679     elsif ( $type =~ /SystemError/ ) {
680         return $self->loc("System error");
681     }
682     elsif ( $type =~ /Forward Transaction/ ) {
683         return $self->loc( "Forwarded Transaction #[_1] to [_2]",
684             $self->Field, $self->Data );
685     }
686     elsif ( $type =~ /Forward Ticket/ ) {
687         return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
688     }
689
690     if ( my $code = $_BriefDescriptions{$type} ) {
691         return $code->($self);
692     }
693
694     return $self->loc(
695         "Default: [_1]/[_2] changed from [_3] to [_4]",
696         $type,
697         $self->Field,
698         (
699             $self->OldValue
700             ? "'" . $self->OldValue . "'"
701             : $self->loc("(no value)")
702         ),
703         "'" . $self->NewValue . "'"
704     );
705 }
706
707 %_BriefDescriptions = (
708     CommentEmailRecord => sub {
709         my $self = shift;
710         return $self->loc("Outgoing email about a comment recorded");
711     },
712     EmailRecord => sub {
713         my $self = shift;
714         return $self->loc("Outgoing email recorded");
715     },
716     Correspond => sub {
717         my $self = shift;
718         return $self->loc("Correspondence added");
719     },
720     Comment => sub {
721         my $self = shift;
722         return $self->loc("Comments added");
723     },
724     CustomField => sub {
725         my $self = shift;
726         my $field = $self->loc('CustomField');
727
728         if ( $self->Field ) {
729             my $cf = RT::CustomField->new( $self->CurrentUser );
730             $cf->SetContextObject( $self->Object );
731             $cf->Load( $self->Field );
732             $field = $cf->Name();
733             $field = $self->loc('a custom field') if !defined($field);
734         }
735
736         my $new = $self->NewValue;
737         my $old = $self->OldValue;
738
739         if ( !defined($old) || $old eq '' ) {
740             return $self->loc("[_1] [_2] added", $field, $new);
741         }
742         elsif ( !defined($new) || $new eq '' ) {
743             return $self->loc("[_1] [_2] deleted", $field, $old);
744         }
745         else {
746             return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
747         }
748     },
749     Untake => sub {
750         my $self = shift;
751         return $self->loc("Untaken");
752     },
753     Take => sub {
754         my $self = shift;
755         return $self->loc("Taken");
756     },
757     Force => sub {
758         my $self = shift;
759         my $Old = RT::User->new( $self->CurrentUser );
760         $Old->Load( $self->OldValue );
761         my $New = RT::User->new( $self->CurrentUser );
762         $New->Load( $self->NewValue );
763
764         return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
765     },
766     Steal => sub {
767         my $self = shift;
768         my $Old = RT::User->new( $self->CurrentUser );
769         $Old->Load( $self->OldValue );
770         return $self->loc("Stolen from [_1]",  $Old->Name);
771     },
772     Give => sub {
773         my $self = shift;
774         my $New = RT::User->new( $self->CurrentUser );
775         $New->Load( $self->NewValue );
776         return $self->loc( "Given to [_1]",  $New->Name );
777     },
778     AddWatcher => sub {
779         my $self = shift;
780         my $principal = RT::Principal->new($self->CurrentUser);
781         $principal->Load($self->NewValue);
782         return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
783     },
784     DelWatcher => sub {
785         my $self = shift;
786         my $principal = RT::Principal->new($self->CurrentUser);
787         $principal->Load($self->OldValue);
788         return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
789     },
790     Subject => sub {
791         my $self = shift;
792         return $self->loc( "Subject changed to [_1]", $self->Data );
793     },
794     AddLink => sub {
795         my $self = shift;
796         my $value;
797         if ( $self->NewValue ) {
798             my $URI = RT::URI->new( $self->CurrentUser );
799             if ( $URI->FromURI( $self->NewValue ) ) {
800                 $value = $URI->Resolver->AsString;
801             }
802             else {
803                 $value = $self->NewValue;
804             }
805             if ( $self->Field eq 'DependsOn' ) {
806                 return $self->loc( "Dependency on [_1] added", $value );
807             }
808             elsif ( $self->Field eq 'DependedOnBy' ) {
809                 return $self->loc( "Dependency by [_1] added", $value );
810
811             }
812             elsif ( $self->Field eq 'RefersTo' ) {
813                 return $self->loc( "Reference to [_1] added", $value );
814             }
815             elsif ( $self->Field eq 'ReferredToBy' ) {
816                 return $self->loc( "Reference by [_1] added", $value );
817             }
818             elsif ( $self->Field eq 'MemberOf' ) {
819                 return $self->loc( "Membership in [_1] added", $value );
820             }
821             elsif ( $self->Field eq 'HasMember' ) {
822                 return $self->loc( "Member [_1] added", $value );
823             }
824             elsif ( $self->Field eq 'MergedInto' ) {
825                 return $self->loc( "Merged into [_1]", $value );
826             }
827         }
828         else {
829             return ( $self->Data );
830         }
831     },
832     DeleteLink => sub {
833         my $self = shift;
834         my $value;
835         if ( $self->OldValue ) {
836             my $URI = RT::URI->new( $self->CurrentUser );
837             if ( $URI->FromURI( $self->OldValue ) ){
838                 $value = $URI->Resolver->AsString;
839             }
840             else {
841                 $value = $self->OldValue;
842             }
843
844             if ( $self->Field eq 'DependsOn' ) {
845                 return $self->loc( "Dependency on [_1] deleted", $value );
846             }
847             elsif ( $self->Field eq 'DependedOnBy' ) {
848                 return $self->loc( "Dependency by [_1] deleted", $value );
849
850             }
851             elsif ( $self->Field eq 'RefersTo' ) {
852                 return $self->loc( "Reference to [_1] deleted", $value );
853             }
854             elsif ( $self->Field eq 'ReferredToBy' ) {
855                 return $self->loc( "Reference by [_1] deleted", $value );
856             }
857             elsif ( $self->Field eq 'MemberOf' ) {
858                 return $self->loc( "Membership in [_1] deleted", $value );
859             }
860             elsif ( $self->Field eq 'HasMember' ) {
861                 return $self->loc( "Member [_1] deleted", $value );
862             }
863         }
864         else {
865             return ( $self->Data );
866         }
867     },
868     Told => sub {
869         my $self = shift;
870         if ( $self->Field eq 'Told' ) {
871             my $t1 = RT::Date->new($self->CurrentUser);
872             $t1->Set(Format => 'ISO', Value => $self->NewValue);
873             my $t2 = RT::Date->new($self->CurrentUser);
874             $t2->Set(Format => 'ISO', Value => $self->OldValue);
875             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
876         }
877         else {
878             return $self->loc( "[_1] changed from [_2] to [_3]",
879                                $self->loc($self->Field),
880                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
881         }
882     },
883     Set => sub {
884         my $self = shift;
885         if ( $self->Field eq 'Password' ) {
886             return $self->loc('Password changed');
887         }
888         elsif ( $self->Field eq 'Queue' ) {
889             my $q1 = RT::Queue->new( $self->CurrentUser );
890             $q1->Load( $self->OldValue );
891             my $q2 = RT::Queue->new( $self->CurrentUser );
892             $q2->Load( $self->NewValue );
893             return $self->loc("[_1] changed from [_2] to [_3]",
894                               $self->loc($self->Field) , $q1->Name , $q2->Name);
895         }
896
897         # Write the date/time change at local time:
898         elsif ($self->Field =~  /Due|Starts|Started|Told|WillResolve/) {
899             my $t1 = RT::Date->new($self->CurrentUser);
900             $t1->Set(Format => 'ISO', Value => $self->NewValue);
901             my $t2 = RT::Date->new($self->CurrentUser);
902             $t2->Set(Format => 'ISO', Value => $self->OldValue);
903             return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
904         }
905         elsif ( $self->Field eq 'Owner' ) {
906             my $Old = RT::User->new( $self->CurrentUser );
907             $Old->Load( $self->OldValue );
908             my $New = RT::User->new( $self->CurrentUser );
909             $New->Load( $self->NewValue );
910
911             if ( $Old->id == RT->Nobody->id ) {
912                 if ( $New->id == $self->Creator ) {
913                     return $self->loc("Taken");
914                 }
915                 else {
916                     return $self->loc( "Given to [_1]",  $New->Name );
917                 }
918             }
919             else {
920                 if ( $New->id == $self->Creator ) {
921                     return $self->loc("Stolen from [_1]",  $Old->Name);
922                 }
923                 elsif ( $Old->id == $self->Creator ) {
924                     if ( $New->id == RT->Nobody->id ) {
925                         return $self->loc("Untaken");
926                     }
927                     else {
928                         return $self->loc( "Given to [_1]", $New->Name );
929                     }
930                 }
931                 else {
932                     return $self->loc(
933                         "Owner forcibly changed from [_1] to [_2]",
934                         $Old->Name, $New->Name );
935                 }
936             }
937         }
938         else {
939             return $self->loc( "[_1] changed from [_2] to [_3]",
940                                $self->loc($self->Field),
941                                ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
942         }
943     },
944     PurgeTransaction => sub {
945         my $self = shift;
946         return $self->loc("Transaction [_1] purged", $self->Data);
947     },
948     AddReminder => sub {
949         my $self = shift;
950         my $ticket = RT::Ticket->new($self->CurrentUser);
951         $ticket->Load($self->NewValue);
952         return $self->loc("Reminder '[_1]' added", $ticket->Subject);
953     },
954     OpenReminder => sub {
955         my $self = shift;
956         my $ticket = RT::Ticket->new($self->CurrentUser);
957         $ticket->Load($self->NewValue);
958         return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
959     
960     },
961     ResolveReminder => sub {
962         my $self = shift;
963         my $ticket = RT::Ticket->new($self->CurrentUser);
964         $ticket->Load($self->NewValue);
965         return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
966     
967     
968     }
969 );
970
971
972
973
974 =head2 IsInbound
975
976 Returns true if the creator of the transaction is a requestor of the ticket.
977 Returns false otherwise
978
979 =cut
980
981 sub IsInbound {
982     my $self = shift;
983     $self->ObjectType eq 'RT::Ticket' or return undef;
984     return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
985 }
986
987
988
989 sub _OverlayAccessible {
990     {
991
992           ObjectType => { public => 1},
993           ObjectId => { public => 1},
994
995     }
996 };
997
998
999
1000
1001 sub _Set {
1002     my $self = shift;
1003     return ( 0, $self->loc('Transactions are immutable') );
1004 }
1005
1006
1007
1008 =head2 _Value
1009
1010 Takes the name of a table column.
1011 Returns its value as a string, if the user passes an ACL check
1012
1013 =cut
1014
1015 sub _Value {
1016     my $self  = shift;
1017     my $field = shift;
1018
1019     #if the field is public, return it.
1020     if ( $self->_Accessible( $field, 'public' ) ) {
1021         return $self->SUPER::_Value( $field );
1022     }
1023
1024     unless ( $self->CurrentUserCanSee ) {
1025         return undef;
1026     }
1027
1028     return $self->SUPER::_Value( $field );
1029 }
1030
1031
1032
1033 =head2 CurrentUserHasRight RIGHT
1034
1035 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1036 passed in here.
1037
1038 =cut
1039
1040 sub CurrentUserHasRight {
1041     my $self  = shift;
1042     my $right = shift;
1043     return $self->CurrentUser->HasRight(
1044         Right  => $right,
1045         Object => $self->Object
1046     );
1047 }
1048
1049 =head2 CurrentUserCanSee
1050
1051 Returns true if current user has rights to see this particular transaction.
1052
1053 This fact depends on type of the transaction, type of an object the transaction
1054 is attached to and may be other conditions, so this method is prefered over
1055 custom implementations.
1056
1057 =cut
1058
1059 sub CurrentUserCanSee {
1060     my $self = shift;
1061
1062     # If it's a comment, we need to be extra special careful
1063     my $type = $self->__Value('Type');
1064     if ( $type eq 'Comment' ) {
1065         unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1066             return 0;
1067         }
1068     }
1069     elsif ( $type eq 'CommentEmailRecord' ) {
1070         unless ( $self->CurrentUserHasRight('ShowTicketComments')
1071             && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1072             return 0;
1073         }
1074     }
1075     elsif ( $type eq 'EmailRecord' ) {
1076         unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1077             return 0;
1078         }
1079     }
1080     # Make sure the user can see the custom field before showing that it changed
1081     elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1082         my $cf = RT::CustomField->new( $self->CurrentUser );
1083         $cf->SetContextObject( $self->Object );
1084         $cf->Load( $cf_id );
1085         return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1086     }
1087
1088     # Transactions that might have changed the ->Object's visibility to
1089     # the current user are marked readable
1090     return 1 if $self->{ _object_is_readable };
1091
1092     # Defer to the object in question
1093     return $self->Object->CurrentUserCanSee("Transaction");
1094 }
1095
1096
1097 sub Ticket {
1098     my $self = shift;
1099     return $self->ObjectId;
1100 }
1101
1102 sub TicketObj {
1103     my $self = shift;
1104     return $self->Object;
1105 }
1106
1107 sub OldValue {
1108     my $self = shift;
1109     if ( my $type = $self->__Value('ReferenceType')
1110          and my $id = $self->__Value('OldReference') )
1111     {
1112         my $Object = $type->new($self->CurrentUser);
1113         $Object->Load( $id );
1114         return $Object->Content;
1115     }
1116     else {
1117         return $self->_Value('OldValue');
1118     }
1119 }
1120
1121 sub NewValue {
1122     my $self = shift;
1123     if ( my $type = $self->__Value('ReferenceType')
1124          and my $id = $self->__Value('NewReference') )
1125     {
1126         my $Object = $type->new($self->CurrentUser);
1127         $Object->Load( $id );
1128         return $Object->Content;
1129     }
1130     else {
1131         return $self->_Value('NewValue');
1132     }
1133 }
1134
1135 sub Object {
1136     my $self  = shift;
1137     my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1138     $Object->Load($self->__Value('ObjectId'));
1139     return $Object;
1140 }
1141
1142 sub FriendlyObjectType {
1143     my $self = shift;
1144     my $type = $self->ObjectType or return undef;
1145     $type =~ s/^RT:://;
1146     return $self->loc($type);
1147 }
1148
1149 =head2 UpdateCustomFields
1150     
1151     Takes a hash of 
1152
1153     CustomField-<<Id>> => Value
1154         or 
1155
1156     Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1157     this transaction's custom fields
1158
1159 =cut
1160
1161 sub UpdateCustomFields {
1162     my $self = shift;
1163     my %args = (@_);
1164
1165     # This method used to have an API that took a hash of a single
1166     # value "ARGSRef", which was a reference to a hash of arguments.
1167     # This was insane. The next few lines of code preserve that API
1168     # while giving us something saner.
1169
1170     # TODO: 3.6: DEPRECATE OLD API
1171
1172     my $args; 
1173
1174     if ($args{'ARGSRef'}) { 
1175         $args = $args{ARGSRef};
1176     } else {
1177         $args = \%args;
1178     }
1179
1180     foreach my $arg ( keys %$args ) {
1181         next
1182           unless ( $arg =~
1183             /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1184         next if $arg =~ /-Magic$/;
1185         next if $arg =~ /-TimeUnits$/;
1186         my $cfid   = $1;
1187         my $values = $args->{$arg};
1188         foreach
1189           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1190         {
1191             next unless (defined($value) && length($value));
1192             $self->_AddCustomFieldValue(
1193                 Field             => $cfid,
1194                 Value             => $value,
1195                 RecordTransaction => 0,
1196             );
1197         }
1198     }
1199 }
1200
1201 =head2 LoadCustomFieldByIdentifier
1202
1203 Finds and returns the custom field of the given name for the
1204 transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1205 look for queue-specific CFs before global ones.
1206
1207 =cut
1208
1209 sub LoadCustomFieldByIdentifier {
1210     my $self  = shift;
1211     my $field = shift;
1212
1213     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1214         if ref $field or $field =~ /^\d+$/;
1215
1216     return $self->SUPER::LoadCustomFieldByIdentifier($field)
1217         unless UNIVERSAL::can( $self->Object, 'QueueObj' );
1218
1219     my $CFs = RT::CustomFields->new( $self->CurrentUser );
1220     $CFs->SetContextObject( $self->Object );
1221     $CFs->Limit( FIELD => 'Name', VALUE => $field );
1222     $CFs->LimitToLookupType($self->CustomFieldLookupType);
1223     $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1224     return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1225 }
1226
1227 =head2 CustomFieldLookupType
1228
1229 Returns the RT::Transaction lookup type, which can 
1230 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1231
1232 =cut
1233
1234
1235 sub CustomFieldLookupType {
1236     "RT::Queue-RT::Ticket-RT::Transaction";
1237 }
1238
1239
1240 =head2 SquelchMailTo
1241
1242 Similar to Ticket class SquelchMailTo method - returns a list of
1243 transaction's squelched addresses.  As transactions are immutable, the
1244 list of squelched recipients cannot be modified after creation.
1245
1246 =cut
1247
1248 sub SquelchMailTo {
1249     my $self = shift;
1250     return () unless $self->CurrentUserCanSee;
1251     return $self->Attributes->Named('SquelchMailTo');
1252 }
1253
1254 =head2 Recipients
1255
1256 Returns the list of email addresses (as L<Email::Address> objects)
1257 that this transaction would send mail to.  There may be duplicates.
1258
1259 =cut
1260
1261 sub Recipients {
1262     my $self = shift;
1263     my @recipients;
1264     foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1265         my $action = $scrip->ActionObj->Action;
1266         next unless $action->isa('RT::Action::SendEmail');
1267
1268         foreach my $type (qw(To Cc Bcc)) {
1269             push @recipients, $action->$type();
1270         }
1271     }
1272
1273     if ( $self->Rules ) {
1274         for my $rule (@{$self->Rules}) {
1275             next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1276             my $data = $rule->{hints}{recipients};
1277             foreach my $type (qw(To Cc Bcc)) {
1278                 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1279             }
1280         }
1281     }
1282     return @recipients;
1283 }
1284
1285 =head2 DeferredRecipients($freq, $include_sent )
1286
1287 Takes the following arguments:
1288
1289 =over
1290
1291 =item * a string to indicate the frequency of digest delivery.  Valid values are "daily", "weekly", or "susp".
1292
1293 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1294
1295 =back
1296
1297 Returns an array of users who should now receive the notification that
1298 was recorded in this transaction.  Returns an empty array if there were
1299 no deferred users, or if $include_sent was not specified and the deferred
1300 notifications have been sent.
1301
1302 =cut
1303
1304 sub DeferredRecipients {
1305     my $self = shift;
1306     my $freq = shift;
1307     my $include_sent = @_? shift : 0;
1308
1309     my $attr = $self->FirstAttribute('DeferredRecipients');
1310
1311     return () unless ($attr);
1312
1313     my $deferred = $attr->Content;
1314
1315     return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1316
1317     # Skip it.
1318    
1319     for my $user (keys %{$deferred->{$freq}}) {
1320         if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) { 
1321             delete $deferred->{$freq}->{$user} 
1322         }
1323     }
1324     # Now get our users.  Easy.
1325     
1326     return keys %{ $deferred->{$freq} };
1327 }
1328
1329
1330
1331 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1332 sub _CacheConfig {
1333   {
1334      'cache_p'        => 1,
1335      'fast_update_p'  => 1,
1336      'cache_for_sec'  => 6000,
1337   }
1338 }
1339
1340
1341 =head2 ACLEquivalenceObjects
1342
1343 This method returns a list of objects for which a user's rights also apply
1344 to this Transaction.
1345
1346 This currently only applies to Transaction Custom Fields on Tickets, so we return
1347 the Ticket's Queue and the Ticket.
1348
1349 This method is called from L<RT::Principal/HasRight>.
1350
1351 =cut
1352
1353 sub ACLEquivalenceObjects {
1354     my $self = shift;
1355
1356     return unless $self->ObjectType eq 'RT::Ticket';
1357     my $object = $self->Object;
1358     return $object,$object->QueueObj;
1359
1360 }
1361
1362
1363
1364
1365
1366 =head2 id
1367
1368 Returns the current value of id.
1369 (In the database, id is stored as int(11).)
1370
1371
1372 =cut
1373
1374
1375 =head2 ObjectType
1376
1377 Returns the current value of ObjectType.
1378 (In the database, ObjectType is stored as varchar(64).)
1379
1380
1381
1382 =head2 SetObjectType VALUE
1383
1384
1385 Set ObjectType to VALUE.
1386 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1387 (In the database, ObjectType will be stored as a varchar(64).)
1388
1389
1390 =cut
1391
1392
1393 =head2 ObjectId
1394
1395 Returns the current value of ObjectId.
1396 (In the database, ObjectId is stored as int(11).)
1397
1398
1399
1400 =head2 SetObjectId VALUE
1401
1402
1403 Set ObjectId to VALUE.
1404 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1405 (In the database, ObjectId will be stored as a int(11).)
1406
1407
1408 =cut
1409
1410
1411 =head2 TimeTaken
1412
1413 Returns the current value of TimeTaken.
1414 (In the database, TimeTaken is stored as int(11).)
1415
1416
1417
1418 =head2 SetTimeTaken VALUE
1419
1420
1421 Set TimeTaken to VALUE.
1422 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1423 (In the database, TimeTaken will be stored as a int(11).)
1424
1425
1426 =cut
1427
1428
1429 =head2 Type
1430
1431 Returns the current value of Type.
1432 (In the database, Type is stored as varchar(20).)
1433
1434
1435
1436 =head2 SetType VALUE
1437
1438
1439 Set Type to VALUE.
1440 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1441 (In the database, Type will be stored as a varchar(20).)
1442
1443
1444 =cut
1445
1446
1447 =head2 Field
1448
1449 Returns the current value of Field.
1450 (In the database, Field is stored as varchar(40).)
1451
1452
1453
1454 =head2 SetField VALUE
1455
1456
1457 Set Field to VALUE.
1458 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1459 (In the database, Field will be stored as a varchar(40).)
1460
1461
1462 =cut
1463
1464
1465 =head2 OldValue
1466
1467 Returns the current value of OldValue.
1468 (In the database, OldValue is stored as varchar(255).)
1469
1470
1471
1472 =head2 SetOldValue VALUE
1473
1474
1475 Set OldValue to VALUE.
1476 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1477 (In the database, OldValue will be stored as a varchar(255).)
1478
1479
1480 =cut
1481
1482
1483 =head2 NewValue
1484
1485 Returns the current value of NewValue.
1486 (In the database, NewValue is stored as varchar(255).)
1487
1488
1489
1490 =head2 SetNewValue VALUE
1491
1492
1493 Set NewValue to VALUE.
1494 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1495 (In the database, NewValue will be stored as a varchar(255).)
1496
1497
1498 =cut
1499
1500
1501 =head2 ReferenceType
1502
1503 Returns the current value of ReferenceType.
1504 (In the database, ReferenceType is stored as varchar(255).)
1505
1506
1507
1508 =head2 SetReferenceType VALUE
1509
1510
1511 Set ReferenceType to VALUE.
1512 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1513 (In the database, ReferenceType will be stored as a varchar(255).)
1514
1515
1516 =cut
1517
1518
1519 =head2 OldReference
1520
1521 Returns the current value of OldReference.
1522 (In the database, OldReference is stored as int(11).)
1523
1524
1525
1526 =head2 SetOldReference VALUE
1527
1528
1529 Set OldReference to VALUE.
1530 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1531 (In the database, OldReference will be stored as a int(11).)
1532
1533
1534 =cut
1535
1536
1537 =head2 NewReference
1538
1539 Returns the current value of NewReference.
1540 (In the database, NewReference is stored as int(11).)
1541
1542
1543
1544 =head2 SetNewReference VALUE
1545
1546
1547 Set NewReference to VALUE.
1548 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1549 (In the database, NewReference will be stored as a int(11).)
1550
1551
1552 =cut
1553
1554
1555 =head2 Data
1556
1557 Returns the current value of Data.
1558 (In the database, Data is stored as varchar(255).)
1559
1560
1561
1562 =head2 SetData VALUE
1563
1564
1565 Set Data to VALUE.
1566 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1567 (In the database, Data will be stored as a varchar(255).)
1568
1569
1570 =cut
1571
1572
1573 =head2 Creator
1574
1575 Returns the current value of Creator.
1576 (In the database, Creator is stored as int(11).)
1577
1578
1579 =cut
1580
1581
1582 =head2 Created
1583
1584 Returns the current value of Created.
1585 (In the database, Created is stored as datetime.)
1586
1587
1588 =cut
1589
1590
1591
1592 sub _CoreAccessible {
1593     {
1594
1595         id =>
1596                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1597         ObjectType =>
1598                 {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
1599         ObjectId =>
1600                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1601         TimeTaken =>
1602                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1603         Type =>
1604                 {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
1605         Field =>
1606                 {read => 1, write => 1, sql_type => 12, length => 40,  is_blob => 0,  is_numeric => 0,  type => 'varchar(40)', default => ''},
1607         OldValue =>
1608                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1609         NewValue =>
1610                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1611         ReferenceType =>
1612                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1613         OldReference =>
1614                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1615         NewReference =>
1616                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
1617         Data =>
1618                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
1619         Creator =>
1620                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
1621         Created =>
1622                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
1623
1624  }
1625 };
1626
1627 RT::Base->_ImportOverlays();
1628
1629 1;