6bb09341a07225843592386a128a3eb113c777ae
[freeside.git] / rt / lib / RT / ObjectCustomFieldValue.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 package RT::ObjectCustomFieldValue;
50
51 use strict;
52 use warnings;
53 use base 'RT::Record';
54
55 use RT::Interface::Web;
56 use Regexp::Common qw(RE_net_IPv4);
57 use Regexp::IPv6 qw($IPv6_re);
58 use Regexp::Common::net::CIDR;
59 require Net::CIDR;
60
61 # Allow the empty IPv6 address
62 $IPv6_re = qr/(?:$IPv6_re|::)/;
63
64 use RT::CustomField;
65
66 sub Table {'ObjectCustomFieldValues'}
67
68
69
70
71 sub Create {
72     my $self = shift;
73     my %args = (
74         CustomField     => 0,
75         ObjectType      => '',
76         ObjectId        => 0,
77         Disabled        => 0,
78         Content         => '',
79         LargeContent    => undef,
80         ContentType     => '',
81         ContentEncoding => '',
82         @_,
83     );
84
85     my $cf = RT::CustomField->new( $self->CurrentUser );
86     $cf->Load( $args{CustomField} );
87
88     my ($val, $msg) = $cf->_CanonicalizeValue(\%args);
89     return ($val, $msg) unless $val;
90
91     my $encoded = Encode::encode("UTF-8", $args{'Content'});
92     if ( defined $args{'Content'} && length( $encoded ) > 255 ) {
93         if ( defined $args{'LargeContent'} && length $args{'LargeContent'} ) {
94             $RT::Logger->error("Content is longer than 255 bytes and LargeContent specified");
95         }
96         else {
97             # _EncodeLOB, and thus LargeContent, takes bytes; Content is
98             # in characters.  Encode it; this may replace illegal
99             # codepoints (e.g. \x{FDD0}) with \x{FFFD}.
100             $args{'LargeContent'} = Encode::encode("UTF-8",$args{'Content'});
101             $args{'Content'} = undef;
102             $args{'ContentType'} ||= 'text/plain';
103         }
104     }
105
106     ( $args{'ContentEncoding'}, $args{'LargeContent'} ) =
107         $self->_EncodeLOB( $args{'LargeContent'}, $args{'ContentType'} )
108             if defined $args{'LargeContent'};
109
110     ( my $id, $msg ) = $self->SUPER::Create(
111         CustomField     => $args{'CustomField'},
112         ObjectType      => $args{'ObjectType'},
113         ObjectId        => $args{'ObjectId'},
114         Disabled        => $args{'Disabled'},
115         Content         => $args{'Content'},
116         LargeContent    => $args{'LargeContent'},
117         ContentType     => $args{'ContentType'},
118         ContentEncoding => $args{'ContentEncoding'},
119     );
120
121     if ( $id ) {
122         my $new_value = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
123         $new_value->Load( $id );
124         my $ocfv_key = $new_value->GetOCFVCacheKey();
125         if ( $RT::ObjectCustomFieldValues::_OCFV_CACHE->{$ocfv_key} ) {
126             push @{ $RT::ObjectCustomFieldValues::_OCFV_CACHE->{$ocfv_key} },
127               {
128                 'ObjectId'       => $new_value->Id,
129                 'CustomFieldObj' => $new_value->CustomFieldObj,
130                 'Content'        => $new_value->_Value('Content'),
131                 'LargeContent'   => $new_value->LargeContent,
132               };
133         }
134     }
135
136     return wantarray ? ( $id, $msg ) : $id;
137 }
138
139
140 sub LargeContent {
141     my $self = shift;
142     return $self->_DecodeLOB(
143         $self->ContentType,
144         $self->ContentEncoding,
145         $self->_Value( 'LargeContent', decode_utf8 => 0 )
146     );
147 }
148
149
150 =head2 LoadByCols
151
152 =cut
153
154 sub LoadByCols {
155     my $self = shift;
156     my %args = (@_);
157     my $cf;
158     if ( $args{CustomField} ) {
159         $cf = RT::CustomField->new( $self->CurrentUser );
160         $cf->Load( $args{CustomField} );
161
162         my ($ok, $msg) = $cf->_CanonicalizeValue(\%args);
163         return ($ok, $msg) unless $ok;
164     }
165     return $self->SUPER::LoadByCols(%args);
166 }
167
168 =head2 LoadByTicketContentAndCustomField { Ticket => TICKET, CustomField => CUSTOMFIELD, Content => CONTENT }
169
170 Loads a custom field value by Ticket, Content and which CustomField it's tied to
171
172 =cut
173
174
175 sub LoadByTicketContentAndCustomField {
176     my $self = shift;
177     my %args = (
178         Ticket => undef,
179         CustomField => undef,
180         Content => undef,
181         @_
182     );
183
184     return $self->LoadByCols(
185         Content => $args{'Content'},
186         CustomField => $args{'CustomField'},
187         ObjectType => 'RT::Ticket',
188         ObjectId => $args{'Ticket'},
189         Disabled => 0
190     );
191 }
192
193 sub LoadByObjectContentAndCustomField {
194     my $self = shift;
195     my %args = (
196         Object => undef,
197         CustomField => undef,
198         Content => undef,
199         @_
200     );
201
202     my $obj = $args{'Object'} or return;
203
204     return $self->LoadByCols(
205         Content => $args{'Content'},
206         CustomField => $args{'CustomField'},
207         ObjectType => ref($obj),
208         ObjectId => $obj->Id,
209         Disabled => 0
210     );
211 }
212
213 =head2 CustomFieldObj
214
215 Returns the CustomField Object which has the id returned by CustomField
216
217 =cut
218
219 sub CustomFieldObj {
220     my $self = shift;
221     my $CustomField = RT::CustomField->new( $self->CurrentUser );
222     $CustomField->SetContextObject( $self->Object );
223     $CustomField->Load( $self->__Value('CustomField') );
224     return $CustomField;
225 }
226
227
228 =head2 Content
229
230 Return this custom field's content. If there's no "regular"
231 content, try "LargeContent"
232
233 =cut
234
235 my $re_ip_sunit = qr/[0-1][0-9][0-9]|2[0-4][0-9]|25[0-5]/;
236 my $re_ip_serialized = qr/$re_ip_sunit(?:\.$re_ip_sunit){3}/;
237
238 sub Content {
239     my $self = shift;
240
241     return undef unless $self->CustomFieldObj->CurrentUserHasRight('SeeCustomField');
242
243     my $content = $self->_Value('Content');
244     if (   $self->CustomFieldObj->Type eq 'IPAddress'
245         || $self->CustomFieldObj->Type eq 'IPAddressRange' )
246     {
247
248         if ( $content =~ /^\s*($re_ip_serialized)\s*$/o ) {
249             $content = sprintf "%d.%d.%d.%d", split /\./, $1;
250         }
251
252         return $content if $self->CustomFieldObj->Type eq 'IPAddress';
253
254         my $large_content = $self->__Value('LargeContent');
255         if ( $large_content =~ /^\s*($re_ip_serialized)\s*$/o ) {
256             my $eIP = sprintf "%d.%d.%d.%d", split /\./, $1;
257             if ( $content eq $eIP ) {
258                 return $content;
259             }
260             else {
261                 return $content . "-" . $eIP;
262             }
263         }
264         elsif ( $large_content =~ /^\s*($IPv6_re)\s*$/o ) {
265             my $eIP = $1;
266             if ( $content eq $eIP ) {
267                 return $content;
268             }
269             else {
270                 return $content . "-" . $eIP;
271             }
272         }
273         else {
274             return $content;
275         }
276     }
277
278     if ( !(defined $content && length $content) && $self->ContentType && $self->ContentType eq 'text/plain' ) {
279         return $self->LargeContent;
280     } else {
281         return $content;
282     }
283 }
284
285 =head2 Object
286
287 Returns the object this value applies to
288
289 =cut
290
291 sub Object {
292     my $self  = shift;
293     my $Object = $self->__Value('ObjectType')->new( $self->CurrentUser );
294     $Object->LoadById( $self->__Value('ObjectId') );
295     return $Object;
296 }
297
298
299 =head2 Delete
300
301 Disable this value. Used to remove "current" values from records while leaving them in the history.
302
303 =cut
304
305
306 sub Delete {
307     my $self = shift;
308     my ( $ret, $msg ) = $self->SetDisabled( 1 );
309     if ( $ret ) {
310         my $ocfv_key = $self->GetOCFVCacheKey();
311         if ( $RT::ObjectCustomFieldValues::_OCFV_CACHE->{$ocfv_key} ) {
312             @{ $RT::ObjectCustomFieldValues::_OCFV_CACHE->{$ocfv_key} } =
313               grep { $_->{'ObjectId'} != $self->Id } @{ $RT::ObjectCustomFieldValues::_OCFV_CACHE->{$ocfv_key} };
314         }
315     }
316     return wantarray ? ( $ret, $msg ) : $ret;
317 }
318
319 =head2 _FillInTemplateURL URL
320
321 Takes a URL containing placeholders and returns the URL as filled in for this 
322 ObjectCustomFieldValue. The values for the placeholders will be URI-escaped.
323
324 Available placeholders:
325
326 =over
327
328 =item __id__
329
330 The id of the object in question.
331
332 =item __CustomField__
333
334 The value of this custom field for the object in question.
335
336 =item __WebDomain__, __WebPort__, __WebPath__, __WebBaseURL__ and __WebURL__
337
338 The value of the config option.
339
340 =back
341
342 =cut
343
344 {
345 my %placeholders = (
346     id          => { value => sub { $_[0]->ObjectId }, escape => 1 },
347     CustomField => { value => sub { $_[0]->Content }, escape => 1 },
348     WebDomain   => { value => sub { RT->Config->Get('WebDomain') } },
349     WebPort     => { value => sub { RT->Config->Get('WebPort') } },
350     WebPath     => { value => sub { RT->Config->Get('WebPath') } },
351     WebBaseURL  => { value => sub { RT->Config->Get('WebBaseURL') } },
352     WebURL      => { value => sub { RT->Config->Get('WebURL') } },
353 );
354
355 sub _FillInTemplateURL {
356     my $self = shift;
357     my $url = shift;
358
359     return undef unless defined $url && length $url;
360
361     # special case, whole value should be an URL
362     if ( $url =~ /^__CustomField__/ ) {
363         my $value = $self->Content;
364         # protect from potentially malicious URLs
365         if ( $value =~ /^\s*(?:javascript|data):/i ) {
366             my $object = $self->Object;
367             $RT::Logger->error(
368                 "Potentially dangerous URL type in custom field '". $self->CustomFieldObj->Name ."'"
369                 ." on ". ref($object) ." #". $object->id
370             );
371             return undef;
372         }
373         $url =~ s/^__CustomField__/$value/;
374     }
375
376     # default value, uri-escape
377     for my $key (keys %placeholders) {
378         $url =~ s{__${key}__}{
379             my $value = $placeholders{$key}{'value'}->( $self );
380             $value = '' if !defined($value);
381             RT::Interface::Web::EscapeURI(\$value) if $placeholders{$key}{'escape'};
382             $value
383         }gxe;
384     }
385
386     return $url;
387 } }
388
389
390 =head2 ValueLinkURL
391
392 Returns a filled in URL template for this ObjectCustomFieldValue, suitable for 
393 constructing a hyperlink in RT's webui. Returns undef if this custom field doesn't have
394 a LinkValueTo
395
396 =cut
397
398 sub LinkValueTo {
399     my $self = shift;
400     return $self->_FillInTemplateURL($self->CustomFieldObj->LinkValueTo);
401 }
402
403
404
405 =head2 ValueIncludeURL
406
407 Returns a filled in URL template for this ObjectCustomFieldValue, suitable for 
408 constructing a hyperlink in RT's webui. Returns undef if this custom field doesn't have
409 a IncludeContentForValue
410
411 =cut
412
413 sub IncludeContentForValue {
414     my $self = shift;
415     return $self->_FillInTemplateURL($self->CustomFieldObj->IncludeContentForValue);
416 }
417
418
419 sub ParseIPRange {
420     my $self = shift;
421     my $value = shift or return;
422     $value = lc $value;
423     $value =~ s!^\s+!!;
424     $value =~ s!\s+$!!;
425     
426     if ( $value =~ /^$RE{net}{CIDR}{IPv4}{-keep}$/go ) {
427         my $cidr = join( '.', map $_||0, (split /\./, $1)[0..3] ) ."/$2";
428         $value = (Net::CIDR::cidr2range( $cidr ))[0] || $value;
429     }
430     elsif ( $value =~ /^$IPv6_re(?:\/\d+)?$/o ) {
431         $value = (Net::CIDR::cidr2range( $value ))[0] || $value;
432     }
433     
434     my ($sIP, $eIP);
435     if ( $value =~ /^($RE{net}{IPv4})$/o ) {
436         $sIP = $eIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
437     }
438     elsif ( $value =~ /^($RE{net}{IPv4})-($RE{net}{IPv4})$/o ) {
439         $sIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
440         $eIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $2;
441     }
442     elsif ( $value =~ /^($IPv6_re)$/o ) {
443         $sIP = $self->ParseIP( $1 );
444         $eIP = $sIP;
445     }
446     elsif ( $value =~ /^($IPv6_re)-($IPv6_re)$/o ) {
447         ($sIP, $eIP) = ( $1, $2 );
448         $sIP = $self->ParseIP( $sIP );
449         $eIP = $self->ParseIP( $eIP );
450     }
451     else {
452         return;
453     }
454
455     ($sIP, $eIP) = ($eIP, $sIP) if $sIP gt $eIP;
456     
457     return $sIP, $eIP;
458 }
459
460 sub ParseIP {
461     my $self = shift;
462     my $value = shift or return;
463     $value = lc $value;
464     $value =~ s!^\s+!!;
465     $value =~ s!\s+$!!;
466
467     if ( $value =~ /^($RE{net}{IPv4})$/o ) {
468         return sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
469     }
470     elsif ( $value =~ /^$IPv6_re$/o ) {
471
472         # up_fields are before '::'
473         # low_fields are after '::' but without v4
474         # v4_fields are the v4
475         my ( @up_fields, @low_fields, @v4_fields );
476         my $v6;
477         if ( $value =~ /(.*:)(\d+\..*)/ ) {
478             ( $v6, my $v4 ) = ( $1, $2 );
479             chop $v6 unless $v6 =~ /::$/;
480             while ( $v4 =~ /(\d+)\.(\d+)/g ) {
481                 push @v4_fields, sprintf '%.2x%.2x', $1, $2;
482             }
483         }
484         else {
485             $v6 = $value;
486         }
487
488         my ( $up, $low );
489         if ( $v6 =~ /::/ ) {
490             ( $up, $low ) = split /::/, $v6;
491         }
492         else {
493             $up = $v6;
494         }
495
496         @up_fields = split /:/, $up;
497         @low_fields = split /:/, $low if $low;
498
499         my @zero_fields =
500           ('0000') x ( 8 - @v4_fields - @up_fields - @low_fields );
501         my @fields = ( @up_fields, @zero_fields, @low_fields, @v4_fields );
502
503         return join ':', map { sprintf "%.4x", hex "0x$_" } @fields;
504     }
505     return;
506 }
507
508
509 =head2 GetOCFVCacheKey
510
511 Get the OCFV cache key for this object
512
513 =cut
514
515 sub GetOCFVCacheKey {
516     my $self = shift;
517     my $ocfv_key = "CustomField-" . $self->CustomField
518         . '-ObjectType-' . $self->ObjectType
519         . '-ObjectId-' . $self->ObjectId;
520     return $ocfv_key;
521 }
522
523 =head2 id
524
525 Returns the current value of id.
526 (In the database, id is stored as int(11).)
527
528
529 =cut
530
531
532 =head2 CustomField
533
534 Returns the current value of CustomField.
535 (In the database, CustomField is stored as int(11).)
536
537
538
539 =head2 SetCustomField VALUE
540
541
542 Set CustomField to VALUE.
543 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
544 (In the database, CustomField will be stored as a int(11).)
545
546
547 =cut
548
549 =head2 ObjectType
550
551 Returns the current value of ObjectType.
552 (In the database, ObjectType is stored as varchar(255).)
553
554
555
556 =head2 SetObjectType VALUE
557
558
559 Set ObjectType to VALUE.
560 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
561 (In the database, ObjectType will be stored as a varchar(255).)
562
563
564 =cut
565
566
567 =head2 ObjectId
568
569 Returns the current value of ObjectId.
570 (In the database, ObjectId is stored as int(11).)
571
572
573
574 =head2 SetObjectId VALUE
575
576
577 Set ObjectId to VALUE.
578 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
579 (In the database, ObjectId will be stored as a int(11).)
580
581
582 =cut
583
584
585 =head2 SortOrder
586
587 Returns the current value of SortOrder.
588 (In the database, SortOrder is stored as int(11).)
589
590
591
592 =head2 SetSortOrder VALUE
593
594
595 Set SortOrder to VALUE.
596 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
597 (In the database, SortOrder will be stored as a int(11).)
598
599
600 =cut
601
602
603 =head2 Content
604
605 Returns the current value of Content.
606 (In the database, Content is stored as varchar(255).)
607
608
609
610 =head2 SetContent VALUE
611
612
613 Set Content to VALUE.
614 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
615 (In the database, Content will be stored as a varchar(255).)
616
617
618 =cut
619
620
621 =head2 LargeContent
622
623 Returns the current value of LargeContent.
624 (In the database, LargeContent is stored as longblob.)
625
626
627
628 =head2 SetLargeContent VALUE
629
630
631 Set LargeContent to VALUE.
632 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
633 (In the database, LargeContent will be stored as a longblob.)
634
635
636 =cut
637
638
639 =head2 ContentType
640
641 Returns the current value of ContentType.
642 (In the database, ContentType is stored as varchar(80).)
643
644
645
646 =head2 SetContentType VALUE
647
648
649 Set ContentType to VALUE.
650 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
651 (In the database, ContentType will be stored as a varchar(80).)
652
653
654 =cut
655
656
657 =head2 ContentEncoding
658
659 Returns the current value of ContentEncoding.
660 (In the database, ContentEncoding is stored as varchar(80).)
661
662
663
664 =head2 SetContentEncoding VALUE
665
666
667 Set ContentEncoding to VALUE.
668 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
669 (In the database, ContentEncoding will be stored as a varchar(80).)
670
671
672 =cut
673
674
675 =head2 Creator
676
677 Returns the current value of Creator.
678 (In the database, Creator is stored as int(11).)
679
680
681 =cut
682
683
684 =head2 Created
685
686 Returns the current value of Created.
687 (In the database, Created is stored as datetime.)
688
689
690 =cut
691
692
693 =head2 LastUpdatedBy
694
695 Returns the current value of LastUpdatedBy.
696 (In the database, LastUpdatedBy is stored as int(11).)
697
698
699 =cut
700
701
702 =head2 LastUpdated
703
704 Returns the current value of LastUpdated.
705 (In the database, LastUpdated is stored as datetime.)
706
707
708 =cut
709
710
711 =head2 Disabled
712
713 Returns the current value of Disabled.
714 (In the database, Disabled is stored as smallint(6).)
715
716
717
718 =head2 SetDisabled VALUE
719
720
721 Set Disabled to VALUE.
722 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
723 (In the database, Disabled will be stored as a smallint(6).)
724
725
726 =cut
727
728
729
730 sub _CoreAccessible {
731     {
732
733         id =>
734                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
735         CustomField =>
736                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
737         ObjectType =>
738                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
739         ObjectId =>
740                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
741         SortOrder =>
742                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
743         Content =>
744                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
745         LargeContent =>
746                 {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'longblob', default => ''},
747         ContentType =>
748                 {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
749         ContentEncoding =>
750                 {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
751         Creator =>
752                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
753         Created =>
754                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
755         LastUpdatedBy =>
756                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
757         LastUpdated =>
758                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
759         Disabled =>
760                 {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
761
762  }
763 };
764
765 sub FindDependencies {
766     my $self = shift;
767     my ($walker, $deps) = @_;
768
769     $self->SUPER::FindDependencies($walker, $deps);
770
771     $deps->Add( out => $self->CustomFieldObj );
772     $deps->Add( out => $self->Object );
773 }
774
775 RT::Base->_ImportOverlays();
776
777 1;