rt 4.2.16
[freeside.git] / rt / lib / RT / Transaction.pm
index 48d4e8c..52f3abf 100755 (executable)
@@ -2,7 +2,7 @@
 #
 # COPYRIGHT:
 #
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
@@ -82,9 +82,12 @@ use RT::Attachments;
 use RT::Scrips;
 use RT::Ruleset;
 
 use RT::Scrips;
 use RT::Ruleset;
 
-use HTML::FormatText;
-use HTML::TreeBuilder;
+use HTML::FormatText::WithLinks::AndTables;
+use HTML::Scrubber;
 
 
+# For EscapeHTML() and decode_entities()
+require RT::Interface::Web;
+require HTML::Entities;
 
 sub Table {'Transactions'}
 
 
 sub Table {'Transactions'}
 
@@ -141,11 +144,11 @@ sub Create {
         OldValue  => $args{'OldValue'},
         NewValue  => $args{'NewValue'},
         Created   => $args{'Created'},
         OldValue  => $args{'OldValue'},
         NewValue  => $args{'NewValue'},
         Created   => $args{'Created'},
-       ObjectType => $args{'ObjectType'},
-       ObjectId => $args{'ObjectId'},
-       ReferenceType => $args{'ReferenceType'},
-       OldReference => $args{'OldReference'},
-       NewReference => $args{'NewReference'},
+        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
     );
 
     # Parameters passed in during an import that we probably don't want to touch, otherwise
@@ -173,41 +176,47 @@ sub Create {
         Content => RT::User->CanonicalizeEmailAddress($_)
     ) for @{$args{'SquelchMailTo'} || []};
 
         Content => RT::User->CanonicalizeEmailAddress($_)
     ) for @{$args{'SquelchMailTo'} || []};
 
-    #Provide a way to turn off scrips if we need to
-        $RT::Logger->debug('About to think about scrips for transaction #' .$self->Id);
-    if ( $args{'ActivateScrips'} and $args{'ObjectType'} eq 'RT::Ticket' ) {
-       $self->{'scrips'} = RT::Scrips->new(RT->SystemUser);
+    my @return = ( $id, $self->loc("Transaction Created") );
 
 
-        $RT::Logger->debug('About to prepare scrips for transaction #' .$self->Id); 
+    return @return unless $args{'ObjectType'} eq 'RT::Ticket';
 
 
-        $self->{'scrips'}->Prepare(
-            Stage       => 'TransactionCreate',
-            Type        => $args{'Type'},
-            Ticket      => $args{'ObjectId'},
-            Transaction => $self->id,
-        );
+    # 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;
+    }
 
 
-       # 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);
-        }
+    $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 ( $id, $self->loc("Transaction Created") );
+    return @return;
 }
 
 
 }
 
 
@@ -305,11 +314,25 @@ sub 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) : () );
+}
+
+
+
 =head2 Content PARAMHASH
 
 If this transaction has attached mime objects, returns the body of the first
 textual part (as defined in RT::I18N::IsTextualContentType).  Otherwise,
 =head2 Content PARAMHASH
 
 If this transaction has attached mime objects, returns the body of the first
 textual part (as defined in RT::I18N::IsTextualContentType).  Otherwise,
-returns undef.
+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 $RT::MessageBoxWidth - 2 or 70.
 
 Takes a paramhash.  If the $args{'Quote'} parameter is set, wraps this message 
 at $args{'Wrap'}.  $args{'Wrap'} defaults to $RT::MessageBoxWidth - 2 or 70.
@@ -333,21 +356,28 @@ sub Content {
     );
 
     my $content;
     );
 
     my $content;
-    if ( my $content_obj =
-        $self->ContentObj( $args{Type} ? ( Type => $args{Type} ) : () ) )
+    if ( my $content_obj = 
+        $self->ContentObj( $args{Type} ? ( Type => $args{Type}) : () ) )
     {
         $content = $content_obj->Content ||'';
 
         if ( lc $content_obj->ContentType eq 'text/html' ) {
     {
         $content = $content_obj->Content ||'';
 
         if ( lc $content_obj->ContentType eq 'text/html' ) {
-            $content =~ s/<p>--\s+<br \/>.*?$//s if $args{'Quote'};
+            $content =~ s/(?:(<\/div>)|<p>|<br\s*\/?>|<div(\s+class="[^"]+")?>)\s*--\s+<br\s*\/?>.*?$/$1/s if $args{'Quote'};
 
             if ($args{Type} ne 'text/html') {
 
             if ($args{Type} ne 'text/html') {
-                my $tree = HTML::TreeBuilder->new_from_content( $content );
-                $content = HTML::FormatText->new(
-                    leftmargin  => 0,
-                    rightmargin => 78,
-                )->format( $tree);
-                $tree->delete;
+                $content = RT::Interface::Email::ConvertHTMLToText($content);
+            } else {
+                # Scrub out <html>, <head>, <meta>, and <body>, 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 {
             }
         }
         else {
@@ -357,7 +387,7 @@ sub Content {
                 $content =~ s/&/&#38;/g;
                 $content =~ s/</&lt;/g;
                 $content =~ s/>/&gt;/g;
                 $content =~ s/&/&#38;/g;
                 $content =~ s/</&lt;/g;
                 $content =~ s/>/&gt;/g;
-                $content = "<pre>$content</pre>";
+                $content = qq|<pre style="white-space: pre-wrap; font-family: monospace;">$content</pre>|;
             }
         }
     }
             }
         }
     }
@@ -368,25 +398,18 @@ sub Content {
     }
 
     if ( $args{'Quote'} ) {
     }
 
     if ( $args{'Quote'} ) {
-
-        # What's the longest line like?
-        my $max = 0;
-        foreach ( split ( /\n/, $content ) ) {
-            $max = length if length > $max;
+        if ($args{Type} eq 'text/html') {
+            $content = '<div class="gmail_quote">'
+                . $self->QuoteHeader
+                . '<br /><blockquote class="gmail_quote" type="cite">'
+                . $content
+                . '</blockquote></div><br /><br />';
+        } else {
+            $content = $self->ApplyQuoteWrap(content => $content,
+                                             cols    => $args{'Wrap'} );
+
+            $content = $self->QuoteHeader . "\n$content\n\n";
         }
         }
-
-        if ( $max > $args{'Wrap'}+6 ) { # 76 ) {
-            require Text::Wrapper;
-            my $wrapper = Text::Wrapper->new(
-                columns    => $args{'Wrap'},
-                body_start => ( $max > 70 * 3 ? '   ' : '' ),
-                par_start  => ''
-            );
-            $content = $wrapper->wrap($content);
-        }
-
-        $content =~ s/^/> /gm;
-        $content = $self->QuoteHeader . "\n$content\n\n";
     }
 
     return ($content);
     }
 
     return ($content);
@@ -405,6 +428,84 @@ sub QuoteHeader {
     return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
 }
 
     return $self->loc("On [_1], [_2] wrote:", $self->CreatedAsString, $self->CreatorObj->Name);
 }
 
+=head2 ApplyQuoteWrap PARAMHASH
+
+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
 
 
 =head2 Addresses
 
@@ -413,14 +514,14 @@ Returns a hashref of addresses related to this transaction. See L<RT::Attachment
 =cut
 
 sub Addresses {
 =cut
 
 sub Addresses {
-       my $self = shift;
+    my $self = shift;
 
 
-       if (my $attach = $self->Attachments->First) {   
-               return $attach->Addresses;
-       }
-       else {
-               return {};
-       }
+    if (my $attach = $self->Attachments->First) {
+        return $attach->Addresses;
+    }
+    else {
+        return {};
+    }
 
 }
 
 
 }
 
@@ -446,8 +547,36 @@ sub ContentObj {
 
     return undef unless ($Attachment);
 
 
     return undef unless ($Attachment);
 
+    my $Attachments = $self->Attachments;
+    while ( my $Attachment = $Attachments->Next ) {
+        if ( my $content = _FindPreferredContentObj( %args, Attachment => $Attachment ) ) {
+            return $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;
+    }
+
+    return;
+}
+
+
+sub _FindPreferredContentObj {
+    my %args = @_;
+    my $Attachment = $args{Attachment};
+
+    # If we don't have any content, return undef now.
+    return undef unless $Attachment;
+
     # If it's a textual part, just return the body.
     # If it's a textual part, just return the body.
-    if ( RT::I18N::IsTextualContentType($Attachment->ContentType) ) {
+    if ( _IsDisplayableTextualContentType($Attachment->ContentType) ) {
         return ($Attachment);
     }
 
         return ($Attachment);
     }
 
@@ -457,7 +586,7 @@ sub ContentObj {
     elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
         my $kids = $Attachment->Children;
         while (my $child = $kids->Next) {
     elsif ( $Attachment->ContentType =~ m|^multipart/mixed|i ) {
         my $kids = $Attachment->Children;
         while (my $child = $kids->Next) {
-            my $ret =  $self->ContentObj(%args, Attachment => $child);
+            my $ret =  _FindPreferredContentObj(%args, Attachment => $child);
             return $ret if ($ret);
         }
     }
             return $ret if ($ret);
         }
     }
@@ -471,14 +600,28 @@ sub ContentObj {
             if ( my $first = $plain_parts->First ) {
                 return $first;
             }
             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
 
 
-        # If that fails, return the first textual part which has some content.
-        my $all_parts = $self->Attachments;
-        while ( my $part = $all_parts->Next ) {
-            next unless RT::I18N::IsTextualContentType($part->ContentType)
-                        && $part->Content;
-            return $part;
+    elsif ( $Attachment->ContentType =~ '^message/rfc822' ) {
+        my $children = $Attachment->Children;
+        while ( my $child = $children->Next ) {
+            if ( my $content = _FindPreferredContentObj( %args, Attachment => $child ) ) {
+                return $content;
+            }
         }
     }
 
         }
     }
 
@@ -486,6 +629,18 @@ sub ContentObj {
     return (undef);
 }
 
     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;
+}
 
 
 =head2 Subject
 
 
 =head2 Subject
@@ -621,112 +776,216 @@ Returns a text string which briefly describes this transaction
 
 =cut
 
 
 =cut
 
-sub BriefDescription {
+{
+    my $scrubber = HTML::Scrubber->new(default => 0); # deny everything
+
+    sub BriefDescription {
+        my $self = shift;
+        my $desc = $self->BriefDescriptionAsHTML;
+           $desc = $scrubber->scrub($desc);
+           $desc = HTML::Entities::decode_entities($desc);
+        return $desc;
+    }
+}
+
+=head2 BriefDescriptionAsHTML
+
+Returns an HTML string which briefly describes this transaction.
+
+=cut
+
+sub BriefDescriptionAsHTML {
     my $self = shift;
 
     unless ( $self->CurrentUserCanSee ) {
         return ( $self->loc("Permission Denied") );
     }
 
     my $self = shift;
 
     unless ( $self->CurrentUserCanSee ) {
         return ( $self->loc("Permission Denied") );
     }
 
-    my $type = $self->Type;    #cache this, rather than calling it 30 times
+    my ($objecttype, $type, $field) = ($self->ObjectType, $self->Type, $self->Field);
 
     unless ( defined $type ) {
         return $self->loc("No transaction type specified");
     }
 
 
     unless ( defined $type ) {
         return $self->loc("No transaction type specified");
     }
 
-    my $obj_type = $self->FriendlyObjectType;
+    my ($template, @params);
+
+    my @code = grep { ref eq 'CODE' } map { $_BriefDescriptions{$_} }
+        ( $field
+            ? ("$objecttype-$type-$field", "$type-$field")
+            : () ),
+        "$objecttype-$type", $type;
 
 
-    if ( $type eq 'Create' ) {
-        return ( $self->loc( "[_1] created", $obj_type ) );
+    if (@code) {
+        ($template, @params) = $code[0]->($self);
     }
     }
-    elsif ( $type eq 'Enabled' ) {
-        return ( $self->loc( "[_1] enabled", $obj_type ) );
+
+    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)")
+            ),
+        );
     }
     }
-    elsif ( $type eq 'Disabled' ) {
-        return ( $self->loc( "[_1] disabled", $obj_type ) );
+    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);
     }
     }
-    elsif ( $type =~ /Status/ ) {
+}
+
+sub _FormatUser {
+    my $self = shift;
+    my $user = shift;
+    return [
+        \'<span class="user" data-replace="user" data-user-id="', $user->id, \'">',
+        $user->Format,
+        \'</span>'
+    ];
+}
+
+%_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' ) {
         if ( $self->Field eq 'Status' ) {
             if ( $self->NewValue eq 'deleted' ) {
-                return ( $self->loc( "[_1] deleted", $obj_type ) );
+                return ( "[_1] deleted", $self->FriendlyObjectType );   #loc()
             }
             else {
             }
             else {
-                my $canon = $self->Object->can("QueueObj")
-                    ? sub { $self->Object->QueueObj->Lifecycle->CanonicalCase(@_) }
+                my $canon = $self->Object->DOES("RT::Record::Role::Status")
+                    ? sub { $self->Object->LifecycleObj->CanonicalCase(@_) }
                     : sub { return $_[0] };
                 return (
                     : sub { return $_[0] };
                 return (
-                    $self->loc(
-                        "Status changed from [_1] to [_2]",
-                        "'" . $self->loc( $canon->($self->OldValue) ) . "'",
-                        "'" . $self->loc( $canon->($self->NewValue) ) . "'"
-                    )
-                );
-
+                    "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 (
             }
         }
 
         # Generic:
         my $no_value = $self->loc("(no value)");
         return (
-            $self->loc(
-                "[_1] changed from [_2] to [_3]",
-                $self->Field,
-                ( $self->OldValue ? "'" . $self->OldValue . "'" : $no_value ),
-                "'" . $self->NewValue . "'"
-            )
-        );
-    }
-    elsif ( $type =~ /SystemError/ ) {
-        return $self->loc("System error");
-    }
-    elsif ( $type =~ /Forward Transaction/ ) {
-        return $self->loc( "Forwarded Transaction #[_1] to [_2]",
-            $self->Field, $self->Data );
-    }
-    elsif ( $type =~ /Forward Ticket/ ) {
-        return $self->loc( "Forwarded Ticket to [_1]", $self->Data );
-    }
-
-    if ( my $code = $_BriefDescriptions{$type} ) {
-        return $code->($self);
-    }
+            "[_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 $self->loc(
-        "Default: [_1]/[_2] changed from [_3] to [_4]",
-        $type,
-        $self->Field,
-        (
-            $self->OldValue
-            ? "'" . $self->OldValue . "'"
-            : $self->loc("(no value)")
-        ),
-        "'" . $self->NewValue . "'"
-    );
-}
+        return ( "Forwarded [_3]Transaction #[_1][_4] to [_2]",
+            $self->Field, $recipients,
+            [\'<a href="#txn-', $self->Field, \'">'], \'</a>'); #loc()
+    },
+    "Forward Ticket" => sub {
+        my $self = shift;
+        my $recipients = join ", ", map {
+            RT::User->Format( Address => $_, CurrentUser => $self->CurrentUser )
+        } RT::EmailParser->ParseEmailAddress($self->Data);
 
 
-%_BriefDescriptions = (
+        return ( "Forwarded Ticket to [_1]", $recipients ); #loc()
+    },
     CommentEmailRecord => sub {
         my $self = shift;
     CommentEmailRecord => sub {
         my $self = shift;
-        return $self->loc("Outgoing email about a comment recorded");
+        return ("Outgoing email about a comment recorded"); #loc()
     },
     EmailRecord => sub {
         my $self = shift;
     },
     EmailRecord => sub {
         my $self = shift;
-        return $self->loc("Outgoing email recorded");
+        return ("Outgoing email recorded"); #loc()
     },
     Correspond => sub {
         my $self = shift;
     },
     Correspond => sub {
         my $self = shift;
-        return $self->loc("Correspondence added");
+        return ("Correspondence added");    #loc()
     },
     Comment => sub {
         my $self = shift;
     },
     Comment => sub {
         my $self = shift;
-        return $self->loc("Comments added");
+        return ("Comments added");          #loc()
     },
     CustomField => sub {
         my $self = shift;
         my $field = $self->loc('CustomField');
 
     },
     CustomField => sub {
         my $self = shift;
         my $field = $self->loc('CustomField');
 
+        my $cf;
         if ( $self->Field ) {
         if ( $self->Field ) {
-            my $cf = RT::CustomField->new( $self->CurrentUser );
+            $cf = RT::CustomField->new( $self->CurrentUser );
             $cf->SetContextObject( $self->Object );
             $cf->Load( $self->Field );
             $field = $cf->Name();
             $cf->SetContextObject( $self->Object );
             $cf->Load( $self->Field );
             $field = $cf->Name();
@@ -736,23 +995,61 @@ sub BriefDescription {
         my $new = $self->NewValue;
         my $old = $self->OldValue;
 
         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 '' ) {
         if ( !defined($old) || $old eq '' ) {
-            return $self->loc("[_1] [_2] added", $field, $new);
+            return ("[_1] [_2] added", $field, $new);   #loc()
         }
         elsif ( !defined($new) || $new eq '' ) {
         }
         elsif ( !defined($new) || $new eq '' ) {
-            return $self->loc("[_1] [_2] deleted", $field, $old);
+            return ("[_1] [_2] deleted", $field, $old); #loc()
         }
         else {
         }
         else {
-            return $self->loc("[_1] [_2] changed to [_3]", $field, $old, $new);
+            return ("[_1] [_2] changed to [_3]", $field, $old, $new);   #loc()
         }
     },
     Untake => sub {
         my $self = shift;
         }
     },
     Untake => sub {
         my $self = shift;
-        return $self->loc("Untaken");
+        return ("Untaken"); #loc()
     },
     Take => sub {
         my $self = shift;
     },
     Take => sub {
         my $self = shift;
-        return $self->loc("Taken");
+        return ("Taken"); #loc()
     },
     Force => sub {
         my $self = shift;
     },
     Force => sub {
         my $self = shift;
@@ -761,35 +1058,42 @@ sub BriefDescription {
         my $New = RT::User->new( $self->CurrentUser );
         $New->Load( $self->NewValue );
 
         my $New = RT::User->new( $self->CurrentUser );
         $New->Load( $self->NewValue );
 
-        return $self->loc("Owner forcibly changed from [_1] to [_2]" , $Old->Name , $New->Name);
+        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 );
     },
     Steal => sub {
         my $self = shift;
         my $Old = RT::User->new( $self->CurrentUser );
         $Old->Load( $self->OldValue );
-        return $self->loc("Stolen from [_1]",  $Old->Name);
+        return ("Stolen from [_1]", $self->_FormatUser($Old));   #loc()
     },
     Give => sub {
         my $self = shift;
         my $New = RT::User->new( $self->CurrentUser );
         $New->Load( $self->NewValue );
     },
     Give => sub {
         my $self = shift;
         my $New = RT::User->new( $self->CurrentUser );
         $New->Load( $self->NewValue );
-        return $self->loc( "Given to [_1]",  $New->Name );
+        return ( "Given to [_1]", $self->_FormatUser($New));    #loc()
     },
     AddWatcher => sub {
         my $self = shift;
         my $principal = RT::Principal->new($self->CurrentUser);
         $principal->Load($self->NewValue);
     },
     AddWatcher => sub {
         my $self = shift;
         my $principal = RT::Principal->new($self->CurrentUser);
         $principal->Load($self->NewValue);
-        return $self->loc( "[_1] [_2] added", $self->Field, $principal->Object->Name);
+        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);
     },
     DelWatcher => sub {
         my $self = shift;
         my $principal = RT::Principal->new($self->CurrentUser);
         $principal->Load($self->OldValue);
-        return $self->loc( "[_1] [_2] deleted", $self->Field, $principal->Object->Name);
+        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;
     },
     Subject => sub {
         my $self = shift;
-        return $self->loc( "Subject changed to [_1]", $self->Data );
+        return ( "Subject changed to [_1]", $self->Data );  #loc()
     },
     AddLink => sub {
         my $self = shift;
     },
     AddLink => sub {
         my $self = shift;
@@ -797,36 +1101,40 @@ sub BriefDescription {
         if ( $self->NewValue ) {
             my $URI = RT::URI->new( $self->CurrentUser );
             if ( $URI->FromURI( $self->NewValue ) ) {
         if ( $self->NewValue ) {
             my $URI = RT::URI->new( $self->CurrentUser );
             if ( $URI->FromURI( $self->NewValue ) ) {
-                $value = $URI->Resolver->AsString;
+                $value = [
+                    \'<a href="', $URI->AsHREF, \'">',
+                    $URI->AsString,
+                    \'</a>'
+                ];
             }
             else {
                 $value = $self->NewValue;
             }
             }
             else {
                 $value = $self->NewValue;
             }
+
             if ( $self->Field eq 'DependsOn' ) {
             if ( $self->Field eq 'DependsOn' ) {
-                return $self->loc( "Dependency on [_1] added", $value );
+                return ( "Dependency on [_1] added", $value );  #loc()
             }
             elsif ( $self->Field eq 'DependedOnBy' ) {
             }
             elsif ( $self->Field eq 'DependedOnBy' ) {
-                return $self->loc( "Dependency by [_1] added", $value );
-
+                return ( "Dependency by [_1] added", $value );  #loc()
             }
             elsif ( $self->Field eq 'RefersTo' ) {
             }
             elsif ( $self->Field eq 'RefersTo' ) {
-                return $self->loc( "Reference to [_1] added", $value );
+                return ( "Reference to [_1] added", $value );   #loc()
             }
             elsif ( $self->Field eq 'ReferredToBy' ) {
             }
             elsif ( $self->Field eq 'ReferredToBy' ) {
-                return $self->loc( "Reference by [_1] added", $value );
+                return ( "Reference by [_1] added", $value );   #loc()
             }
             elsif ( $self->Field eq 'MemberOf' ) {
             }
             elsif ( $self->Field eq 'MemberOf' ) {
-                return $self->loc( "Membership in [_1] added", $value );
+                return ( "Membership in [_1] added", $value );  #loc()
             }
             elsif ( $self->Field eq 'HasMember' ) {
             }
             elsif ( $self->Field eq 'HasMember' ) {
-                return $self->loc( "Member [_1] added", $value );
+                return ( "Member [_1] added", $value );         #loc()
             }
             elsif ( $self->Field eq 'MergedInto' ) {
             }
             elsif ( $self->Field eq 'MergedInto' ) {
-                return $self->loc( "Merged into [_1]", $value );
+                return ( "Merged into [_1]", $value );          #loc()
             }
         }
         else {
             }
         }
         else {
-            return ( $self->Data );
+            return ( "[_1]", $self->Data ); #loc()
         }
     },
     DeleteLink => sub {
         }
     },
     DeleteLink => sub {
@@ -834,35 +1142,38 @@ sub BriefDescription {
         my $value;
         if ( $self->OldValue ) {
             my $URI = RT::URI->new( $self->CurrentUser );
         my $value;
         if ( $self->OldValue ) {
             my $URI = RT::URI->new( $self->CurrentUser );
-            if ( $URI->FromURI( $self->OldValue ) ){
-                $value = $URI->Resolver->AsString;
+            if ( $URI->FromURI( $self->OldValue ) ) {
+                $value = [
+                    \'<a href="', $URI->AsHREF, \'">',
+                    $URI->AsString,
+                    \'</a>'
+                ];
             }
             else {
                 $value = $self->OldValue;
             }
 
             if ( $self->Field eq 'DependsOn' ) {
             }
             else {
                 $value = $self->OldValue;
             }
 
             if ( $self->Field eq 'DependsOn' ) {
-                return $self->loc( "Dependency on [_1] deleted", $value );
+                return ( "Dependency on [_1] deleted", $value );    #loc()
             }
             elsif ( $self->Field eq 'DependedOnBy' ) {
             }
             elsif ( $self->Field eq 'DependedOnBy' ) {
-                return $self->loc( "Dependency by [_1] deleted", $value );
-
+                return ( "Dependency by [_1] deleted", $value );    #loc()
             }
             elsif ( $self->Field eq 'RefersTo' ) {
             }
             elsif ( $self->Field eq 'RefersTo' ) {
-                return $self->loc( "Reference to [_1] deleted", $value );
+                return ( "Reference to [_1] deleted", $value );     #loc()
             }
             elsif ( $self->Field eq 'ReferredToBy' ) {
             }
             elsif ( $self->Field eq 'ReferredToBy' ) {
-                return $self->loc( "Reference by [_1] deleted", $value );
+                return ( "Reference by [_1] deleted", $value );     #loc()
             }
             elsif ( $self->Field eq 'MemberOf' ) {
             }
             elsif ( $self->Field eq 'MemberOf' ) {
-                return $self->loc( "Membership in [_1] deleted", $value );
+                return ( "Membership in [_1] deleted", $value );    #loc()
             }
             elsif ( $self->Field eq 'HasMember' ) {
             }
             elsif ( $self->Field eq 'HasMember' ) {
-                return $self->loc( "Member [_1] deleted", $value );
+                return ( "Member [_1] deleted", $value );           #loc()
             }
         }
         else {
             }
         }
         else {
-            return ( $self->Data );
+            return ( "[_1]", $self->Data ); #loc()
         }
     },
     Told => sub {
         }
     },
     Told => sub {
@@ -872,35 +1183,35 @@ sub BriefDescription {
             $t1->Set(Format => 'ISO', Value => $self->NewValue);
             my $t2 = RT::Date->new($self->CurrentUser);
             $t2->Set(Format => 'ISO', Value => $self->OldValue);
             $t1->Set(Format => 'ISO', Value => $self->NewValue);
             my $t2 = RT::Date->new($self->CurrentUser);
             $t2->Set(Format => 'ISO', Value => $self->OldValue);
-            return $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
+            return ( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );    #loc()
         }
         else {
         }
         else {
-            return $self->loc( "[_1] changed from [_2] to [_3]",
-                               $self->loc($self->Field),
-                               ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
+            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' ) {
         }
     },
     Set => sub {
         my $self = shift;
         if ( $self->Field eq 'Password' ) {
-            return $self->loc('Password changed');
+            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 );
         }
         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 $self->loc("[_1] changed from [_2] to [_3]",
-                              $self->loc($self->Field) , $q1->Name , $q2->Name);
+            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:
         }
 
         # Write the date/time change at local time:
-        elsif ($self->Field =~  /Due|Starts|Started|Told|WillResolve/) {
+        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);
             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 $self->loc( "[_1] changed from [_2] to [_3]", $self->loc($self->Field), $t2->AsString, $t1->AsString );
+            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 );
         }
         elsif ( $self->Field eq 'Owner' ) {
             my $Old = RT::User->new( $self->CurrentUser );
@@ -910,62 +1221,138 @@ sub BriefDescription {
 
             if ( $Old->id == RT->Nobody->id ) {
                 if ( $New->id == $self->Creator ) {
 
             if ( $Old->id == RT->Nobody->id ) {
                 if ( $New->id == $self->Creator ) {
-                    return $self->loc("Taken");
+                    return ("Taken");   #loc()
                 }
                 else {
                 }
                 else {
-                    return $self->loc( "Given to [_1]",  $New->Name );
+                    return ( "Given to [_1]", $self->_FormatUser($New) );    #loc()
                 }
             }
             else {
                 if ( $New->id == $self->Creator ) {
                 }
             }
             else {
                 if ( $New->id == $self->Creator ) {
-                    return $self->loc("Stolen from [_1]",  $Old->Name);
+                    return ("Stolen from [_1]",  $self->_FormatUser($Old) );   #loc()
                 }
                 elsif ( $Old->id == $self->Creator ) {
                     if ( $New->id == RT->Nobody->id ) {
                 }
                 elsif ( $Old->id == $self->Creator ) {
                     if ( $New->id == RT->Nobody->id ) {
-                        return $self->loc("Untaken");
+                        return ("Untaken"); #loc()
                     }
                     else {
                     }
                     else {
-                        return $self->loc( "Given to [_1]", $New->Name );
+                        return ( "Given to [_1]", $self->_FormatUser($New) ); #loc()
                     }
                 }
                 else {
                     }
                 }
                 else {
-                    return $self->loc(
+                    return (
                         "Owner forcibly changed from [_1] to [_2]",
                         "Owner forcibly changed from [_1] to [_2]",
-                        $Old->Name, $New->Name );
+                        map { $self->_FormatUser($_) } $Old, $New
+                    );   #loc()
                 }
             }
         }
         else {
                 }
             }
         }
         else {
-            return $self->loc( "[_1] changed from [_2] to [_3]",
-                               $self->loc($self->Field),
-                               ($self->OldValue? "'".$self->OldValue ."'" : $self->loc("(no value)")) , "'". $self->NewValue."'" );
+            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;
         }
     },
     PurgeTransaction => sub {
         my $self = shift;
-        return $self->loc("Transaction [_1] purged", $self->Data);
+        return ("Transaction [_1] purged", $self->Data);    #loc()
     },
     AddReminder => sub {
         my $self = shift;
         my $ticket = RT::Ticket->new($self->CurrentUser);
         $ticket->Load($self->NewValue);
     },
     AddReminder => sub {
         my $self = shift;
         my $ticket = RT::Ticket->new($self->CurrentUser);
         $ticket->Load($self->NewValue);
-        return $self->loc("Reminder '[_1]' added", $ticket->Subject);
+        if ( $ticket->CurrentUserHasRight('ShowTicket') ) {
+            my $subject = [
+                \'<a href="', RT->Config->Get('WebPath'),
+                "/Ticket/Reminders.html?id=", $self->ObjectId,
+                "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
+            ];
+            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);
     },
     OpenReminder => sub {
         my $self = shift;
         my $ticket = RT::Ticket->new($self->CurrentUser);
         $ticket->Load($self->NewValue);
-        return $self->loc("Reminder '[_1]' reopened", $ticket->Subject);
-    
+        if ( $ticket->CurrentUserHasRight('ShowTicket') ) {
+            my $subject = [
+                \'<a href="', RT->Config->Get('WebPath'),
+                "/Ticket/Reminders.html?id=", $self->ObjectId,
+                "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
+            ];
+            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);
     },
     ResolveReminder => sub {
         my $self = shift;
         my $ticket = RT::Ticket->new($self->CurrentUser);
         $ticket->Load($self->NewValue);
-        return $self->loc("Reminder '[_1]' completed", $ticket->Subject);
-    
-    
-    }
+        if ( $ticket->CurrentUserHasRight('ShowTicket') ) {
+            my $subject = [
+                \'<a href="', RT->Config->Get('WebPath'),
+                "/Ticket/Reminders.html?id=", $self->ObjectId,
+                "#reminder-", $ticket->id, \'">', $ticket->Subject, \'</a>'
+            ];
+            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()
+    },
 );
 
 
 );
 
 
@@ -1029,23 +1416,6 @@ sub _Value {
 }
 
 
 }
 
 
-
-=head2 CurrentUserHasRight RIGHT
-
-Calls $self->CurrentUser->HasQueueRight for the right passed in here.
-passed in here.
-
-=cut
-
-sub CurrentUserHasRight {
-    my $self  = shift;
-    my $right = shift;
-    return $self->CurrentUser->HasRight(
-        Right  => $right,
-        Object => $self->Object
-    );
-}
-
 =head2 CurrentUserCanSee
 
 Returns true if current user has rights to see this particular transaction.
 =head2 CurrentUserCanSee
 
 Returns true if current user has rights to see this particular transaction.
@@ -1054,31 +1424,18 @@ 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.
 
 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;
 
 =cut
 
 sub CurrentUserCanSee {
     my $self = shift;
 
-    # If it's a comment, we need to be extra special careful
-    my $type = $self->__Value('Type');
-    if ( $type eq 'Comment' ) {
-        unless ( $self->CurrentUserHasRight('ShowTicketComments') ) {
-            return 0;
-        }
-    }
-    elsif ( $type eq 'CommentEmailRecord' ) {
-        unless ( $self->CurrentUserHasRight('ShowTicketComments')
-            && $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
-            return 0;
-        }
-    }
-    elsif ( $type eq 'EmailRecord' ) {
-        unless ( $self->CurrentUserHasRight('ShowOutgoingEmail') ) {
-            return 0;
-        }
-    }
+    return 1 if $self->CurrentUser->PrincipalObj->Id == RT->SystemUser->Id;
+
     # Make sure the user can see the custom field before showing that it changed
     # Make sure the user can see the custom field before showing that it changed
-    elsif ( $type eq 'CustomField' and my $cf_id = $self->__Value('Field') ) {
+    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 );
         my $cf = RT::CustomField->new( $self->CurrentUser );
         $cf->SetContextObject( $self->Object );
         $cf->Load( $cf_id );
@@ -1090,7 +1447,7 @@ sub CurrentUserCanSee {
     return 1 if $self->{ _object_is_readable };
 
     # Defer to the object in question
     return 1 if $self->{ _object_is_readable };
 
     # Defer to the object in question
-    return $self->Object->CurrentUserCanSee("Transaction");
+    return $self->Object->CurrentUserCanSee("Transaction", $self);
 }
 
 
 }
 
 
@@ -1106,11 +1463,7 @@ sub TicketObj {
 
 sub OldValue {
     my $self = shift;
 
 sub OldValue {
     my $self = shift;
-    if ( my $type = $self->__Value('ReferenceType')
-         and my $id = $self->__Value('OldReference') )
-    {
-        my $Object = $type->new($self->CurrentUser);
-        $Object->Load( $id );
+    if ( my $Object = $self->OldReferenceObject ) {
         return $Object->Content;
     }
     else {
         return $Object->Content;
     }
     else {
@@ -1120,11 +1473,7 @@ sub OldValue {
 
 sub NewValue {
     my $self = shift;
 
 sub NewValue {
     my $self = shift;
-    if ( my $type = $self->__Value('ReferenceType')
-         and my $id = $self->__Value('NewReference') )
-    {
-        my $Object = $type->new($self->CurrentUser);
-        $Object->Load( $id );
+    if ( my $Object = $self->NewReferenceObject ) {
         return $Object->Content;
     }
     else {
         return $Object->Content;
     }
     else {
@@ -1139,22 +1488,53 @@ sub Object {
     return $Object;
 }
 
     return $Object;
 }
 
+=head2 NewReferenceObject
+
+=head2 OldReferenceObject
+
+Returns an object of the class specified by the column C<ReferenceType> and
+loaded with the id specified by the column C<NewReference> or C<OldReference>.
+C<ReferenceType> is assumed to be an L<RT::Record> subclass.
+
+The object may be unloaded (check C<< $object->id >>) if the reference is
+corrupt (such as if the referenced record was improperly deleted).
+
+Returns undef if either C<ReferenceType> or C<NewReference>/C<OldReference> is
+false.
+
+=cut
+
+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;
 sub FriendlyObjectType {
     my $self = shift;
-    my $type = $self->ObjectType or return undef;
-    $type =~ s/^RT:://;
-    return $self->loc($type);
+    return $self->loc( $self->Object->RecordType );
 }
 
 =head2 UpdateCustomFields
 }
 
 =head2 UpdateCustomFields
-    
-    Takes a hash of 
 
 
-    CustomField-<<Id>> => Value
-        or 
+Takes a hash of:
+
+    CustomField-C<Id> => Value
+
+or:
 
 
-    Object-RT::Transaction-CustomField-<<Id>> => Value parameters to update
-    this transaction's custom fields
+    Object-RT::Transaction-CustomField-C<Id> => Value
+
+parameters to update this transaction's custom fields.
 
 =cut
 
 
 =cut
 
@@ -1166,12 +1546,9 @@ sub UpdateCustomFields {
     # 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.
     # 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.
-
-    # TODO: 3.6: DEPRECATE OLD API
-
-    my $args; 
-
-    if ($args{'ARGSRef'}) { 
+    my $args;
+    if ($args{'ARGSRef'}) {
+        RT->Deprecated( Arguments => "ARGSRef", Remove => "4.4" );
         $args = $args{ARGSRef};
     } else {
         $args = \%args;
         $args = $args{ARGSRef};
     } else {
         $args = \%args;
@@ -1185,6 +1562,8 @@ sub UpdateCustomFields {
         next if $arg =~ /-TimeUnits$/;
         my $cfid   = $1;
         my $values = $args->{$arg};
         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 )
         {
         foreach
           my $value ( UNIVERSAL::isa( $values, 'ARRAY' ) ? @$values : $values )
         {
@@ -1218,7 +1597,7 @@ sub LoadCustomFieldByIdentifier {
 
     my $CFs = RT::CustomFields->new( $self->CurrentUser );
     $CFs->SetContextObject( $self->Object );
 
     my $CFs = RT::CustomFields->new( $self->CurrentUser );
     $CFs->SetContextObject( $self->Object );
-    $CFs->Limit( FIELD => 'Name', VALUE => $field );
+    $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 );
     $CFs->LimitToLookupType($self->CustomFieldLookupType);
     $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id);
     return $CFs->First || RT::CustomField->new( $self->CurrentUser );
@@ -1331,8 +1710,6 @@ sub DeferredRecipients {
 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
 sub _CacheConfig {
   {
 # Transactions don't change. by adding this cache config directive, we don't lose pathalogically on long tickets.
 sub _CacheConfig {
   {
-     'cache_p'        => 1,
-     'fast_update_p'  => 1,
      'cache_for_sec'  => 6000,
   }
 }
      'cache_for_sec'  => 6000,
   }
 }
@@ -1593,37 +1970,211 @@ sub _CoreAccessible {
     {
 
         id =>
     {
 
         id =>
-               {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
+                {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
         ObjectType =>
         ObjectType =>
-               {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
         ObjectId =>
         ObjectId =>
-               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+                {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         TimeTaken =>
         TimeTaken =>
-               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+                {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         Type =>
         Type =>
-               {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
         Field =>
         Field =>
-               {read => 1, write => 1, sql_type => 12, length => 40,  is_blob => 0,  is_numeric => 0,  type => 'varchar(40)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 40,  is_blob => 0,  is_numeric => 0,  type => 'varchar(40)', default => ''},
         OldValue =>
         OldValue =>
-               {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
         NewValue =>
         NewValue =>
-               {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
         ReferenceType =>
         ReferenceType =>
-               {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
         OldReference =>
         OldReference =>
-               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
+                {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
         NewReference =>
         NewReference =>
-               {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
+                {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
         Data =>
         Data =>
-               {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
+                {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
         Creator =>
         Creator =>
-               {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+                {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
         Created =>
         Created =>
-               {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
+                {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;
 RT::Base->_ImportOverlays();
 
 1;