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 if ( defined $args{'MIMEObj'} ) {
153 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
155 $RT::Logger->error("Couldn't add attachment: $msg");
156 return ( 0, $self->loc("Couldn't add attachment") );
161 #Provide a way to turn off scrips if we need to
162 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
163 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
164 $self->{'scrips'} = RT::Scrips->new($RT::SystemUser);
166 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
168 $self->{'scrips'}->Prepare(
169 Stage => 'TransactionCreate',
170 Type => $args{'Type'},
171 Ticket => $args{'ObjectId'},
172 Transaction => $self->id,
174 if ($args{'CommitScrips'} ) {
175 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
176 $self->{'scrips'}->Commit();
180 return ( $id, $self->loc("Transaction Created") );
187 Returns the Scrips object for this transaction.
188 This routine is only useful on a freshly created transaction object.
189 Scrips do not get persisted to the database with transactions.
197 return($self->{'scrips'});
205 Delete this transaction. Currently DOES NOT CHECK ACLS
213 $RT::Handle->BeginTransaction();
215 my $attachments = $self->Attachments;
217 while (my $attachment = $attachments->Next) {
218 my ($id, $msg) = $attachment->Delete();
220 $RT::Handle->Rollback();
221 return($id, $self->loc("System Error: [_1]", $msg));
224 my ($id,$msg) = $self->SUPER::Delete();
226 $RT::Handle->Rollback();
227 return($id, $self->loc("System Error: [_1]", $msg));
229 $RT::Handle->Commit();
235 # {{{ Routines dealing with Attachments
241 Returns the RT::Attachments Object which contains the "top-level"object
242 attachment for this transaction
250 if ( !defined( $self->{'message'} ) ) {
252 $self->{'message'} = new RT::Attachments( $self->CurrentUser );
253 $self->{'message'}->Limit(
254 FIELD => 'TransactionId',
258 $self->{'message'}->ChildrenOf(0);
260 return ( $self->{'message'} );
267 =head2 Content PARAMHASH
269 If this transaction has attached mime objects, returns the first text/plain part.
270 Otherwise, returns undef.
272 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
273 at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
287 my $content_obj = $self->ContentObj;
289 $content = $content_obj->Content;
292 # If all else fails, return a message that we couldn't find any content
294 $content = $self->loc('This transaction appears to have no content');
297 if ( $args{'Quote'} ) {
299 # Remove quoted signature.
300 $content =~ s/\n-- \n(.*?)$//s;
302 # What's the longest line like?
304 foreach ( split ( /\n/, $content ) ) {
305 $max = length if ( length > $max );
309 require Text::Wrapper;
310 my $wrapper = new Text::Wrapper(
311 columns => $args{'Wrap'},
312 body_start => ( $max > 70 * 3 ? ' ' : '' ),
315 $content = $wrapper->wrap($content);
319 . $self->CreatorObj->Name() . ' - '
320 . $self->CreatedAsString() . "]:\n\n" . $content . "\n\n";
321 $content =~ s/^/> /gm;
334 Returns the RT::Attachment object which contains the content for this Transaction
344 # If we don\'t have any content, return undef now.
345 unless ( $self->Attachments->First ) {
349 # Get the set of toplevel attachments to this transaction.
350 my $Attachment = $self->Attachments->First();
352 # If it's a message or a plain part, just return the
354 if ( $Attachment->ContentType() =~ '^(text/plain$|message/)' ) {
355 return ($Attachment);
358 # If it's a multipart object, first try returning the first
361 elsif ( $Attachment->ContentType() =~ '^multipart/' ) {
362 my $plain_parts = $Attachment->Children();
363 $plain_parts->ContentType( VALUE => 'text/plain' );
365 # If we actully found a part, return its content
366 if ( $plain_parts->First && $plain_parts->First->Content ne '' ) {
367 return ( $plain_parts->First );
370 # If that fails, return the first text/plain or message/ part
371 # which has some content.
374 my $all_parts = $self->Attachments;
375 while ( my $part = $all_parts->Next ) {
376 if (( $part->ContentType() =~ '^(text/plain$|message/)' ) && $part->Content() ) {
384 # We found no content. suck
394 If this transaction has attached mime objects, returns the first one's subject
395 Otherwise, returns null
401 if ( $self->Attachments->First ) {
402 return ( $self->Attachments->First->Subject );
411 # {{{ sub Attachments
415 Returns all the RT::Attachment objects which are attached
416 to this transaction. Takes an optional parameter, which is
417 a ContentType that Attachments should be restricted to.
424 unless ( $self->{'attachments'} ) {
425 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
427 #If it's a comment, return an empty object if they don't have the right to see it
428 if ( $self->Type eq 'Comment' ) {
429 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
430 return ( $self->{'attachments'} );
434 #if they ain't got rights to see, return an empty object
435 elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
436 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
437 return ( $self->{'attachments'} );
441 $self->{'attachments'}->Limit( FIELD => 'TransactionId',
442 VALUE => $self->Id );
444 # Get the self->{'attachments'} in the order they're put into
445 # the database. Arguably, we should be returning a tree
446 # of self->{'attachments'}, not a set...but no current app seems to need
449 $self->{'attachments'}->OrderBy( ALIAS => 'main',
454 return ( $self->{'attachments'} );
464 A private method used to attach a mime object to this transaction.
470 my $MIMEObject = shift;
472 if ( !defined($MIMEObject) ) {
474 "$self _Attach: We can't attach a mime object if you don't give us one.\n"
476 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
479 my $Attachment = new RT::Attachment( $self->CurrentUser );
480 my ($id, $msg) = $Attachment->Create(
481 TransactionId => $self->Id,
482 Attachment => $MIMEObject
484 return ( $Attachment, $msg || $self->loc("Attachment created") );
492 # {{{ Routines dealing with Transaction Attributes
494 # {{{ sub Description
498 Returns a text string which describes this transaction
506 #If it's a comment or a comment email record,
507 # we need to be extra special careful
509 if ( $self->__Value('Type') =~ /^Comment/ ) {
510 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
511 return ( $self->loc("Permission Denied") );
515 #if they ain't got rights to see, don't let em
516 elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
517 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
518 return ($self->loc("Permission Denied") );
522 if ( !defined( $self->Type ) ) {
523 return ( $self->loc("No transaction type specified"));
526 return ( $self->loc("[_1] by [_2]",$self->BriefDescription , $self->CreatorObj->Name ));
531 # {{{ sub BriefDescription
533 =head2 BriefDescription
535 Returns a text string which briefly describes this transaction
539 sub BriefDescription {
542 #If it's a comment or a comment email record,
543 # we need to be extra special careful
544 if ( $self->__Value('Type') =~ /^Comment/ ) {
545 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
546 return ( $self->loc("Permission Denied") );
550 #if they ain't got rights to see, don't let em
551 elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) {
552 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
553 return ( $self->loc("Permission Denied") );
557 my $type = $self->Type; #cache this, rather than calling it 30 times
559 if ( !defined($type) ) {
560 return $self->loc("No transaction type specified");
563 my $obj_type = $self->FriendlyObjectType;
565 if ( $type eq 'Create' ) {
566 return ( $self->loc( "[_1] created", $obj_type ) );
568 elsif ( $type =~ /Status/ ) {
569 if ( $self->Field eq 'Status' ) {
570 if ( $self->NewValue eq 'deleted' ) {
571 return ( $self->loc( "[_1] deleted", $obj_type ) );
576 "Status changed from [_1] to [_2]",
577 "'" . $self->loc( $self->OldValue ) . "'",
578 "'" . $self->loc( $self->NewValue ) . "'"
586 my $no_value = $self->loc("(no value)");
589 "[_1] changed from [_2] to [_3]",
591 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
592 "'" . $self->NewValue . "'"
597 if ( my $code = $_BriefDescriptions{$type} ) {
598 return $code->($self);
602 "Default: [_1]/[_2] changed from [_3] to [_4]",
607 ? "'" . $self->OldValue . "'"
608 : $self->loc("(no value)")
610 "'" . $self->NewValue . "'"
614 %_BriefDescriptions = (
615 CommentEmailRecord => sub {
617 return $self->loc("Outgoing email about a comment recorded");
621 return $self->loc("Outgoing email recorded");
625 return $self->loc("Correspondence added");
629 return $self->loc("Comments added");
633 my $field = $self->loc('CustomField');
635 if ( $self->Field ) {
636 my $cf = RT::CustomField->new( $self->CurrentUser );
637 $cf->Load( $self->Field );
638 $field = $cf->Name();
641 if ( $self->OldValue eq '' ) {
642 return ( $self->loc("[_1] [_2] added", $field, $self->NewValue) );
644 elsif ( $self->NewValue eq '' ) {
645 return ( $self->loc("[_1] [_2] deleted", $field, $self->OldValue) );
649 return $self->loc("[_1] [_2] changed to [_3]", $field, $self->OldValue, $self->NewValue );
654 return $self->loc("Untaken");
658 return $self->loc("Taken");
662 my $Old = RT::User->new( $self->CurrentUser );
663 $Old->Load( $self->OldValue );
664 my $New = RT::User->new( $self->CurrentUser );
665 $New->Load( $self->NewValue );
667 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
671 my $Old = RT::User->new( $self->CurrentUser );
672 $Old->Load( $self->OldValue );
673 return $self->loc("Stolen from [_1]", $Old->Name);
677 my $New = RT::User->new( $self->CurrentUser );
678 $New->Load( $self->NewValue );
679 return $self->loc( "Given to [_1]", $New->Name );
683 my $principal = RT::Principal->new($self->CurrentUser);
684 $principal->Load($self->NewValue);
685 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
689 my $principal = RT::Principal->new($self->CurrentUser);
690 $principal->Load($self->OldValue);
691 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
695 return $self->loc( "Subject changed to [_1]", $self->Data );
700 if ( $self->NewValue ) {
701 my $URI = RT::URI->new( $self->CurrentUser );
702 $URI->FromURI( $self->NewValue );
703 if ( $URI->Resolver ) {
704 $value = $URI->Resolver->AsString;
707 $value = $self->NewValue;
709 if ( $self->Field eq 'DependsOn' ) {
710 return $self->loc( "Dependency on [_1] added", $value );
712 elsif ( $self->Field eq 'DependedOnBy' ) {
713 return $self->loc( "Dependency by [_1] added", $value );
716 elsif ( $self->Field eq 'RefersTo' ) {
717 return $self->loc( "Reference to [_1] added", $value );
719 elsif ( $self->Field eq 'ReferredToBy' ) {
720 return $self->loc( "Reference by [_1] added", $value );
722 elsif ( $self->Field eq 'MemberOf' ) {
723 return $self->loc( "Membership in [_1] added", $value );
725 elsif ( $self->Field eq 'HasMember' ) {
726 return $self->loc( "Member [_1] added", $value );
728 elsif ( $self->Field eq 'MergedInto' ) {
729 return $self->loc( "Merged into [_1]", $value );
733 return ( $self->Data );
739 if ( $self->OldValue ) {
740 my $URI = RT::URI->new( $self->CurrentUser );
741 $URI->FromURI( $self->OldValue );
742 if ( $URI->Resolver ) {
743 $value = $URI->Resolver->AsString;
746 $value = $self->OldValue;
749 if ( $self->Field eq 'DependsOn' ) {
750 return $self->loc( "Dependency on [_1] deleted", $value );
752 elsif ( $self->Field eq 'DependedOnBy' ) {
753 return $self->loc( "Dependency by [_1] deleted", $value );
756 elsif ( $self->Field eq 'RefersTo' ) {
757 return $self->loc( "Reference to [_1] deleted", $value );
759 elsif ( $self->Field eq 'ReferredToBy' ) {
760 return $self->loc( "Reference by [_1] deleted", $value );
762 elsif ( $self->Field eq 'MemberOf' ) {
763 return $self->loc( "Membership in [_1] deleted", $value );
765 elsif ( $self->Field eq 'HasMember' ) {
766 return $self->loc( "Member [_1] deleted", $value );
770 return ( $self->Data );
775 if ( $self->Field eq 'Password' ) {
776 return $self->loc('Password changed');
778 elsif ( $self->Field eq 'Queue' ) {
779 my $q1 = new RT::Queue( $self->CurrentUser );
780 $q1->Load( $self->OldValue );
781 my $q2 = new RT::Queue( $self->CurrentUser );
782 $q2->Load( $self->NewValue );
783 return $self->loc("[_1] changed from [_2] to [_3]", $self->Field , $q1->Name , $q2->Name);
786 # Write the date/time change at local time:
787 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
788 my $t1 = new RT::Date($self->CurrentUser);
789 $t1->Set(Format => 'ISO', Value => $self->NewValue);
790 my $t2 = new RT::Date($self->CurrentUser);
791 $t2->Set(Format => 'ISO', Value => $self->OldValue);
792 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, $t2->AsString, $t1->AsString );
795 return $self->loc( "[_1] changed from [_2] to [_3]", $self->Field, ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
798 PurgeTransaction => sub {
800 return $self->loc("Transaction [_1] purged", $self->Data);
806 # {{{ Utility methods
812 Returns true if the creator of the transaction is a requestor of the ticket.
813 Returns false otherwise
819 $self->ObjectType eq 'RT::Ticket' or return undef;
820 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
827 sub _OverlayAccessible {
830 ObjectType => { public => 1},
831 ObjectId => { public => 1},
844 return ( 0, $self->loc('Transactions are immutable') );
853 Takes the name of a table column.
854 Returns its value as a string, if the user passes an ACL check
863 #if the field is public, return it.
864 if ( $self->_Accessible( $field, 'public' ) ) {
865 return ( $self->__Value($field) );
869 #If it's a comment, we need to be extra special careful
870 if ( $self->__Value('Type') eq 'Comment' ) {
871 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
875 elsif ( $self->__Value('Type') eq 'CommentEmailRecord' ) {
876 unless ( $self->CurrentUserHasRight('ShowTicketComments')
877 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
882 elsif ( $self->__Value('Type') eq 'EmailRecord' ) {
883 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail')) {
888 # Make sure the user can see the custom field before showing that it changed
889 elsif ( ( $self->__Value('Type') eq 'CustomField' ) && $self->__Value('Field') ) {
890 my $cf = RT::CustomField->new( $self->CurrentUser );
891 $cf->Load( $self->__Value('Field') );
892 return (undef) unless ( $cf->CurrentUserHasRight('SeeCustomField') );
896 #if they ain't got rights to see, don't let em
897 elsif ($self->__Value('ObjectType') eq "RT::Ticket") {
898 unless ( $self->CurrentUserHasRight('ShowTicket') ) {
903 return ( $self->__Value($field) );
909 # {{{ sub CurrentUserHasRight
911 =head2 CurrentUserHasRight RIGHT
913 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
918 sub CurrentUserHasRight {
922 $self->CurrentUser->HasRight(
924 Object => $self->TicketObj
933 return $self->ObjectId;
938 return $self->Object;
943 if ( my $type = $self->__Value('ReferenceType')
944 and my $id = $self->__Value('OldReference') )
946 my $Object = $type->new($self->CurrentUser);
947 $Object->Load( $id );
948 return $Object->Content;
951 return $self->__Value('OldValue');
957 if ( my $type = $self->__Value('ReferenceType')
958 and my $id = $self->__Value('NewReference') )
960 my $Object = $type->new($self->CurrentUser);
961 $Object->Load( $id );
962 return $Object->Content;
965 return $self->__Value('NewValue');
971 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
972 $Object->Load($self->__Value('ObjectId'));
976 sub FriendlyObjectType {
978 my $type = $self->ObjectType or return undef;
980 return $self->loc($type);
983 =head2 UpdateCustomFields
987 CustomField-<<Id>> => Value
990 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
991 this transaction's custom fields
995 sub UpdateCustomFields {
999 # This method used to have an API that took a hash of a single
1000 # value "ARGSRef", which was a reference to a hash of arguments.
1001 # This was insane. The next few lines of code preserve that API
1002 # while giving us something saner.
1005 # TODO: 3.6: DEPRECATE OLD API
1009 if ($args{'ARGSRef'}) {
1010 $args = $args{ARGSRef};
1015 foreach my $arg ( keys %$args ) {
1018 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1019 next if $arg =~ /-Magic$/;
1021 my $values = $args->{$arg};
1023 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1025 next unless length($value);
1026 $self->_AddCustomFieldValue(
1029 RecordTransaction => 0,
1037 =head2 CustomFieldValues
1039 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1045 sub CustomFieldValues {
1049 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1051 unless ( $field =~ /^\d+$/o ) {
1052 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1053 $CFs->Limit( FIELD => 'Name', VALUE => $field);
1054 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1055 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1056 $field = $CFs->First->id if $CFs->First;
1059 return $self->SUPER::CustomFieldValues($field);
1064 # {{{ sub CustomFieldLookupType
1066 =head2 CustomFieldLookupType
1068 Returns the RT::Transaction lookup type, which can
1069 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1075 sub CustomFieldLookupType {
1076 "RT::Queue-RT::Ticket-RT::Transaction";
1079 # Transactions don't change. by adding this cache congif directiove, we don't lose pathalogically on long tickets.
1083 'fast_update_p' => 1,
1084 'cache_for_sec' => 6000,