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