X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FTransaction.pm;h=52f3abff431f90717e3a4abb2ae6d7b14f4399ed;hb=44dd00a3ff974a17999e86e64488e996edc71e3c;hp=ee1f069b24ce8692967d645e9cd32518f03379dd;hpb=0ebeec96313dd7edfca340f01f8fbbbac1f4aa1d;p=freeside.git diff --git a/rt/lib/RT/Transaction.pm b/rt/lib/RT/Transaction.pm index ee1f069b2..52f3abff4 100755 --- a/rt/lib/RT/Transaction.pm +++ b/rt/lib/RT/Transaction.pm @@ -1,10 +1,54 @@ -# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Transaction.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $ -# Copyright 1999-2001 Jesse Vincent -# Released under the terms of the GNU Public License +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC +# +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} =head1 NAME - RT::Transaction - RT\'s transaction object + RT::Transaction - RT's transaction object =head1 SYNOPSIS @@ -21,30 +65,31 @@ It can have arbitrary MIME attachments. =head1 METHODS -=begin testing - -ok(require RT::TestHarness); -ok(require RT::Transaction); - -=end testing =cut + package RT::Transaction; -use RT::Record; -@ISA= qw(RT::Record); - +use base 'RT::Record'; +use strict; +use warnings; + + +use vars qw( %_BriefDescriptions $PreferredContentType ); + use RT::Attachments; +use RT::Scrips; +use RT::Ruleset; -# {{{ sub _Init -sub _Init { - my $self = shift; - $self->{'table'} = "Transactions"; - return ($self->SUPER::_Init(@_)); +use HTML::FormatText::WithLinks::AndTables; +use HTML::Scrubber; -} -# }}} +# For EscapeHTML() and decode_entities() +require RT::Interface::Web; +require HTML::Entities; + +sub Table {'Transactions'} # {{{ sub Create @@ -52,299 +97,551 @@ sub _Init { Create a new transaction. -This routine should _never_ be called anything other Than RT::Ticket. It should not be called -from client code. Ever. Not ever. If you do this, we will hunt you down. and break your kneecaps. +This routine should _never_ be called by anything other than RT::Ticket. +It should not be called +from client code. Ever. Not ever. If you do this, we will hunt you down and break your kneecaps. Then the unpleasant stuff will start. TODO: Document what gets passed to this =cut -sub Create { +sub Create { my $self = shift; - my %args = ( id => undef, - TimeTaken => 0, - Ticket => 0 , - Type => 'undefined', - Data => '', - Field => undef, - OldValue => undef, - NewValue => undef, - MIMEObj => undef, - ActivateScrips => 1, - @_ - ); - + my %args = ( + id => undef, + TimeTaken => 0, + Type => 'undefined', + Data => '', + Field => undef, + OldValue => undef, + NewValue => undef, + MIMEObj => undef, + ActivateScrips => 1, + CommitScrips => 1, + ObjectType => 'RT::Ticket', + ObjectId => 0, + ReferenceType => undef, + OldReference => undef, + NewReference => undef, + SquelchMailTo => undef, + CustomFields => {}, + @_ + ); + + $args{ObjectId} ||= $args{Ticket}; + #if we didn't specify a ticket, we need to bail - unless ( $args{'Ticket'} ) { - return(0, "RT::Transaction->Create couldn't, as you didn't specify a ticket id"); + unless ( $args{'ObjectId'} && $args{'ObjectType'}) { + return ( 0, $self->loc( "Transaction->Create couldn't, as you didn't specify an object type and id")); } - + #lets create our transaction - my $id = $self->SUPER::Create(Ticket => $args{'Ticket'}, - TimeTaken => $args{'TimeTaken'}, - Type => $args{'Type'}, - Data => $args{'Data'}, - Field => $args{'Field'}, - OldValue => $args{'OldValue'}, - NewValue => $args{'NewValue'}, - Created => $args{'Created'} - ); + my %params = ( + Type => $args{'Type'}, + Data => $args{'Data'}, + Field => $args{'Field'}, + OldValue => $args{'OldValue'}, + NewValue => $args{'NewValue'}, + Created => $args{'Created'}, + ObjectType => $args{'ObjectType'}, + ObjectId => $args{'ObjectId'}, + ReferenceType => $args{'ReferenceType'}, + OldReference => $args{'OldReference'}, + NewReference => $args{'NewReference'}, + ); + + # Parameters passed in during an import that we probably don't want to touch, otherwise + foreach my $attr (qw(id Creator Created LastUpdated TimeTaken LastUpdatedBy)) { + $params{$attr} = $args{$attr} if ($args{$attr}); + } + + my $id = $self->SUPER::Create(%params); $self->Load($id); - $self->_Attach($args{'MIMEObj'}) - if defined $args{'MIMEObj'}; - - #Provide a way to turn off scrips if we need to - if ($args{'ActivateScrips'}) { - - #We're really going to need a non-acled ticket for the scrips to work - my $TicketAsSystem = RT::Ticket->new($RT::SystemUser); - $TicketAsSystem->Load($args{'Ticket'}) || - $RT::Logger->err("$self couldn't load ticket $args{'Ticket'}\n"); - - my $TransAsSystem = RT::Transaction->new($RT::SystemUser); - $TransAsSystem->Load($self->id) || - $RT::Logger->err("$self couldn't load a copy of itself as superuser\n"); - - # {{{ Deal with Scrips - - #Load a scripscopes object - use RT::Scrips; - my $PossibleScrips = RT::Scrips->new($RT::SystemUser); - - $PossibleScrips->LimitToQueue($TicketAsSystem->QueueObj->Id); #Limit it to $Ticket->QueueObj->Id - $PossibleScrips->LimitToGlobal(); # or to "global" - my $ConditionsAlias = $PossibleScrips->NewAlias('ScripConditions'); - - $PossibleScrips->Join(ALIAS1 => 'main', FIELD1 => 'ScripCondition', - ALIAS2 => $ConditionsAlias, FIELD2=> 'id'); - - - #We only want things where the scrip applies to this sort of transaction - $PossibleScrips->Limit(ALIAS=> $ConditionsAlias, - FIELD=>'ApplicableTransTypes', - OPERATOR => 'LIKE', - VALUE => $args{'Type'}, - ENTRYAGGREGATOR => 'OR', - ); - - # Or where the scrip applies to any transaction - $PossibleScrips->Limit(ALIAS=> $ConditionsAlias, - FIELD=>'ApplicableTransTypes', - OPERATOR => 'LIKE', - VALUE => "Any", - ENTRYAGGREGATOR => 'OR', - ); - - #Iterate through each script and check it's applicability. + if ( defined $args{'MIMEObj'} ) { + my ($id, $msg) = $self->_Attach( $args{'MIMEObj'} ); + unless ( $id ) { + $RT::Logger->error("Couldn't add attachment: $msg"); + return ( 0, $self->loc("Couldn't add attachment") ); + } + } + + # Set up any custom fields passed at creation. Has to happen + # before scrips. - while (my $Scrip = $PossibleScrips->Next()) { - - #TODO: properly deal with errors raised in this scrip loop - - #$RT::Logger->debug("$self now dealing with ".$Scrip->Id. "\n"); - eval { - local $SIG{__DIE__} = sub { $RT::Logger->error($_[0])}; - - - #Load the scrip's Condition object - $Scrip->ConditionObj->LoadCondition(TicketObj => $TicketAsSystem, - TransactionObj => $TransAsSystem); - - - #If it's applicable, prepare and commit it - - $RT::Logger->debug ("$self: Checking condition ".$Scrip->ConditionObj->Name. "...\n"); - - if ( $Scrip->IsApplicable() ) { - - $RT::Logger->debug ("$self: Matches condition ".$Scrip->ConditionObj->Name. "...\n"); - #TODO: handle some errors here - - $Scrip->ActionObj->LoadAction(TicketObj => $TicketAsSystem, - TransactionObj => $TransAsSystem); - - - if ($Scrip->Prepare()) { - $RT::Logger->debug("$self: Prepared " . - $Scrip->ActionObj->Name . "\n"); - if ($Scrip->Commit()) { - $RT::Logger->debug("$self: Committed " . - $Scrip->ActionObj->Name . "\n"); - } - else { - $RT::Logger->info("$self: Failed to commit ". - $Scrip->ActionObj->Name . "\n"); - } - } - else { - $RT::Logger->info("$self: Failed to prepare " . - $Scrip->ActionObj->Name . "\n"); - } - - #We're done with it. lets clean up. - #TODO: something else isn't letting these get garbage collected. check em out. - $Scrip->ActionObj->DESTROY(); - $Scrip->ConditionObj->DESTROY; - } - - - else { - $RT::Logger->debug ("$self: Doesn't match condition ".$Scrip->ConditionObj->Name. "...\n"); - - # TODO: why doesn't this catch all the ScripObjs we create. - # and why do we explictly need to destroy them? - $Scrip->ConditionObj->DESTROY; - } - } - } - - # }}} - - } - - return ($id, "Transaction Created"); -} - -# }}} - -# {{{ sub Delete + $self->UpdateCustomFields(%{ $args{'CustomFields'} }); + + $self->AddAttribute( + Name => 'SquelchMailTo', + Content => RT::User->CanonicalizeEmailAddress($_) + ) for @{$args{'SquelchMailTo'} || []}; + + my @return = ( $id, $self->loc("Transaction Created") ); + + return @return unless $args{'ObjectType'} eq 'RT::Ticket'; + + # Provide a way to turn off scrips if we need to + unless ( $args{'ActivateScrips'} ) { + $RT::Logger->debug('Skipping scrips for transaction #' .$self->Id); + return @return; + } + + $self->{'scrips'} = RT::Scrips->new(RT->SystemUser); + + $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id); + + $self->{'scrips'}->Prepare( + Stage => 'TransactionCreate', + Type => $args{'Type'}, + Ticket => $args{'ObjectId'}, + Transaction => $self->id, + ); + + # Entry point of the rule system + my $ticket = RT::Ticket->new(RT->SystemUser); + $ticket->Load($args{'ObjectId'}); + my $txn = RT::Transaction->new($RT::SystemUser); + $txn->Load($self->id); + + my $rules = $self->{rules} = RT::Ruleset->FindAllRules( + Stage => 'TransactionCreate', + Type => $args{'Type'}, + TicketObj => $ticket, + TransactionObj => $txn, + ); + + if ($args{'CommitScrips'} ) { + $RT::Logger->debug('About to commit scrips for transaction #' .$self->Id); + $self->{'scrips'}->Commit(); + RT::Ruleset->CommitRules($rules); + } + + return @return; +} + + +=head2 Scrips + +Returns the Scrips object for this transaction. +This routine is only useful on a freshly created transaction object. +Scrips do not get persisted to the database with transactions. + + +=cut + + +sub Scrips { + my $self = shift; + return($self->{'scrips'}); +} + + +=head2 Rules + +Returns the array of Rule objects for this transaction. +This routine is only useful on a freshly created transaction object. +Rules do not get persisted to the database with transactions. + + +=cut + + +sub Rules { + my $self = shift; + return($self->{'rules'}); +} + + + +=head2 Delete + +Delete this transaction. Currently DOES NOT CHECK ACLS + +=cut sub Delete { my $self = shift; - return (0, 'Deleting this object could break referential integrity'); + + + $RT::Handle->BeginTransaction(); + + my $attachments = $self->Attachments; + + while (my $attachment = $attachments->Next) { + my ($id, $msg) = $attachment->Delete(); + unless ($id) { + $RT::Handle->Rollback(); + return($id, $self->loc("System Error: [_1]", $msg)); + } + } + my ($id,$msg) = $self->SUPER::Delete(); + unless ($id) { + $RT::Handle->Rollback(); + return($id, $self->loc("System Error: [_1]", $msg)); + } + $RT::Handle->Commit(); + return ($id,$msg); } -# }}} -# {{{ Routines dealing with Attachments -# {{{ sub Message =head2 Message - Returns the RT::Attachments Object which contains the "top-level" object - attachment for this transaction +Returns the L object which contains the "top-level" object +attachment for this transaction. =cut -sub Message { - +sub Message { my $self = shift; + + # XXX: Where is ACL check? - if (!defined ($self->{'message'}) ){ - - $self->{'message'} = new RT::Attachments($self->CurrentUser); - $self->{'message'}->Limit(FIELD => 'TransactionId', - VALUE => $self->Id); - - $self->{'message'}->ChildrenOf(0); - } - return($self->{'message'}); + unless ( defined $self->{'message'} ) { + + $self->{'message'} = RT::Attachments->new( $self->CurrentUser ); + $self->{'message'}->Limit( + FIELD => 'TransactionId', + VALUE => $self->Id + ); + $self->{'message'}->ChildrenOf(0); + } else { + $self->{'message'}->GotoFirstItem; + } + return $self->{'message'}; +} + + + +=head2 HasContent + +Returns whether this transaction has attached mime objects. + +=cut + +sub HasContent { + my $self = shift; + my $type = $PreferredContentType || ''; + return !!$self->ContentObj( $type ? ( Type => $type) : () ); } -# }}} -# {{{ sub Content + =head2 Content PARAMHASH -If this transaction has attached mime objects, returns the first text/ part. -Otherwise, returns undef. +If this transaction has attached mime objects, returns the body of the first +textual part (as defined in RT::I18N::IsTextualContentType). Otherwise, +returns the message "This transaction appears to have no content". Takes a paramhash. If the $args{'Quote'} parameter is set, wraps this message -at $args{'Wrap'}. $args{'Wrap'} defaults to 70. +at $args{'Wrap'}. $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70. +If $args{'Type'} is set to C, this will return an HTML +part of the message, if available. Otherwise it looks for a text/plain +part. If $args{'Type'} is missing, it defaults to the value of +C<$RT::Transaction::PreferredContentType>, if that's missing too, +defaults to textual. =cut sub Content { my $self = shift; - my %args = ( Quote => 0, - Wrap => 70, - @_ ); + my %args = ( + Type => $PreferredContentType || '', + Quote => 0, + Wrap => 70, + Wrap => ( $RT::MessageBoxWidth || 72 ) - 2, + @_ + ); + + my $content; + if ( my $content_obj = + $self->ContentObj( $args{Type} ? ( Type => $args{Type}) : () ) ) + { + $content = $content_obj->Content ||''; + + if ( lc $content_obj->ContentType eq 'text/html' ) { + $content =~ s/(?:(<\/div>)|

||)\s*--\s+.*?$/$1/s if $args{'Quote'}; + + if ($args{Type} ne 'text/html') { + $content = RT::Interface::Email::ConvertHTMLToText($content); + } else { + # Scrub out , , , and , and + # leave all else untouched. + my $scrubber = HTML::Scrubber->new(); + $scrubber->rules( + html => 0, + head => 0, + meta => 0, + body => 0, + ); + $scrubber->default( 1 => { '*' => 1 } ); + $content = $scrubber->scrub( $content ); + } + } + else { + $content =~ s/\n-- \n.*?$//s if $args{'Quote'}; + if ($args{Type} eq 'text/html') { + # Extremely simple text->html converter + $content =~ s/&/&/g; + $content =~ s//>/g; + $content = qq|

$content
|; + } + } + } + + # If all else fails, return a message that we couldn't find any content + else { + $content = $self->loc('This transaction appears to have no content'); + } + + if ( $args{'Quote'} ) { + if ($args{Type} eq 'text/html') { + $content = '
' + . $self->QuoteHeader + . '
' + . $content + . '


'; + } else { + $content = $self->ApplyQuoteWrap(content => $content, + cols => $args{'Wrap'} ); + + $content = $self->QuoteHeader . "\n$content\n\n"; + } + } + + return ($content); +} + +=head2 QuoteHeader + +Returns text prepended to content when transaction is quoted +(see C argument in L). By default returns +localized "On wrote:\n". + +=cut + +sub QuoteHeader { + my $self = shift; + return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name); +} - my $content = undef; +=head2 ApplyQuoteWrap PARAMHASH - # If we don\'t have any content, return undef now. - unless ($self->Message->First) { - return (undef); - } - +Wrapper to calculate wrap criteria and apply quote wrapping if needed. + +=cut + +sub ApplyQuoteWrap { + my $self = shift; + my %args = @_; + my $content = $args{content}; + + # What's the longest line like? + my $max = 0; + foreach ( split ( /\n/, $args{content} ) ) { + $max = length if length > $max; + } + + if ( $max > 76 ) { + require Text::Quoted; + require Text::Wrapper; + + my $structure = Text::Quoted::extract($args{content}); + $content = $self->QuoteWrap(content_ref => $structure, + cols => $args{cols}, + max => $max ); + } + + $content =~ s/^/> /gm; # use regex since string might be multi-line + return $content; +} + +=head2 QuoteWrap PARAMHASH + +Wrap the contents of transactions based on Wrap settings, maintaining +the quote character from the original. + +=cut + +sub QuoteWrap { + my $self = shift; + my %args = @_; + my $ref = $args{content_ref}; + my $final_string; + + if ( ref $ref eq 'ARRAY' ){ + foreach my $array (@$ref){ + $final_string .= $self->QuoteWrap(content_ref => $array, + cols => $args{cols}, + max => $args{max} ); + } + } + elsif ( ref $ref eq 'HASH' ){ + return $ref->{quoter} . "\n" if $ref->{empty}; # Blank line + + my $col = $args{cols} - (length $ref->{quoter}); + my $wrapper = Text::Wrapper->new( columns => $col ); + + # Wrap on individual lines to honor incoming line breaks + # Otherwise deliberate separate lines (like a list or a sig) + # all get combined incorrectly into single paragraphs. + + my @lines = split /\n/, $ref->{text}; + my $wrap = join '', map { $wrapper->wrap($_) } @lines; + my $quoter = $ref->{quoter}; + + # Only add the space if actually quoting + $quoter .= ' ' if length $quoter; + $wrap =~ s/^/$quoter/mg; # use regex since string might be multi-line + + return $wrap; + } + else{ + $RT::Logger->warning("Can't apply quoting with $ref"); + return; + } + return $final_string; +} + + +=head2 Addresses + +Returns a hashref of addresses related to this transaction. See L for details. + +=cut + +sub Addresses { + my $self = shift; + + if (my $attach = $self->Attachments->First) { + return $attach->Addresses; + } + else { + return {}; + } + +} + + + +=head2 ContentObj + +Returns the RT::Attachment object which contains the content for this Transaction + +=cut + + +sub ContentObj { + my $self = shift; + my %args = ( Type => $PreferredContentType, Attachment => undef, @_ ); + + # If we don't have any content, return undef now. # Get the set of toplevel attachments to this transaction. - my $MIMEObj = $self->Message->First(); - - # If it's a message or a plain part, just return the - # body. - if ($MIMEObj->ContentType() =~ '^(text|message)(/|$)') { - $content = $MIMEObj->Content(); + + my $Attachment = $args{'Attachment'}; + + $Attachment ||= $self->Attachments->First; + + return undef unless ($Attachment); + + my $Attachments = $self->Attachments; + while ( my $Attachment = $Attachments->Next ) { + if ( my $content = _FindPreferredContentObj( %args, Attachment => $Attachment ) ) { + return $content; + } } - - # If it's a multipart object, first try returning the first - # text/plain part. - - elsif ($MIMEObj->ContentType() =~ '^multipart/') { - my $plain_parts = $MIMEObj->Children(); - $plain_parts->ContentType(VALUE => 'text/plain'); - - # If we actully found a part, return its content - if ($plain_parts->First && - $plain_parts->First->Content ne '') { - $content = $plain_parts->First->Content; - } - - # If that fails, return the first text/ or message/ part - # which has some content. - - else { - my $all_parts = $MIMEObj->Children(); - while (($content == undef) && - (my $part = $all_parts->Next)) { - if (($part->ContentType() =~ '^(text|message)(/|$)') and - ($part->Content())) { - $content = $part->Content; - } - } - } + # If that fails, return the first top-level textual part which has some content. + # We probably really want this to become "recurse, looking for the other type of + # displayable". For now, this maintains backcompat + my $all_parts = $self->Attachments; + while ( my $part = $all_parts->Next ) { + next unless _IsDisplayableTextualContentType($part->ContentType) + && $part->Content; + return $part; } - # If all else fails, return a message that we couldn't find - # any content - else { - $content = 'This transaction appears to have no content'; - } - if ($args{'Quote'}) { - # Remove quoted signature. - $content =~ s/\n-- \n(.*)$//s; + return; +} + - # What's the longest line like? - foreach (split (/\n/,$content)) { - $max=length if ( length > $max); - } +sub _FindPreferredContentObj { + my %args = @_; + my $Attachment = $args{Attachment}; - if ($max>76) { - require Text::Wrapper; - my $wrapper=new Text::Wrapper - ( - columns => $args{'Wrap'}, - body_start => ($max > 70*3 ? ' ' : ''), - par_start => '' - ); - $content=$wrapper->wrap($content); - } + # If we don't have any content, return undef now. + return undef unless $Attachment; - $content =~ s/^/> /gm; - $content = '[' . $self->CreatorObj->Name() . ' - ' . $self->CreatedAsString() - . "]:\n\n" - . $content . "\n\n"; + # If it's a textual part, just return the body. + if ( _IsDisplayableTextualContentType($Attachment->ContentType) ) { + return ($Attachment); + } + + # If it's a multipart object, first try returning the first part with preferred + # MIME type ('text/plain' by default). + + elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) { + my $kids = $Attachment->Children; + while (my $child = $kids->Next) { + my $ret = _FindPreferredContentObj(%args, Attachment => $child); + return $ret if ($ret); + } + } + elsif ( $Attachment->ContentType =~ m|^multipart/|i ) { + if ( $args{Type} ) { + my $plain_parts = $Attachment->Children; + $plain_parts->ContentType( VALUE => $args{Type} ); + $plain_parts->LimitNotEmpty; + + # If we actully found a part, return its content + if ( my $first = $plain_parts->First ) { + return $first; + } + } else { + my $parts = $Attachment->Children; + $parts->LimitNotEmpty; + + # If we actully found a part, return its content + while (my $part = $parts->Next) { + next unless _IsDisplayableTextualContentType($part->ContentType); + return $part; + } + + } + } + + # If this is a message/rfc822 mail, we need to dig into it in order to find + # the actual textual content + elsif ( $Attachment->ContentType =~ '^message/rfc822' ) { + my $children = $Attachment->Children; + while ( my $child = $children->Next ) { + if ( my $content = _FindPreferredContentObj( %args, Attachment => $child ) ) { + return $content; + } + } } - return ($content); + # We found no content. suck + return (undef); +} + +=head2 _IsDisplayableTextualContentType + +We may need to pull this out to another module later, but for now, this +is better than RT::I18N::IsTextualContentType because that believes that +a message/rfc822 email is displayable, despite it having no content + +=cut + +sub _IsDisplayableTextualContentType { + my $type = shift; + ($type =~ m{^text/(?:plain|html)\b}i) ? 1 : 0; } -# }}} -# {{{ sub Subject =head2 Subject @@ -355,71 +652,48 @@ Otherwise, returns null sub Subject { my $self = shift; - if ($self->Message->First) { - return ($self->Message->First->Subject); - } - else { - return (undef); - } + return undef unless my $first = $self->Attachments->First; + return $first->Subject; } -# }}} -# {{{ sub Attachments + =head2 Attachments - Returns all the RT::Attachment objects which are attached +Returns all the RT::Attachment objects which are attached to this transaction. Takes an optional parameter, which is a ContentType that Attachments should be restricted to. =cut - -sub Attachments { +sub Attachments { my $self = shift; - my $Types = ''; - $Types = shift if (@_); - my $Attachments = new RT::Attachments($self->CurrentUser); - - #If it's a comment, return an empty object if they don't have the right to see it - if ($self->Type eq 'Comment') { - unless ($self->CurrentUserHasRight('ShowTicketComments')) { - return ($Attachments); - } - } - #if they ain't got rights to see, return an empty object - else { - unless ($self->CurrentUserHasRight('ShowTicket')) { - return ($Attachments); - } + if ( $self->{'attachments'} ) { + $self->{'attachments'}->GotoFirstItem; + return $self->{'attachments'}; } - - $Attachments->Limit(FIELD => 'TransactionId', - VALUE => $self->Id); - # Get the attachments in the order they're put into + $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser ); + + unless ( $self->CurrentUserCanSee ) { + $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl'); + return $self->{'attachments'}; + } + + $self->{'attachments'}->Limit( FIELD => 'TransactionId', VALUE => $self->Id ); + + # Get the self->{'attachments'} in the order they're put into # the database. Arguably, we should be returning a tree - # of attachments, not a set...but no current app seems to need - # it. + # of self->{'attachments'}, not a set...but no current app seems to need + # it. - $Attachments->OrderBy(ALIAS => 'main', - FIELD => 'Id', - ORDER => 'asc'); + $self->{'attachments'}->OrderBy( FIELD => 'id', ORDER => 'ASC' ); - if ($Types) { - $Attachments->ContentType( VALUE => "$Types", - OPERATOR => "LIKE"); - } - - - return($Attachments); - + return $self->{'attachments'}; } -# }}} -# {{{ sub _Attach =head2 _Attach @@ -427,50 +701,52 @@ A private method used to attach a mime object to this transaction. =cut -sub _Attach { - my $self = shift; +sub _Attach { + my $self = shift; my $MIMEObject = shift; - - if (!defined($MIMEObject)) { - $RT::Logger->error("$self _Attach: We can't attach a mime object if you don't give us one.\n"); - return(0, "$self: no attachment specified"); + + unless ( defined $MIMEObject ) { + $RT::Logger->error("We can't attach a mime object if you don't give us one."); + return ( 0, $self->loc("[_1]: no attachment specified", $self) ); } - - - use RT::Attachment; - my $Attachment = new RT::Attachment ($self->CurrentUser); - $Attachment->Create(TransactionId => $self->Id, - Attachment => $MIMEObject); - return ($Attachment, "Attachment created"); - + + my $Attachment = RT::Attachment->new( $self->CurrentUser ); + my ($id, $msg) = $Attachment->Create( + TransactionId => $self->Id, + Attachment => $MIMEObject + ); + return ( $Attachment, $msg || $self->loc("Attachment created") ); } -# }}} -# }}} -# {{{ Routines dealing with Transaction Attributes +sub ContentAsMIME { + my $self = shift; -# {{{ sub TicketObj + # RT::Attachments doesn't limit ACLs as strictly as RT::Transaction does + # since it has less information available without looking to it's parent + # transaction. Check ACLs here before we go any further. + return unless $self->CurrentUserCanSee; -=head2 TicketObj + my $attachments = RT::Attachments->new( $self->CurrentUser ); + $attachments->OrderBy( FIELD => 'id', ORDER => 'ASC' ); + $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->id ); + $attachments->Limit( FIELD => 'Parent', VALUE => 0 ); + $attachments->RowsPerPage(1); -Returns this transaction's ticket object. + my $top = $attachments->First; + return unless $top; -=cut + my $entity = MIME::Entity->build( + Type => 'message/rfc822', + Description => 'transaction ' . $self->id, + Data => $top->ContentAsMIME(Children => 1)->as_string, + ); -sub TicketObj { - my $self = shift; - if (! exists $self->{'TicketObj'}) { - $self->{'TicketObj'} = new RT::Ticket($self->CurrentUser); - $self->{'TicketObj'}->Load($self->Ticket); - } - - return $self->{'TicketObj'}; + return $entity; } -# }}} -# {{{ sub Description + =head2 Description @@ -478,35 +754,21 @@ Returns a text string which describes this transaction =cut - -sub Description { +sub Description { my $self = shift; - #Check those ACLs - #If it's a comment, we need to be extra special careful - if ($self->__Value('Type') eq 'Comment') { - unless ($self->CurrentUserHasRight('ShowTicketComments')) { - return (0, "Permission Denied"); - } - } - - #if they ain't got rights to see, don't let em - else { - unless ($self->CurrentUserHasRight('ShowTicket')) { - return (0, "Permission Denied"); - } + unless ( $self->CurrentUserCanSee ) { + return ( $self->loc("Permission Denied") ); } - if (!defined($self->Type)) { - return("No transaction type specified"); + unless ( defined $self->Type ) { + return ( $self->loc("No transaction type specified")); } - - return ($self->BriefDescription . " by " . $self->CreatorObj->Name); + + return $self->loc("[_1] by [_2]", $self->BriefDescription , $self->CreatorObj->Name ); } -# }}} -# {{{ sub BriefDescription =head2 BriefDescription @@ -514,167 +776,587 @@ Returns a text string which briefly describes this transaction =cut +{ + my $scrubber = HTML::Scrubber->new(default => 0); # deny everything -sub BriefDescription { - my $self = shift; + sub BriefDescription { + my $self = shift; + my $desc = $self->BriefDescriptionAsHTML; + $desc = $scrubber->scrub($desc); + $desc = HTML::Entities::decode_entities($desc); + return $desc; + } +} - #Check those ACLs - #If it's a comment, we need to be extra special careful - if ($self->__Value('Type') eq 'Comment') { - unless ($self->CurrentUserHasRight('ShowTicketComments')) { - return (0, "Permission Denied"); - } - } +=head2 BriefDescriptionAsHTML - #if they ain't got rights to see, don't let em - else { - unless ($self->CurrentUserHasRight('ShowTicket')) { - return (0, "Permission Denied"); - } - } +Returns an HTML string which briefly describes this transaction. - if (!defined($self->Type)) { - return("No transaction type specified"); - } - - if ($self->Type eq 'Create'){ - return("Ticket created"); - } - elsif ($self->Type =~ /Status/) { - if ($self->Field eq 'Status') { - if ($self->NewValue eq 'dead') { - return ("Ticket killed"); - } - else { - return( "Status changed from ". $self->OldValue . - " to ". $self->NewValue); - - } - } - # Generic: - return ($self->Field." changed from ".($self->OldValue||"(empty value)"). - " to ".$self->NewValue ); - } - - if ($self->Type eq 'Correspond') { - return("Correspondence added"); - } - - elsif ($self->Type eq 'Comment') { - return( "Comments added"); - } - - elsif ($self->Type eq 'Keyword') { - - my $field = 'Keyword'; - - if ($self->Field) { - my $keywordsel = new RT::KeywordSelect ($self->CurrentUser); - $keywordsel->Load($self->Field); - $field = $keywordsel->Name(); - } - - if ($self->OldValue eq '') { - return ($field." ".$self->NewValue." added"); - } - elsif ($self->NewValue eq '') { - return ($field." ".$self->OldValue." deleted"); - - } - else { - return ($field." ".$self->OldValue . " changed to ". - $self->NewValue); - } - } - - elsif ($self->Type eq 'Untake'){ - return( "Untaken"); - } - - elsif ($self->Type eq "Take") { - return( "Taken"); - } - - elsif ($self->Type eq "Force") { - my $Old = RT::User->new($self->CurrentUser); - $Old->Load($self->OldValue); - my $New = RT::User->new($self->CurrentUser); - $New->Load($self->NewValue); - return "Owner forcibly changed from ".$Old->Name . " to ". $New->Name; - } - elsif ($self->Type eq "Steal") { - my $Old = RT::User->new($self->CurrentUser); - $Old->Load($self->OldValue); - return "Stolen from ".$Old->Name; - } - - elsif ($self->Type eq "Give") { - my $New = RT::User->new($self->CurrentUser); - $New->Load($self->NewValue); - return( "Given to ".$New->Name); - } - - elsif ($self->Type eq 'AddWatcher'){ - return( $self->Field." ". $self->NewValue ." added"); - } - - elsif ($self->Type eq 'DelWatcher'){ - return( $self->Field." ".$self->OldValue ." deleted"); +=cut + +sub BriefDescriptionAsHTML { + my $self = shift; + + unless ( $self->CurrentUserCanSee ) { + return ( $self->loc("Permission Denied") ); } - - elsif ($self->Type eq 'Subject') { - return( "Subject changed to ".$self->Data); + + my ($objecttype, $type, $field) = ($self->ObjectType, $self->Type, $self->Field); + + unless ( defined $type ) { + return $self->loc("No transaction type specified"); } - elsif ($self->Type eq 'Told') { - return( "User notified"); + + my ($template, @params); + + my @code = grep { ref eq 'CODE' } map { $_BriefDescriptions{$_} } + ( $field + ? ("$objecttype-$type-$field", "$type-$field") + : () ), + "$objecttype-$type", $type; + + if (@code) { + ($template, @params) = $code[0]->($self); } - - elsif ($self->Type eq 'AddLink') { - return ($self->Data); - } - elsif ($self->Type eq 'DeleteLink') { - return ($self->Data); - } - elsif ($self->Type eq 'Set') { - if ($self->Field eq 'Queue') { - my $q1 = new RT::Queue($self->CurrentUser); - $q1->Load($self->OldValue); - my $q2 = new RT::Queue($self->CurrentUser); - $q2->Load($self->NewValue); - return ($self->Field . " changed from " . $q1->Name . " to ". - $q2->Name); - } - - # Write the date/time change at local time: - elsif ($self->Field =~ /Due|Starts|Started|Told/) { - my $t1 = new RT::Date($self->CurrentUser); - $t1->Set(Format => 'ISO', Value => $self->NewValue); - my $t2 = new RT::Date($self->CurrentUser); - $t2->Set(Format => 'ISO', Value => $self->OldValue); - return ($self->Field . " changed from " . $t2->AsString . - " to ".$t1->AsString); - } - else { - return ($self->Field . " changed from " . $self->OldValue . - " to ".$self->NewValue); - } - } - elsif ($self->Type eq 'PurgeTransaction') { - return ("Transaction ".$self->Data. " purged"); + + unless ($template) { + ($template, @params) = ( + "Default: [_1]/[_2] changed from [_3] to [_4]", #loc + $type, + $field, + ( + $self->OldValue + ? "'" . $self->OldValue . "'" + : $self->loc("(no value)") + ), + ( + $self->NewValue + ? "'" . $self->NewValue . "'" + : $self->loc("(no value)") + ), + ); } - else { - return ("Default: ". $self->Type ."/". $self->Field . - " changed from " . $self->OldValue . - " to ".$self->NewValue); - + return $self->loc($template, $self->_ProcessReturnValues(@params)); +} + +sub _ProcessReturnValues { + my $self = shift; + my @values = @_; + return map { + if (ref eq 'ARRAY') { $_ = join "", $self->_ProcessReturnValues(@$_) } + elsif (ref eq 'SCALAR') { $_ = $$_ } + else { RT::Interface::Web::EscapeHTML(\$_) } + $_ + } @values; +} + +sub _FormatPrincipal { + my $self = shift; + my $principal = shift; + if ($principal->IsUser) { + return $self->_FormatUser( $principal->Object ); + } else { + return $self->loc("group [_1]", $principal->Object->Name); } } -# }}} +sub _FormatUser { + my $self = shift; + my $user = shift; + return [ + \'', + $user->Format, + \'' + ]; +} + +%_BriefDescriptions = ( + Create => sub { + my $self = shift; + return ( "[_1] created", $self->FriendlyObjectType ); #loc() + }, + Enabled => sub { + my $self = shift; + return ( "[_1] enabled", $self->Field ? $self->loc($self->Field) : $self->FriendlyObjectType ); #loc() + }, + Disabled => sub { + my $self = shift; + return ( "[_1] disabled", $self->Field ? $self->loc($self->Field) : $self->FriendlyObjectType ); #loc() + }, + Status => sub { + my $self = shift; + if ( $self->Field eq 'Status' ) { + if ( $self->NewValue eq 'deleted' ) { + return ( "[_1] deleted", $self->FriendlyObjectType ); #loc() + } + else { + my $canon = $self->Object->DOES("RT::Record::Role::Status") + ? sub { $self->Object->LifecycleObj->CanonicalCase(@_) } + : sub { return $_[0] }; + return ( + "Status changed from [_1] to [_2]", + "'" . $self->loc( $canon->($self->OldValue) ) . "'", + "'" . $self->loc( $canon->($self->NewValue) ) . "'" + ); # loc() + } + } + + # Generic: + my $no_value = $self->loc("(no value)"); + return ( + "[_1] changed from [_2] to [_3]", + $self->Field, + ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ), + "'" . $self->NewValue . "'" + ); #loc() + }, + SystemError => sub { + my $self = shift; + return $self->Data // ("System error"); #loc() + }, + AttachmentTruncate => sub { + my $self = shift; + if ( defined $self->Data ) { + return ( "File '[_1]' truncated because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).", + $self->Data, $self->OldValue, $self->NewValue ); #loc() + } + else { + return ( "Content truncated because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).", + $self->OldValue, $self->NewValue ); #loc() + } + }, + AttachmentDrop => sub { + my $self = shift; + if ( defined $self->Data ) { + return ( "File '[_1]' dropped because its size ([_2] bytes) exceeded configured maximum size setting ([_3] bytes).", + $self->Data, $self->OldValue, $self->NewValue ); #loc() + } + else { + return ( "Content dropped because its size ([_1] bytes) exceeded configured maximum size setting ([_2] bytes).", + $self->OldValue, $self->NewValue ); #loc() + } + }, + AttachmentError => sub { + my $self = shift; + if ( defined $self->Data ) { + return ( "File '[_1]' insert failed. See error log for details.", $self->Data ); #loc() + } + else { + return ( "Content insert failed. See error log for details." ); #loc() + } + }, + "Forward Transaction" => sub { + my $self = shift; + my $recipients = join ", ", map { + RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser ) + } RT::EmailParser->ParseEmailAddress($self->Data); + + return ( "Forwarded [_3]Transaction #[_1][_4] to [_2]", + $self->Field, $recipients, + [\''], \''); #loc() + }, + "Forward Ticket" => sub { + my $self = shift; + my $recipients = join ", ", map { + RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser ) + } RT::EmailParser->ParseEmailAddress($self->Data); + + return ( "Forwarded Ticket to [_1]", $recipients ); #loc() + }, + CommentEmailRecord => sub { + my $self = shift; + return ("Outgoing email about a comment recorded"); #loc() + }, + EmailRecord => sub { + my $self = shift; + return ("Outgoing email recorded"); #loc() + }, + Correspond => sub { + my $self = shift; + return ("Correspondence added"); #loc() + }, + Comment => sub { + my $self = shift; + return ("Comments added"); #loc() + }, + CustomField => sub { + my $self = shift; + my $field = $self->loc('CustomField'); + + my $cf; + if ( $self->Field ) { + $cf = RT::CustomField->new( $self->CurrentUser ); + $cf->SetContextObject( $self->Object ); + $cf->Load( $self->Field ); + $field = $cf->Name(); + $field = $self->loc('a custom field') if !defined($field); + } + + my $new = $self->NewValue; + my $old = $self->OldValue; + + if ( $cf ) { + + if ( $cf->Type eq 'DateTime' ) { + if ($old) { + my $date = RT::Date->new( $self->CurrentUser ); + $date->Set( Format => 'ISO', Value => $old ); + $old = $date->AsString; + } + + if ($new) { + my $date = RT::Date->new( $self->CurrentUser ); + $date->Set( Format => 'ISO', Value => $new ); + $new = $date->AsString; + } + } + elsif ( $cf->Type eq 'Date' ) { + if ($old) { + my $date = RT::Date->new( $self->CurrentUser ); + $date->Set( + Format => 'unknown', + Value => $old, + Timezone => 'UTC', + ); + $old = $date->AsString( Time => 0, Timezone => 'UTC' ); + } + + if ($new) { + my $date = RT::Date->new( $self->CurrentUser ); + $date->Set( + Format => 'unknown', + Value => $new, + Timezone => 'UTC', + ); + $new = $date->AsString( Time => 0, Timezone => 'UTC' ); + } + } + } + + if ( !defined($old) || $old eq '' ) { + return ("[_1] [_2] added", $field, $new); #loc() + } + elsif ( !defined($new) || $new eq '' ) { + return ("[_1] [_2] deleted", $field, $old); #loc() + } + else { + return ("[_1] [_2] changed to [_3]", $field, $old, $new); #loc() + } + }, + Untake => sub { + my $self = shift; + return ("Untaken"); #loc() + }, + Take => sub { + my $self = shift; + return ("Taken"); #loc() + }, + Force => sub { + my $self = shift; + my $Old = RT::User->new( $self->CurrentUser ); + $Old->Load( $self->OldValue ); + my $New = RT::User->new( $self->CurrentUser ); + $New->Load( $self->NewValue ); + + return ("Owner forcibly changed from [_1] to [_2]", + map { $self->_FormatUser($_) } $Old, $New); #loc() + }, + Steal => sub { + my $self = shift; + my $Old = RT::User->new( $self->CurrentUser ); + $Old->Load( $self->OldValue ); + return ("Stolen from [_1]", $self->_FormatUser($Old)); #loc() + }, + Give => sub { + my $self = shift; + my $New = RT::User->new( $self->CurrentUser ); + $New->Load( $self->NewValue ); + return ( "Given to [_1]", $self->_FormatUser($New)); #loc() + }, + AddWatcher => sub { + my $self = shift; + my $principal = RT::Principal->new($self->CurrentUser); + $principal->Load($self->NewValue); + return ( "[_1] [_2] added", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc() + }, + DelWatcher => sub { + my $self = shift; + my $principal = RT::Principal->new($self->CurrentUser); + $principal->Load($self->OldValue); + return ( "[_1] [_2] deleted", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc() + }, + SetWatcher => sub { + my $self = shift; + my $principal = RT::Principal->new($self->CurrentUser); + $principal->Load($self->NewValue); + return ( "[_1] set to [_2]", $self->loc($self->Field), $self->_FormatPrincipal($principal)); #loc() + }, + Subject => sub { + my $self = shift; + return ( "Subject changed to [_1]", $self->Data ); #loc() + }, + AddLink => sub { + my $self = shift; + my $value; + if ( $self->NewValue ) { + my $URI = RT::URI->new( $self->CurrentUser ); + if ( $URI->FromURI( $self->NewValue ) ) { + $value = [ + \'', + $URI->AsString, + \'' + ]; + } + else { + $value = $self->NewValue; + } + + if ( $self->Field eq 'DependsOn' ) { + return ( "Dependency on [_1] added", $value ); #loc() + } + elsif ( $self->Field eq 'DependedOnBy' ) { + return ( "Dependency by [_1] added", $value ); #loc() + } + elsif ( $self->Field eq 'RefersTo' ) { + return ( "Reference to [_1] added", $value ); #loc() + } + elsif ( $self->Field eq 'ReferredToBy' ) { + return ( "Reference by [_1] added", $value ); #loc() + } + elsif ( $self->Field eq 'MemberOf' ) { + return ( "Membership in [_1] added", $value ); #loc() + } + elsif ( $self->Field eq 'HasMember' ) { + return ( "Member [_1] added", $value ); #loc() + } + elsif ( $self->Field eq 'MergedInto' ) { + return ( "Merged into [_1]", $value ); #loc() + } + } + else { + return ( "[_1]", $self->Data ); #loc() + } + }, + DeleteLink => sub { + my $self = shift; + my $value; + if ( $self->OldValue ) { + my $URI = RT::URI->new( $self->CurrentUser ); + if ( $URI->FromURI( $self->OldValue ) ) { + $value = [ + \'', + $URI->AsString, + \'' + ]; + } + else { + $value = $self->OldValue; + } + + if ( $self->Field eq 'DependsOn' ) { + return ( "Dependency on [_1] deleted", $value ); #loc() + } + elsif ( $self->Field eq 'DependedOnBy' ) { + return ( "Dependency by [_1] deleted", $value ); #loc() + } + elsif ( $self->Field eq 'RefersTo' ) { + return ( "Reference to [_1] deleted", $value ); #loc() + } + elsif ( $self->Field eq 'ReferredToBy' ) { + return ( "Reference by [_1] deleted", $value ); #loc() + } + elsif ( $self->Field eq 'MemberOf' ) { + return ( "Membership in [_1] deleted", $value ); #loc() + } + elsif ( $self->Field eq 'HasMember' ) { + return ( "Member [_1] deleted", $value ); #loc() + } + } + else { + return ( "[_1]", $self->Data ); #loc() + } + }, + Told => sub { + my $self = shift; + if ( $self->Field eq 'Told' ) { + my $t1 = RT::Date->new($self->CurrentUser); + $t1->Set(Format => 'ISO', Value => $self->NewValue); + my $t2 = RT::Date->new($self->CurrentUser); + $t2->Set(Format => 'ISO', Value => $self->OldValue); + return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc() + } + else { + return ( "[_1] changed from [_2] to [_3]", + $self->loc($self->Field), + ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" ); #loc() + } + }, + Set => sub { + my $self = shift; + if ( $self->Field eq 'Password' ) { + return ('Password changed'); #loc() + } + elsif ( $self->Field eq 'Queue' ) { + my $q1 = RT::Queue->new( $self->CurrentUser ); + $q1->Load( $self->OldValue ); + my $q2 = RT::Queue->new( $self->CurrentUser ); + $q2->Load( $self->NewValue ); + return ("[_1] changed from [_2] to [_3]", + $self->loc($self->Field), $q1->Name // '#'.$q1->id, $q2->Name // '#'.$q2->id); #loc() + } + + # Write the date/time change at local time: + elsif ($self->Field =~ /^(?:Due|Starts|Started|Told|WillResolve)$/) { + my $t1 = RT::Date->new($self->CurrentUser); + $t1->Set(Format => 'ISO', Value => $self->NewValue); + my $t2 = RT::Date->new($self->CurrentUser); + $t2->Set(Format => 'ISO', Value => $self->OldValue); + return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString ); #loc() + } + elsif ( $self->Field eq 'Owner' ) { + my $Old = RT::User->new( $self->CurrentUser ); + $Old->Load( $self->OldValue ); + my $New = RT::User->new( $self->CurrentUser ); + $New->Load( $self->NewValue ); + + if ( $Old->id == RT->Nobody->id ) { + if ( $New->id == $self->Creator ) { + return ("Taken"); #loc() + } + else { + return ( "Given to [_1]", $self->_FormatUser($New) ); #loc() + } + } + else { + if ( $New->id == $self->Creator ) { + return ("Stolen from [_1]", $self->_FormatUser($Old) ); #loc() + } + elsif ( $Old->id == $self->Creator ) { + if ( $New->id == RT->Nobody->id ) { + return ("Untaken"); #loc() + } + else { + return ( "Given to [_1]", $self->_FormatUser($New) ); #loc() + } + } + else { + return ( + "Owner forcibly changed from [_1] to [_2]", + map { $self->_FormatUser($_) } $Old, $New + ); #loc() + } + } + } + else { + return ( "[_1] changed from [_2] to [_3]", + $self->loc($self->Field), + ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")), + ($self->NewValue? "'".$self->NewValue ."'" : $self->loc("(no value)"))); #loc() + } + }, + "Set-TimeWorked" => sub { + my $self = shift; + my $old = $self->OldValue || 0; + my $new = $self->NewValue || 0; + my $duration = $new - $old; + if ($duration < 0) { + return ("Adjusted time worked by [quant,_1,minute,minutes]", $duration); # loc() + } + elsif ($duration < 60) { + return ("Worked [quant,_1,minute,minutes]", $duration); # loc() + } else { + return ("Worked [quant,_1,hour,hours] ([quant,_2,minute,minutes])", sprintf("%.1f", $duration / 60), $duration); # loc() + } + }, + PurgeTransaction => sub { + my $self = shift; + return ("Transaction [_1] purged", $self->Data); #loc() + }, + AddReminder => sub { + my $self = shift; + my $ticket = RT::Ticket->new($self->CurrentUser); + $ticket->Load($self->NewValue); + if ( $ticket->CurrentUserHasRight('ShowTicket') ) { + my $subject = [ + \'id, \'">', $ticket->Subject, \'' + ]; + return ("Reminder '[_1]' added", $subject); #loc() + } else { + return ("Reminder added"); #loc() + } + }, + OpenReminder => sub { + my $self = shift; + my $ticket = RT::Ticket->new($self->CurrentUser); + $ticket->Load($self->NewValue); + if ( $ticket->CurrentUserHasRight('ShowTicket') ) { + my $subject = [ + \'id, \'">', $ticket->Subject, \'' + ]; + return ("Reminder '[_1]' reopened", $subject); #loc() + } else { + return ("Reminder reopened"); #loc() + } + }, + ResolveReminder => sub { + my $self = shift; + my $ticket = RT::Ticket->new($self->CurrentUser); + $ticket->Load($self->NewValue); + if ( $ticket->CurrentUserHasRight('ShowTicket') ) { + my $subject = [ + \'id, \'">', $ticket->Subject, \'' + ]; + return ("Reminder '[_1]' completed", $subject); #loc() + } else { + return ("Reminder completed"); #loc() + } + }, + AddMember => sub { + my $self = shift; + my $principal = RT::Principal->new($self->CurrentUser); + $principal->Load($self->Field); + + if ($principal->IsUser) { + return ("Added user '[_1]'", $principal->Object->Name); #loc() + } + else { + return ("Added group '[_1]'", $principal->Object->Name); #loc() + } + }, + DeleteMember => sub { + my $self = shift; + my $principal = RT::Principal->new($self->CurrentUser); + $principal->Load($self->Field); + + if ($principal->IsUser) { + return ("Removed user '[_1]'", $principal->Object->Name); #loc() + } + else { + return ("Removed group '[_1]'", $principal->Object->Name); #loc() + } + }, + AddMembership => sub { + my $self = shift; + my $principal = RT::Principal->new($self->CurrentUser); + $principal->Load($self->Field); + return ("Added to group '[_1]'", $principal->Object->Name); #loc() + }, + DeleteMembership => sub { + my $self = shift; + my $principal = RT::Principal->new($self->CurrentUser); + $principal->Load($self->Field); + return ("Removed from group '[_1]'", $principal->Object->Name); #loc() + }, +); + -# {{{ Utility methods -# {{{ sub IsInbound =head2 IsInbound @@ -684,46 +1366,31 @@ Returns false otherwise =cut sub IsInbound { - my $self=shift; - return ($self->TicketObj->IsRequestor($self->CreatorObj)); + my $self = shift; + $self->ObjectType eq 'RT::Ticket' or return undef; + return ( $self->TicketObj->IsRequestor( $self->CreatorObj->PrincipalId ) ); } -# }}} -# }}} -# {{{ sub _Accessible +sub _OverlayAccessible { + { -sub _Accessible { - my $self = shift; - my %Cols = ( - TimeTaken => 'read', - Ticket => 'read/public', - Type=> 'read', - Field => 'read', - Data => 'read', - NewValue => 'read', - OldValue => 'read', - Creator => 'read/auto', - Created => 'read/auto', - ); - return $self->SUPER::_Accessible(@_, %Cols); -} + ObjectType => { public => 1}, + ObjectId => { public => 1}, + + } +}; -# }}} -# }}} -# {{{ sub _Set sub _Set { my $self = shift; - return(0, 'Transactions are immutable'); + return ( 0, $self->loc('Transactions are immutable') ); } -# }}} -# {{{ sub _Value =head2 _Value @@ -732,52 +1399,782 @@ Returns its value as a string, if the user passes an ACL check =cut -sub _Value { - - my $self = shift; +sub _Value { + my $self = shift; my $field = shift; - - + #if the field is public, return it. - if ($self->_Accessible($field, 'public')) { - return($self->__Value($field)); - - } - #If it's a comment, we need to be extra special careful - if ($self->__Value('Type') eq 'Comment') { - unless ($self->CurrentUserHasRight('ShowTicketComments')) { - return (undef); - } - } - #if they ain't got rights to see, don't let em - else { - unless ($self->CurrentUserHasRight('ShowTicket')) { - return (undef); - } - } - - return($self->__Value($field)); - + if ( $self->_Accessible( $field, 'public' ) ) { + return $self->SUPER::_Value( $field ); + } + + unless ( $self->CurrentUserCanSee ) { + return undef; + } + + return $self->SUPER::_Value( $field ); +} + + +=head2 CurrentUserCanSee + +Returns true if current user has rights to see this particular transaction. + +This fact depends on type of the transaction, type of an object the transaction +is attached to and may be other conditions, so this method is prefered over +custom implementations. + +It always returns true if current user is system user. + +=cut + +sub CurrentUserCanSee { + my $self = shift; + + return 1 if $self->CurrentUser->PrincipalObj->Id == RT->SystemUser->Id; + + # Make sure the user can see the custom field before showing that it changed + my $type = $self->__Value('Type'); + if ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) { + my $cf = RT::CustomField->new( $self->CurrentUser ); + $cf->SetContextObject( $self->Object ); + $cf->Load( $cf_id ); + return 0 unless $cf->CurrentUserHasRight('SeeCustomField'); + } + + # Transactions that might have changed the ->Object's visibility to + # the current user are marked readable + return 1 if $self->{ _object_is_readable }; + + # Defer to the object in question + return $self->Object->CurrentUserCanSee("Transaction", $self); +} + + +sub Ticket { + my $self = shift; + return $self->ObjectId; +} + +sub TicketObj { + my $self = shift; + return $self->Object; +} + +sub OldValue { + my $self = shift; + if ( my $Object = $self->OldReferenceObject ) { + return $Object->Content; + } + else { + return $self->_Value('OldValue'); + } +} + +sub NewValue { + my $self = shift; + if ( my $Object = $self->NewReferenceObject ) { + return $Object->Content; + } + else { + return $self->_Value('NewValue'); + } } -# }}} +sub Object { + my $self = shift; + my $Object = $self->__Value('ObjectType')->new($self->CurrentUser); + $Object->Load($self->__Value('ObjectId')); + return $Object; +} + +=head2 NewReferenceObject + +=head2 OldReferenceObject -# {{{ sub CurrentUserHasRight +Returns an object of the class specified by the column C and +loaded with the id specified by the column C or C. +C is assumed to be an L subclass. -=head2 CurrentUserHasRight RIGHT +The object may be unloaded (check C<< $object->id >>) if the reference is +corrupt (such as if the referenced record was improperly deleted). -Calls $self->CurrentUser->HasQueueRight for the right passed in here. -passed in here. +Returns undef if either C or C/C is +false. =cut -sub CurrentUserHasRight { +sub NewReferenceObject { $_[0]->_ReferenceObject("New") } +sub OldReferenceObject { $_[0]->_ReferenceObject("Old") } + +sub _ReferenceObject { + my $self = shift; + my $which = shift; + my $type = $self->__Value("ReferenceType"); + my $id = $self->__Value("${which}Reference"); + return unless $type and $id; + + my $object = $type->new($self->CurrentUser); + $object->Load( $id ); + return $object; +} + +sub FriendlyObjectType { my $self = shift; - my $right = shift; - return ($self->CurrentUser->HasQueueRight(Right => "$right", - TicketObj => $self->TicketObj)); + return $self->loc( $self->Object->RecordType ); +} + +=head2 UpdateCustomFields + +Takes a hash of: + + CustomField-C => Value + +or: + + Object-RT::Transaction-CustomField-C => Value + +parameters to update this transaction's custom fields. + +=cut + +sub UpdateCustomFields { + my $self = shift; + my %args = (@_); + + # This method used to have an API that took a hash of a single + # value "ARGSRef", which was a reference to a hash of arguments. + # This was insane. The next few lines of code preserve that API + # while giving us something saner. + my $args; + if ($args{'ARGSRef'}) { + RT->Deprecated( Arguments => "ARGSRef", Remove => "4.4" ); + $args = $args{ARGSRef}; + } else { + $args = \%args; + } + + foreach my $arg ( keys %$args ) { + next + unless ( $arg =~ + /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ ); + next if $arg =~ /-Magic$/; + next if $arg =~ /-TimeUnits$/; + my $cfid = $1; + my $values = $args->{$arg}; + my $cf = $self->LoadCustomFieldByIdentifier($cfid); + next unless $cf->ObjectTypeFromLookupType($cf->__Value('LookupType'))->isa(ref $self); + foreach + my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values ) + { + next unless (defined($value) && length($value)); + $self->_AddCustomFieldValue( + Field => $cfid, + Value => $value, + RecordTransaction => 0, + ); + } + } +} + +=head2 LoadCustomFieldByIdentifier + +Finds and returns the custom field of the given name for the +transaction, overriding L to +look for queue-specific CFs before global ones. + +=cut + +sub LoadCustomFieldByIdentifier { + my $self = shift; + my $field = shift; + + return $self->SUPER::LoadCustomFieldByIdentifier($field) + if ref $field or $field =~ /^\d+$/; + + return $self->SUPER::LoadCustomFieldByIdentifier($field) + unless UNIVERSAL::can( $self->Object, 'QueueObj' ); + + my $CFs = RT::CustomFields->new( $self->CurrentUser ); + $CFs->SetContextObject( $self->Object ); + $CFs->Limit( FIELD => 'Name', VALUE => $field, CASESENSITIVE => 0 ); + $CFs->LimitToLookupType($self->CustomFieldLookupType); + $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id); + return $CFs->First || RT::CustomField->new( $self->CurrentUser ); +} + +=head2 CustomFieldLookupType + +Returns the RT::Transaction lookup type, which can +be passed to RT::CustomField->Create() via the 'LookupType' hash key. + +=cut + + +sub CustomFieldLookupType { + "RT::Queue-RT::Ticket-RT::Transaction"; +} + + +=head2 SquelchMailTo + +Similar to Ticket class SquelchMailTo method - returns a list of +transaction's squelched addresses. As transactions are immutable, the +list of squelched recipients cannot be modified after creation. + +=cut + +sub SquelchMailTo { + my $self = shift; + return () unless $self->CurrentUserCanSee; + return $self->Attributes->Named('SquelchMailTo'); +} + +=head2 Recipients + +Returns the list of email addresses (as L objects) +that this transaction would send mail to. There may be duplicates. + +=cut + +sub Recipients { + my $self = shift; + my @recipients; + foreach my $scrip ( @{ $self->Scrips->Prepared } ) { + my $action = $scrip->ActionObj->Action; + next unless $action->isa('RT::Action::SendEmail'); + + foreach my $type (qw(To Cc Bcc)) { + push @recipients, $action->$type(); + } + } + + if ( $self->Rules ) { + for my $rule (@{$self->Rules}) { + next unless $rule->{hints} && $rule->{hints}{class} eq 'SendEmail'; + my $data = $rule->{hints}{recipients}; + foreach my $type (qw(To Cc Bcc)) { + push @recipients, map {Email::Address->new($_)} @{$data->{$type}}; + } + } + } + return @recipients; +} + +=head2 DeferredRecipients($freq, $include_sent ) + +Takes the following arguments: + +=over + +=item * a string to indicate the frequency of digest delivery. Valid values are "daily", "weekly", or "susp". + +=item * an optional argument which, if true, will return addresses even if this notification has been marked as 'sent' for this transaction. + +=back + +Returns an array of users who should now receive the notification that +was recorded in this transaction. Returns an empty array if there were +no deferred users, or if $include_sent was not specified and the deferred +notifications have been sent. + +=cut + +sub DeferredRecipients { + my $self = shift; + my $freq = shift; + my $include_sent = @_? shift : 0; + + my $attr = $self->FirstAttribute('DeferredRecipients'); + + return () unless ($attr); + + my $deferred = $attr->Content; + + return () unless ( ref($deferred) eq 'HASH' && exists $deferred->{$freq} ); + + # Skip it. + + for my $user (keys %{$deferred->{$freq}}) { + if ($deferred->{$freq}->{$user}->{_sent} && !$include_sent) { + delete $deferred->{$freq}->{$user} + } + } + # Now get our users. Easy. + + return keys %{ $deferred->{$freq} }; +} + + + +# Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets. +sub _CacheConfig { + { + 'cache_for_sec' => 6000, + } +} + + +=head2 ACLEquivalenceObjects + +This method returns a list of objects for which a user's rights also apply +to this Transaction. + +This currently only applies to Transaction Custom Fields on Tickets, so we return +the Ticket's Queue and the Ticket. + +This method is called from L. + +=cut + +sub ACLEquivalenceObjects { + my $self = shift; + + return unless $self->ObjectType eq 'RT::Ticket'; + my $object = $self->Object; + return $object,$object->QueueObj; + +} + + + + + +=head2 id + +Returns the current value of id. +(In the database, id is stored as int(11).) + + +=cut + + +=head2 ObjectType + +Returns the current value of ObjectType. +(In the database, ObjectType is stored as varchar(64).) + + + +=head2 SetObjectType VALUE + + +Set ObjectType to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, ObjectType will be stored as a varchar(64).) + + +=cut + + +=head2 ObjectId + +Returns the current value of ObjectId. +(In the database, ObjectId is stored as int(11).) + + + +=head2 SetObjectId VALUE + + +Set ObjectId to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, ObjectId will be stored as a int(11).) + + +=cut + + +=head2 TimeTaken + +Returns the current value of TimeTaken. +(In the database, TimeTaken is stored as int(11).) + + + +=head2 SetTimeTaken VALUE + + +Set TimeTaken to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, TimeTaken will be stored as a int(11).) + + +=cut + + +=head2 Type + +Returns the current value of Type. +(In the database, Type is stored as varchar(20).) + + + +=head2 SetType VALUE + + +Set Type to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Type will be stored as a varchar(20).) + + +=cut + + +=head2 Field + +Returns the current value of Field. +(In the database, Field is stored as varchar(40).) + + + +=head2 SetField VALUE + + +Set Field to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Field will be stored as a varchar(40).) + + +=cut + + +=head2 OldValue + +Returns the current value of OldValue. +(In the database, OldValue is stored as varchar(255).) + + + +=head2 SetOldValue VALUE + + +Set OldValue to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, OldValue will be stored as a varchar(255).) + + +=cut + + +=head2 NewValue + +Returns the current value of NewValue. +(In the database, NewValue is stored as varchar(255).) + + + +=head2 SetNewValue VALUE + + +Set NewValue to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, NewValue will be stored as a varchar(255).) + + +=cut + + +=head2 ReferenceType + +Returns the current value of ReferenceType. +(In the database, ReferenceType is stored as varchar(255).) + + + +=head2 SetReferenceType VALUE + + +Set ReferenceType to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, ReferenceType will be stored as a varchar(255).) + + +=cut + + +=head2 OldReference + +Returns the current value of OldReference. +(In the database, OldReference is stored as int(11).) + + + +=head2 SetOldReference VALUE + + +Set OldReference to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, OldReference will be stored as a int(11).) + + +=cut + + +=head2 NewReference + +Returns the current value of NewReference. +(In the database, NewReference is stored as int(11).) + + + +=head2 SetNewReference VALUE + + +Set NewReference to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, NewReference will be stored as a int(11).) + + +=cut + + +=head2 Data + +Returns the current value of Data. +(In the database, Data is stored as varchar(255).) + + + +=head2 SetData VALUE + + +Set Data to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Data will be stored as a varchar(255).) + + +=cut + + +=head2 Creator + +Returns the current value of Creator. +(In the database, Creator is stored as int(11).) + + +=cut + + +=head2 Created + +Returns the current value of Created. +(In the database, Created is stored as datetime.) + + +=cut + + + +sub _CoreAccessible { + { + + id => + {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, + ObjectType => + {read => 1, write => 1, sql_type => 12, length => 64, is_blob => 0, is_numeric => 0, type => 'varchar(64)', default => ''}, + ObjectId => + {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, + TimeTaken => + {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, + Type => + {read => 1, write => 1, sql_type => 12, length => 20, is_blob => 0, is_numeric => 0, type => 'varchar(20)', default => ''}, + Field => + {read => 1, write => 1, sql_type => 12, length => 40, is_blob => 0, is_numeric => 0, type => 'varchar(40)', default => ''}, + OldValue => + {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, + NewValue => + {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, + ReferenceType => + {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, + OldReference => + {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, + NewReference => + {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''}, + Data => + {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''}, + Creator => + {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'}, + Created => + {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''}, + + } +}; + +sub FindDependencies { + my $self = shift; + my ($walker, $deps) = @_; + + $self->SUPER::FindDependencies($walker, $deps); + + $deps->Add( out => $self->Object ); + $deps->Add( in => $self->Attachments ); + + my $type = $self->Type; + if ($type eq "CustomField") { + my $cf = RT::CustomField->new( RT->SystemUser ); + $cf->Load( $self->Field ); + $deps->Add( out => $cf ); + } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) { + for my $field (qw/OldValue NewValue/) { + my $user = RT::User->new( RT->SystemUser ); + $user->Load( $self->$field ); + $deps->Add( out => $user ); + } + } elsif ($type eq "DelWatcher") { + my $principal = RT::Principal->new( RT->SystemUser ); + $principal->Load( $self->OldValue ); + $deps->Add( out => $principal->Object ); + } elsif ($type eq "AddWatcher") { + my $principal = RT::Principal->new( RT->SystemUser ); + $principal->Load( $self->NewValue ); + $deps->Add( out => $principal->Object ); + } elsif ($type eq "DeleteLink") { + if ($self->OldValue) { + my $base = RT::URI->new( $self->CurrentUser ); + $base->FromURI( $self->OldValue ); + $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object; + } + } elsif ($type eq "AddLink") { + if ($self->NewValue) { + my $base = RT::URI->new( $self->CurrentUser ); + $base->FromURI( $self->NewValue ); + $deps->Add( out => $base->Object ) if $base->Resolver and $base->Object; + } + } elsif ($type eq "Set" and $self->Field eq "Queue") { + for my $field (qw/OldValue NewValue/) { + my $queue = RT::Queue->new( RT->SystemUser ); + $queue->Load( $self->$field ); + $deps->Add( out => $queue ); + } + } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) { + my $ticket = RT::Ticket->new( RT->SystemUser ); + $ticket->Load( $self->NewValue ); + $deps->Add( out => $ticket ); + } +} + +sub __DependsOn { + my $self = shift; + my %args = ( + Shredder => undef, + Dependencies => undef, + @_, + ); + my $deps = $args{'Dependencies'}; + + $deps->_PushDependencies( + BaseObject => $self, + Flags => RT::Shredder::Constants::DEPENDS_ON, + TargetObjects => $self->Attachments, + Shredder => $args{'Shredder'} + ); + + return $self->SUPER::__DependsOn( %args ); +} + +sub Serialize { + my $self = shift; + my %args = (@_); + my %store = $self->SUPER::Serialize(@_); + + my $type = $store{Type}; + if ($type eq "CustomField") { + my $cf = RT::CustomField->new( RT->SystemUser ); + $cf->Load( $store{Field} ); + $store{Field} = \($cf->UID); + + $store{OldReference} = \($self->OldReferenceObject->UID) if $self->OldReference; + $store{NewReference} = \($self->NewReferenceObject->UID) if $self->NewReference; + } elsif ($type =~ /^(Take|Untake|Force|Steal|Give)$/) { + for my $field (qw/OldValue NewValue/) { + my $user = RT::User->new( RT->SystemUser ); + $user->Load( $store{$field} ); + $store{$field} = \($user->UID); + } + } elsif ($type eq "DelWatcher") { + my $principal = RT::Principal->new( RT->SystemUser ); + $principal->Load( $store{OldValue} ); + $store{OldValue} = \($principal->UID); + } elsif ($type eq "AddWatcher") { + my $principal = RT::Principal->new( RT->SystemUser ); + $principal->Load( $store{NewValue} ); + $store{NewValue} = \($principal->UID); + } elsif ($type eq "DeleteLink") { + if ($store{OldValue}) { + my $base = RT::URI->new( $self->CurrentUser ); + $base->FromURI( $store{OldValue} ); + if ($base->Resolver && (my $object = $base->Object)) { + if ($args{serializer}->Observe(object => $object)) { + $store{OldValue} = \($object->UID); + } + elsif ($args{serializer}{HyperlinkUnmigrated}) { + $store{OldValue} = $base->AsHREF; + } + else { + $store{OldValue} = "(not migrated)"; + } + } + } + } elsif ($type eq "AddLink") { + if ($store{NewValue}) { + my $base = RT::URI->new( $self->CurrentUser ); + $base->FromURI( $store{NewValue} ); + if ($base->Resolver && (my $object = $base->Object)) { + if ($args{serializer}->Observe(object => $object)) { + $store{NewValue} = \($object->UID); + } + elsif ($args{serializer}{HyperlinkUnmigrated}) { + $store{NewValue} = $base->AsHREF; + } + else { + $store{NewValue} = "(not migrated)"; + } + } + } + } elsif ($type eq "Set" and $store{Field} eq "Queue") { + for my $field (qw/OldValue NewValue/) { + my $queue = RT::Queue->new( RT->SystemUser ); + $queue->Load( $store{$field} ); + if ($args{serializer}->Observe(object => $queue)) { + $store{$field} = \($queue->UID); + } + else { + $store{$field} = "$RT::Organization: " . $queue->Name . " (not migrated)"; + + } + } + } elsif ($type =~ /^(Add|Open|Resolve)Reminder$/) { + my $ticket = RT::Ticket->new( RT->SystemUser ); + $ticket->Load( $store{NewValue} ); + $store{NewValue} = \($ticket->UID); + } + + return %store; +} + +sub PreInflate { + my $class = shift; + my ($importer, $uid, $data) = @_; + + if ($data->{Object} and ref $data->{Object}) { + my $on_uid = ${ $data->{Object} }; + return if $importer->ShouldSkipTransaction($on_uid); + } + + if ($data->{Type} eq "DeleteLink" and ref $data->{OldValue}) { + my $uid = ${ $data->{OldValue} }; + my $obj = $importer->LookupObj( $uid ); + $data->{OldValue} = $obj->URI; + } elsif ($data->{Type} eq "AddLink" and ref $data->{NewValue}) { + my $uid = ${ $data->{NewValue} }; + my $obj = $importer->LookupObj( $uid ); + $data->{NewValue} = $obj->URI; + } + + return $class->SUPER::PreInflate( $importer, $uid, $data ); } -# }}} +RT::Base->_ImportOverlays(); 1;