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