1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2017 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 return $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'},
125 return $self->_DecodeLOB(
127 $self->ContentEncoding,
128 $self->_Value( 'LargeContent', decode_utf8 => 0 )
141 if ( $args{CustomField} ) {
142 $cf = RT::CustomField->new( $self->CurrentUser );
143 $cf->Load( $args{CustomField} );
145 my ($ok, $msg) = $cf->_CanonicalizeValue(\%args);
146 return ($ok, $msg) unless $ok;
148 return $self->SUPER::LoadByCols(%args);
151 =head2 LoadByTicketContentAndCustomField { Ticket => TICKET, CustomField => CUSTOMFIELD, Content => CONTENT }
153 Loads a custom field value by Ticket, Content and which CustomField it's tied to
158 sub LoadByTicketContentAndCustomField {
162 CustomField => undef,
167 return $self->LoadByCols(
168 Content => $args{'Content'},
169 CustomField => $args{'CustomField'},
170 ObjectType => 'RT::Ticket',
171 ObjectId => $args{'Ticket'},
176 sub LoadByObjectContentAndCustomField {
180 CustomField => undef,
185 my $obj = $args{'Object'} or return;
187 return $self->LoadByCols(
188 Content => $args{'Content'},
189 CustomField => $args{'CustomField'},
190 ObjectType => ref($obj),
191 ObjectId => $obj->Id,
196 =head2 CustomFieldObj
198 Returns the CustomField Object which has the id returned by CustomField
204 my $CustomField = RT::CustomField->new( $self->CurrentUser );
205 $CustomField->SetContextObject( $self->Object );
206 $CustomField->Load( $self->__Value('CustomField') );
213 Return this custom field's content. If there's no "regular"
214 content, try "LargeContent"
218 my $re_ip_sunit = qr/[0-1][0-9][0-9]|2[0-4][0-9]|25[0-5]/;
219 my $re_ip_serialized = qr/$re_ip_sunit(?:\.$re_ip_sunit){3}/;
224 return undef unless $self->CustomFieldObj->CurrentUserHasRight('SeeCustomField');
226 my $content = $self->_Value('Content');
227 if ( $self->CustomFieldObj->Type eq 'IPAddress'
228 || $self->CustomFieldObj->Type eq 'IPAddressRange' )
231 if ( $content =~ /^\s*($re_ip_serialized)\s*$/o ) {
232 $content = sprintf "%d.%d.%d.%d", split /\./, $1;
235 return $content if $self->CustomFieldObj->Type eq 'IPAddress';
237 my $large_content = $self->__Value('LargeContent');
238 if ( $large_content =~ /^\s*($re_ip_serialized)\s*$/o ) {
239 my $eIP = sprintf "%d.%d.%d.%d", split /\./, $1;
240 if ( $content eq $eIP ) {
244 return $content . "-" . $eIP;
247 elsif ( $large_content =~ /^\s*($IPv6_re)\s*$/o ) {
249 if ( $content eq $eIP ) {
253 return $content . "-" . $eIP;
261 if ( !(defined $content && length $content) && $self->ContentType && $self->ContentType eq 'text/plain' ) {
262 return $self->LargeContent;
270 Returns the object this value applies to
276 my $Object = $self->__Value('ObjectType')->new( $self->CurrentUser );
277 $Object->LoadById( $self->__Value('ObjectId') );
284 Disable this value. Used to remove "current" values from records while leaving them in the history.
291 return $self->SetDisabled(1);
294 =head2 _FillInTemplateURL URL
296 Takes a URL containing placeholders and returns the URL as filled in for this
297 ObjectCustomFieldValue. The values for the placeholders will be URI-escaped.
299 Available placeholders:
305 The id of the object in question.
307 =item __CustomField__
309 The value of this custom field for the object in question.
311 =item __WebDomain__, __WebPort__, __WebPath__, __WebBaseURL__ and __WebURL__
313 The value of the config option.
321 id => { value => sub { $_[0]->ObjectId }, escape => 1 },
322 CustomField => { value => sub { $_[0]->Content }, escape => 1 },
323 WebDomain => { value => sub { RT->Config->Get('WebDomain') } },
324 WebPort => { value => sub { RT->Config->Get('WebPort') } },
325 WebPath => { value => sub { RT->Config->Get('WebPath') } },
326 WebBaseURL => { value => sub { RT->Config->Get('WebBaseURL') } },
327 WebURL => { value => sub { RT->Config->Get('WebURL') } },
330 sub _FillInTemplateURL {
334 return undef unless defined $url && length $url;
336 # special case, whole value should be an URL
337 if ( $url =~ /^__CustomField__/ ) {
338 my $value = $self->Content;
339 # protect from potentially malicious URLs
340 if ( $value =~ /^\s*(?:javascript|data):/i ) {
341 my $object = $self->Object;
343 "Potentially dangerous URL type in custom field '". $self->CustomFieldObj->Name ."'"
344 ." on ". ref($object) ." #". $object->id
348 $url =~ s/^__CustomField__/$value/;
351 # default value, uri-escape
352 for my $key (keys %placeholders) {
353 $url =~ s{__${key}__}{
354 my $value = $placeholders{$key}{'value'}->( $self );
355 $value = '' if !defined($value);
356 RT::Interface::Web::EscapeURI(\$value) if $placeholders{$key}{'escape'};
367 Returns a filled in URL template for this ObjectCustomFieldValue, suitable for
368 constructing a hyperlink in RT's webui. Returns undef if this custom field doesn't have
375 return $self->_FillInTemplateURL($self->CustomFieldObj->LinkValueTo);
380 =head2 ValueIncludeURL
382 Returns a filled in URL template for this ObjectCustomFieldValue, suitable for
383 constructing a hyperlink in RT's webui. Returns undef if this custom field doesn't have
384 a IncludeContentForValue
388 sub IncludeContentForValue {
390 return $self->_FillInTemplateURL($self->CustomFieldObj->IncludeContentForValue);
396 my $value = shift or return;
401 if ( $value =~ /^$RE{net}{CIDR}{IPv4}{-keep}$/go ) {
402 my $cidr = join( '.', map $_||0, (split /\./, $1)[0..3] ) ."/$2";
403 $value = (Net::CIDR::cidr2range( $cidr ))[0] || $value;
405 elsif ( $value =~ /^$IPv6_re(?:\/\d+)?$/o ) {
406 $value = (Net::CIDR::cidr2range( $value ))[0] || $value;
410 if ( $value =~ /^($RE{net}{IPv4})$/o ) {
411 $sIP = $eIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
413 elsif ( $value =~ /^($RE{net}{IPv4})-($RE{net}{IPv4})$/o ) {
414 $sIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
415 $eIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $2;
417 elsif ( $value =~ /^($IPv6_re)$/o ) {
418 $sIP = $self->ParseIP( $1 );
421 elsif ( $value =~ /^($IPv6_re)-($IPv6_re)$/o ) {
422 ($sIP, $eIP) = ( $1, $2 );
423 $sIP = $self->ParseIP( $sIP );
424 $eIP = $self->ParseIP( $eIP );
430 ($sIP, $eIP) = ($eIP, $sIP) if $sIP gt $eIP;
437 my $value = shift or return;
442 if ( $value =~ /^($RE{net}{IPv4})$/o ) {
443 return sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
445 elsif ( $value =~ /^$IPv6_re$/o ) {
447 # up_fields are before '::'
448 # low_fields are after '::' but without v4
449 # v4_fields are the v4
450 my ( @up_fields, @low_fields, @v4_fields );
452 if ( $value =~ /(.*:)(\d+\..*)/ ) {
453 ( $v6, my $v4 ) = ( $1, $2 );
454 chop $v6 unless $v6 =~ /::$/;
455 while ( $v4 =~ /(\d+)\.(\d+)/g ) {
456 push @v4_fields, sprintf '%.2x%.2x', $1, $2;
465 ( $up, $low ) = split /::/, $v6;
471 @up_fields = split /:/, $up;
472 @low_fields = split /:/, $low if $low;
475 ('0000') x ( 8 - @v4_fields - @up_fields - @low_fields );
476 my @fields = ( @up_fields, @zero_fields, @low_fields, @v4_fields );
478 return join ':', map { sprintf "%.4x", hex "0x$_" } @fields;
486 Returns the current value of id.
487 (In the database, id is stored as int(11).)
495 Returns the current value of CustomField.
496 (In the database, CustomField is stored as int(11).)
500 =head2 SetCustomField VALUE
503 Set CustomField to VALUE.
504 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
505 (In the database, CustomField will be stored as a int(11).)
512 Returns the current value of ObjectType.
513 (In the database, ObjectType is stored as varchar(255).)
517 =head2 SetObjectType VALUE
520 Set ObjectType to VALUE.
521 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
522 (In the database, ObjectType will be stored as a varchar(255).)
530 Returns the current value of ObjectId.
531 (In the database, ObjectId is stored as int(11).)
535 =head2 SetObjectId VALUE
538 Set ObjectId to VALUE.
539 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
540 (In the database, ObjectId will be stored as a int(11).)
548 Returns the current value of SortOrder.
549 (In the database, SortOrder is stored as int(11).)
553 =head2 SetSortOrder VALUE
556 Set SortOrder to VALUE.
557 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
558 (In the database, SortOrder will be stored as a int(11).)
566 Returns the current value of Content.
567 (In the database, Content is stored as varchar(255).)
571 =head2 SetContent VALUE
574 Set Content to VALUE.
575 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
576 (In the database, Content will be stored as a varchar(255).)
584 Returns the current value of LargeContent.
585 (In the database, LargeContent is stored as longblob.)
589 =head2 SetLargeContent VALUE
592 Set LargeContent to VALUE.
593 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
594 (In the database, LargeContent will be stored as a longblob.)
602 Returns the current value of ContentType.
603 (In the database, ContentType is stored as varchar(80).)
607 =head2 SetContentType VALUE
610 Set ContentType to VALUE.
611 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
612 (In the database, ContentType will be stored as a varchar(80).)
618 =head2 ContentEncoding
620 Returns the current value of ContentEncoding.
621 (In the database, ContentEncoding is stored as varchar(80).)
625 =head2 SetContentEncoding VALUE
628 Set ContentEncoding to VALUE.
629 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
630 (In the database, ContentEncoding will be stored as a varchar(80).)
638 Returns the current value of Creator.
639 (In the database, Creator is stored as int(11).)
647 Returns the current value of Created.
648 (In the database, Created is stored as datetime.)
656 Returns the current value of LastUpdatedBy.
657 (In the database, LastUpdatedBy is stored as int(11).)
665 Returns the current value of LastUpdated.
666 (In the database, LastUpdated is stored as datetime.)
674 Returns the current value of Disabled.
675 (In the database, Disabled is stored as smallint(6).)
679 =head2 SetDisabled VALUE
682 Set Disabled to VALUE.
683 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
684 (In the database, Disabled will be stored as a smallint(6).)
691 sub _CoreAccessible {
695 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
697 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
699 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
701 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
703 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
705 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
707 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'longblob', default => ''},
709 {read => 1, write => 1, sql_type => 12, length => 80, is_blob => 0, is_numeric => 0, type => 'varchar(80)', default => ''},
711 {read => 1, write => 1, sql_type => 12, length => 80, is_blob => 0, is_numeric => 0, type => 'varchar(80)', default => ''},
713 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
715 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
717 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
719 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
721 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
726 sub FindDependencies {
728 my ($walker, $deps) = @_;
730 $self->SUPER::FindDependencies($walker, $deps);
732 $deps->Add( out => $self->CustomFieldObj );
733 $deps->Add( out => $self->Object );
736 RT::Base->_ImportOverlays();