X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FLink.pm;h=0dadc3be8cc5250b7fbd47f161cca51969ee8c07;hp=885ffe3edc1a4074d7811f457ba3bd5f922e1294;hb=7322f2afedcc2f427e997d1535a503613a83f088;hpb=0ebeec96313dd7edfca340f01f8fbbbac1f4aa1d diff --git a/rt/lib/RT/Link.pm b/rt/lib/RT/Link.pm index 885ffe3ed..0dadc3be8 100644 --- a/rt/lib/RT/Link.pm +++ b/rt/lib/RT/Link.pm @@ -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 -# This software is redistributable under the terms of the GNU GPL +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC +# +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} =head1 NAME @@ -15,33 +59,82 @@ 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 @@ -50,89 +143,173 @@ Returns undef on failure or a Link Id on success. =cut -sub Create { +sub Create { 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 - 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 -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 { - 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 @@ -140,234 +317,279 @@ sub TargetObj { 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 -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 -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 -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 -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 -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 -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 -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 $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; -