1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2007 Best Practical Solutions, LLC
6 # <jesse@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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/copyleft/gpl.html.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
50 RT::Transaction - RT\'s transaction object
60 Each RT::Transaction describes an atomic change to a ticket object
61 or an update to an RT::Ticket object.
62 It can have arbitrary MIME attachments.
69 ok(require RT::Transaction);
76 package RT::Transaction;
79 no warnings qw(redefine);
81 use vars qw( %_BriefDescriptions );
87 use HTML::TreeBuilder;
94 Create a new transaction.
96 This routine should _never_ be called by anything other than RT::Ticket.
97 It should not be called
98 from client code. Ever. Not ever. If you do this, we will hunt you down and break your kneecaps.
99 Then the unpleasant stuff will start.
101 TODO: Document what gets passed to this
118 ObjectType => 'RT::Ticket',
120 ReferenceType => undef,
121 OldReference => undef,
122 NewReference => undef,
126 $args{ObjectId} ||= $args{Ticket};
128 #if we didn't specify a ticket, we need to bail
129 unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
130 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
135 #lets create our transaction
137 Type => $args{'Type'},
138 Data => $args{'Data'},
139 Field => $args{'Field'},
140 OldValue => $args{'OldValue'},
141 NewValue => $args{'NewValue'},
142 Created => $args{'Created'},
143 ObjectType => $args{'ObjectType'},
144 ObjectId => $args{'ObjectId'},
145 ReferenceType => $args{'ReferenceType'},
146 OldReference => $args{'OldReference'},
147 NewReference => $args{'NewReference'},
150 # Parameters passed in during an import that we probably don't want to touch, otherwise
151 foreach my $attr qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy) {
152 $params{$attr} = $args{$attr} if ($args{$attr});
155 my $id = $self->SUPER::Create(%params);
157 if ( defined $args{'MIMEObj'} ) {
158 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
160 $RT::Logger->error("Couldn't add attachment: $msg");
161 return ( 0, $self->loc("Couldn't add attachment") );
166 #Provide a way to turn off scrips if we need to
167 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
168 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
169 $self->{'scrips'} = RT::Scrips->new($RT::SystemUser);
171 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
173 $self->{'scrips'}->Prepare(
174 Stage => 'TransactionCreate',
175 Type => $args{'Type'},
176 Ticket => $args{'ObjectId'},
177 Transaction => $self->id,
179 if ($args{'CommitScrips'} ) {
180 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
181 $self->{'scrips'}->Commit();
185 return ( $id, $self->loc("Transaction Created") );
192 Returns the Scrips object for this transaction.
193 This routine is only useful on a freshly created transaction object.
194 Scrips do not get persisted to the database with transactions.
202 return($self->{'scrips'});
210 Delete this transaction. Currently DOES NOT CHECK ACLS
218 $RT::Handle->BeginTransaction();
220 my $attachments = $self->Attachments;
222 while (my $attachment = $attachments->Next) {
223 my ($id, $msg) = $attachment->Delete();
225 $RT::Handle->Rollback();
226 return($id, $self->loc("System Error: [_1]", $msg));
229 my ($id,$msg) = $self->SUPER::Delete();
231 $RT::Handle->Rollback();
232 return($id, $self->loc("System Error: [_1]", $msg));
234 $RT::Handle->Commit();
240 # {{{ Routines dealing with Attachments
246 Returns the RT::Attachments Object which contains the "top-level"object
247 attachment for this transaction
255 if ( !defined( $self->{'message'} ) ) {
257 $self->{'message'} = new RT::Attachments( $self->CurrentUser );
258 $self->{'message'}->Limit(
259 FIELD => 'TransactionId',
263 $self->{'message'}->ChildrenOf(0);
265 return ( $self->{'message'} );
272 =head2 Content PARAMHASH
274 If this transaction has attached mime objects, returns the first text/plain part.
275 Otherwise, returns undef.
277 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
278 at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
292 if (my $content_obj = $self->ContentObj) {
293 $content = $content_obj->Content;
295 if ($content_obj->ContentType =~ m{^text/html$}i) {
296 $content = HTML::FormatText->new(leftmargin => 0, rightmargin => 78)->format( HTML::TreeBuilder->new_from_content( $content));
301 # If all else fails, return a message that we couldn't find any content
303 $content = $self->loc('This transaction appears to have no content');
306 if ( $args{'Quote'} ) {
308 # Remove quoted signature.
309 $content =~ s/\n-- \n(.*?)$//s;
311 # What's the longest line like?
313 foreach ( split ( /\n/, $content ) ) {
314 $max = length if ( length > $max );
318 require Text::Wrapper;
319 my $wrapper = new Text::Wrapper(
320 columns => $args{'Wrap'},
321 body_start => ( $max > 70 * 3 ? ' ' : '' ),
324 $content = $wrapper->wrap($content);
327 $content =~ s/^/> /gm;
328 $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString(), $self->CreatorObj->Name())
341 Returns the RT::Attachment object which contains the content for this Transaction
350 # If we don\'t have any content, return undef now.
351 unless ( $self->Attachments->First ) {
355 # Get the set of toplevel attachments to this transaction.
356 my $Attachment = $self->Attachments->First();
358 # If it's a message or a plain part, just return the
360 if ( $Attachment->ContentType() =~ '^(?:text/plain$|text/html|message/)' ) {
361 return ($Attachment);
364 # If it's a multipart object, first try returning the first
367 elsif ( $Attachment->ContentType() =~ '^multipart/' ) {
368 my $plain_parts = $Attachment->Children();
369 $plain_parts->ContentType( VALUE => 'text/plain' );
371 # If we actully found a part, return its content
372 if ( $plain_parts->First && $plain_parts->First->Content ne '' ) {
373 return ( $plain_parts->First );
377 # If that fails, return the first text/plain or message/ part
378 # which has some content.
381 my $all_parts = $self->Attachments();
382 while ( my $part = $all_parts->Next ) {
383 if (( $part->ContentType() =~ '^(text/plain$|message/)' ) && $part->Content() ) {
391 # We found no content. suck
401 If this transaction has attached mime objects, returns the first one's subject
402 Otherwise, returns null
408 if ( $self->Attachments->First ) {
409 return ( $self->Attachments->First->Subject );
418 # {{{ sub Attachments
422 Returns all the RT::Attachment objects which are attached
423 to this transaction. Takes an optional parameter, which is
424 a ContentType that Attachments should be restricted to.
431 unless ( $self->{'attachments'} ) {
432 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
434 #If it's a comment, return an empty object if they don't have the right to see it
435 if ( $self->Type eq 'Comment' ) {
436 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
437 return ( $self->{'attachments'} );
441 #if they ain't got rights to see, return an empty object
442 elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
443 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
444 return ( $self->{'attachments'} );
448 $self->{'attachments'}->Limit( FIELD => 'TransactionId',
449 VALUE => $self->Id );
451 # Get the self->{'attachments'} in the order they're put into
452 # the database. Arguably, we should be returning a tree
453 # of self->{'attachments'}, not a set...but no current app seems to need
456 $self->{'attachments'}->OrderBy( ALIAS => 'main',
461 return ( $self->{'attachments'} );
471 A private method used to attach a mime object to this transaction.
477 my $MIMEObject = shift;
479 if ( !defined($MIMEObject) ) {
481 "$self _Attach: We can't attach a mime object if you don't give us one.\n"
483 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
486 my $Attachment = new RT::Attachment( $self->CurrentUser );
487 my ($id, $msg) = $Attachment->Create(
488 TransactionId => $self->Id,
489 Attachment => $MIMEObject
491 return ( $Attachment, $msg || $self->loc("Attachment created") );
499 # {{{ Routines dealing with Transaction Attributes
501 # {{{ sub Description
505 Returns a text string which describes this transaction
513 #If it's a comment or a comment email record,
514 # we need to be extra special careful
516 if ( $self->__Value('Type') =~ /^Comment/ ) {
517 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
518 return ( $self->loc("Permission Denied") );
522 #if they ain't got rights to see, don't let em
523 elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
524 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
525 return ($self->loc("Permission Denied") );
529 if ( !defined( $self->Type ) ) {
530 return ( $self->loc("No transaction type specified"));
533 return ( $self->loc("[_1] by [_2]",$self->BriefDescription , $self->CreatorObj->Name ));
538 # {{{ sub BriefDescription
540 =head2 BriefDescription
542 Returns a text string which briefly describes this transaction
546 sub BriefDescription {
549 #If it's a comment or a comment email record,
550 # we need to be extra special careful
551 if ( $self->__Value('Type') =~ /^Comment/ ) {
552 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
553 return ( $self->loc("Permission Denied") );
557 #if they ain't got rights to see, don't let em
558 elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
559 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
560 return ( $self->loc("Permission Denied") );
564 my $type = $self->Type; #cache this, rather than calling it 30 times
566 if ( !defined($type) ) {
567 return $self->loc("No transaction type specified");
570 my $obj_type = $self->FriendlyObjectType;
572 if ( $type eq 'Create' ) {
573 return ( $self->loc( "[_1] created", $obj_type ) );
575 elsif ( $type =~ /Status/ ) {
576 if ( $self->Field eq 'Status' ) {
577 if ( $self->NewValue eq 'deleted' ) {
578 return ( $self->loc( "[_1] deleted", $obj_type ) );
583 "Status changed from [_1] to [_2]",
584 "'" . $self->loc( $self->OldValue ) . "'",
585 "'" . $self->loc( $self->NewValue ) . "'"
593 my $no_value = $self->loc("(no value)");
596 "[_1] changed from [_2] to [_3]",
598 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
599 "'" . $self->NewValue . "'"
604 if ( my $code = $_BriefDescriptions{$type} ) {
605 return $code->($self);
609 "Default: [_1]/[_2] changed from [_3] to [_4]",
614 ? "'" . $self->OldValue . "'"
615 : $self->loc("(no value)")
617 "'" . $self->NewValue . "'"
621 %_BriefDescriptions = (
622 CommentEmailRecord => sub {
624 return $self->loc("Outgoing email about a comment recorded");
628 return $self->loc("Outgoing email recorded");
632 return $self->loc("Correspondence added");
636 return $self->loc("Comments added");
640 my $field = $self->loc('CustomField');
642 if ( $self->Field ) {
643 my $cf = RT::CustomField->new( $self->CurrentUser );
644 $cf->Load( $self->Field );
645 $field = $cf->Name();
648 if ( $self->OldValue eq '' ) {
649 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
651 elsif ( $self->NewValue eq '' ) {
652 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
656 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
661 return $self->loc("Untaken");
665 return $self->loc("Taken");
669 my $Old = RT::User->new( $self->CurrentUser );
670 $Old->Load( $self->OldValue );
671 my $New = RT::User->new( $self->CurrentUser );
672 $New->Load( $self->NewValue );
674 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
678 my $Old = RT::User->new( $self->CurrentUser );
679 $Old->Load( $self->OldValue );
680 return $self->loc("Stolen from [_1]", $Old->Name);
684 my $New = RT::User->new( $self->CurrentUser );
685 $New->Load( $self->NewValue );
686 return $self->loc( "Given to [_1]", $New->Name );
690 my $principal = RT::Principal->new($self->CurrentUser);
691 $principal->Load($self->NewValue);
692 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
696 my $principal = RT::Principal->new($self->CurrentUser);
697 $principal->Load($self->OldValue);
698 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
702 return $self->loc( "Subject changed to [_1]", $self->Data );
707 if ( $self->NewValue ) {
708 my $URI = RT::URI->new( $self->CurrentUser );
709 $URI->FromURI( $self->NewValue );
710 if ( $URI->Resolver ) {
711 $value = $URI->Resolver->AsString;
714 $value = $self->NewValue;
716 if ( $self->Field eq 'DependsOn' ) {
717 return $self->loc( "Dependency on [_1] added", $value );
719 elsif ( $self->Field eq 'DependedOnBy' ) {
720 return $self->loc( "Dependency by [_1] added", $value );
723 elsif ( $self->Field eq 'RefersTo' ) {
724 return $self->loc( "Reference to [_1] added", $value );
726 elsif ( $self->Field eq 'ReferredToBy' ) {
727 return $self->loc( "Reference by [_1] added", $value );
729 elsif ( $self->Field eq 'MemberOf' ) {
730 return $self->loc( "Membership in [_1] added", $value );
732 elsif ( $self->Field eq 'HasMember' ) {
733 return $self->loc( "Member [_1] added", $value );
735 elsif ( $self->Field eq 'MergedInto' ) {
736 return $self->loc( "Merged into [_1]", $value );
740 return ( $self->Data );
746 if ( $self->OldValue ) {
747 my $URI = RT::URI->new( $self->CurrentUser );
748 $URI->FromURI( $self->OldValue );
749 if ( $URI->Resolver ) {
750 $value = $URI->Resolver->AsString;
753 $value = $self->OldValue;
756 if ( $self->Field eq 'DependsOn' ) {
757 return $self->loc( "Dependency on [_1] deleted", $value );
759 elsif ( $self->Field eq 'DependedOnBy' ) {
760 return $self->loc( "Dependency by [_1] deleted", $value );
763 elsif ( $self->Field eq 'RefersTo' ) {
764 return $self->loc( "Reference to [_1] deleted", $value );
766 elsif ( $self->Field eq 'ReferredToBy' ) {
767 return $self->loc( "Reference by [_1] deleted", $value );
769 elsif ( $self->Field eq 'MemberOf' ) {
770 return $self->loc( "Membership in [_1] deleted", $value );
772 elsif ( $self->Field eq 'HasMember' ) {
773 return $self->loc( "Member [_1] deleted", $value );
777 return ( $self->Data );
782 if ( $self->Field eq 'Password' ) {
783 return $self->loc('Password changed');
785 elsif ( $self->Field eq 'Queue' ) {
786 my $q1 = new RT::Queue( $self->CurrentUser );
787 $q1->Load( $self->OldValue );
788 my $q2 = new RT::Queue( $self->CurrentUser );
789 $q2->Load( $self->NewValue );
790 return $self->loc("[_1] changed from [_2] to [_3]", $self->Field , $q1->Name , $q2->Name);
793 # Write the date/time change at local time:
794 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
795 my $t1 = new RT::Date($self->CurrentUser);
796 $t1->Set(Format => 'ISO', Value => $self->NewValue);
797 my $t2 = new RT::Date($self->CurrentUser);
798 $t2->Set(Format => 'ISO', Value => $self->OldValue);
799 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $t2->AsString, $t1->AsString );
802 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
805 PurgeTransaction => sub {
807 return $self->loc("Transaction [_1] purged", $self->Data);
811 my $ticket = RT::Ticket->new($self->CurrentUser);
812 $ticket->Load($self->NewValue);
813 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
815 OpenReminder => sub {
817 my $ticket = RT::Ticket->new($self->CurrentUser);
818 $ticket->Load($self->NewValue);
819 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
822 ResolveReminder => sub {
824 my $ticket = RT::Ticket->new($self->CurrentUser);
825 $ticket->Load($self->NewValue);
826 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
834 # {{{ Utility methods
840 Returns true if the creator of the transaction is a requestor of the ticket.
841 Returns false otherwise
847 $self->ObjectType eq 'RT::Ticket' or return undef;
848 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
855 sub _OverlayAccessible {
858 ObjectType => { public => 1},
859 ObjectId => { public => 1},
872 return ( 0, $self->loc('Transactions are immutable') );
881 Takes the name of a table column.
882 Returns its value as a string, if the user passes an ACL check
891 #if the field is public, return it.
892 if ( $self->_Accessible( $field, 'public' ) ) {
893 return ( $self->__Value($field) );
897 #If it's a comment, we need to be extra special careful
898 if ( $self->__Value('Type') eq 'Comment' ) {
899 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
903 elsif ( $self->__Value('Type') eq 'CommentEmailRecord' ) {
904 unless ( $self->CurrentUserHasRight('ShowTicketComments')
905 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
910 elsif ( $self->__Value('Type') eq 'EmailRecord' ) {
911 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail')) {
916 # Make sure the user can see the custom field before showing that it changed
917 elsif ( ( $self->__Value('Type') eq 'CustomField' ) && $self->__Value('Field') ) {
918 my $cf = RT::CustomField->new( $self->CurrentUser );
919 $cf->Load( $self->__Value('Field') );
920 return (undef) unless ( $cf->CurrentUserHasRight('SeeCustomField') );
924 #if they ain't got rights to see, don't let em
925 elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
926 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
931 return ( $self->__Value($field) );
937 # {{{ sub CurrentUserHasRight
939 =head2 CurrentUserHasRight RIGHT
941 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
946 sub CurrentUserHasRight {
950 $self->CurrentUser->HasRight(
952 Object => $self->TicketObj
961 return $self->ObjectId;
966 return $self->Object;
971 if ( my $type = $self->__Value('ReferenceType')
972 and my $id = $self->__Value('OldReference') )
974 my $Object = $type->new($self->CurrentUser);
975 $Object->Load( $id );
976 return $Object->Content;
979 return $self->__Value('OldValue');
985 if ( my $type = $self->__Value('ReferenceType')
986 and my $id = $self->__Value('NewReference') )
988 my $Object = $type->new($self->CurrentUser);
989 $Object->Load( $id );
990 return $Object->Content;
993 return $self->__Value('NewValue');
999 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1000 $Object->Load($self->__Value('ObjectId'));
1004 sub FriendlyObjectType {
1006 my $type = $self->ObjectType or return undef;
1008 return $self->loc($type);
1011 =head2 UpdateCustomFields
1015 CustomField-<<Id>> => Value
1018 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1019 this transaction's custom fields
1023 sub UpdateCustomFields {
1027 # This method used to have an API that took a hash of a single
1028 # value "ARGSRef", which was a reference to a hash of arguments.
1029 # This was insane. The next few lines of code preserve that API
1030 # while giving us something saner.
1033 # TODO: 3.6: DEPRECATE OLD API
1037 if ($args{'ARGSRef'}) {
1038 $args = $args{ARGSRef};
1043 foreach my $arg ( keys %$args ) {
1046 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1047 next if $arg =~ /-Magic$/;
1049 my $values = $args->{$arg};
1051 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1053 next unless length($value);
1054 $self->_AddCustomFieldValue(
1057 RecordTransaction => 0,
1065 =head2 CustomFieldValues
1067 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1073 sub CustomFieldValues {
1077 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1079 unless ( $field =~ /^\d+$/o ) {
1080 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1081 $CFs->Limit( FIELD => 'Name', VALUE => $field);
1082 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1083 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1084 $field = $CFs->First->id if $CFs->First;
1087 return $self->SUPER::CustomFieldValues($field);
1092 # {{{ sub CustomFieldLookupType
1094 =head2 CustomFieldLookupType
1096 Returns the RT::Transaction lookup type, which can
1097 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1103 sub CustomFieldLookupType {
1104 "RT::Queue-RT::Ticket-RT::Transaction";
1107 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
1111 'fast_update_p' => 1,
1112 'cache_for_sec' => 6000,