1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6 # <sales@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/licenses/old-licenses/gpl-2.0.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 }}}
51 RT::Transaction - RT\'s transaction object
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.
72 package RT::Transaction;
74 use base 'RT::Record';
79 use vars qw( %_BriefDescriptions $PreferredContentType );
86 use HTML::TreeBuilder;
89 sub Table {'Transactions'}
95 Create a new transaction.
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.
102 TODO: Document what gets passed to this
119 ObjectType => 'RT::Ticket',
121 ReferenceType => undef,
122 OldReference => undef,
123 NewReference => undef,
124 SquelchMailTo => undef,
129 $args{ObjectId} ||= $args{Ticket};
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"));
137 # Set up any custom fields passed at creation. Has to happen
140 $self->UpdateCustomFields(%{ $args{'CustomFields'} });
142 #lets create our transaction
144 Type => $args{'Type'},
145 Data => $args{'Data'},
146 Field => $args{'Field'},
147 OldValue => $args{'OldValue'},
148 NewValue => $args{'NewValue'},
149 Created => $args{'Created'},
150 ObjectType => $args{'ObjectType'},
151 ObjectId => $args{'ObjectId'},
152 ReferenceType => $args{'ReferenceType'},
153 OldReference => $args{'OldReference'},
154 NewReference => $args{'NewReference'},
157 # Parameters passed in during an import that we probably don't want to touch, otherwise
158 foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
159 $params{$attr} = $args{$attr} if ($args{$attr});
162 my $id = $self->SUPER::Create(%params);
164 if ( defined $args{'MIMEObj'} ) {
165 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
167 $RT::Logger->error("Couldn't add attachment: $msg");
168 return ( 0, $self->loc("Couldn't add attachment") );
173 Name => 'SquelchMailTo',
174 Content => RT::User->CanonicalizeEmailAddress($_)
175 ) for @{$args{'SquelchMailTo'} || []};
177 #Provide a way to turn off scrips if we need to
178 $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
179 if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
180 $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
182 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
184 $self->{'scrips'}->Prepare(
185 Stage => 'TransactionCreate',
186 Type => $args{'Type'},
187 Ticket => $args{'ObjectId'},
188 Transaction => $self->id,
191 # Entry point of the rule system
192 my $ticket = RT::Ticket->new(RT->SystemUser);
193 $ticket->Load($args{'ObjectId'});
194 my $txn = RT::Transaction->new($RT::SystemUser);
195 $txn->Load($self->id);
197 my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
198 Stage => 'TransactionCreate',
199 Type => $args{'Type'},
200 TicketObj => $ticket,
201 TransactionObj => $txn,
204 if ($args{'CommitScrips'} ) {
205 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
206 $self->{'scrips'}->Commit();
207 RT::Ruleset->CommitRules($rules);
211 return ( $id, $self->loc("Transaction Created") );
217 Returns the Scrips object for this transaction.
218 This routine is only useful on a freshly created transaction object.
219 Scrips do not get persisted to the database with transactions.
227 return($self->{'scrips'});
233 Returns the array of Rule objects for this transaction.
234 This routine is only useful on a freshly created transaction object.
235 Rules do not get persisted to the database with transactions.
243 return($self->{'rules'});
250 Delete this transaction. Currently DOES NOT CHECK ACLS
258 $RT::Handle->BeginTransaction();
260 my $attachments = $self->Attachments;
262 while (my $attachment = $attachments->Next) {
263 my ($id, $msg) = $attachment->Delete();
265 $RT::Handle->Rollback();
266 return($id, $self->loc("System Error: [_1]", $msg));
269 my ($id,$msg) = $self->SUPER::Delete();
271 $RT::Handle->Rollback();
272 return($id, $self->loc("System Error: [_1]", $msg));
274 $RT::Handle->Commit();
283 Returns the L<RT::Attachments> object which contains the "top-level" object
284 attachment for this transaction.
291 # XXX: Where is ACL check?
293 unless ( defined $self->{'message'} ) {
295 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
296 $self->{'message'}->Limit(
297 FIELD => 'TransactionId',
300 $self->{'message'}->ChildrenOf(0);
302 $self->{'message'}->GotoFirstItem;
304 return $self->{'message'};
309 =head2 Content PARAMHASH
311 If this transaction has attached mime objects, returns the body of the first
312 textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
315 Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
316 at $args{'Wrap'}. $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
318 If $args{'Type'} is set to C<text/html>, this will return an HTML
319 part of the message, if available. Otherwise it looks for a text/plain
320 part. If $args{'Type'} is missing, it defaults to the value of
321 C<$RT::Transaction::PreferredContentType>, if that's missing too,
329 Type => $PreferredContentType || '',
332 Wrap => ( $RT::MessageBoxWidth || 72 ) - 2,
337 if ( my $content_obj =
338 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
340 $content = $content_obj->Content ||'';
342 if ( lc $content_obj->ContentType eq 'text/html' ) {
343 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
345 if ($args{Type} ne 'text/html') {
346 my $tree = HTML::TreeBuilder->new_from_content( $content );
347 $content = HTML::FormatText->new(
355 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
356 if ($args{Type} eq 'text/html') {
357 # Extremely simple text->html converter
358 $content =~ s/&/&/g;
359 $content =~ s/</</g;
360 $content =~ s/>/>/g;
361 $content = "<pre>$content</pre>";
366 # If all else fails, return a message that we couldn't find any content
368 $content = $self->loc('This transaction appears to have no content');
371 if ( $args{'Quote'} ) {
373 # What's the longest line like?
375 foreach ( split ( /\n/, $content ) ) {
376 $max = length if length > $max;
379 if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
380 require Text::Wrapper;
381 my $wrapper = Text::Wrapper->new(
382 columns => $args{'Wrap'},
383 body_start => ( $max > 70 * 3 ? ' ' : '' ),
386 $content = $wrapper->wrap($content);
389 $content =~ s/^/> /gm;
390 $content = $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name)
401 Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
408 if (my $attach = $self->Attachments->First) {
409 return $attach->Addresses;
421 Returns the RT::Attachment object which contains the content for this Transaction
428 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
430 # If we don't have any content, return undef now.
431 # Get the set of toplevel attachments to this transaction.
433 my $Attachment = $args{'Attachment'};
435 $Attachment ||= $self->Attachments->First;
437 return undef unless ($Attachment);
439 # If it's a textual part, just return the body.
440 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
441 return ($Attachment);
444 # If it's a multipart object, first try returning the first part with preferred
445 # MIME type ('text/plain' by default).
447 elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
448 my $kids = $Attachment->Children;
449 while (my $child = $kids->Next) {
450 my $ret = $self->ContentObj(%args, Attachment => $child);
451 return $ret if ($ret);
454 elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
456 my $plain_parts = $Attachment->Children;
457 $plain_parts->ContentType( VALUE => $args{Type} );
458 $plain_parts->LimitNotEmpty;
460 # If we actully found a part, return its content
461 if ( my $first = $plain_parts->First ) {
466 # If that fails, return the first textual part which has some content.
467 my $all_parts = $self->Attachments;
468 while ( my $part = $all_parts->Next ) {
469 next unless RT::I18N::IsTextualContentType($part->ContentType)
475 # We found no content. suck
483 If this transaction has attached mime objects, returns the first one's subject
484 Otherwise, returns null
490 return undef unless my $first = $self->Attachments->First;
491 return $first->Subject;
498 Returns all the RT::Attachment objects which are attached
499 to this transaction. Takes an optional parameter, which is
500 a ContentType that Attachments should be restricted to.
507 if ( $self->{'attachments'} ) {
508 $self->{'attachments'}->GotoFirstItem;
509 return $self->{'attachments'};
512 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
514 unless ( $self->CurrentUserCanSee ) {
515 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
516 return $self->{'attachments'};
519 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
521 # Get the self->{'attachments'} in the order they're put into
522 # the database. Arguably, we should be returning a tree
523 # of self->{'attachments'}, not a set...but no current app seems to need
526 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
528 return $self->{'attachments'};
535 A private method used to attach a mime object to this transaction.
541 my $MIMEObject = shift;
543 unless ( defined $MIMEObject ) {
544 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
545 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
548 my $Attachment = RT::Attachment->new( $self->CurrentUser );
549 my ($id, $msg) = $Attachment->Create(
550 TransactionId => $self->Id,
551 Attachment => $MIMEObject
553 return ( $Attachment, $msg || $self->loc("Attachment created") );
561 # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
562 # since it has less information available without looking to it's parent
563 # transaction. Check ACLs here before we go any further.
564 return unless $self->CurrentUserCanSee;
566 my $attachments = RT::Attachments->new( $self->CurrentUser );
567 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
568 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
569 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
570 $attachments->RowsPerPage(1);
572 my $top = $attachments->First;
575 my $entity = MIME::Entity->build(
576 Type => 'message/rfc822',
577 Description => 'transaction ' . $self->id,
578 Data => $top->ContentAsMIME(Children => 1)->as_string,
588 Returns a text string which describes this transaction
595 unless ( $self->CurrentUserCanSee ) {
596 return ( $self->loc("Permission Denied") );
599 unless ( defined $self->Type ) {
600 return ( $self->loc("No transaction type specified"));
603 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
608 =head2 BriefDescription
610 Returns a text string which briefly describes this transaction
614 sub BriefDescription {
617 unless ( $self->CurrentUserCanSee ) {
618 return ( $self->loc("Permission Denied") );
621 my $type = $self->Type; #cache this, rather than calling it 30 times
623 unless ( defined $type ) {
624 return $self->loc("No transaction type specified");
627 my $obj_type = $self->FriendlyObjectType;
629 if ( $type eq 'Create' ) {
630 return ( $self->loc( "[_1] created", $obj_type ) );
632 elsif ( $type eq 'Enabled' ) {
633 return ( $self->loc( "[_1] enabled", $obj_type ) );
635 elsif ( $type eq 'Disabled' ) {
636 return ( $self->loc( "[_1] disabled", $obj_type ) );
638 elsif ( $type =~ /Status/ ) {
639 if ( $self->Field eq 'Status' ) {
640 if ( $self->NewValue eq 'deleted' ) {
641 return ( $self->loc( "[_1] deleted", $obj_type ) );
646 "Status changed from [_1] to [_2]",
647 "'" . $self->loc( $self->OldValue ) . "'",
648 "'" . $self->loc( $self->NewValue ) . "'"
656 my $no_value = $self->loc("(no value)");
659 "[_1] changed from [_2] to [_3]",
661 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
662 "'" . $self->NewValue . "'"
666 elsif ( $type =~ /SystemError/ ) {
667 return $self->loc("System error");
669 elsif ( $type =~ /Forward Transaction/ ) {
670 return $self->loc( "Forwarded Transaction #[_1] to [_2]",
671 $self->Field, $self->Data );
673 elsif ( $type =~ /Forward Ticket/ ) {
674 return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
677 if ( my $code = $_BriefDescriptions{$type} ) {
678 return $code->($self);
682 "Default: [_1]/[_2] changed from [_3] to [_4]",
687 ? "'" . $self->OldValue . "'"
688 : $self->loc("(no value)")
690 "'" . $self->NewValue . "'"
694 %_BriefDescriptions = (
695 CommentEmailRecord => sub {
697 return $self->loc("Outgoing email about a comment recorded");
701 return $self->loc("Outgoing email recorded");
705 return $self->loc("Correspondence added");
709 return $self->loc("Comments added");
713 my $field = $self->loc('CustomField');
715 if ( $self->Field ) {
716 my $cf = RT::CustomField->new( $self->CurrentUser );
717 $cf->SetContextObject( $self->Object );
718 $cf->Load( $self->Field );
719 $field = $cf->Name();
720 $field = $self->loc('a custom field') if !defined($field);
723 my $new = $self->NewValue;
724 my $old = $self->OldValue;
726 if ( !defined($old) || $old eq '' ) {
727 return $self->loc("[_1] [_2] added", $field, $new);
729 elsif ( !defined($new) || $new eq '' ) {
730 return $self->loc("[_1] [_2] deleted", $field, $old);
733 return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
738 return $self->loc("Untaken");
742 return $self->loc("Taken");
746 my $Old = RT::User->new( $self->CurrentUser );
747 $Old->Load( $self->OldValue );
748 my $New = RT::User->new( $self->CurrentUser );
749 $New->Load( $self->NewValue );
751 return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
755 my $Old = RT::User->new( $self->CurrentUser );
756 $Old->Load( $self->OldValue );
757 return $self->loc("Stolen from [_1]", $Old->Name);
761 my $New = RT::User->new( $self->CurrentUser );
762 $New->Load( $self->NewValue );
763 return $self->loc( "Given to [_1]", $New->Name );
767 my $principal = RT::Principal->new($self->CurrentUser);
768 $principal->Load($self->NewValue);
769 return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
773 my $principal = RT::Principal->new($self->CurrentUser);
774 $principal->Load($self->OldValue);
775 return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
779 return $self->loc( "Subject changed to [_1]", $self->Data );
784 if ( $self->NewValue ) {
785 my $URI = RT::URI->new( $self->CurrentUser );
786 $URI->FromURI( $self->NewValue );
787 if ( $URI->Resolver ) {
788 $value = $URI->Resolver->AsString;
791 $value = $self->NewValue;
793 if ( $self->Field eq 'DependsOn' ) {
794 return $self->loc( "Dependency on [_1] added", $value );
796 elsif ( $self->Field eq 'DependedOnBy' ) {
797 return $self->loc( "Dependency by [_1] added", $value );
800 elsif ( $self->Field eq 'RefersTo' ) {
801 return $self->loc( "Reference to [_1] added", $value );
803 elsif ( $self->Field eq 'ReferredToBy' ) {
804 return $self->loc( "Reference by [_1] added", $value );
806 elsif ( $self->Field eq 'MemberOf' ) {
807 return $self->loc( "Membership in [_1] added", $value );
809 elsif ( $self->Field eq 'HasMember' ) {
810 return $self->loc( "Member [_1] added", $value );
812 elsif ( $self->Field eq 'MergedInto' ) {
813 return $self->loc( "Merged into [_1]", $value );
817 return ( $self->Data );
823 if ( $self->OldValue ) {
824 my $URI = RT::URI->new( $self->CurrentUser );
825 $URI->FromURI( $self->OldValue );
826 if ( $URI->Resolver ) {
827 $value = $URI->Resolver->AsString;
830 $value = $self->OldValue;
833 if ( $self->Field eq 'DependsOn' ) {
834 return $self->loc( "Dependency on [_1] deleted", $value );
836 elsif ( $self->Field eq 'DependedOnBy' ) {
837 return $self->loc( "Dependency by [_1] deleted", $value );
840 elsif ( $self->Field eq 'RefersTo' ) {
841 return $self->loc( "Reference to [_1] deleted", $value );
843 elsif ( $self->Field eq 'ReferredToBy' ) {
844 return $self->loc( "Reference by [_1] deleted", $value );
846 elsif ( $self->Field eq 'MemberOf' ) {
847 return $self->loc( "Membership in [_1] deleted", $value );
849 elsif ( $self->Field eq 'HasMember' ) {
850 return $self->loc( "Member [_1] deleted", $value );
854 return ( $self->Data );
859 if ( $self->Field eq 'Told' ) {
860 my $t1 = RT::Date->new($self->CurrentUser);
861 $t1->Set(Format => 'ISO', Value => $self->NewValue);
862 my $t2 = RT::Date->new($self->CurrentUser);
863 $t2->Set(Format => 'ISO', Value => $self->OldValue);
864 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
867 return $self->loc( "[_1] changed from [_2] to [_3]",
868 $self->loc($self->Field),
869 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
874 if ( $self->Field eq 'Password' ) {
875 return $self->loc('Password changed');
877 elsif ( $self->Field eq 'Queue' ) {
878 my $q1 = RT::Queue->new( $self->CurrentUser );
879 $q1->Load( $self->OldValue );
880 my $q2 = RT::Queue->new( $self->CurrentUser );
881 $q2->Load( $self->NewValue );
882 return $self->loc("[_1] changed from [_2] to [_3]",
883 $self->loc($self->Field) , $q1->Name , $q2->Name);
886 # Write the date/time change at local time:
887 elsif ($self->Field =~ /Due|Starts|Started|Told|WillResolve/) {
888 my $t1 = RT::Date->new($self->CurrentUser);
889 $t1->Set(Format => 'ISO', Value => $self->NewValue);
890 my $t2 = RT::Date->new($self->CurrentUser);
891 $t2->Set(Format => 'ISO', Value => $self->OldValue);
892 return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
894 elsif ( $self->Field eq 'Owner' ) {
895 my $Old = RT::User->new( $self->CurrentUser );
896 $Old->Load( $self->OldValue );
897 my $New = RT::User->new( $self->CurrentUser );
898 $New->Load( $self->NewValue );
900 if ( $Old->id == RT->Nobody->id ) {
901 if ( $New->id == $self->Creator ) {
902 return $self->loc("Taken");
905 return $self->loc( "Given to [_1]", $New->Name );
909 if ( $New->id == $self->Creator ) {
910 return $self->loc("Stolen from [_1]", $Old->Name);
912 elsif ( $Old->id == $self->Creator ) {
913 if ( $New->id == RT->Nobody->id ) {
914 return $self->loc("Untaken");
917 return $self->loc( "Given to [_1]", $New->Name );
922 "Owner forcibly changed from [_1] to [_2]",
923 $Old->Name, $New->Name );
928 return $self->loc( "[_1] changed from [_2] to [_3]",
929 $self->loc($self->Field),
930 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
933 PurgeTransaction => sub {
935 return $self->loc("Transaction [_1] purged", $self->Data);
939 my $ticket = RT::Ticket->new($self->CurrentUser);
940 $ticket->Load($self->NewValue);
941 return $self->loc("Reminder '[_1]' added", $ticket->Subject);
943 OpenReminder => sub {
945 my $ticket = RT::Ticket->new($self->CurrentUser);
946 $ticket->Load($self->NewValue);
947 return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
950 ResolveReminder => sub {
952 my $ticket = RT::Ticket->new($self->CurrentUser);
953 $ticket->Load($self->NewValue);
954 return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
965 Returns true if the creator of the transaction is a requestor of the ticket.
966 Returns false otherwise
972 $self->ObjectType eq 'RT::Ticket' or return undef;
973 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
978 sub _OverlayAccessible {
981 ObjectType => { public => 1},
982 ObjectId => { public => 1},
992 return ( 0, $self->loc('Transactions are immutable') );
999 Takes the name of a table column.
1000 Returns its value as a string, if the user passes an ACL check
1008 #if the field is public, return it.
1009 if ( $self->_Accessible( $field, 'public' ) ) {
1010 return $self->SUPER::_Value( $field );
1013 unless ( $self->CurrentUserCanSee ) {
1017 return $self->SUPER::_Value( $field );
1022 =head2 CurrentUserHasRight RIGHT
1024 Calls $self->CurrentUser->HasQueueRight for the right passed in here.
1029 sub CurrentUserHasRight {
1032 return $self->CurrentUser->HasRight(
1034 Object => $self->Object
1038 =head2 CurrentUserCanSee
1040 Returns true if current user has rights to see this particular transaction.
1042 This fact depends on type of the transaction, type of an object the transaction
1043 is attached to and may be other conditions, so this method is prefered over
1044 custom implementations.
1048 sub CurrentUserCanSee {
1051 # If it's a comment, we need to be extra special careful
1052 my $type = $self->__Value('Type');
1053 if ( $type eq 'Comment' ) {
1054 unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
1058 elsif ( $type eq 'CommentEmailRecord' ) {
1059 unless ( $self->CurrentUserHasRight('ShowTicketComments')
1060 && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1064 elsif ( $type eq 'EmailRecord' ) {
1065 unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
1069 # Make sure the user can see the custom field before showing that it changed
1070 elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
1071 my $cf = RT::CustomField->new( $self->CurrentUser );
1072 $cf->SetContextObject( $self->Object );
1073 $cf->Load( $cf_id );
1074 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1076 # Defer to the object in question
1077 return $self->Object->CurrentUserCanSee("Transaction");
1083 return $self->ObjectId;
1088 return $self->Object;
1093 if ( my $type = $self->__Value('ReferenceType')
1094 and my $id = $self->__Value('OldReference') )
1096 my $Object = $type->new($self->CurrentUser);
1097 $Object->Load( $id );
1098 return $Object->Content;
1101 return $self->_Value('OldValue');
1107 if ( my $type = $self->__Value('ReferenceType')
1108 and my $id = $self->__Value('NewReference') )
1110 my $Object = $type->new($self->CurrentUser);
1111 $Object->Load( $id );
1112 return $Object->Content;
1115 return $self->_Value('NewValue');
1121 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1122 $Object->Load($self->__Value('ObjectId'));
1126 sub FriendlyObjectType {
1128 my $type = $self->ObjectType or return undef;
1130 return $self->loc($type);
1133 =head2 UpdateCustomFields
1137 CustomField-<<Id>> => Value
1140 Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
1141 this transaction's custom fields
1145 sub UpdateCustomFields {
1149 # This method used to have an API that took a hash of a single
1150 # value "ARGSRef", which was a reference to a hash of arguments.
1151 # This was insane. The next few lines of code preserve that API
1152 # while giving us something saner.
1154 # TODO: 3.6: DEPRECATE OLD API
1158 if ($args{'ARGSRef'}) {
1159 $args = $args{ARGSRef};
1164 foreach my $arg ( keys %$args ) {
1167 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
1168 next if $arg =~ /-Magic$/;
1169 next if $arg =~ /-TimeUnits$/;
1171 my $values = $args->{$arg};
1173 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1175 next unless (defined($value) && length($value));
1176 $self->_AddCustomFieldValue(
1179 RecordTransaction => 0,
1187 =head2 CustomFieldValues
1189 Do name => id mapping (if needed) before falling back to RT::Record's CustomFieldValues
1195 sub CustomFieldValues {
1199 if ( UNIVERSAL::can( $self->Object, 'QueueObj' ) ) {
1201 # XXX: $field could be undef when we want fetch values for all CFs
1202 # do we want to cover this situation somehow here?
1203 unless ( defined $field && $field =~ /^\d+$/o ) {
1204 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1205 $CFs->SetContextObject( $self->Object );
1206 $CFs->Limit( FIELD => 'Name', VALUE => $field );
1207 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1208 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1209 $field = $CFs->First->id if $CFs->First;
1212 return $self->SUPER::CustomFieldValues($field);
1217 =head2 CustomFieldLookupType
1219 Returns the RT::Transaction lookup type, which can
1220 be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1225 sub CustomFieldLookupType {
1226 "RT::Queue-RT::Ticket-RT::Transaction";
1230 =head2 SquelchMailTo
1232 Similar to Ticket class SquelchMailTo method - returns a list of
1233 transaction's squelched addresses. As transactions are immutable, the
1234 list of squelched recipients cannot be modified after creation.
1240 return () unless $self->CurrentUserCanSee;
1241 return $self->Attributes->Named('SquelchMailTo');
1246 Returns the list of email addresses (as L<Email::Address> objects)
1247 that this transaction would send mail to. There may be duplicates.
1254 foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1255 my $action = $scrip->ActionObj->Action;
1256 next unless $action->isa('RT::Action::SendEmail');
1258 foreach my $type (qw(To Cc Bcc)) {
1259 push @recipients, $action->$type();
1263 if ( $self->Rules ) {
1264 for my $rule (@{$self->Rules}) {
1265 next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1266 my $data = $rule->{hints}{recipients};
1267 foreach my $type (qw(To Cc Bcc)) {
1268 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1275 =head2 DeferredRecipients($freq, $include_sent )
1277 Takes the following arguments:
1281 =item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1283 =item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1287 Returns an array of users who should now receive the notification that
1288 was recorded in this transaction. Returns an empty array if there were
1289 no deferred users, or if $include_sent was not specified and the deferred
1290 notifications have been sent.
1294 sub DeferredRecipients {
1297 my $include_sent = @_? shift : 0;
1299 my $attr = $self->FirstAttribute('DeferredRecipients');
1301 return () unless ($attr);
1303 my $deferred = $attr->Content;
1305 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1309 for my $user (keys %{$deferred->{$freq}}) {
1310 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1311 delete $deferred->{$freq}->{$user}
1314 # Now get our users. Easy.
1316 return keys %{ $deferred->{$freq} };
1321 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1325 'fast_update_p' => 1,
1326 'cache_for_sec' => 6000,
1331 =head2 ACLEquivalenceObjects
1333 This method returns a list of objects for which a user's rights also apply
1334 to this Transaction.
1336 This currently only applies to Transaction Custom Fields on Tickets, so we return
1337 the Ticket's Queue and the Ticket.
1339 This method is called from L<RT::Principal/HasRight>.
1343 sub ACLEquivalenceObjects {
1346 return unless $self->ObjectType eq 'RT::Ticket';
1347 my $object = $self->Object;
1348 return $object,$object->QueueObj;
1358 Returns the current value of id.
1359 (In the database, id is stored as int(11).)
1367 Returns the current value of ObjectType.
1368 (In the database, ObjectType is stored as varchar(64).)
1372 =head2 SetObjectType VALUE
1375 Set ObjectType to VALUE.
1376 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1377 (In the database, ObjectType will be stored as a varchar(64).)
1385 Returns the current value of ObjectId.
1386 (In the database, ObjectId is stored as int(11).)
1390 =head2 SetObjectId VALUE
1393 Set ObjectId to VALUE.
1394 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1395 (In the database, ObjectId will be stored as a int(11).)
1403 Returns the current value of TimeTaken.
1404 (In the database, TimeTaken is stored as int(11).)
1408 =head2 SetTimeTaken VALUE
1411 Set TimeTaken to VALUE.
1412 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1413 (In the database, TimeTaken will be stored as a int(11).)
1421 Returns the current value of Type.
1422 (In the database, Type is stored as varchar(20).)
1426 =head2 SetType VALUE
1430 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1431 (In the database, Type will be stored as a varchar(20).)
1439 Returns the current value of Field.
1440 (In the database, Field is stored as varchar(40).)
1444 =head2 SetField VALUE
1448 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1449 (In the database, Field will be stored as a varchar(40).)
1457 Returns the current value of OldValue.
1458 (In the database, OldValue is stored as varchar(255).)
1462 =head2 SetOldValue VALUE
1465 Set OldValue to VALUE.
1466 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1467 (In the database, OldValue will be stored as a varchar(255).)
1475 Returns the current value of NewValue.
1476 (In the database, NewValue is stored as varchar(255).)
1480 =head2 SetNewValue VALUE
1483 Set NewValue to VALUE.
1484 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1485 (In the database, NewValue will be stored as a varchar(255).)
1491 =head2 ReferenceType
1493 Returns the current value of ReferenceType.
1494 (In the database, ReferenceType is stored as varchar(255).)
1498 =head2 SetReferenceType VALUE
1501 Set ReferenceType to VALUE.
1502 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1503 (In the database, ReferenceType will be stored as a varchar(255).)
1511 Returns the current value of OldReference.
1512 (In the database, OldReference is stored as int(11).)
1516 =head2 SetOldReference VALUE
1519 Set OldReference to VALUE.
1520 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1521 (In the database, OldReference will be stored as a int(11).)
1529 Returns the current value of NewReference.
1530 (In the database, NewReference is stored as int(11).)
1534 =head2 SetNewReference VALUE
1537 Set NewReference to VALUE.
1538 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1539 (In the database, NewReference will be stored as a int(11).)
1547 Returns the current value of Data.
1548 (In the database, Data is stored as varchar(255).)
1552 =head2 SetData VALUE
1556 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1557 (In the database, Data will be stored as a varchar(255).)
1565 Returns the current value of Creator.
1566 (In the database, Creator is stored as int(11).)
1574 Returns the current value of Created.
1575 (In the database, Created is stored as datetime.)
1582 sub _CoreAccessible {
1586 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1588 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
1590 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1592 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1594 {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''},
1596 {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', default => ''},
1598 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1600 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1602 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1604 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1606 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1608 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1610 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1612 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1617 RT::Base->_ImportOverlays();