rt 4.2.15
[freeside.git] / rt / lib / RT / Link.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 =head1 NAME
50
51   RT::Link - an RT Link object
52
53 =head1 SYNOPSIS
54
55   use RT::Link;
56
57 =head1 DESCRIPTION
58
59 This module should never be called directly by client code. it's an internal module which
60 should only be accessed through exported APIs in Ticket other similar objects.
61
62 =cut
63
64
65 package RT::Link;
66
67 use strict;
68 use warnings;
69
70
71
72 use base 'RT::Record';
73
74 sub Table {'Links'}
75 use Carp;
76 use RT::URI;
77 use List::Util 'first';
78 use List::MoreUtils 'uniq';
79
80 # Helper tables for links mapping to make it easier
81 # to build and parse links between objects.
82 our %TYPEMAP = (
83     MemberOf        => { Type => 'MemberOf',    Mode => 'Target',   Display => 0 },
84     Parents         => { Type => 'MemberOf',    Mode => 'Target',   Display => 1 },
85     Parent          => { Type => 'MemberOf',    Mode => 'Target',   Display => 0 },
86     Members         => { Type => 'MemberOf',    Mode => 'Base',     Display => 0 },
87     Member          => { Type => 'MemberOf',    Mode => 'Base',     Display => 0 },
88     Children        => { Type => 'MemberOf',    Mode => 'Base',     Display => 1 },
89     Child           => { Type => 'MemberOf',    Mode => 'Base',     Display => 0 },
90     HasMember       => { Type => 'MemberOf',    Mode => 'Base',     Display => 0 },
91     RefersTo        => { Type => 'RefersTo',    Mode => 'Target',   Display => 1 },
92     ReferredToBy    => { Type => 'RefersTo',    Mode => 'Base',     Display => 1 },
93     DependsOn       => { Type => 'DependsOn',   Mode => 'Target',   Display => 1 },
94     DependedOnBy    => { Type => 'DependsOn',   Mode => 'Base',     Display => 1 },
95     MergedInto      => { Type => 'MergedInto',  Mode => 'Target',   Display => 1 },
96 );
97 our %DIRMAP = (
98     MemberOf    => { Base => 'MemberOf',    Target => 'HasMember'    },
99     RefersTo    => { Base => 'RefersTo',    Target => 'ReferredToBy' },
100     DependsOn   => { Base => 'DependsOn',   Target => 'DependedOnBy' },
101     MergedInto  => { Base => 'MergedInto',  Target => 'MergedInto'   },
102 );
103
104 __PACKAGE__->_BuildDisplayAs;
105
106 my %DISPLAY_AS;
107 sub _BuildDisplayAs {
108     %DISPLAY_AS = ();
109     foreach my $in_db ( uniq map { $_->{Type} } values %TYPEMAP ) {
110         foreach my $mode (qw(Base Target)) {
111             $DISPLAY_AS{$in_db}{$mode} = first {
112                    $TYPEMAP{$_}{Display}
113                 && $TYPEMAP{$_}{Type} eq $in_db
114                 && $TYPEMAP{$_}{Mode} eq $mode
115             } keys %TYPEMAP;
116         }
117     }
118 }
119
120 =head1 CLASS METHODS
121
122 =head2 DisplayTypes
123
124 Returns a list of the standard link Types for display, including directional
125 variants but not aliases.
126
127 =cut
128
129 sub DisplayTypes {
130     sort { $a cmp $b }
131     uniq
132     grep { defined }
133      map { values %$_ }
134   values %DISPLAY_AS
135 }
136
137 =head1 METHODS
138
139 =head2 Create PARAMHASH
140
141 Create a new link object. Takes 'Base', 'Target' and 'Type'.
142 Returns undef on failure or a Link Id on success.
143
144 =cut
145
146 sub Create {
147     my $self = shift;
148     my %args = ( Base   => undef,
149                  Target => undef,
150                  Type   => undef,
151                  @_ );
152
153     my $base = RT::URI->new( $self->CurrentUser );
154     unless ($base->FromURI( $args{'Base'} )) {
155         my $msg = $self->loc("Couldn't resolve base '[_1]' into a URI.", $args{'Base'});
156         $RT::Logger->warning( "$self $msg" );
157         return wantarray ? (undef, $msg) : undef;
158     }
159
160     my $target = RT::URI->new( $self->CurrentUser );
161     unless ($target->FromURI( $args{'Target'} )) {
162         my $msg = $self->loc("Couldn't resolve target '[_1]' into a URI.", $args{'Target'});
163         $RT::Logger->warning( "$self $msg" );
164         return wantarray ? (undef, $msg) : undef;
165     }
166
167     my $base_id   = 0;
168     my $target_id = 0;
169
170
171
172
173     if ( $base->IsLocal ) {
174         my $object = $base->Object;
175         unless (UNIVERSAL::can($object, 'Id')) {
176             return (undef, $self->loc("[_1] appears to be a local object, but can't be found in the database", $args{'Base'}));
177         
178         }
179         $base_id = $object->Id if UNIVERSAL::isa($object, 'RT::Ticket');
180     }
181     if ( $target->IsLocal ) {
182         my $object = $target->Object;
183         unless (UNIVERSAL::can($object, 'Id')) {
184             return (undef, $self->loc("[_1] appears to be a local object, but can't be found in the database", $args{'Target'}));
185         
186         }
187         $target_id = $object->Id if UNIVERSAL::isa($object, 'RT::Ticket');
188     }
189
190     # We don't want references to ourself
191     if ( $base->URI eq $target->URI ) {
192         return ( 0, $self->loc("Can't link a ticket to itself") );
193     }
194
195     # }}}
196
197     my ( $id, $msg ) = $self->SUPER::Create( Base        => $base->URI,
198                                              Target      => $target->URI,
199                                              LocalBase   => $base_id,
200                                              LocalTarget => $target_id,
201                                              Type        => $args{'Type'} );
202     return ( $id, $msg );
203 }
204
205  # sub LoadByParams
206
207 =head2 LoadByParams
208
209   Load an RT::Link object from the database.  Takes three parameters
210   
211   Base => undef,
212   Target => undef,
213   Type =>undef
214  
215   Base and Target are expected to be integers which refer to Tickets or URIs
216   Type is the link type
217
218 =cut
219
220 sub LoadByParams {
221     my $self = shift;
222     my %args = ( Base   => undef,
223                  Target => undef,
224                  Type   => undef,
225                  @_ );
226
227     my $base = RT::URI->new($self->CurrentUser);
228     $base->FromURI( $args{'Base'} )
229         or return wantarray ? (0, $self->loc("Couldn't parse Base URI: [_1]", $args{Base})) : 0;
230
231     my $target = RT::URI->new($self->CurrentUser);
232     $target->FromURI( $args{'Target'} )
233         or return wantarray ? (0, $self->loc("Couldn't parse Target URI: [_1]", $args{Target})) : 0;
234
235     my ( $id, $msg ) = $self->LoadByCols( Base   => $base->URI,
236                                           Type   => $args{'Type'},
237                                           Target => $target->URI );
238
239     unless ($id) {
240         return wantarray ? ( 0, $self->loc("Couldn't load link: [_1]", $msg) ) : 0;
241     } else {
242         return wantarray ? ($id, $msg) : $id;
243     }
244 }
245
246
247 =head2 Load
248
249   Load an RT::Link object from the database.  Takes one parameter, the id of an entry in the links table.
250
251
252 =cut
253
254 sub Load {
255     my $self       = shift;
256     my $identifier = shift;
257
258
259
260
261     if ( $identifier !~ /^\d+$/ ) {
262         return wantarray ? ( 0, $self->loc("That's not a numerical id") ) : 0;
263     }
264     else {
265         my ( $id, $msg ) = $self->LoadById($identifier);
266         unless ( $self->Id ) {
267             return wantarray ? ( 0, $self->loc("Couldn't load link") ) : 0;
268         }
269         return wantarray ? ( $id, $msg ) : $id;
270     }
271 }
272
273
274
275
276 =head2 TargetURI
277
278 returns an RT::URI object for the "Target" of this link.
279
280 =cut
281
282 sub TargetURI {
283     my $self = shift;
284     my $URI = RT::URI->new($self->CurrentUser);
285     $URI->FromURI($self->Target);
286     return ($URI);
287 }
288
289
290 =head2 TargetObj
291
292 =cut
293
294 sub TargetObj {
295     my $self = shift;
296     return $self->TargetURI->Object;
297 }
298
299
300 =head2 BaseURI
301
302 returns an RT::URI object for the "Base" of this link.
303
304 =cut
305
306 sub BaseURI {
307     my $self = shift;
308     my $URI = RT::URI->new($self->CurrentUser);
309     $URI->FromURI($self->Base);
310     return ($URI);
311 }
312
313
314 =head2 BaseObj
315
316 =cut
317
318 sub BaseObj {
319   my $self = shift;
320   return $self->BaseURI->Object;
321 }
322
323 =head2 id
324
325 Returns the current value of id.
326 (In the database, id is stored as int(11).)
327
328
329 =cut
330
331
332 =head2 Base
333
334 Returns the current value of Base.
335 (In the database, Base is stored as varchar(240).)
336
337
338
339 =head2 SetBase VALUE
340
341
342 Set Base to VALUE.
343 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
344 (In the database, Base will be stored as a varchar(240).)
345
346
347 =cut
348
349
350 =head2 Target
351
352 Returns the current value of Target.
353 (In the database, Target is stored as varchar(240).)
354
355
356
357 =head2 SetTarget VALUE
358
359
360 Set Target to VALUE.
361 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
362 (In the database, Target will be stored as a varchar(240).)
363
364
365 =cut
366
367
368 =head2 Type
369
370 Returns the current value of Type.
371 (In the database, Type is stored as varchar(20).)
372
373
374
375 =head2 SetType VALUE
376
377
378 Set Type to VALUE.
379 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
380 (In the database, Type will be stored as a varchar(20).)
381
382
383 =cut
384
385
386 =head2 LocalTarget
387
388 Returns the current value of LocalTarget.
389 (In the database, LocalTarget is stored as int(11).)
390
391
392
393 =head2 SetLocalTarget VALUE
394
395
396 Set LocalTarget to VALUE.
397 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
398 (In the database, LocalTarget will be stored as a int(11).)
399
400
401 =cut
402
403
404 =head2 LocalBase
405
406 Returns the current value of LocalBase.
407 (In the database, LocalBase is stored as int(11).)
408
409
410
411 =head2 SetLocalBase VALUE
412
413
414 Set LocalBase to VALUE.
415 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
416 (In the database, LocalBase will be stored as a int(11).)
417
418
419 =cut
420
421
422 =head2 LastUpdatedBy
423
424 Returns the current value of LastUpdatedBy.
425 (In the database, LastUpdatedBy is stored as int(11).)
426
427
428 =cut
429
430
431 =head2 LastUpdated
432
433 Returns the current value of LastUpdated.
434 (In the database, LastUpdated is stored as datetime.)
435
436
437 =cut
438
439
440 =head2 Creator
441
442 Returns the current value of Creator.
443 (In the database, Creator is stored as int(11).)
444
445
446 =cut
447
448
449 =head2 Created
450
451 Returns the current value of Created.
452 (In the database, Created is stored as datetime.)
453
454
455 =cut
456
457
458
459 sub _CoreAccessible {
460     {
461
462         id =>
463                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
464         Base =>
465                 {read => 1, write => 1, sql_type => 12, length => 240,  is_blob => 0,  is_numeric => 0,  type => 'varchar(240)', default => ''},
466         Target =>
467                 {read => 1, write => 1, sql_type => 12, length => 240,  is_blob => 0,  is_numeric => 0,  type => 'varchar(240)', default => ''},
468         Type =>
469                 {read => 1, write => 1, sql_type => 12, length => 20,  is_blob => 0,  is_numeric => 0,  type => 'varchar(20)', default => ''},
470         LocalTarget =>
471                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
472         LocalBase =>
473                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
474         LastUpdatedBy =>
475                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
476         LastUpdated =>
477                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
478         Creator =>
479                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
480         Created =>
481                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
482
483  }
484 };
485
486 sub FindDependencies {
487     my $self = shift;
488     my ($walker, $deps) = @_;
489
490     $self->SUPER::FindDependencies($walker, $deps);
491
492     $deps->Add( out => $self->BaseObj )   if $self->BaseObj   and $self->BaseObj->id;
493     $deps->Add( out => $self->TargetObj ) if $self->TargetObj and $self->TargetObj->id;
494 }
495
496 sub __DependsOn {
497     my $self = shift;
498     my %args = (
499         Shredder => undef,
500         Dependencies => undef,
501         @_,
502     );
503     my $deps = $args{'Dependencies'};
504     my $list = [];
505
506 # AddLink transactions
507     my $map = { %RT::Link::TYPEMAP };
508     my $link_meta = $map->{ $self->Type };
509     unless ( $link_meta && $link_meta->{'Mode'} && $link_meta->{'Type'} ) {
510         RT::Shredder::Exception->throw( 'Wrong link link_meta, no record for '. $self->Type );
511     }
512     if ( $self->BaseURI->IsLocal ) {
513         my $objs = $self->BaseObj->Transactions;
514         $objs->Limit(
515             FIELD    => 'Type',
516             OPERATOR => '=',
517             VALUE    => 'AddLink',
518         );
519         $objs->Limit( FIELD => 'NewValue', VALUE => $self->Target );
520         while ( my ($k, $v) = each %$map ) {
521             next unless $v->{'Type'} eq $link_meta->{'Type'};
522             next unless $v->{'Mode'} eq $link_meta->{'Mode'};
523             $objs->Limit( FIELD => 'Field', VALUE => $k );
524         }
525         push( @$list, $objs );
526     }
527
528     my %reverse = ( Base => 'Target', Target => 'Base' );
529     if ( $self->TargetURI->IsLocal ) {
530         my $objs = $self->TargetObj->Transactions;
531         $objs->Limit(
532             FIELD    => 'Type',
533             OPERATOR => '=',
534             VALUE    => 'AddLink',
535         );
536         $objs->Limit( FIELD => 'NewValue', VALUE => $self->Base );
537         while ( my ($k, $v) = each %$map ) {
538             next unless $v->{'Type'} eq $link_meta->{'Type'};
539             next unless $v->{'Mode'} eq $reverse{ $link_meta->{'Mode'} };
540             $objs->Limit( FIELD => 'Field', VALUE => $k );
541         }
542         push( @$list, $objs );
543     }
544
545     $deps->_PushDependencies(
546         BaseObject => $self,
547         Flags => RT::Shredder::Constants::DEPENDS_ON|RT::Shredder::Constants::WIPE_AFTER,
548         TargetObjects => $list,
549         Shredder => $args{'Shredder'}
550     );
551     return $self->SUPER::__DependsOn( %args );
552 }
553
554 sub Serialize {
555     my $self = shift;
556     my %args = (@_);
557     my %store = $self->SUPER::Serialize(@_);
558
559     delete $store{LocalBase}   if $store{Base};
560     delete $store{LocalTarget} if $store{Target};
561
562     for my $dir (qw/Base Target/) {
563         my $uri = $self->${\($dir.'URI')};
564         my $object = $self->${\($dir.'Obj')};
565
566         if ($uri->IsLocal) {
567             if ($args{serializer}->Observe(object => $object)) {
568                 # no action needed; the object is being migrated
569             }
570             elsif ($args{serializer}{HyperlinkUnmigrated}) {
571                 # object is not being migrated; hyperlinkify
572                 $store{$dir} = $uri->AsHREF;
573             }
574             else {
575                 # object is not being migrated and hyperlinks not desired,
576                 # so drop this RT::Link altogether
577                 return;
578             }
579         }
580     }
581
582     return %store;
583 }
584
585
586 sub PreInflate {
587     my $class = shift;
588     my ($importer, $uid, $data) = @_;
589
590     for my $dir (qw/Base Target/) {
591         my $uid_ref = $data->{$dir};
592         next unless $uid_ref and ref $uid_ref;
593
594         my $to_uid = ${ $uid_ref };
595         my $obj = $importer->LookupObj( $to_uid );
596         if ($obj) {
597             $data->{$dir} = $obj->URI;
598             $data->{"Local$dir"} = $obj->Id if $obj->isa("RT::Ticket");
599         } else {
600             $data->{$dir} = "";
601             $importer->Postpone(
602                 for => $to_uid,
603                 uid => $uid,
604                 uri => $dir,
605                 column => ($to_uid =~ /RT::Ticket/ ? "Local$dir" : undef),
606             );
607         }
608
609     }
610
611     return $class->SUPER::PreInflate( $importer, $uid, $data );
612 }
613
614 RT::Base->_ImportOverlays();
615
616 1;