Putting 4.2.0 on top of 4.0.17
[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
af59614d
MKG
85use HTML::FormatText::WithLinks::AndTables;
86use HTML::Scrubber;
84fb5b46 87
af59614d
MKG
88# For EscapeHTML() and decode_entities()
89require RT::Interface::Web;
90require HTML::Entities;
84fb5b46
MKG
91
92sub Table {'Transactions'}
93
94# {{{ sub Create
95
96=head2 Create
97
98Create a new transaction.
99
100This routine should _never_ be called by anything other than RT::Ticket.
101It should not be called
102from client code. Ever. Not ever. If you do this, we will hunt you down and break your kneecaps.
103Then the unpleasant stuff will start.
104
105TODO: Document what gets passed to this
106
107=cut
108
109sub Create {
110 my $self = shift;
111 my %args = (
112 id => undef,
113 TimeTaken => 0,
114 Type => 'undefined',
115 Data => '',
116 Field => undef,
117 OldValue => undef,
118 NewValue => undef,
119 MIMEObj => undef,
120 ActivateScrips => 1,
121 CommitScrips => 1,
122 ObjectType => 'RT::Ticket',
123 ObjectId => 0,
124 ReferenceType => undef,
125 OldReference => undef,
126 NewReference => undef,
127 SquelchMailTo => undef,
128 @_
129 );
130
131 $args{ObjectId} ||= $args{Ticket};
132
133 #if we didn't specify a ticket, we need to bail
134 unless ( $args{'ObjectId'} && $args{'ObjectType'}) {
135 return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id"));
136 }
137
138
139
140 #lets create our transaction
141 my %params = (
142 Type => $args{'Type'},
143 Data => $args{'Data'},
144 Field => $args{'Field'},
145 OldValue => $args{'OldValue'},
146 NewValue => $args{'NewValue'},
147 Created => $args{'Created'},
af59614d
MKG
148 ObjectType => $args{'ObjectType'},
149 ObjectId => $args{'ObjectId'},
150 ReferenceType => $args{'ReferenceType'},
151 OldReference => $args{'OldReference'},
152 NewReference => $args{'NewReference'},
84fb5b46
MKG
153 );
154
155 # Parameters passed in during an import that we probably don't want to touch, otherwise
156 foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) {
157 $params{$attr} = $args{$attr} if ($args{$attr});
158 }
159
160 my $id = $self->SUPER::Create(%params);
161 $self->Load($id);
162 if ( defined $args{'MIMEObj'} ) {
163 my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} );
164 unless ( $id ) {
165 $RT::Logger->error("Couldn't add attachment: $msg");
166 return ( 0, $self->loc("Couldn't add attachment") );
167 }
168 }
169
170 $self->AddAttribute(
171 Name => 'SquelchMailTo',
172 Content => RT::User->CanonicalizeEmailAddress($_)
173 ) for @{$args{'SquelchMailTo'} || []};
174
af59614d 175 my @return = ( $id, $self->loc("Transaction Created") );
84fb5b46 176
af59614d 177 return @return unless $args{'ObjectType'} eq 'RT::Ticket';
84fb5b46 178
af59614d
MKG
179 # Provide a way to turn off scrips if we need to
180 unless ( $args{'ActivateScrips'} ) {
181 $RT::Logger->debug('Skipping scrips for transaction #' .$self->Id);
182 return @return;
183 }
84fb5b46 184
af59614d
MKG
185 $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
186
187 $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id);
188
189 $self->{'scrips'}->Prepare(
190 Stage => 'TransactionCreate',
191 Type => $args{'Type'},
192 Ticket => $args{'ObjectId'},
193 Transaction => $self->id,
194 );
195
196 # Entry point of the rule system
197 my $ticket = RT::Ticket->new(RT->SystemUser);
198 $ticket->Load($args{'ObjectId'});
199 my $txn = RT::Transaction->new($RT::SystemUser);
200 $txn->Load($self->id);
201
202 my $rules = $self->{rules} = RT::Ruleset->FindAllRules(
203 Stage => 'TransactionCreate',
204 Type => $args{'Type'},
205 TicketObj => $ticket,
206 TransactionObj => $txn,
207 );
208
209 if ($args{'CommitScrips'} ) {
210 $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id);
211 $self->{'scrips'}->Commit();
212 RT::Ruleset->CommitRules($rules);
84fb5b46
MKG
213 }
214
af59614d 215 return @return;
84fb5b46
MKG
216}
217
218
219=head2 Scrips
220
221Returns the Scrips object for this transaction.
222This routine is only useful on a freshly created transaction object.
223Scrips do not get persisted to the database with transactions.
224
225
226=cut
227
228
229sub Scrips {
230 my $self = shift;
231 return($self->{'scrips'});
232}
233
234
235=head2 Rules
236
237Returns the array of Rule objects for this transaction.
238This routine is only useful on a freshly created transaction object.
239Rules do not get persisted to the database with transactions.
240
241
242=cut
243
244
245sub Rules {
246 my $self = shift;
247 return($self->{'rules'});
248}
249
250
251
252=head2 Delete
253
254Delete this transaction. Currently DOES NOT CHECK ACLS
255
256=cut
257
258sub Delete {
259 my $self = shift;
260
261
262 $RT::Handle->BeginTransaction();
263
264 my $attachments = $self->Attachments;
265
266 while (my $attachment = $attachments->Next) {
267 my ($id, $msg) = $attachment->Delete();
268 unless ($id) {
269 $RT::Handle->Rollback();
270 return($id, $self->loc("System Error: [_1]", $msg));
271 }
272 }
273 my ($id,$msg) = $self->SUPER::Delete();
274 unless ($id) {
275 $RT::Handle->Rollback();
276 return($id, $self->loc("System Error: [_1]", $msg));
277 }
278 $RT::Handle->Commit();
279 return ($id,$msg);
280}
281
282
283
284
285=head2 Message
286
287Returns the L<RT::Attachments> object which contains the "top-level" object
288attachment for this transaction.
289
290=cut
291
292sub Message {
293 my $self = shift;
294
295 # XXX: Where is ACL check?
296
297 unless ( defined $self->{'message'} ) {
298
299 $self->{'message'} = RT::Attachments->new( $self->CurrentUser );
300 $self->{'message'}->Limit(
301 FIELD => 'TransactionId',
302 VALUE => $self->Id
303 );
304 $self->{'message'}->ChildrenOf(0);
305 } else {
306 $self->{'message'}->GotoFirstItem;
307 }
308 return $self->{'message'};
309}
310
311
312
313=head2 Content PARAMHASH
314
315If this transaction has attached mime objects, returns the body of the first
316textual part (as defined in RT::I18N::IsTextualContentType). Otherwise,
317returns undef.
318
319Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message
320at $args{'Wrap'}. $args{'Wrap'} defaults to 70.
321
322If $args{'Type'} is set to C<text/html>, this will return an HTML
323part of the message, if available. Otherwise it looks for a text/plain
324part. If $args{'Type'} is missing, it defaults to the value of
325C<$RT::Transaction::PreferredContentType>, if that's missing too,
326defaults to textual.
327
328=cut
329
330sub Content {
331 my $self = shift;
332 my %args = (
333 Type => $PreferredContentType || '',
334 Quote => 0,
335 Wrap => 70,
336 @_
337 );
338
339 my $content;
340 if ( my $content_obj =
341 $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
342 {
343 $content = $content_obj->Content ||'';
344
345 if ( lc $content_obj->ContentType eq 'text/html' ) {
346 $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
347
348 if ($args{Type} ne 'text/html') {
af59614d 349 $content = RT::Interface::Email::ConvertHTMLToText($content);
84fb5b46
MKG
350 }
351 }
352 else {
353 $content =~ s/\n-- \n.*?$//s if $args{'Quote'};
354 if ($args{Type} eq 'text/html') {
355 # Extremely simple text->html converter
356 $content =~ s/&/&#38;/g;
357 $content =~ s/</&lt;/g;
358 $content =~ s/>/&gt;/g;
359 $content = "<pre>$content</pre>";
360 }
361 }
362 }
363
364 # If all else fails, return a message that we couldn't find any content
365 else {
366 $content = $self->loc('This transaction appears to have no content');
367 }
368
369 if ( $args{'Quote'} ) {
01e3b242
MKG
370 $content = $self->ApplyQuoteWrap(content => $content,
371 cols => $args{'Wrap'} );
84fb5b46 372
c36a7e1d 373 $content = $self->QuoteHeader . "\n$content\n\n";
84fb5b46
MKG
374 }
375
376 return ($content);
377}
378
c36a7e1d
MKG
379=head2 QuoteHeader
380
381Returns text prepended to content when transaction is quoted
382(see C<Quote> argument in L</Content>). By default returns
383localized "On <date> <user name> wrote:\n".
384
385=cut
386
387sub QuoteHeader {
388 my $self = shift;
389 return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
390}
84fb5b46 391
01e3b242
MKG
392=head2 ApplyQuoteWrap PARAMHASH
393
394Wrapper to calculate wrap criteria and apply quote wrapping if needed.
395
396=cut
397
398sub ApplyQuoteWrap {
399 my $self = shift;
400 my %args = @_;
401 my $content = $args{content};
402
403 # What's the longest line like?
404 my $max = 0;
405 foreach ( split ( /\n/, $args{content} ) ) {
406 $max = length if length > $max;
407 }
408
409 if ( $max > 76 ) {
410 require Text::Quoted;
411 require Text::Wrapper;
412
413 my $structure = Text::Quoted::extract($args{content});
414 $content = $self->QuoteWrap(content_ref => $structure,
415 cols => $args{cols},
416 max => $max );
417 }
418
419 $content =~ s/^/> /gm; # use regex since string might be multi-line
420 return $content;
421}
422
423=head2 QuoteWrap PARAMHASH
424
425Wrap the contents of transactions based on Wrap settings, maintaining
426the quote character from the original.
427
428=cut
429
430sub QuoteWrap {
431 my $self = shift;
432 my %args = @_;
433 my $ref = $args{content_ref};
434 my $final_string;
435
436 if ( ref $ref eq 'ARRAY' ){
437 foreach my $array (@$ref){
438 $final_string .= $self->QuoteWrap(content_ref => $array,
439 cols => $args{cols},
440 max => $args{max} );
441 }
442 }
443 elsif ( ref $ref eq 'HASH' ){
444 return $ref->{quoter} . "\n" if $ref->{empty}; # Blank line
445
446 my $col = $args{cols} - (length $ref->{quoter});
447 my $wrapper = Text::Wrapper->new( columns => $col );
448
449 # Wrap on individual lines to honor incoming line breaks
450 # Otherwise deliberate separate lines (like a list or a sig)
451 # all get combined incorrectly into single paragraphs.
452
453 my @lines = split /\n/, $ref->{text};
454 my $wrap = join '', map { $wrapper->wrap($_) } @lines;
455 my $quoter = $ref->{quoter};
456
457 # Only add the space if actually quoting
458 $quoter .= ' ' if length $quoter;
459 $wrap =~ s/^/$quoter/mg; # use regex since string might be multi-line
460
461 return $wrap;
462 }
463 else{
464 $RT::Logger->warning("Can't apply quoting with $ref");
465 return;
466 }
467 return $final_string;
468}
469
84fb5b46
MKG
470
471=head2 Addresses
472
473Returns a hashref of addresses related to this transaction. See L<RT::Attachment/Addresses> for details.
474
475=cut
476
477sub Addresses {
af59614d 478 my $self = shift;
84fb5b46 479
af59614d
MKG
480 if (my $attach = $self->Attachments->First) {
481 return $attach->Addresses;
482 }
483 else {
484 return {};
485 }
84fb5b46
MKG
486
487}
488
489
490
491=head2 ContentObj
492
493Returns the RT::Attachment object which contains the content for this Transaction
494
495=cut
496
497
498sub ContentObj {
499 my $self = shift;
500 my %args = ( Type => $PreferredContentType, Attachment => undef, @_ );
501
502 # If we don't have any content, return undef now.
503 # Get the set of toplevel attachments to this transaction.
504
505 my $Attachment = $args{'Attachment'};
506
507 $Attachment ||= $self->Attachments->First;
508
509 return undef unless ($Attachment);
510
511 # If it's a textual part, just return the body.
512 if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
513 return ($Attachment);
514 }
515
516 # If it's a multipart object, first try returning the first part with preferred
517 # MIME type ('text/plain' by default).
518
519 elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
520 my $kids = $Attachment->Children;
521 while (my $child = $kids->Next) {
522 my $ret = $self->ContentObj(%args, Attachment => $child);
523 return $ret if ($ret);
524 }
525 }
526 elsif ( $Attachment->ContentType =~ m|^multipart/|i ) {
527 if ( $args{Type} ) {
528 my $plain_parts = $Attachment->Children;
529 $plain_parts->ContentType( VALUE => $args{Type} );
530 $plain_parts->LimitNotEmpty;
531
532 # If we actully found a part, return its content
533 if ( my $first = $plain_parts->First ) {
534 return $first;
535 }
536 }
537
538 # If that fails, return the first textual part which has some content.
539 my $all_parts = $self->Attachments;
540 while ( my $part = $all_parts->Next ) {
541 next unless RT::I18N::IsTextualContentType($part->ContentType)
542 && $part->Content;
543 return $part;
544 }
545 }
546
547 # We found no content. suck
548 return (undef);
549}
550
551
552
553=head2 Subject
554
555If this transaction has attached mime objects, returns the first one's subject
556Otherwise, returns null
557
558=cut
559
560sub Subject {
561 my $self = shift;
562 return undef unless my $first = $self->Attachments->First;
563 return $first->Subject;
564}
565
566
567
568=head2 Attachments
569
570Returns all the RT::Attachment objects which are attached
571to this transaction. Takes an optional parameter, which is
572a ContentType that Attachments should be restricted to.
573
574=cut
575
576sub Attachments {
577 my $self = shift;
578
579 if ( $self->{'attachments'} ) {
580 $self->{'attachments'}->GotoFirstItem;
581 return $self->{'attachments'};
582 }
583
584 $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser );
585
586 unless ( $self->CurrentUserCanSee ) {
587 $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl');
588 return $self->{'attachments'};
589 }
590
591 $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id );
592
593 # Get the self->{'attachments'} in the order they're put into
594 # the database. Arguably, we should be returning a tree
595 # of self->{'attachments'}, not a set...but no current app seems to need
596 # it.
597
598 $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' );
599
600 return $self->{'attachments'};
601}
602
603
604
605=head2 _Attach
606
607A private method used to attach a mime object to this transaction.
608
609=cut
610
611sub _Attach {
612 my $self = shift;
613 my $MIMEObject = shift;
614
615 unless ( defined $MIMEObject ) {
616 $RT::Logger->error("We can't attach a mime object if you don't give us one.");
617 return ( 0, $self->loc("[_1]: no attachment specified", $self) );
618 }
619
620 my $Attachment = RT::Attachment->new( $self->CurrentUser );
621 my ($id, $msg) = $Attachment->Create(
622 TransactionId => $self->Id,
623 Attachment => $MIMEObject
624 );
625 return ( $Attachment, $msg || $self->loc("Attachment created") );
626}
627
628
629
630sub ContentAsMIME {
631 my $self = shift;
632
633 # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does
634 # since it has less information available without looking to it's parent
635 # transaction. Check ACLs here before we go any further.
636 return unless $self->CurrentUserCanSee;
637
638 my $attachments = RT::Attachments->new( $self->CurrentUser );
639 $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' );
640 $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id );
641 $attachments->Limit( FIELD => 'Parent', VALUE => 0 );
642 $attachments->RowsPerPage(1);
643
644 my $top = $attachments->First;
645 return unless $top;
646
647 my $entity = MIME::Entity->build(
648 Type => 'message/rfc822',
649 Description => 'transaction ' . $self->id,
650 Data => $top->ContentAsMIME(Children => 1)->as_string,
651 );
652
653 return $entity;
654}
655
656
657
658=head2 Description
659
660Returns a text string which describes this transaction
661
662=cut
663
664sub Description {
665 my $self = shift;
666
667 unless ( $self->CurrentUserCanSee ) {
668 return ( $self->loc("Permission Denied") );
669 }
670
671 unless ( defined $self->Type ) {
672 return ( $self->loc("No transaction type specified"));
673 }
674
675 return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name );
676}
677
678
679
680=head2 BriefDescription
681
682Returns a text string which briefly describes this transaction
683
684=cut
685
af59614d
MKG
686{
687 my $scrubber = HTML::Scrubber->new(default => 0); # deny everything
688
689 sub BriefDescription {
690 my $self = shift;
691 my $desc = $self->BriefDescriptionAsHTML;
692 $desc = $scrubber->scrub($desc);
693 $desc = HTML::Entities::decode_entities($desc);
694 return $desc;
695 }
696}
697
698=head2 BriefDescriptionAsHTML
699
700Returns an HTML string which briefly describes this transaction.
701
702=cut
703
704sub BriefDescriptionAsHTML {
84fb5b46
MKG
705 my $self = shift;
706
707 unless ( $self->CurrentUserCanSee ) {
708 return ( $self->loc("Permission Denied") );
709 }
710
af59614d 711 my ($objecttype, $type, $field) = ($self->ObjectType, $self->Type, $self->Field);
84fb5b46
MKG
712
713 unless ( defined $type ) {
714 return $self->loc("No transaction type specified");
715 }
716
af59614d
MKG
717 my ($template, @params);
718
719 my @code = grep { ref eq 'CODE' } map { $_BriefDescriptions{$_} }
720 ( $field
721 ? ("$objecttype-$type-$field", "$type-$field")
722 : () ),
723 "$objecttype-$type", $type;
84fb5b46 724
af59614d
MKG
725 if (@code) {
726 ($template, @params) = $code[0]->($self);
84fb5b46 727 }
af59614d
MKG
728
729 unless ($template) {
730 ($template, @params) = (
731 "Default: [_1]/[_2] changed from [_3] to [_4]", #loc
732 $type,
733 $field,
734 (
735 $self->OldValue
736 ? "'" . $self->OldValue . "'"
737 : $self->loc("(no value)")
738 ),
739 (
740 $self->NewValue
741 ? "'" . $self->NewValue . "'"
742 : $self->loc("(no value)")
743 ),
744 );
84fb5b46 745 }
af59614d
MKG
746 return $self->loc($template, $self->_ProcessReturnValues(@params));
747}
748
749sub _ProcessReturnValues {
750 my $self = shift;
751 my @values = @_;
752 return map {
753 if (ref eq 'ARRAY') { $_ = join "", $self->_ProcessReturnValues(@$_) }
754 elsif (ref eq 'SCALAR') { $_ = $$_ }
755 else { RT::Interface::Web::EscapeHTML(\$_) }
756 $_
757 } @values;
758}
759
760sub _FormatPrincipal {
761 my $self = shift;
762 my $principal = shift;
763 if ($principal->IsUser) {
764 return $self->_FormatUser( $principal->Object );
765 } else {
766 return $self->loc("group [_1]", $principal->Object->Name);
84fb5b46 767 }
af59614d
MKG
768}
769
770sub _FormatUser {
771 my $self = shift;
772 my $user = shift;
773 return [
774 \'<span class="user" data-replace="user" data-user-id="', $user->id, \'">',
775 $user->Format,
776 \'</span>'
777 ];
778}
779
780%_BriefDescriptions = (
781 Create => sub {
782 my $self = shift;
783 return ( "[_1] created", $self->FriendlyObjectType ); #loc
784 },
785 Enabled => sub {
786 my $self = shift;
787 return ( "[_1] enabled", $self->FriendlyObjectType ); #loc
788 },
789 Disabled => sub {
790 my $self = shift;
791 return ( "[_1] disabled", $self->FriendlyObjectType ); #loc
792 },
793 Status => sub {
794 my $self = shift;
84fb5b46
MKG
795 if ( $self->Field eq 'Status' ) {
796 if ( $self->NewValue eq 'deleted' ) {
af59614d 797 return ( "[_1] deleted", $self->FriendlyObjectType ); #loc
84fb5b46
MKG
798 }
799 else {
af59614d
MKG
800 my $canon = $self->Object->DOES("RT::Record::Role::Status")
801 ? sub { $self->Object->LifecycleObj->CanonicalCase(@_) }
5b0d0914 802 : sub { return $_[0] };
84fb5b46 803 return (
af59614d
MKG
804 "Status changed from [_1] to [_2]", #loc
805 "'" . $self->loc( $canon->($self->OldValue) ) . "'",
806 "'" . $self->loc( $canon->($self->NewValue) ) . "'"
84fb5b46 807 );
84fb5b46
MKG
808 }
809 }
810
811 # Generic:
812 my $no_value = $self->loc("(no value)");
813 return (
af59614d
MKG
814 "[_1] changed from [_2] to [_3]", #loc
815 $self->Field,
816 ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
817 "'" . $self->NewValue . "'"
84fb5b46 818 );
af59614d
MKG
819 },
820 SystemError => sub {
821 my $self = shift;
822 return ("System error"); #loc
823 },
824 "Forward Transaction" => sub {
825 my $self = shift;
826 my $recipients = join ", ", map {
827 RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
828 } RT::EmailParser->ParseEmailAddress($self->Data);
84fb5b46 829
af59614d
MKG
830 return ( "Forwarded [_3]Transaction #[_1][_4] to [_2]", #loc
831 $self->Field, $recipients,
832 [\'<a href="#txn-', $self->Field, \'">'], \'</a>');
833 },
834 "Forward Ticket" => sub {
835 my $self = shift;
836 my $recipients = join ", ", map {
837 RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
838 } RT::EmailParser->ParseEmailAddress($self->Data);
84fb5b46 839
af59614d
MKG
840 return ( "Forwarded Ticket to [_1]", $recipients ); #loc
841 },
84fb5b46
MKG
842 CommentEmailRecord => sub {
843 my $self = shift;
af59614d 844 return ("Outgoing email about a comment recorded"); #loc
84fb5b46
MKG
845 },
846 EmailRecord => sub {
847 my $self = shift;
af59614d 848 return ("Outgoing email recorded"); #loc
84fb5b46
MKG
849 },
850 Correspond => sub {
851 my $self = shift;
af59614d 852 return ("Correspondence added"); #loc
84fb5b46
MKG
853 },
854 Comment => sub {
855 my $self = shift;
af59614d 856 return ("Comments added"); #loc
84fb5b46
MKG
857 },
858 CustomField => sub {
859 my $self = shift;
860 my $field = $self->loc('CustomField');
861
af59614d 862 my $cf;
84fb5b46 863 if ( $self->Field ) {
af59614d 864 $cf = RT::CustomField->new( $self->CurrentUser );
84fb5b46
MKG
865 $cf->SetContextObject( $self->Object );
866 $cf->Load( $self->Field );
867 $field = $cf->Name();
868 $field = $self->loc('a custom field') if !defined($field);
869 }
870
871 my $new = $self->NewValue;
872 my $old = $self->OldValue;
873
af59614d
MKG
874 if ( $cf ) {
875
876 if ( $cf->Type eq 'DateTime' ) {
877 if ($old) {
878 my $date = RT::Date->new( $self->CurrentUser );
879 $date->Set( Format => 'ISO', Value => $old );
880 $old = $date->AsString;
881 }
882
883 if ($new) {
884 my $date = RT::Date->new( $self->CurrentUser );
885 $date->Set( Format => 'ISO', Value => $new );
886 $new = $date->AsString;
887 }
888 }
889 elsif ( $cf->Type eq 'Date' ) {
890 if ($old) {
891 my $date = RT::Date->new( $self->CurrentUser );
892 $date->Set(
893 Format => 'unknown',
894 Value => $old,
895 Timezone => 'UTC',
896 );
897 $old = $date->AsString( Time => 0, Timezone => 'UTC' );
898 }
899
900 if ($new) {
901 my $date = RT::Date->new( $self->CurrentUser );
902 $date->Set(
903 Format => 'unknown',
904 Value => $new,
905 Timezone => 'UTC',
906 );
907 $new = $date->AsString( Time => 0, Timezone => 'UTC' );
908 }
909 }
910 }
911
84fb5b46 912 if ( !defined($old) || $old eq '' ) {
af59614d 913 return ("[_1] [_2] added", $field, $new); #loc
84fb5b46
MKG
914 }
915 elsif ( !defined($new) || $new eq '' ) {
af59614d 916 return ("[_1] [_2] deleted", $field, $old); #loc
84fb5b46
MKG
917 }
918 else {
af59614d 919 return ("[_1] [_2] changed to [_3]", $field, $old, $new); #loc
84fb5b46
MKG
920 }
921 },
922 Untake => sub {
923 my $self = shift;
af59614d 924 return ("Untaken"); #loc
84fb5b46
MKG
925 },
926 Take => sub {
927 my $self = shift;
af59614d 928 return ("Taken"); #loc
84fb5b46
MKG
929 },
930 Force => sub {
931 my $self = shift;
932 my $Old = RT::User->new( $self->CurrentUser );
933 $Old->Load( $self->OldValue );
934 my $New = RT::User->new( $self->CurrentUser );
935 $New->Load( $self->NewValue );
936
af59614d
MKG
937 return ("Owner forcibly changed from [_1] to [_2]", #loc
938 map { $self->_FormatUser($_) } $Old, $New);
84fb5b46
MKG
939 },
940 Steal => sub {
941 my $self = shift;
942 my $Old = RT::User->new( $self->CurrentUser );
943 $Old->Load( $self->OldValue );
af59614d 944 return ("Stolen from [_1]", $self->_FormatUser($Old)); #loc
84fb5b46
MKG
945 },
946 Give => sub {
947 my $self = shift;
948 my $New = RT::User->new( $self->CurrentUser );
949 $New->Load( $self->NewValue );
af59614d 950 return ( "Given to [_1]", $self->_FormatUser($New)); #loc
84fb5b46
MKG
951 },
952 AddWatcher => sub {
953 my $self = shift;
954 my $principal = RT::Principal->new($self->CurrentUser);
955 $principal->Load($self->NewValue);
af59614d 956 return ( "[_1] [_2] added", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc
84fb5b46
MKG
957 },
958 DelWatcher => sub {
959 my $self = shift;
960 my $principal = RT::Principal->new($self->CurrentUser);
961 $principal->Load($self->OldValue);
af59614d
MKG
962 return ( "[_1] [_2] deleted", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc
963 },
964 SetWatcher => sub {
965 my $self = shift;
966 my $principal = RT::Principal->new($self->CurrentUser);
967 $principal->Load($self->NewValue);
968 return ( "[_1] set to [_2]", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc
84fb5b46
MKG
969 },
970 Subject => sub {
971 my $self = shift;
af59614d 972 return ( "Subject changed to [_1]", $self->Data ); #loc
84fb5b46
MKG
973 },
974 AddLink => sub {
975 my $self = shift;
976 my $value;
977 if ( $self->NewValue ) {
978 my $URI = RT::URI->new( $self->CurrentUser );
403d7b0b 979 if ( $URI->FromURI( $self->NewValue ) ) {
af59614d
MKG
980 $value = [
981 \'<a href="', $URI->AsHREF, \'">',
982 $URI->AsString,
983 \'</a>'
984 ];
84fb5b46
MKG
985 }
986 else {
987 $value = $self->NewValue;
988 }
af59614d 989
84fb5b46 990 if ( $self->Field eq 'DependsOn' ) {
af59614d 991 return ( "Dependency on [_1] added", $value ); #loc
84fb5b46
MKG
992 }
993 elsif ( $self->Field eq 'DependedOnBy' ) {
af59614d 994 return ( "Dependency by [_1] added", $value ); #loc
84fb5b46
MKG
995 }
996 elsif ( $self->Field eq 'RefersTo' ) {
af59614d 997 return ( "Reference to [_1] added", $value ); #loc
84fb5b46
MKG
998 }
999 elsif ( $self->Field eq 'ReferredToBy' ) {
af59614d 1000 return ( "Reference by [_1] added", $value ); #loc
84fb5b46
MKG
1001 }
1002 elsif ( $self->Field eq 'MemberOf' ) {
af59614d 1003 return ( "Membership in [_1] added", $value ); #loc
84fb5b46
MKG
1004 }
1005 elsif ( $self->Field eq 'HasMember' ) {
af59614d 1006 return ( "Member [_1] added", $value ); #loc
84fb5b46
MKG
1007 }
1008 elsif ( $self->Field eq 'MergedInto' ) {
af59614d 1009 return ( "Merged into [_1]", $value ); #loc
84fb5b46
MKG
1010 }
1011 }
1012 else {
af59614d 1013 return ( "[_1]", $self->Data ); #loc
84fb5b46
MKG
1014 }
1015 },
1016 DeleteLink => sub {
1017 my $self = shift;
1018 my $value;
1019 if ( $self->OldValue ) {
1020 my $URI = RT::URI->new( $self->CurrentUser );
af59614d
MKG
1021 if ( $URI->FromURI( $self->OldValue ) ) {
1022 $value = [
1023 \'<a href="', $URI->AsHREF, \'">',
1024 $URI->AsString,
1025 \'</a>'
1026 ];
84fb5b46
MKG
1027 }
1028 else {
1029 $value = $self->OldValue;
1030 }
1031
1032 if ( $self->Field eq 'DependsOn' ) {
af59614d 1033 return ( "Dependency on [_1] deleted", $value ); #loc
84fb5b46
MKG
1034 }
1035 elsif ( $self->Field eq 'DependedOnBy' ) {
af59614d 1036 return ( "Dependency by [_1] deleted", $value ); #loc
84fb5b46
MKG
1037 }
1038 elsif ( $self->Field eq 'RefersTo' ) {
af59614d 1039 return ( "Reference to [_1] deleted", $value ); #loc
84fb5b46
MKG
1040 }
1041 elsif ( $self->Field eq 'ReferredToBy' ) {
af59614d 1042 return ( "Reference by [_1] deleted", $value ); #loc
84fb5b46
MKG
1043 }
1044 elsif ( $self->Field eq 'MemberOf' ) {
af59614d 1045 return ( "Membership in [_1] deleted", $value ); #loc
84fb5b46
MKG
1046 }
1047 elsif ( $self->Field eq 'HasMember' ) {
af59614d 1048 return ( "Member [_1] deleted", $value ); #loc
84fb5b46
MKG
1049 }
1050 }
1051 else {
af59614d 1052 return ( "[_1]", $self->Data ); #loc
84fb5b46
MKG
1053 }
1054 },
1055 Told => sub {
1056 my $self = shift;
1057 if ( $self->Field eq 'Told' ) {
1058 my $t1 = RT::Date->new($self->CurrentUser);
1059 $t1->Set(Format => 'ISO', Value => $self->NewValue);
1060 my $t2 = RT::Date->new($self->CurrentUser);
1061 $t2->Set(Format => 'ISO', Value => $self->OldValue);
af59614d 1062 return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc
84fb5b46
MKG
1063 }
1064 else {
af59614d
MKG
1065 return ( "[_1] changed from [_2] to [_3]", #loc
1066 $self->loc($self->Field),
1067 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
84fb5b46
MKG
1068 }
1069 },
1070 Set => sub {
1071 my $self = shift;
1072 if ( $self->Field eq 'Password' ) {
af59614d 1073 return ('Password changed'); #loc
84fb5b46
MKG
1074 }
1075 elsif ( $self->Field eq 'Queue' ) {
1076 my $q1 = RT::Queue->new( $self->CurrentUser );
1077 $q1->Load( $self->OldValue );
1078 my $q2 = RT::Queue->new( $self->CurrentUser );
1079 $q2->Load( $self->NewValue );
af59614d
MKG
1080 return ("[_1] changed from [_2] to [_3]", #loc
1081 $self->loc($self->Field) , $q1->Name , $q2->Name);
84fb5b46
MKG
1082 }
1083
1084 # Write the date/time change at local time:
1085 elsif ($self->Field =~ /Due|Starts|Started|Told/) {
1086 my $t1 = RT::Date->new($self->CurrentUser);
1087 $t1->Set(Format => 'ISO', Value => $self->NewValue);
1088 my $t2 = RT::Date->new($self->CurrentUser);
1089 $t2->Set(Format => 'ISO', Value => $self->OldValue);
af59614d 1090 return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc
84fb5b46
MKG
1091 }
1092 elsif ( $self->Field eq 'Owner' ) {
1093 my $Old = RT::User->new( $self->CurrentUser );
1094 $Old->Load( $self->OldValue );
1095 my $New = RT::User->new( $self->CurrentUser );
1096 $New->Load( $self->NewValue );
1097
1098 if ( $Old->id == RT->Nobody->id ) {
1099 if ( $New->id == $self->Creator ) {
af59614d 1100 return ("Taken"); #loc
84fb5b46
MKG
1101 }
1102 else {
af59614d 1103 return ( "Given to [_1]", $self->_FormatUser($New) ); #loc
84fb5b46
MKG
1104 }
1105 }
1106 else {
1107 if ( $New->id == $self->Creator ) {
af59614d 1108 return ("Stolen from [_1]", $self->_FormatUser($Old) ); #loc
84fb5b46
MKG
1109 }
1110 elsif ( $Old->id == $self->Creator ) {
1111 if ( $New->id == RT->Nobody->id ) {
af59614d 1112 return ("Untaken"); #loc
84fb5b46
MKG
1113 }
1114 else {
af59614d 1115 return ( "Given to [_1]", $self->_FormatUser($New) ); #loc
84fb5b46
MKG
1116 }
1117 }
1118 else {
af59614d
MKG
1119 return (
1120 "Owner forcibly changed from [_1] to [_2]", #loc
1121 map { $self->_FormatUser($_) } $Old, $New
1122 );
84fb5b46
MKG
1123 }
1124 }
1125 }
1126 else {
af59614d
MKG
1127 return ( "[_1] changed from [_2] to [_3]", #loc
1128 $self->loc($self->Field),
1129 ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
1130 }
1131 },
1132 "Set-TimeWorked" => sub {
1133 my $self = shift;
1134 my $old = $self->OldValue || 0;
1135 my $new = $self->NewValue || 0;
1136 my $duration = $new - $old;
1137 if ($duration < 0) {
1138 return ("Adjusted time worked by [quant,_1,minute,minutes]", $duration);
1139 }
1140 elsif ($duration < 60) {
1141 return ("Worked [quant,_1,minute,minutes]", $duration);
1142 } else {
1143 return ("Worked [quant,_1,hour,hours] ([numf,_2] minutes)", sprintf("%.1f", $duration / 60), $duration);
84fb5b46
MKG
1144 }
1145 },
1146 PurgeTransaction => sub {
1147 my $self = shift;
af59614d 1148 return ("Transaction [_1] purged", $self->Data); #loc
84fb5b46
MKG
1149 },
1150 AddReminder => sub {
1151 my $self = shift;
1152 my $ticket = RT::Ticket->new($self->CurrentUser);
1153 $ticket->Load($self->NewValue);
af59614d
MKG
1154 my $subject = [
1155 \'<a href="', RT->Config->Get('WebPath'),
1156 "/Ticket/Reminders.html?id=", $self->ObjectId,
1157 "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1158 ];
1159 return ("Reminder '[_1]' added", $subject); #loc
84fb5b46
MKG
1160 },
1161 OpenReminder => sub {
1162 my $self = shift;
1163 my $ticket = RT::Ticket->new($self->CurrentUser);
1164 $ticket->Load($self->NewValue);
af59614d
MKG
1165 my $subject = [
1166 \'<a href="', RT->Config->Get('WebPath'),
1167 "/Ticket/Reminders.html?id=", $self->ObjectId,
1168 "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1169 ];
1170 return ("Reminder '[_1]' reopened", $subject); #loc
84fb5b46
MKG
1171 },
1172 ResolveReminder => sub {
1173 my $self = shift;
1174 my $ticket = RT::Ticket->new($self->CurrentUser);
1175 $ticket->Load($self->NewValue);
af59614d
MKG
1176 my $subject = [
1177 \'<a href="', RT->Config->Get('WebPath'),
1178 "/Ticket/Reminders.html?id=", $self->ObjectId,
1179 "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
1180 ];
1181 return ("Reminder '[_1]' completed", $subject); #loc
84fb5b46
MKG
1182 }
1183);
1184
1185
1186
1187
1188=head2 IsInbound
1189
1190Returns true if the creator of the transaction is a requestor of the ticket.
1191Returns false otherwise
1192
1193=cut
1194
1195sub IsInbound {
1196 my $self = shift;
1197 $self->ObjectType eq 'RT::Ticket' or return undef;
1198 return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) );
1199}
1200
1201
1202
1203sub _OverlayAccessible {
1204 {
1205
1206 ObjectType => { public => 1},
1207 ObjectId => { public => 1},
1208
1209 }
1210};
1211
1212
1213
1214
1215sub _Set {
1216 my $self = shift;
1217 return ( 0, $self->loc('Transactions are immutable') );
1218}
1219
1220
1221
1222=head2 _Value
1223
1224Takes the name of a table column.
1225Returns its value as a string, if the user passes an ACL check
1226
1227=cut
1228
1229sub _Value {
1230 my $self = shift;
1231 my $field = shift;
1232
1233 #if the field is public, return it.
1234 if ( $self->_Accessible( $field, 'public' ) ) {
1235 return $self->SUPER::_Value( $field );
1236 }
1237
1238 unless ( $self->CurrentUserCanSee ) {
1239 return undef;
1240 }
1241
1242 return $self->SUPER::_Value( $field );
1243}
1244
1245
84fb5b46
MKG
1246=head2 CurrentUserCanSee
1247
1248Returns true if current user has rights to see this particular transaction.
1249
1250This fact depends on type of the transaction, type of an object the transaction
1251is attached to and may be other conditions, so this method is prefered over
1252custom implementations.
1253
1254=cut
1255
1256sub CurrentUserCanSee {
1257 my $self = shift;
1258
84fb5b46 1259 # Make sure the user can see the custom field before showing that it changed
af59614d
MKG
1260 my $type = $self->__Value('Type');
1261 if ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
84fb5b46
MKG
1262 my $cf = RT::CustomField->new( $self->CurrentUser );
1263 $cf->SetContextObject( $self->Object );
1264 $cf->Load( $cf_id );
1265 return 0 unless $cf->CurrentUserHasRight('SeeCustomField');
1266 }
403d7b0b
MKG
1267
1268 # Transactions that might have changed the ->Object's visibility to
1269 # the current user are marked readable
1270 return 1 if $self->{ _object_is_readable };
1271
84fb5b46 1272 # Defer to the object in question
af59614d 1273 return $self->Object->CurrentUserCanSee("Transaction", $self);
84fb5b46
MKG
1274}
1275
1276
1277sub Ticket {
1278 my $self = shift;
1279 return $self->ObjectId;
1280}
1281
1282sub TicketObj {
1283 my $self = shift;
1284 return $self->Object;
1285}
1286
1287sub OldValue {
1288 my $self = shift;
af59614d 1289 if ( my $Object = $self->OldReferenceObject ) {
84fb5b46
MKG
1290 return $Object->Content;
1291 }
1292 else {
1293 return $self->_Value('OldValue');
1294 }
1295}
1296
1297sub NewValue {
1298 my $self = shift;
af59614d 1299 if ( my $Object = $self->NewReferenceObject ) {
84fb5b46
MKG
1300 return $Object->Content;
1301 }
1302 else {
1303 return $self->_Value('NewValue');
1304 }
1305}
1306
1307sub Object {
1308 my $self = shift;
1309 my $Object = $self->__Value('ObjectType')->new($self->CurrentUser);
1310 $Object->Load($self->__Value('ObjectId'));
1311 return $Object;
1312}
1313
af59614d
MKG
1314=head2 NewReferenceObject
1315
1316=head2 OldReferenceObject
1317
1318Returns an object of the class specified by the column C<ReferenceType> and
1319loaded with the id specified by the column C<NewReference> or C<OldReference>.
1320C<ReferenceType> is assumed to be an L<RT::Record> subclass.
1321
1322The object may be unloaded (check C<< $object->id >>) if the reference is
1323corrupt (such as if the referenced record was improperly deleted).
1324
1325Returns undef if either C<ReferenceType> or C<NewReference>/C<OldReference> is
1326false.
1327
1328=cut
1329
1330sub NewReferenceObject { $_[0]->_ReferenceObject("New") }
1331sub OldReferenceObject { $_[0]->_ReferenceObject("Old") }
1332
1333sub _ReferenceObject {
1334 my $self = shift;
1335 my $which = shift;
1336 my $type = $self->__Value("ReferenceType");
1337 my $id = $self->__Value("${which}Reference");
1338 return unless $type and $id;
1339
1340 my $object = $type->new($self->CurrentUser);
1341 $object->Load( $id );
1342 return $object;
1343}
1344
84fb5b46
MKG
1345sub FriendlyObjectType {
1346 my $self = shift;
af59614d 1347 return $self->loc( $self->Object->RecordType );
84fb5b46
MKG
1348}
1349
1350=head2 UpdateCustomFields
84fb5b46 1351
af59614d 1352Takes a hash of:
84fb5b46 1353
af59614d
MKG
1354 CustomField-C<Id> => Value
1355
1356or:
1357
1358 Object-RT::Transaction-CustomField-C<Id> => Value
1359
1360parameters to update this transaction's custom fields.
84fb5b46
MKG
1361
1362=cut
1363
1364sub UpdateCustomFields {
1365 my $self = shift;
1366 my %args = (@_);
1367
1368 # This method used to have an API that took a hash of a single
1369 # value "ARGSRef", which was a reference to a hash of arguments.
1370 # This was insane. The next few lines of code preserve that API
1371 # while giving us something saner.
af59614d
MKG
1372 my $args;
1373 if ($args{'ARGSRef'}) {
1374 RT->Deprecated( Arguments => "ARGSRef", Remove => "4.4" );
84fb5b46
MKG
1375 $args = $args{ARGSRef};
1376 } else {
1377 $args = \%args;
1378 }
1379
1380 foreach my $arg ( keys %$args ) {
1381 next
1382 unless ( $arg =~
1383 /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ );
af59614d 1384 next if $arg =~ /-Magic$/;
84fb5b46
MKG
1385 my $cfid = $1;
1386 my $values = $args->{$arg};
1387 foreach
1388 my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
1389 {
1390 next unless (defined($value) && length($value));
1391 $self->_AddCustomFieldValue(
1392 Field => $cfid,
1393 Value => $value,
1394 RecordTransaction => 0,
1395 );
1396 }
1397 }
1398}
1399
403d7b0b 1400=head2 LoadCustomFieldByIdentifier
84fb5b46 1401
403d7b0b
MKG
1402Finds and returns the custom field of the given name for the
1403transaction, overriding L<RT::Record/LoadCustomFieldByIdentifier> to
1404look for queue-specific CFs before global ones.
84fb5b46
MKG
1405
1406=cut
1407
403d7b0b 1408sub LoadCustomFieldByIdentifier {
84fb5b46
MKG
1409 my $self = shift;
1410 my $field = shift;
1411
403d7b0b
MKG
1412 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1413 if ref $field or $field =~ /^\d+$/;
84fb5b46 1414
403d7b0b
MKG
1415 return $self->SUPER::LoadCustomFieldByIdentifier($field)
1416 unless UNIVERSAL::can( $self->Object, 'QueueObj' );
84fb5b46 1417
403d7b0b
MKG
1418 my $CFs = RT::CustomFields->new( $self->CurrentUser );
1419 $CFs->SetContextObject( $self->Object );
af59614d 1420 $CFs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 );
403d7b0b
MKG
1421 $CFs->LimitToLookupType($self->CustomFieldLookupType);
1422 $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
1423 return $CFs->First || RT::CustomField->new( $self->CurrentUser );
1424}
84fb5b46
MKG
1425
1426=head2 CustomFieldLookupType
1427
1428Returns the RT::Transaction lookup type, which can
1429be passed to RT::CustomField->Create() via the 'LookupType' hash key.
1430
1431=cut
1432
1433
1434sub CustomFieldLookupType {
1435 "RT::Queue-RT::Ticket-RT::Transaction";
1436}
1437
1438
1439=head2 SquelchMailTo
1440
1441Similar to Ticket class SquelchMailTo method - returns a list of
1442transaction's squelched addresses. As transactions are immutable, the
1443list of squelched recipients cannot be modified after creation.
1444
1445=cut
1446
1447sub SquelchMailTo {
1448 my $self = shift;
1449 return () unless $self->CurrentUserCanSee;
1450 return $self->Attributes->Named('SquelchMailTo');
1451}
1452
1453=head2 Recipients
1454
1455Returns the list of email addresses (as L<Email::Address> objects)
1456that this transaction would send mail to. There may be duplicates.
1457
1458=cut
1459
1460sub Recipients {
1461 my $self = shift;
1462 my @recipients;
1463 foreach my $scrip ( @{ $self->Scrips->Prepared } ) {
1464 my $action = $scrip->ActionObj->Action;
1465 next unless $action->isa('RT::Action::SendEmail');
1466
1467 foreach my $type (qw(To Cc Bcc)) {
1468 push @recipients, $action->$type();
1469 }
1470 }
1471
1472 if ( $self->Rules ) {
1473 for my $rule (@{$self->Rules}) {
1474 next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail';
1475 my $data = $rule->{hints}{recipients};
1476 foreach my $type (qw(To Cc Bcc)) {
1477 push @recipients, map {Email::Address->new($_)} @{$data->{$type}};
1478 }
1479 }
1480 }
1481 return @recipients;
1482}
1483
1484=head2 DeferredRecipients($freq, $include_sent )
1485
1486Takes the following arguments:
1487
1488=over
1489
1490=item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp".
1491
1492=item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction.
1493
1494=back
1495
1496Returns an array of users who should now receive the notification that
1497was recorded in this transaction. Returns an empty array if there were
1498no deferred users, or if $include_sent was not specified and the deferred
1499notifications have been sent.
1500
1501=cut
1502
1503sub DeferredRecipients {
1504 my $self = shift;
1505 my $freq = shift;
1506 my $include_sent = @_? shift : 0;
1507
1508 my $attr = $self->FirstAttribute('DeferredRecipients');
1509
1510 return () unless ($attr);
1511
1512 my $deferred = $attr->Content;
1513
1514 return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} );
1515
1516 # Skip it.
1517
1518 for my $user (keys %{$deferred->{$freq}}) {
1519 if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) {
1520 delete $deferred->{$freq}->{$user}
1521 }
1522 }
1523 # Now get our users. Easy.
1524
1525 return keys %{ $deferred->{$freq} };
1526}
1527
1528
1529
1530# Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
1531sub _CacheConfig {
1532 {
1533 'cache_p' => 1,
1534 'fast_update_p' => 1,
1535 'cache_for_sec' => 6000,
1536 }
1537}
1538
1539
1540=head2 ACLEquivalenceObjects
1541
1542This method returns a list of objects for which a user's rights also apply
1543to this Transaction.
1544
1545This currently only applies to Transaction Custom Fields on Tickets, so we return
1546the Ticket's Queue and the Ticket.
1547
1548This method is called from L<RT::Principal/HasRight>.
1549
1550=cut
1551
1552sub ACLEquivalenceObjects {
1553 my $self = shift;
1554
1555 return unless $self->ObjectType eq 'RT::Ticket';
1556 my $object = $self->Object;
1557 return $object,$object->QueueObj;
1558
1559}
1560
1561
1562
1563
1564
1565=head2 id
1566
1567Returns the current value of id.
1568(In the database, id is stored as int(11).)
1569
1570
1571=cut
1572
1573
1574=head2 ObjectType
1575
1576Returns the current value of ObjectType.
1577(In the database, ObjectType is stored as varchar(64).)
1578
1579
1580
1581=head2 SetObjectType VALUE
1582
1583
1584Set ObjectType to VALUE.
1585Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1586(In the database, ObjectType will be stored as a varchar(64).)
1587
1588
1589=cut
1590
1591
1592=head2 ObjectId
1593
1594Returns the current value of ObjectId.
1595(In the database, ObjectId is stored as int(11).)
1596
1597
1598
1599=head2 SetObjectId VALUE
1600
1601
1602Set ObjectId to VALUE.
1603Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1604(In the database, ObjectId will be stored as a int(11).)
1605
1606
1607=cut
1608
1609
1610=head2 TimeTaken
1611
1612Returns the current value of TimeTaken.
1613(In the database, TimeTaken is stored as int(11).)
1614
1615
1616
1617=head2 SetTimeTaken VALUE
1618
1619
1620Set TimeTaken to VALUE.
1621Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1622(In the database, TimeTaken will be stored as a int(11).)
1623
1624
1625=cut
1626
1627
1628=head2 Type
1629
1630Returns the current value of Type.
1631(In the database, Type is stored as varchar(20).)
1632
1633
1634
1635=head2 SetType VALUE
1636
1637
1638Set Type to VALUE.
1639Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1640(In the database, Type will be stored as a varchar(20).)
1641
1642
1643=cut
1644
1645
1646=head2 Field
1647
1648Returns the current value of Field.
1649(In the database, Field is stored as varchar(40).)
1650
1651
1652
1653=head2 SetField VALUE
1654
1655
1656Set Field to VALUE.
1657Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1658(In the database, Field will be stored as a varchar(40).)
1659
1660
1661=cut
1662
1663
1664=head2 OldValue
1665
1666Returns the current value of OldValue.
1667(In the database, OldValue is stored as varchar(255).)
1668
1669
1670
1671=head2 SetOldValue VALUE
1672
1673
1674Set OldValue to VALUE.
1675Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1676(In the database, OldValue will be stored as a varchar(255).)
1677
1678
1679=cut
1680
1681
1682=head2 NewValue
1683
1684Returns the current value of NewValue.
1685(In the database, NewValue is stored as varchar(255).)
1686
1687
1688
1689=head2 SetNewValue VALUE
1690
1691
1692Set NewValue to VALUE.
1693Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1694(In the database, NewValue will be stored as a varchar(255).)
1695
1696
1697=cut
1698
1699
1700=head2 ReferenceType
1701
1702Returns the current value of ReferenceType.
1703(In the database, ReferenceType is stored as varchar(255).)
1704
1705
1706
1707=head2 SetReferenceType VALUE
1708
1709
1710Set ReferenceType to VALUE.
1711Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1712(In the database, ReferenceType will be stored as a varchar(255).)
1713
1714
1715=cut
1716
1717
1718=head2 OldReference
1719
1720Returns the current value of OldReference.
1721(In the database, OldReference is stored as int(11).)
1722
1723
1724
1725=head2 SetOldReference VALUE
1726
1727
1728Set OldReference to VALUE.
1729Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1730(In the database, OldReference will be stored as a int(11).)
1731
1732
1733=cut
1734
1735
1736=head2 NewReference
1737
1738Returns the current value of NewReference.
1739(In the database, NewReference is stored as int(11).)
1740
1741
1742
1743=head2 SetNewReference VALUE
1744
1745
1746Set NewReference to VALUE.
1747Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1748(In the database, NewReference will be stored as a int(11).)
1749
1750
1751=cut
1752
1753
1754=head2 Data
1755
1756Returns the current value of Data.
1757(In the database, Data is stored as varchar(255).)
1758
1759
1760
1761=head2 SetData VALUE
1762
1763
1764Set Data to VALUE.
1765Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1766(In the database, Data will be stored as a varchar(255).)
1767
1768
1769=cut
1770
1771
1772=head2 Creator
1773
1774Returns the current value of Creator.
1775(In the database, Creator is stored as int(11).)
1776
1777
1778=cut
1779
1780
1781=head2 Created
1782
1783Returns the current value of Created.
1784(In the database, Created is stored as datetime.)
1785
1786
1787=cut
1788
1789
1790
1791sub _CoreAccessible {
1792 {
1793
1794 id =>
af59614d 1795 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
84fb5b46 1796 ObjectType =>
af59614d 1797 {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''},
84fb5b46 1798 ObjectId =>
af59614d 1799 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
84fb5b46 1800 TimeTaken =>
af59614d 1801 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
84fb5b46 1802 Type =>
af59614d 1803 {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''},
84fb5b46 1804 Field =>
af59614d 1805 {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', default => ''},
84fb5b46 1806 OldValue =>
af59614d 1807 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
84fb5b46 1808 NewValue =>
af59614d 1809 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
84fb5b46 1810 ReferenceType =>
af59614d 1811 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
84fb5b46 1812 OldReference =>
af59614d 1813 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
84fb5b46 1814 NewReference =>
af59614d 1815 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
84fb5b46 1816 Data =>
af59614d 1817 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
84fb5b46 1818 Creator =>
af59614d 1819 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
84fb5b46 1820 Created =>
af59614d 1821 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
84fb5b46
MKG
1822
1823 }
1824};
1825
af59614d
MKG
1826sub FindDependencies {
1827 my $self = shift;
1828 my ($walker, $deps) = @_;
1829
1830 $self->SUPER::FindDependencies($walker, $deps);
1831
1832 $deps->Add( out => $self->Object );
1833 $deps->Add( in => $self->Attachments );
1834
1835 my $type = $self->Type;
1836 if ($type eq "CustomField") {
1837 my $cf = RT::CustomField->new( RT->SystemUser );
1838 $cf->Load( $self->Field );
1839 $deps->Add( out => $cf );
1840 } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
1841 for my $field (qw/OldValue NewValue/) {
1842 my $user = RT::User->new( RT->SystemUser );
1843 $user->Load( $self->$field );
1844 $deps->Add( out => $user );
1845 }
1846 } elsif ($type eq "DelWatcher") {
1847 my $principal = RT::Principal->new( RT->SystemUser );
1848 $principal->Load( $self->OldValue );
1849 $deps->Add( out => $principal->Object );
1850 } elsif ($type eq "AddWatcher") {
1851 my $principal = RT::Principal->new( RT->SystemUser );
1852 $principal->Load( $self->NewValue );
1853 $deps->Add( out => $principal->Object );
1854 } elsif ($type eq "DeleteLink") {
1855 if ($self->OldValue) {
1856 my $base = RT::URI->new( $self->CurrentUser );
1857 $base->FromURI( $self->OldValue );
1858 $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
1859 }
1860 } elsif ($type eq "AddLink") {
1861 if ($self->NewValue) {
1862 my $base = RT::URI->new( $self->CurrentUser );
1863 $base->FromURI( $self->NewValue );
1864 $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object;
1865 }
1866 } elsif ($type eq "Set" and $self->Field eq "Queue") {
1867 for my $field (qw/OldValue NewValue/) {
1868 my $queue = RT::Queue->new( RT->SystemUser );
1869 $queue->Load( $self->$field );
1870 $deps->Add( out => $queue );
1871 }
1872 } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
1873 my $ticket = RT::Ticket->new( RT->SystemUser );
1874 $ticket->Load( $self->NewValue );
1875 $deps->Add( out => $ticket );
1876 }
1877}
1878
1879sub Serialize {
1880 my $self = shift;
1881 my %args = (@_);
1882 my %store = $self->SUPER::Serialize(@_);
1883
1884 my $type = $store{Type};
1885 if ($type eq "CustomField") {
1886 my $cf = RT::CustomField->new( RT->SystemUser );
1887 $cf->Load( $store{Field} );
1888 $store{Field} = \($cf->UID);
1889 } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) {
1890 for my $field (qw/OldValue NewValue/) {
1891 my $user = RT::User->new( RT->SystemUser );
1892 $user->Load( $store{$field} );
1893 $store{$field} = \($user->UID);
1894 }
1895 } elsif ($type eq "DelWatcher") {
1896 my $principal = RT::Principal->new( RT->SystemUser );
1897 $principal->Load( $store{OldValue} );
1898 $store{OldValue} = \($principal->UID);
1899 } elsif ($type eq "AddWatcher") {
1900 my $principal = RT::Principal->new( RT->SystemUser );
1901 $principal->Load( $store{NewValue} );
1902 $store{NewValue} = \($principal->UID);
1903 } elsif ($type eq "DeleteLink") {
1904 if ($store{OldValue}) {
1905 my $base = RT::URI->new( $self->CurrentUser );
1906 $base->FromURI( $store{OldValue} );
1907 $store{OldValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
1908 }
1909 } elsif ($type eq "AddLink") {
1910 if ($store{NewValue}) {
1911 my $base = RT::URI->new( $self->CurrentUser );
1912 $base->FromURI( $store{NewValue} );
1913 $store{NewValue} = \($base->Object->UID) if $base->Resolver and $base->Object;
1914 }
1915 } elsif ($type eq "Set" and $store{Field} eq "Queue") {
1916 for my $field (qw/OldValue NewValue/) {
1917 my $queue = RT::Queue->new( RT->SystemUser );
1918 $queue->Load( $store{$field} );
1919 $store{$field} = \($queue->UID);
1920 }
1921 } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) {
1922 my $ticket = RT::Ticket->new( RT->SystemUser );
1923 $ticket->Load( $store{NewValue} );
1924 $store{NewValue} = \($ticket->UID);
1925 }
1926
1927 return %store;
1928}
1929
1930sub PreInflate {
1931 my $class = shift;
1932 my ($importer, $uid, $data) = @_;
1933
1934 if ($data->{Object} and ref $data->{Object}) {
1935 my $on_uid = ${ $data->{Object} };
1936 return if $importer->ShouldSkipTransaction($on_uid);
1937 }
1938
1939 if ($data->{Type} eq "DeleteLink" and ref $data->{OldValue}) {
1940 my $uid = ${ $data->{OldValue} };
1941 my $obj = $importer->LookupObj( $uid );
1942 $data->{OldValue} = $obj->URI;
1943 } elsif ($data->{Type} eq "AddLink" and ref $data->{NewValue}) {
1944 my $uid = ${ $data->{NewValue} };
1945 my $obj = $importer->LookupObj( $uid );
1946 $data->{NewValue} = $obj->URI;
1947 }
1948
1949 return $class->SUPER::PreInflate( $importer, $uid, $data );
1950}
1951
84fb5b46
MKG
1952RT::Base->_ImportOverlays();
1953
19541;