RT 4.2.11, ticket#13852
[freeside.git] / rt / lib / RT / Link.pm
index 885ffe3..f82cf51 100644 (file)
@@ -1,6 +1,50 @@
-# $Header: /home/cvs/cvsroot/freeside/rt/lib/RT/Link.pm,v 1.1 2002-08-12 06:17:07 ivan Exp $
-# (c) 1996-1999 Jesse Vincent <jesse@fsck.com>
-# This software is redistributable under the terms of the GNU GPL
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
+#                                          <sales@bestpractical.com>
+#
+# (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
 
 
 =head1 NAME
 
 This module should never be called directly by client code. it's an internal module which
 should only be accessed through exported APIs in Ticket other similar objects.
 
 This module should never be called directly by client code. it's an internal module which
 should only be accessed through exported APIs in Ticket other similar objects.
 
-=head1 METHODS
+=cut
 
 
 
 
-=begin testing
+package RT::Link;
 
 
-ok (require RT::TestHarness);
-ok (require RT::Link);
+use strict;
+use warnings;
 
 
-=end testing
 
 
-=cut
 
 
-package RT::Link;
-use RT::Record;
-use Carp;
-@ISA= qw(RT::Record);
+use base 'RT::Record';
 
 
-# {{{ sub _Init
-sub _Init  {
-  my $self  = shift;
-  $self->{'table'} = "Links";
-  return ($self->SUPER::_Init(@_));
+sub Table {'Links'}
+use Carp;
+use RT::URI;
+use List::Util 'first';
+use List::MoreUtils 'uniq';
+
+# Helper tables for links mapping to make it easier
+# to build and parse links between objects.
+our %TYPEMAP = (
+    MemberOf        => { Type => 'MemberOf',    Mode => 'Target',   Display => 0 },
+    Parents         => { Type => 'MemberOf',    Mode => 'Target',   Display => 1 },
+    Parent          => { Type => 'MemberOf',    Mode => 'Target',   Display => 0 },
+    Members         => { Type => 'MemberOf',    Mode => 'Base',     Display => 0 },
+    Member          => { Type => 'MemberOf',    Mode => 'Base',     Display => 0 },
+    Children        => { Type => 'MemberOf',    Mode => 'Base',     Display => 1 },
+    Child           => { Type => 'MemberOf',    Mode => 'Base',     Display => 0 },
+    HasMember       => { Type => 'MemberOf',    Mode => 'Base',     Display => 0 },
+    RefersTo        => { Type => 'RefersTo',    Mode => 'Target',   Display => 1 },
+    ReferredToBy    => { Type => 'RefersTo',    Mode => 'Base',     Display => 1 },
+    DependsOn       => { Type => 'DependsOn',   Mode => 'Target',   Display => 1 },
+    DependedOnBy    => { Type => 'DependsOn',   Mode => 'Base',     Display => 1 },
+    MergedInto      => { Type => 'MergedInto',  Mode => 'Target',   Display => 1 },
+);
+our %DIRMAP = (
+    MemberOf    => { Base => 'MemberOf',    Target => 'HasMember'    },
+    RefersTo    => { Base => 'RefersTo',    Target => 'ReferredToBy' },
+    DependsOn   => { Base => 'DependsOn',   Target => 'DependedOnBy' },
+    MergedInto  => { Base => 'MergedInto',  Target => 'MergedInto'   },
+);
+
+__PACKAGE__->_BuildDisplayAs;
+
+my %DISPLAY_AS;
+sub _BuildDisplayAs {
+    %DISPLAY_AS = ();
+    foreach my $in_db ( uniq map { $_->{Type} } values %TYPEMAP ) {
+        foreach my $mode (qw(Base Target)) {
+            $DISPLAY_AS{$in_db}{$mode} = first {
+                   $TYPEMAP{$_}{Display}
+                && $TYPEMAP{$_}{Type} eq $in_db
+                && $TYPEMAP{$_}{Mode} eq $mode
+            } keys %TYPEMAP;
+        }
+    }
 }
 
 }
 
-# }}}
+=head1 CLASS METHODS
 
 
-# {{{ sub Create 
+=head2 DisplayTypes
+
+Returns a list of the standard link Types for display, including directional
+variants but not aliases.
+
+=cut
+
+sub DisplayTypes {
+    sort { $a cmp $b }
+    uniq
+    grep { defined }
+     map { values %$_ }
+  values %DISPLAY_AS
+}
+
+=head1 METHODS
 
 =head2 Create PARAMHASH
 
 
 =head2 Create PARAMHASH
 
@@ -50,89 +143,173 @@ Returns undef on failure or a Link Id on success.
 
 =cut
 
 
 =cut
 
-sub Create  {
+sub Create {
     my $self = shift;
     my $self = shift;
-    my %args = ( Base => undef,
-                Target => undef,
-                Type => undef,
-                @_ # get the real argumentlist
-              );
-    
-    my $BaseURI = $self->CanonicalizeURI($args{'Base'});
-    my $TargetURI = $self->CanonicalizeURI($args{'Target'});
-    
-    unless (defined $BaseURI) {
-       $RT::Logger->warning ("$self couldn't resolve base:'".$args{'Base'}.
-                             "' into a URI\n");
-       return (undef);
+    my %args = ( Base   => undef,
+                 Target => undef,
+                 Type   => undef,
+                 @_ );
+
+    my $base = RT::URI->new( $self->CurrentUser );
+    unless ($base->FromURI( $args{'Base'} )) {
+        my $msg = $self->loc("Couldn't resolve base '[_1]' into a URI.", $args{'Base'});
+        $RT::Logger->warning( "$self $msg" );
+        return wantarray ? (undef, $msg) : undef;
+    }
+
+    my $target = RT::URI->new( $self->CurrentUser );
+    unless ($target->FromURI( $args{'Target'} )) {
+        my $msg = $self->loc("Couldn't resolve target '[_1]' into a URI.", $args{'Target'});
+        $RT::Logger->warning( "$self $msg" );
+        return wantarray ? (undef, $msg) : undef;
+    }
+
+    my $base_id   = 0;
+    my $target_id = 0;
+
+
+
+
+    if ( $base->IsLocal ) {
+        my $object = $base->Object;
+        unless (UNIVERSAL::can($object, 'Id')) {
+            return (undef, $self->loc("[_1] appears to be a local object, but can't be found in the database", $args{'Base'}));
+        
+        }
+        $base_id = $object->Id if UNIVERSAL::isa($object, 'RT::Ticket');
+    }
+    if ( $target->IsLocal ) {
+        my $object = $target->Object;
+        unless (UNIVERSAL::can($object, 'Id')) {
+            return (undef, $self->loc("[_1] appears to be a local object, but can't be found in the database", $args{'Target'}));
+        
+        }
+        $target_id = $object->Id if UNIVERSAL::isa($object, 'RT::Ticket');
     }
     }
-    unless (defined $TargetURI) {
-       $RT::Logger->warning ("$self couldn't resolve target:'".$args{'Target'}.
-                             "' into a URI\n");
-       return(undef);
+
+    # We don't want references to ourself
+    if ( $base->URI eq $target->URI ) {
+        return ( 0, $self->loc("Can't link a ticket to itself") );
     }
     }
-    
-    my $LocalBase = $self->_IsLocal($BaseURI);
-    my $LocalTarget = $self->_IsLocal($TargetURI);
-    my $id = $self->SUPER::Create(Base => "$BaseURI",
-                                 Target => "$TargetURI",
-                                 LocalBase => $LocalBase, 
-                                 LocalTarget => $LocalTarget,
-                                 Type => $args{'Type'});
-    return ($id);
+
+    # }}}
+
+    my ( $id, $msg ) = $self->SUPER::Create( Base        => $base->URI,
+                                             Target      => $target->URI,
+                                             LocalBase   => $base_id,
+                                             LocalTarget => $target_id,
+                                             Type        => $args{'Type'} );
+    return ( $id, $msg );
 }
 
 }
 
-# }}}
+ # sub LoadByParams
+
+=head2 LoadByParams
+
+  Load an RT::Link object from the database.  Takes three parameters
+  
+  Base => undef,
+  Target => undef,
+  Type =>undef
  
  
-# {{{ sub Load 
+  Base and Target are expected to be integers which refer to Tickets or URIs
+  Type is the link type
+
+=cut
+
+sub LoadByParams {
+    my $self = shift;
+    my %args = ( Base   => undef,
+                 Target => undef,
+                 Type   => undef,
+                 @_ );
+
+    my $base = RT::URI->new($self->CurrentUser);
+    $base->FromURI( $args{'Base'} )
+        or return wantarray ? (0, $self->loc("Couldn't parse Base URI: [_1]", $args{Base})) : 0;
+
+    my $target = RT::URI->new($self->CurrentUser);
+    $target->FromURI( $args{'Target'} )
+        or return wantarray ? (0, $self->loc("Couldn't parse Target URI: [_1]", $args{Target})) : 0;
+
+    my ( $id, $msg ) = $self->LoadByCols( Base   => $base->URI,
+                                          Type   => $args{'Type'},
+                                          Target => $target->URI );
+
+    unless ($id) {
+        return wantarray ? ( 0, $self->loc("Couldn't load link: [_1]", $msg) ) : 0;
+    } else {
+        return wantarray ? ($id, $msg) : $id;
+    }
+}
+
 
 =head2 Load
 
 
 =head2 Load
 
-  Load an RT::Link object from the database.  Takes one parameter or three.
-  One parameter is the id of an entry in the links table.  Three parameters are a tuple of (base, linktype, target);
+  Load an RT::Link object from the database.  Takes one parameter, the id of an entry in the links table.
 
 
 =cut
 
 
 
 =cut
 
-sub Load  {
-  my $self = shift;
-  my $identifier = shift;
-  my $linktype = shift if (@_);
-  my $target = shift if (@_);
-  
-  if ($target) {
-      my $BaseURI = $self->CanonicalizeURI($identifier);
-      my $TargetURI = $self->CanonicalizeURI($target);
-      $self->LoadByCols( Base => $BaseURI,
-                        Type => $linktype,
-                        Target => $TargetURI
-                      ) || return (0, "Couldn't load link");
-  }
-  
-  elsif ($identifier =~ /^\d+$/) {
-      $self->LoadById($identifier) ||
-       return (0, "Couldn't load link");
-  }
-  else {
-       return (0, "That's not a numerical id");
-  }
+sub Load {
+    my $self       = shift;
+    my $identifier = shift;
+
+
+
+
+    if ( $identifier !~ /^\d+$/ ) {
+        return wantarray ? ( 0, $self->loc("That's not a numerical id") ) : 0;
+    }
+    else {
+        my ( $id, $msg ) = $self->LoadById($identifier);
+        unless ( $self->Id ) {
+            return wantarray ? ( 0, $self->loc("Couldn't load link") ) : 0;
+        }
+        return wantarray ? ( $id, $msg ) : $id;
+    }
 }
 
 }
 
-# }}}
 
 
-# {{{ sub TargetObj 
+
+
+=head2 TargetURI
+
+returns an RT::URI object for the "Target" of this link.
+
+=cut
+
+sub TargetURI {
+    my $self = shift;
+    my $URI = RT::URI->new($self->CurrentUser);
+    $URI->FromURI($self->Target);
+    return ($URI);
+}
+
 
 =head2 TargetObj
 
 =cut
 
 sub TargetObj {
 
 =head2 TargetObj
 
 =cut
 
 sub TargetObj {
-  my $self = shift;
-   return $self->_TicketObj('base',$self->Target);
+    my $self = shift;
+    return $self->TargetURI->Object;
+}
+
+
+=head2 BaseURI
+
+returns an RT::URI object for the "Base" of this link.
+
+=cut
+
+sub BaseURI {
+    my $self = shift;
+    my $URI = RT::URI->new($self->CurrentUser);
+    $URI->FromURI($self->Base);
+    return ($URI);
 }
 }
-# }}}
 
 
-# {{{ sub BaseObj
 
 =head2 BaseObj
 
 
 =head2 BaseObj
 
@@ -140,234 +317,279 @@ sub TargetObj {
 
 sub BaseObj {
   my $self = shift;
 
 sub BaseObj {
   my $self = shift;
-  return $self->_TicketObj('target',$self->Base);
+  return $self->BaseURI->Object;
 }
 }
-# }}}
 
 
-# {{{ sub _TicketObj
-sub _TicketObj {
-  my $self = shift;
-  my $name = shift;
-  my $ref = shift;
-  my $tag="$name\_obj";
-  
-  unless (exists $self->{$tag}) {
+=head2 id
 
 
-  $self->{$tag}=RT::Ticket->new($self->CurrentUser);
+Returns the current value of id.
+(In the database, id is stored as int(11).)
 
 
-  #If we can get an actual ticket, load it up.
-  if ($self->_IsLocal($ref)) {
-      $self->{$tag}->Load($ref);
-    }
-  }
-  return $self->{$tag};
-}
-# }}}
 
 
-# {{{ sub _Accessible 
-sub _Accessible  {
-  my $self = shift;
-  my %Cols = (
-             LocalBase => 'read',
-             LocalTarget => 'read',
-             Base => 'read',
-             Target => 'read',
-             Type => 'read',
-             Creator => 'read/auto',
-             Created => 'read/auto',
-             LastUpdatedBy => 'read/auto',
-             LastUpdated => 'read/auto'
-            );
-  return($self->SUPER::_Accessible(@_, %Cols));
-}
-# }}}
+=cut
+
+
+=head2 Base
 
 
+Returns the current value of Base.
+(In the database, Base is stored as varchar(240).)
 
 
-# Static methods:
 
 
-# {{{ sub BaseIsLocal
 
 
-=head2 BaseIsLocal
+=head2 SetBase VALUE
+
+
+Set Base to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Base will be stored as a varchar(240).)
 
 
-Returns true if the base of this link is a local ticket
 
 =cut
 
 
 =cut
 
-sub BaseIsLocal {
-  my $self = shift;
-  return $self->_IsLocal($self->Base);
-}
 
 
-# }}}
+=head2 Target
+
+Returns the current value of Target.
+(In the database, Target is stored as varchar(240).)
 
 
-# {{{ sub TargetIsLocal
 
 
-=head2 TargetIsLocal
 
 
-Returns true if the target of this link is a local ticket
+=head2 SetTarget VALUE
+
+
+Set Target to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, Target will be stored as a varchar(240).)
+
 
 =cut
 
 
 =cut
 
-sub TargetIsLocal {
-  my $self = shift;
-  return $self->_IsLocal($self->Target);
-}
 
 
-# }}}
+=head2 Type
+
+Returns the current value of Type.
+(In the database, Type is stored as varchar(20).)
+
+
+
+=head2 SetType VALUE
 
 
-# {{{ sub _IsLocal
 
 
-=head2 _IsLocal URI 
+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).)
 
 
-When handed a URI returns the local ticket id if it\'s local. otherwise returns undef.
 
 =cut
 
 
 =cut
 
-sub _IsLocal {
-    my $self = shift;
-    my $URI=shift;
-    unless ($URI) {
-       $RT::Logger->warning ("$self _IsLocal called without a URI\n");
-       return (undef);
-    }
-    # TODO: More thorough check
-    if ($URI =~ /^$RT::TicketBaseURI(\d+)$/) {
-       return($1);
-    }
-    else {
-       return (undef);
-    }
-}
-# }}}
+
+=head2 LocalTarget
+
+Returns the current value of LocalTarget.
+(In the database, LocalTarget is stored as int(11).)
+
 
 
 
 
-# {{{ sub BaseAsHREF 
+=head2 SetLocalTarget VALUE
 
 
-=head2 BaseAsHREF
 
 
-Returns an HTTP url to access the base of this link
+Set LocalTarget to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, LocalTarget will be stored as a int(11).)
+
 
 =cut
 
 
 =cut
 
-sub BaseAsHREF {
-  my $self = shift;
-  return $self->AsHREF($self->Base);
-}
-# }}}
 
 
-# {{{ sub TargetAsHREF 
+=head2 LocalBase
+
+Returns the current value of LocalBase.
+(In the database, LocalBase is stored as int(11).)
+
 
 
-=head2 TargetAsHREF
 
 
-return an HTTP url to access the target of this link
+=head2 SetLocalBase VALUE
+
+
+Set LocalBase to VALUE.
+Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
+(In the database, LocalBase will be stored as a int(11).)
+
 
 =cut
 
 
 =cut
 
-sub TargetAsHREF {
-  my $self = shift;
-  return $self->AsHREF($self->Target);
-}
-# }}}
 
 
-# {{{ sub AsHREF - Converts Link URIs to HTTP URLs
-=head2 URI
+=head2 LastUpdatedBy
+
+Returns the current value of LastUpdatedBy.
+(In the database, LastUpdatedBy is stored as int(11).)
 
 
-Takes a URI and returns an http: url to access that object.
 
 =cut
 
 =cut
-sub AsHREF {
-    my $self=shift;
-    my $URI=shift;
-    if ($self->_IsLocal($URI)) {
-       my $url=$RT::WebURL . "Ticket/Display.html?id=$URI";
-       return($url);
-    } 
-    else {
-       my ($protocol) = $URI =~ m|(.*?)://|;
-       unless (exists $RT::URI2HTTP{$protocol}) {
-           $RT::Logger->warning("Linking for protocol $protocol not defined in the config file!");
-           return("");
-       }
-       return $RT::URI2HTTP{$protocol}->($URI);
-    }
-}
 
 
-# }}}
 
 
-# {{{ sub GetContent - gets the content from a link
-sub GetContent {
-    my ($self, $URI)= @_;
-    if ($self->_IsLocal($URI)) {
-       die "stub";
-    } else {
-       # Find protocol
-       if ($URI =~ m|^(.*?)://|) {
-           if (exists $RT::ContentFromURI{$1}) {
-               return $RT::ContentFromURI{$1}->($URI);
-           } else {
-               warn "No sub exists for fetching the content from a $1 in $URI";
-           }
-       } else {
-           warn "No protocol specified in $URI";
-       }
-    }
-}
-# }}}
+=head2 LastUpdated
 
 
-# {{{ sub CanonicalizeURI
+Returns the current value of LastUpdated.
+(In the database, LastUpdated is stored as datetime.)
 
 
-=head2 CanonicalizeURI
 
 
-Takes a single argument: some form of ticket identifier. 
-Returns its canonicalized URI.
+=cut
+
+
+=head2 Creator
+
+Returns the current value of Creator.
+(In the database, Creator is stored as int(11).)
 
 
-Bug: ticket aliases can't have :// in them. URIs must have :// in them.
 
 =cut
 
 
 =cut
 
-sub CanonicalizeURI {
+
+=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 => ''},
+        Base =>
+                {read => 1, write => 1, sql_type => 12, length => 240,  is_blob => 0,  is_numeric => 0,  type => 'varchar(240)', default => ''},
+        Target =>
+                {read => 1, write => 1, sql_type => 12, length => 240,  is_blob => 0,  is_numeric => 0,  type => 'varchar(240)', default => ''},
+        Type =>
+                {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
+        LocalTarget =>
+                {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        LocalBase =>
+                {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        LastUpdatedBy =>
+                {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
+        LastUpdated =>
+                {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', 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 $self = shift;
-    my $id = shift;
-    
-    
-    #If it's a local URI, load the ticket object and return its URI
-    if ($id =~ /^$RT::TicketBaseURI/) {
-       my $ticket = new RT::Ticket($self->CurrentUser);
-       $ticket->Load($id);
-       #If we couldn't find a ticket, return undef.
-       return undef unless (defined $ticket->Id);
-       #$RT::Logger->debug("$self -> CanonicalizeURI was passed $id and returned ".$ticket->URI ." (uri)\n");
-       return ($ticket->URI);
+    my ($walker, $deps) = @_;
+
+    $self->SUPER::FindDependencies($walker, $deps);
+
+    $deps->Add( out => $self->BaseObj )   if $self->BaseObj   and $self->BaseObj->id;
+    $deps->Add( out => $self->TargetObj ) if $self->TargetObj and $self->TargetObj->id;
+}
+
+sub __DependsOn {
+    my $self = shift;
+    my %args = (
+        Shredder => undef,
+        Dependencies => undef,
+        @_,
+    );
+    my $deps = $args{'Dependencies'};
+    my $list = [];
+
+# AddLink transactions
+    my $map = { %RT::Link::TYPEMAP };
+    my $link_meta = $map->{ $self->Type };
+    unless ( $link_meta && $link_meta->{'Mode'} && $link_meta->{'Type'} ) {
+        RT::Shredder::Exception->throw( 'Wrong link link_meta, no record for '. $self->Type );
     }
     }
-    #If it's a remote URI, we're going to punt for now
-    elsif ($id =~ '://' ) {
-       return ($id);
+    if ( $self->BaseURI->IsLocal ) {
+        my $objs = $self->BaseObj->Transactions;
+        $objs->Limit(
+            FIELD    => 'Type',
+            OPERATOR => '=',
+            VALUE    => 'AddLink',
+        );
+        $objs->Limit( FIELD => 'NewValue', VALUE => $self->Target );
+        while ( my ($k, $v) = each %$map ) {
+            next unless $v->{'Type'} eq $link_meta->{'Type'};
+            next unless $v->{'Mode'} eq $link_meta->{'Mode'};
+            $objs->Limit( FIELD => 'Field', VALUE => $k );
+        }
+        push( @$list, $objs );
     }
     }
-  
-    #If the base is an integer, load it as a ticket 
-    elsif ( $id =~ /^\d+$/ ) {
-       
-       #$RT::Logger->debug("$self -> CanonicalizeURI was passed $id. It's a ticket id.\n");
-       my $ticket = new RT::Ticket($self->CurrentUser);
-       $ticket->Load($id);
-       #If we couldn't find a ticket, return undef.
-       return undef unless (defined $ticket->Id);
-       #$RT::Logger->debug("$self returned ".$ticket->URI ." (id #)\n");
-       return ($ticket->URI);
+
+    my %reverse = ( Base => 'Target', Target => 'Base' );
+    if ( $self->TargetURI->IsLocal ) {
+        my $objs = $self->TargetObj->Transactions;
+        $objs->Limit(
+            FIELD    => 'Type',
+            OPERATOR => '=',
+            VALUE    => 'AddLink',
+        );
+        $objs->Limit( FIELD => 'NewValue', VALUE => $self->Base );
+        while ( my ($k, $v) = each %$map ) {
+            next unless $v->{'Type'} eq $link_meta->{'Type'};
+            next unless $v->{'Mode'} eq $reverse{ $link_meta->{'Mode'} };
+            $objs->Limit( FIELD => 'Field', VALUE => $k );
+        }
+        push( @$list, $objs );
     }
 
     }
 
-    #It's not a URI. It's not a numerical ticket ID
-    else { 
-     
-       #If we couldn't find a ticket, return undef.
-       return( undef);
-    
+    $deps->_PushDependencies(
+        BaseObject => $self,
+        Flags => RT::Shredder::Constants::DEPENDS_ON|RT::Shredder::Constants::WIPE_AFTER,
+        TargetObjects => $list,
+        Shredder => $args{'Shredder'}
+    );
+    return $self->SUPER::__DependsOn( %args );
+}
+
+sub Serialize {
+    my $self = shift;
+    my %args = (@_);
+    my %store = $self->SUPER::Serialize(@_);
+
+    delete $store{LocalBase}   if $store{Base};
+    delete $store{LocalTarget} if $store{Target};
+    return %store;
+}
+
+
+sub PreInflate {
+    my $class = shift;
+    my ($importer, $uid, $data) = @_;
+
+    for my $dir (qw/Base Target/) {
+        my $uid_ref = $data->{$dir};
+        next unless $uid_ref and ref $uid_ref;
+
+        my $to_uid = ${ $uid_ref };
+        my $obj = $importer->LookupObj( $to_uid );
+        if ($obj) {
+            $data->{$dir} = $obj->URI;
+            $data->{"Local$dir"} = $obj->Id if $obj->isa("RT::Ticket");
+        } else {
+            $data->{$dir} = "";
+            $importer->Postpone(
+                for => $to_uid,
+                uid => $uid,
+                uri => $dir,
+                column => ($to_uid =~ /RT::Ticket/ ? "Local$dir" : undef),
+            );
+        }
+
     }
 
     }
 
+    return $class->SUPER::PreInflate( $importer, $uid, $data );
 }
 
 }
 
-# }}}
+RT::Base->_ImportOverlays();
 
 1;
 
 1;