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