1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
49 package RT::ObjectCustomFieldValue;
53 use base 'RT::Record';
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;
61 # Allow the empty IPv6 address
62 $IPv6_re = qr/(?:$IPv6_re|::)/;
66 sub Table {'ObjectCustomFieldValues'}
79 LargeContent => undef,
81 ContentEncoding => '',
85 my $cf = RT::CustomField->new( $self->CurrentUser );
86 $cf->Load( $args{CustomField} );
88 my ($val, $msg) = $cf->_CanonicalizeValue(\%args);
89 return ($val, $msg) unless $val;
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");
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';
106 ( $args{'ContentEncoding'}, $args{'LargeContent'} ) =
107 $self->_EncodeLOB( $args{'LargeContent'}, $args{'ContentType'} )
108 if defined $args{'LargeContent'};
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'},
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} },
128 'ObjectId' => $new_value->Id,
129 'CustomFieldObj' => $new_value->CustomFieldObj,
130 'Content' => $new_value->_Value('Content'),
131 'LargeContent' => $new_value->LargeContent,
136 return wantarray ? ( $id, $msg ) : $id;
142 return $self->_DecodeLOB(
144 $self->ContentEncoding,
145 $self->_Value( 'LargeContent', decode_utf8 => 0 )
158 if ( $args{CustomField} ) {
159 $cf = RT::CustomField->new( $self->CurrentUser );
160 $cf->Load( $args{CustomField} );
162 my ($ok, $msg) = $cf->_CanonicalizeValue(\%args);
163 return ($ok, $msg) unless $ok;
165 return $self->SUPER::LoadByCols(%args);
168 =head2 LoadByTicketContentAndCustomField { Ticket => TICKET, CustomField => CUSTOMFIELD, Content => CONTENT }
170 Loads a custom field value by Ticket, Content and which CustomField it's tied to
175 sub LoadByTicketContentAndCustomField {
179 CustomField => undef,
184 return $self->LoadByCols(
185 Content => $args{'Content'},
186 CustomField => $args{'CustomField'},
187 ObjectType => 'RT::Ticket',
188 ObjectId => $args{'Ticket'},
193 sub LoadByObjectContentAndCustomField {
197 CustomField => undef,
202 my $obj = $args{'Object'} or return;
204 return $self->LoadByCols(
205 Content => $args{'Content'},
206 CustomField => $args{'CustomField'},
207 ObjectType => ref($obj),
208 ObjectId => $obj->Id,
213 =head2 CustomFieldObj
215 Returns the CustomField Object which has the id returned by CustomField
221 my $CustomField = RT::CustomField->new( $self->CurrentUser );
222 $CustomField->SetContextObject( $self->Object );
223 $CustomField->Load( $self->__Value('CustomField') );
230 Return this custom field's content. If there's no "regular"
231 content, try "LargeContent"
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}/;
241 return undef unless $self->CustomFieldObj->CurrentUserHasRight('SeeCustomField');
243 my $content = $self->_Value('Content');
244 if ( $self->CustomFieldObj->Type eq 'IPAddress'
245 || $self->CustomFieldObj->Type eq 'IPAddressRange' )
248 if ( $content =~ /^\s*($re_ip_serialized)\s*$/o ) {
249 $content = sprintf "%d.%d.%d.%d", split /\./, $1;
252 return $content if $self->CustomFieldObj->Type eq 'IPAddress';
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 ) {
261 return $content . "-" . $eIP;
264 elsif ( $large_content =~ /^\s*($IPv6_re)\s*$/o ) {
266 if ( $content eq $eIP ) {
270 return $content . "-" . $eIP;
278 if ( !(defined $content && length $content) && $self->ContentType && $self->ContentType eq 'text/plain' ) {
279 return $self->LargeContent;
287 Returns the object this value applies to
293 my $Object = $self->__Value('ObjectType')->new( $self->CurrentUser );
294 $Object->LoadById( $self->__Value('ObjectId') );
301 Disable this value. Used to remove "current" values from records while leaving them in the history.
308 my ( $ret, $msg ) = $self->SetDisabled( 1 );
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} };
316 return wantarray ? ( $ret, $msg ) : $ret;
319 =head2 _FillInTemplateURL URL
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.
324 Available placeholders:
330 The id of the object in question.
332 =item __CustomField__
334 The value of this custom field for the object in question.
336 =item __WebDomain__, __WebPort__, __WebPath__, __WebBaseURL__ and __WebURL__
338 The value of the config option.
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') } },
355 sub _FillInTemplateURL {
359 return undef unless defined $url && length $url;
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;
368 "Potentially dangerous URL type in custom field '". $self->CustomFieldObj->Name ."'"
369 ." on ". ref($object) ." #". $object->id
373 $url =~ s/^__CustomField__/$value/;
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'};
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
400 return $self->_FillInTemplateURL($self->CustomFieldObj->LinkValueTo);
405 =head2 ValueIncludeURL
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
413 sub IncludeContentForValue {
415 return $self->_FillInTemplateURL($self->CustomFieldObj->IncludeContentForValue);
421 my $value = shift or return;
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;
430 elsif ( $value =~ /^$IPv6_re(?:\/\d+)?$/o ) {
431 $value = (Net::CIDR::cidr2range( $value ))[0] || $value;
435 if ( $value =~ /^($RE{net}{IPv4})$/o ) {
436 $sIP = $eIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
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;
442 elsif ( $value =~ /^($IPv6_re)$/o ) {
443 $sIP = $self->ParseIP( $1 );
446 elsif ( $value =~ /^($IPv6_re)-($IPv6_re)$/o ) {
447 ($sIP, $eIP) = ( $1, $2 );
448 $sIP = $self->ParseIP( $sIP );
449 $eIP = $self->ParseIP( $eIP );
455 ($sIP, $eIP) = ($eIP, $sIP) if $sIP gt $eIP;
462 my $value = shift or return;
467 if ( $value =~ /^($RE{net}{IPv4})$/o ) {
468 return sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
470 elsif ( $value =~ /^$IPv6_re$/o ) {
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 );
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;
490 ( $up, $low ) = split /::/, $v6;
496 @up_fields = split /:/, $up;
497 @low_fields = split /:/, $low if $low;
500 ('0000') x ( 8 - @v4_fields - @up_fields - @low_fields );
501 my @fields = ( @up_fields, @zero_fields, @low_fields, @v4_fields );
503 return join ':', map { sprintf "%.4x", hex "0x$_" } @fields;
509 =head2 GetOCFVCacheKey
511 Get the OCFV cache key for this object
515 sub GetOCFVCacheKey {
517 my $ocfv_key = "CustomField-" . $self->CustomField
518 . '-ObjectType-' . $self->ObjectType
519 . '-ObjectId-' . $self->ObjectId;
525 Returns the current value of id.
526 (In the database, id is stored as int(11).)
534 Returns the current value of CustomField.
535 (In the database, CustomField is stored as int(11).)
539 =head2 SetCustomField VALUE
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).)
551 Returns the current value of ObjectType.
552 (In the database, ObjectType is stored as varchar(255).)
556 =head2 SetObjectType VALUE
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).)
569 Returns the current value of ObjectId.
570 (In the database, ObjectId is stored as int(11).)
574 =head2 SetObjectId VALUE
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).)
587 Returns the current value of SortOrder.
588 (In the database, SortOrder is stored as int(11).)
592 =head2 SetSortOrder VALUE
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).)
605 Returns the current value of Content.
606 (In the database, Content is stored as varchar(255).)
610 =head2 SetContent VALUE
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).)
623 Returns the current value of LargeContent.
624 (In the database, LargeContent is stored as longblob.)
628 =head2 SetLargeContent VALUE
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.)
641 Returns the current value of ContentType.
642 (In the database, ContentType is stored as varchar(80).)
646 =head2 SetContentType VALUE
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).)
657 =head2 ContentEncoding
659 Returns the current value of ContentEncoding.
660 (In the database, ContentEncoding is stored as varchar(80).)
664 =head2 SetContentEncoding VALUE
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).)
677 Returns the current value of Creator.
678 (In the database, Creator is stored as int(11).)
686 Returns the current value of Created.
687 (In the database, Created is stored as datetime.)
695 Returns the current value of LastUpdatedBy.
696 (In the database, LastUpdatedBy is stored as int(11).)
704 Returns the current value of LastUpdated.
705 (In the database, LastUpdated is stored as datetime.)
713 Returns the current value of Disabled.
714 (In the database, Disabled is stored as smallint(6).)
718 =head2 SetDisabled VALUE
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).)
730 sub _CoreAccessible {
734 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
736 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
738 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
740 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
742 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
744 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
746 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'longblob', default => ''},
748 {read => 1, write => 1, sql_type => 12, length => 80, is_blob => 0, is_numeric => 0, type => 'varchar(80)', default => ''},
750 {read => 1, write => 1, sql_type => 12, length => 80, is_blob => 0, is_numeric => 0, type => 'varchar(80)', default => ''},
752 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
754 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
756 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
758 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
760 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
765 sub FindDependencies {
767 my ($walker, $deps) = @_;
769 $self->SUPER::FindDependencies($walker, $deps);
771 $deps->Add( out => $self->CustomFieldObj );
772 $deps->Add( out => $self->Object );
775 RT::Base->_ImportOverlays();