1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2005 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., 675 Mass Ave, Cambridge, MA 02139, USA.
28 # CONTRIBUTION SUBMISSION POLICY:
30 # (The following paragraph is not intended to limit the rights granted
31 # to you to modify and distribute this software under the terms of
32 # the GNU General Public License and is only of importance to you if
33 # you choose to contribute your changes and enhancements to the
34 # community by submitting them to Best Practical Solutions, LLC.)
36 # By intentionally submitting any modifications, corrections or
37 # derivatives to this work, or any other work intended for use with
38 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
39 # you are the copyright holder for those contributions and you grant
40 # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable,
41 # royalty-free, perpetual, license to use, copy, create derivative
42 # works based on those contributions, and sublicense and distribute
43 # those contributions and any derivatives thereof.
45 # END BPS TAGGED BLOCK }}}
49 RT::Transaction - RT\'s transaction object
59 Each RT::Transaction describes an atomic change to a ticket object
60 or an update to an RT::Ticket object.
61 It can have arbitrary MIME attachments.
68 ok(require RT::Transaction);
75 package RT::Transaction;
78 no warnings qw(redefine);
80 use vars qw( %_BriefDescriptions );
89 Create a new transaction.
91 This routine should _never_ be called by anything other than RT::Ticket.
92 It should not be called
93 from client code. Ever. Not ever. If you do this, we will hunt you down and break your kneecaps.
94 Then the unpleasant stuff will start.
96 TODO: Document what gets passed to this
113 ObjectType => 'RT::Ticket',
115 ReferenceType => undef,
116 OldReference => undef,
117 NewReference => undef,
121 $args{ObjectId} ||= $args{Ticket};
123 #if we didn't specify a ticket, we need to bail
124 unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
125 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
130 #lets create our transaction
132 Type => $args{'Type'},
133 Data => $args{'Data'},
134 Field => $args{'Field'},
135 OldValue => $args{'OldValue'},
136 NewValue => $args{'NewValue'},
137 Created => $args{'Created'},
138 ObjectType => $args{'ObjectType'},
139 ObjectId => $args{'ObjectId'},
140 ReferenceType => $args{'ReferenceType'},
141 OldReference => $args{'OldReference'},
142 NewReference => $args{'NewReference'},
145 # Parameters passed in during an import that we probably don't want to touch, otherwise
146 foreach my $attr qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy) {
147 $params{$attr} = $args{$attr} if ($args{$attr});
150 my $id = $self->SUPER::Create(%params);
152 $self->_Attach( $args{'MIMEObj'} ) if defined $args{'MIMEObj'};
155 #Provide a way to turn off scrips if we need to
156 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
157 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
158 $self->{'scrips'} = RT::Scrips->new($RT::SystemUser);
160 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
162 $self->{'scrips'}->Prepare(
163 Stage => 'TransactionCreate',
164 Type => $args{'Type'},
165 Ticket => $args{'ObjectId'},
166 Transaction => $self->id,
168 if ($args{'CommitScrips'} ) {
169 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
170 $self->{'scrips'}->Commit();
174 return ( $id, $self->loc("Transaction Created") );
181 Returns the Scrips object for this transaction.
182 This routine is only useful on a freshly created transaction object.
183 Scrips do not get persisted to the database with transactions.
191 return($self->{'scrips'});
199 Delete this transaction. Currently DOES NOT CHECK ACLS
207 $RT::Handle->BeginTransaction();
209 my $attachments = $self->Attachments;
211 while (my $attachment = $attachments->Next) {
212 my ($id, $msg) = $attachment->Delete();
214 $RT::Handle->Rollback();
215 return($id, $self->loc("System Error: [_1]", $msg));
218 my ($id,$msg) = $self->SUPER::Delete();
220 $RT::Handle->Rollback();
221 return($id, $self->loc("System Error: [_1]", $msg));
223 $RT::Handle->Commit();
229 # {{{ Routines dealing with Attachments
235 Returns the RT::Attachments Object which contains the "top-level"object
236 attachment for this transaction
244 if ( !defined( $self->{'message'} ) ) {
246 $self->{'message'} = new RT::Attachments( $self->CurrentUser );
247 $self->{'message'}->Limit(
248 FIELD => 'TransactionId',
252 $self->{'message'}->ChildrenOf(0);
254 return ( $self->{'message'} );
261 =head2 Content PARAMHASH
263 If this transaction has attached mime objects, returns the first text/plain part.
264 Otherwise, returns undef.
266 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
267 at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
281 my $content_obj = $self->ContentObj;
283 $content = $content_obj->Content;
286 # If all else fails, return a message that we couldn't find any content
288 $content = $self->loc('This transaction appears to have no content');
291 if ( $args{'Quote'} ) {
293 # Remove quoted signature.
294 $content =~ s/\n-- \n(.*?)$//s;
296 # What's the longest line like?
298 foreach ( split ( /\n/, $content ) ) {
299 $max = length if ( length > $max );
303 require Text::Wrapper;
304 my $wrapper = new Text::Wrapper(
305 columns => $args{'Wrap'},
306 body_start => ( $max > 70 * 3 ? ' ' : '' ),
309 $content = $wrapper->wrap($content);
313 . $self->CreatorObj->Name() . ' - '
314 . $self->CreatedAsString() . "]:\n\n" . $content . "\n\n";
315 $content =~ s/^/> /gm;
328 Returns the RT::Attachment object which contains the content for this Transaction
338 # If we don\'t have any content, return undef now.
339 unless ( $self->Attachments->First ) {
343 # Get the set of toplevel attachments to this transaction.
344 my $Attachment = $self->Attachments->First();
346 # If it's a message or a plain part, just return the
348 if ( $Attachment->ContentType() =~ '^(text/plain$|message/)' ) {
349 return ($Attachment);
352 # If it's a multipart object, first try returning the first
355 elsif ( $Attachment->ContentType() =~ '^multipart/' ) {
356 my $plain_parts = $Attachment->Children();
357 $plain_parts->ContentType( VALUE => 'text/plain' );
359 # If we actully found a part, return its content
360 if ( $plain_parts->First && $plain_parts->First->Content ne '' ) {
361 return ( $plain_parts->First );
364 # If that fails, return the first text/plain or message/ part
365 # which has some content.
368 my $all_parts = $self->Attachments;
369 while ( my $part = $all_parts->Next ) {
370 if (( $part->ContentType() =~ '^(text/plain$|message/)' ) && $part->Content() ) {
378 # We found no content. suck
388 If this transaction has attached mime objects, returns the first one's subject
389 Otherwise, returns null
395 if ( $self->Attachments->First ) {
396 return ( $self->Attachments->First->Subject );
405 # {{{ sub Attachments
409 Returns all the RT::Attachment objects which are attached
410 to this transaction. Takes an optional parameter, which is
411 a ContentType that Attachments should be restricted to.
418 unless ( $self->{'attachments'} ) {
419 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
421 #If it's a comment, return an empty object if they don't have the right to see it
422 if ( $self->Type eq 'Comment' ) {
423 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
424 return ( $self->{'attachments'} );
428 #if they ain't got rights to see, return an empty object
429 elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
430 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
431 return ( $self->{'attachments'} );
435 $self->{'attachments'}->Limit( FIELD => 'TransactionId',
436 VALUE => $self->Id );
438 # Get the self->{'attachments'} in the order they're put into
439 # the database. Arguably, we should be returning a tree
440 # of self->{'attachments'}, not a set...but no current app seems to need
443 $self->{'attachments'}->OrderBy( ALIAS => 'main',
448 return ( $self->{'attachments'} );
458 A private method used to attach a mime object to this transaction.
464 my $MIMEObject = shift;
466 if ( !defined($MIMEObject) ) {
468 "$self _Attach: We can't attach a mime object if you don't give us one.\n"
470 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
473 my $Attachment = new RT::Attachment( $self->CurrentUser );
475 TransactionId => $self->Id,
476 Attachment => $MIMEObject
478 return ( $Attachment, $self->loc("Attachment created") );
486 # {{{ Routines dealing with Transaction Attributes
488 # {{{ sub Description
492 Returns a text string which describes this transaction
500 #If it's a comment or a comment email record,
501 # we need to be extra special careful
503 if ( $self->__Value('Type') =~ /^Comment/ ) {
504 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
505 return ( $self->loc("Permission Denied") );
509 #if they ain't got rights to see, don't let em
510 elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
511 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
512 return ($self->loc("Permission Denied") );
516 if ( !defined( $self->Type ) ) {
517 return ( $self->loc("No transaction type specified"));
520 return ( $self->loc("[_1] by [_2]",$self->BriefDescription , $self->CreatorObj->Name ));
525 # {{{ sub BriefDescription
527 =head2 BriefDescription
529 Returns a text string which briefly describes this transaction
533 sub BriefDescription {
536 #If it's a comment or a comment email record,
537 # we need to be extra special careful
538 if ( $self->__Value('Type') =~ /^Comment/ ) {
539 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
540 return ( $self->loc("Permission Denied") );
544 #if they ain't got rights to see, don't let em
545 elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
546 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
547 return ( $self->loc("Permission Denied") );
551 my $type = $self->Type; #cache this, rather than calling it 30 times
553 if ( !defined($type) ) {
554 return $self->loc("No transaction type specified");
557 my $obj_type = $self->FriendlyObjectType;
559 if ( $type eq 'Create' ) {
560 return ( $self->loc( "[_1] created", $obj_type ) );
562 elsif ( $type =~ /Status/ ) {
563 if ( $self->Field eq 'Status' ) {
564 if ( $self->NewValue eq 'deleted' ) {
565 return ( $self->loc( "[_1] deleted", $obj_type ) );
570 "Status changed from [_1] to [_2]",
571 "'" . $self->loc( $self->OldValue ) . "'",
572 "'" . $self->loc( $self->NewValue ) . "'"
580 my $no_value = $self->loc("(no value)");
583 "[_1] changed from [_2] to [_3]",
585 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
586 "'" . $self->NewValue . "'"
591 if ( my $code = $_BriefDescriptions{$type} ) {
592 return $code->($self);
596 "Default: [_1]/[_2] changed from [_3] to [_4]",
601 ? "'" . $self->OldValue . "'"
602 : $self->loc("(no value)")
604 "'" . $self->NewValue . "'"
608 %_BriefDescriptions = (
609 CommentEmailRecord => sub {
611 return $self->loc("Outgoing email about a comment recorded");
615 return $self->loc("Outgoing email recorded");
619 return $self->loc("Correspondence added");
623 return $self->loc("Comments added");
627 my $field = $self->loc('CustomField');
629 if ( $self->Field ) {
630 my $cf = RT::CustomField->new( $self->CurrentUser );
631 $cf->Load( $self->Field );
632 $field = $cf->Name();
635 if ( $self->OldValue eq '' ) {
636 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
638 elsif ( $self->NewValue eq '' ) {
639 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
643 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
648 return $self->loc("Untaken");
652 return $self->loc("Taken");
656 my $Old = RT::User->new( $self->CurrentUser );
657 $Old->Load( $self->OldValue );
658 my $New = RT::User->new( $self->CurrentUser );
659 $New->Load( $self->NewValue );
661 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
665 my $Old = RT::User->new( $self->CurrentUser );
666 $Old->Load( $self->OldValue );
667 return $self->loc("Stolen from [_1]", $Old->Name);
671 my $New = RT::User->new( $self->CurrentUser );
672 $New->Load( $self->NewValue );
673 return $self->loc( "Given to [_1]", $New->Name );
677 my $principal = RT::Principal->new($self->CurrentUser);
678 $principal->Load($self->NewValue);
679 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
683 my $principal = RT::Principal->new($self->CurrentUser);
684 $principal->Load($self->OldValue);
685 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
689 return $self->loc( "Subject changed to [_1]", $self->Data );
694 if ( $self->NewValue ) {
695 my $URI = RT::URI->new( $self->CurrentUser );
696 $URI->FromURI( $self->NewValue );
697 if ( $URI->Resolver ) {
698 $value = $URI->Resolver->AsString;
701 $value = $self->NewValue;
703 if ( $self->Field eq 'DependsOn' ) {
704 return $self->loc( "Dependency on [_1] added", $value );
706 elsif ( $self->Field eq 'DependedOnBy' ) {
707 return $self->loc( "Dependency by [_1] added", $value );
710 elsif ( $self->Field eq 'RefersTo' ) {
711 return $self->loc( "Reference to [_1] added", $value );
713 elsif ( $self->Field eq 'ReferredToBy' ) {
714 return $self->loc( "Reference by [_1] added", $value );
716 elsif ( $self->Field eq 'MemberOf' ) {
717 return $self->loc( "Membership in [_1] added", $value );
719 elsif ( $self->Field eq 'HasMember' ) {
720 return $self->loc( "Member [_1] added", $value );
722 elsif ( $self->Field eq 'MergedInto' ) {
723 return $self->loc( "Merged into [_1]", $value );
727 return ( $self->Data );
733 if ( $self->OldValue ) {
734 my $URI = RT::URI->new( $self->CurrentUser );
735 $URI->FromURI( $self->OldValue );
736 if ( $URI->Resolver ) {
737 $value = $URI->Resolver->AsString;
740 $value = $self->OldValue;
743 if ( $self->Field eq 'DependsOn' ) {
744 return $self->loc( "Dependency on [_1] deleted", $value );
746 elsif ( $self->Field eq 'DependedOnBy' ) {
747 return $self->loc( "Dependency by [_1] deleted", $value );
750 elsif ( $self->Field eq 'RefersTo' ) {
751 return $self->loc( "Reference to [_1] deleted", $value );
753 elsif ( $self->Field eq 'ReferredToBy' ) {
754 return $self->loc( "Reference by [_1] deleted", $value );
756 elsif ( $self->Field eq 'MemberOf' ) {
757 return $self->loc( "Membership in [_1] deleted", $value );
759 elsif ( $self->Field eq 'HasMember' ) {
760 return $self->loc( "Member [_1] deleted", $value );
764 return ( $self->Data );
769 if ( $self->Field eq 'Password' ) {
770 return $self->loc('Password changed');
772 elsif ( $self->Field eq 'Queue' ) {
773 my $q1 = new RT::Queue( $self->CurrentUser );
774 $q1->Load( $self->OldValue );
775 my $q2 = new RT::Queue( $self->CurrentUser );
776 $q2->Load( $self->NewValue );
777 return $self->loc("[_1] changed from [_2] to [_3]", $self->Field , $q1->Name , $q2->Name);
780 # Write the date/time change at local time:
781 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
782 my $t1 = new RT::Date($self->CurrentUser);
783 $t1->Set(Format => 'ISO', Value => $self->NewValue);
784 my $t2 = new RT::Date($self->CurrentUser);
785 $t2->Set(Format => 'ISO', Value => $self->OldValue);
786 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $t2->AsString, $t1->AsString );
789 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
792 PurgeTransaction => sub {
794 return $self->loc("Transaction [_1] purged", $self->Data);
800 # {{{ Utility methods
806 Returns true if the creator of the transaction is a requestor of the ticket.
807 Returns false otherwise
813 $self->ObjectType eq 'RT::Ticket' or return undef;
814 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
821 sub _OverlayAccessible {
824 ObjectType => { public => 1},
825 ObjectId => { public => 1},
838 return ( 0, $self->loc('Transactions are immutable') );
847 Takes the name of a table column.
848 Returns its value as a string, if the user passes an ACL check
857 #if the field is public, return it.
858 if ( $self->_Accessible( $field, 'public' ) ) {
859 return ( $self->__Value($field) );
863 #If it's a comment, we need to be extra special careful
864 if ( $self->__Value('Type') eq 'Comment' ) {
865 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
869 elsif ( $self->__Value('Type') eq 'CommentEmailRecord' ) {
870 unless ( $self->CurrentUserHasRight('ShowTicketComments')
871 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
876 elsif ( $self->__Value('Type') eq 'EmailRecord' ) {
877 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail')) {
882 # Make sure the user can see the custom field before showing that it changed
883 elsif ( ( $self->__Value('Type') eq 'CustomField' ) && $self->__Value('Field') ) {
884 my $cf = RT::CustomField->new( $self->CurrentUser );
885 $cf->Load( $self->__Value('Field') );
886 return (undef) unless ( $cf->CurrentUserHasRight('SeeCustomField') );
890 #if they ain't got rights to see, don't let em
891 elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
892 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
897 return ( $self->__Value($field) );
903 # {{{ sub CurrentUserHasRight
905 =head2 CurrentUserHasRight RIGHT
907 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
912 sub CurrentUserHasRight {
916 $self->CurrentUser->HasRight(
918 Object => $self->TicketObj
927 return $self->ObjectId;
932 return $self->Object;
937 if (my $type = $self->__Value('ReferenceType')) {
938 my $Object = $type->new($self->CurrentUser);
939 $Object->Load($self->__Value('OldReference'));
940 return $Object->Content;
943 return $self->__Value('OldValue');
949 if (my $type = $self->__Value('ReferenceType')) {
950 my $Object = $type->new($self->CurrentUser);
951 $Object->Load($self->__Value('NewReference'));
952 return $Object->Content;
955 return $self->__Value('NewValue');
961 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
962 $Object->Load($self->__Value('ObjectId'));
966 sub FriendlyObjectType {
968 my $type = $self->ObjectType or return undef;
970 return $self->loc($type);
973 =head2 UpdateCustomFields
977 CustomField-<<Id>> => Value
980 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
981 this transaction's custom fields
985 sub UpdateCustomFields {
989 # This method used to have an API that took a hash of a single
990 # value "ARGSRef", which was a reference to a hash of arguments.
991 # This was insane. The next few lines of code preserve that API
992 # while giving us something saner.
995 # TODO: 3.6: DEPRECATE OLD API
999 if ($args{'ARGSRef'}) {
1000 $args = $args{ARGSRef};
1005 foreach my $arg ( keys %$args ) {
1008 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1009 next if $arg =~ /-Magic$/;
1011 my $values = $args->{$arg};
1013 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1015 next unless length($value);
1016 $self->_AddCustomFieldValue(
1019 RecordTransaction => 0,
1027 =head2 CustomFieldValues
1029 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1035 sub CustomFieldValues {
1039 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1041 unless ( $field =~ /^\d+$/o ) {
1042 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1043 $CFs->Limit( FIELD => 'Name', VALUE => $field);
1044 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1045 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1046 $field = $CFs->First->id if $CFs->First;
1049 return $self->SUPER::CustomFieldValues($field);
1054 # {{{ sub CustomFieldLookupType
1056 =head2 CustomFieldLookupType
1058 Returns the RT::Transaction lookup type, which can
1059 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1065 sub CustomFieldLookupType {
1066 "RT::Queue-RT::Ticket-RT::Transaction";
1069 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
1073 'fast_update_p' => 1,
1074 'cache_for_sec' => 6000,