1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2014 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;
54 use RT::Interface::Web;
55 use Regexp::Common qw(RE_net_IPv4);
56 use Regexp::IPv6 qw($IPv6_re);
57 use Regexp::Common::net::CIDR;
60 # Allow the empty IPv6 address
61 $IPv6_re = qr/(?:$IPv6_re|::)/;
66 use base 'RT::Record';
68 sub Table {'ObjectCustomFieldValues'}
81 LargeContent => undef,
83 ContentEncoding => '',
87 my $cf = RT::CustomField->new( $self->CurrentUser );
88 $cf->Load( $args{CustomField} );
90 my ($val, $msg) = $cf->_CanonicalizeValue(\%args);
91 return ($val, $msg) unless $val;
93 my $encoded = Encode::encode("UTF-8", $args{'Content'});
94 if ( defined $args{'Content'} && length( $encoded ) > 255 ) {
95 if ( defined $args{'LargeContent'} && length $args{'LargeContent'} ) {
96 $RT::Logger->error("Content is longer than 255 bytes and LargeContent specified");
99 $args{'LargeContent'} = $args{'Content'};
100 $args{'Content'} = '';
101 $args{'ContentType'} ||= 'text/plain';
105 ( $args{'ContentEncoding'}, $args{'LargeContent'} ) =
106 $self->_EncodeLOB( $args{'LargeContent'}, $args{'ContentType'} )
107 if defined $args{'LargeContent'};
109 return $self->SUPER::Create(
110 CustomField => $args{'CustomField'},
111 ObjectType => $args{'ObjectType'},
112 ObjectId => $args{'ObjectId'},
113 Disabled => $args{'Disabled'},
114 Content => $args{'Content'},
115 LargeContent => $args{'LargeContent'},
116 ContentType => $args{'ContentType'},
117 ContentEncoding => $args{'ContentEncoding'},
124 return $self->_DecodeLOB(
126 $self->ContentEncoding,
127 $self->_Value( 'LargeContent', decode_utf8 => 0 )
140 if ( $args{CustomField} ) {
141 $cf = RT::CustomField->new( $self->CurrentUser );
142 $cf->Load( $args{CustomField} );
144 my ($ok, $msg) = $cf->_CanonicalizeValue(\%args);
145 return ($ok, $msg) unless $ok;
147 return $self->SUPER::LoadByCols(%args);
150 =head2 LoadByTicketContentAndCustomField { Ticket => TICKET, CustomField => CUSTOMFIELD, Content => CONTENT }
152 Loads a custom field value by Ticket, Content and which CustomField it's tied to
157 sub LoadByTicketContentAndCustomField {
161 CustomField => undef,
166 return $self->LoadByCols(
167 Content => $args{'Content'},
168 CustomField => $args{'CustomField'},
169 ObjectType => 'RT::Ticket',
170 ObjectId => $args{'Ticket'},
175 sub LoadByObjectContentAndCustomField {
179 CustomField => undef,
184 my $obj = $args{'Object'} or return;
186 return $self->LoadByCols(
187 Content => $args{'Content'},
188 CustomField => $args{'CustomField'},
189 ObjectType => ref($obj),
190 ObjectId => $obj->Id,
195 =head2 CustomFieldObj
197 Returns the CustomField Object which has the id returned by CustomField
203 my $CustomField = RT::CustomField->new( $self->CurrentUser );
204 $CustomField->SetContextObject( $self->Object );
205 $CustomField->Load( $self->__Value('CustomField') );
212 Return this custom field's content. If there's no "regular"
213 content, try "LargeContent"
217 my $re_ip_sunit = qr/[0-1][0-9][0-9]|2[0-4][0-9]|25[0-5]/;
218 my $re_ip_serialized = qr/$re_ip_sunit(?:\.$re_ip_sunit){3}/;
223 return undef unless $self->CustomFieldObj->CurrentUserHasRight('SeeCustomField');
225 my $content = $self->_Value('Content');
226 if ( $self->CustomFieldObj->Type eq 'IPAddress'
227 || $self->CustomFieldObj->Type eq 'IPAddressRange' )
230 if ( $content =~ /^\s*($re_ip_serialized)\s*$/o ) {
231 $content = sprintf "%d.%d.%d.%d", split /\./, $1;
234 return $content if $self->CustomFieldObj->Type eq 'IPAddress';
236 my $large_content = $self->__Value('LargeContent');
237 if ( $large_content =~ /^\s*($re_ip_serialized)\s*$/o ) {
238 my $eIP = sprintf "%d.%d.%d.%d", split /\./, $1;
239 if ( $content eq $eIP ) {
243 return $content . "-" . $eIP;
246 elsif ( $large_content =~ /^\s*($IPv6_re)\s*$/o ) {
248 if ( $content eq $eIP ) {
252 return $content . "-" . $eIP;
260 if ( !(defined $content && length $content) && $self->ContentType && $self->ContentType eq 'text/plain' ) {
261 return $self->LargeContent;
269 Returns the object this value applies to
275 my $Object = $self->__Value('ObjectType')->new( $self->CurrentUser );
276 $Object->LoadById( $self->__Value('ObjectId') );
283 Disable this value. Used to remove "current" values from records while leaving them in the history.
290 return $self->SetDisabled(1);
293 =head2 _FillInTemplateURL URL
295 Takes a URL containing placeholders and returns the URL as filled in for this
296 ObjectCustomFieldValue. The values for the placeholders will be URI-escaped.
298 Available placeholders:
304 The id of the object in question.
306 =item __CustomField__
308 The value of this custom field for the object in question.
310 =item __WebDomain__, __WebPort__, __WebPath__, __WebBaseURL__ and __WebURL__
312 The value of the config option.
320 id => { value => sub { $_[0]->ObjectId }, escape => 1 },
321 CustomField => { value => sub { $_[0]->Content }, escape => 1 },
322 WebDomain => { value => sub { RT->Config->Get('WebDomain') } },
323 WebPort => { value => sub { RT->Config->Get('WebPort') } },
324 WebPath => { value => sub { RT->Config->Get('WebPath') } },
325 WebBaseURL => { value => sub { RT->Config->Get('WebBaseURL') } },
326 WebURL => { value => sub { RT->Config->Get('WebURL') } },
329 sub _FillInTemplateURL {
333 return undef unless defined $url && length $url;
335 # special case, whole value should be an URL
336 if ( $url =~ /^__CustomField__/ ) {
337 my $value = $self->Content;
338 # protect from potentially malicious URLs
339 if ( $value =~ /^\s*(?:javascript|data):/i ) {
340 my $object = $self->Object;
342 "Potentially dangerous URL type in custom field '". $self->CustomFieldObj->Name ."'"
343 ." on ". ref($object) ." #". $object->id
347 $url =~ s/^__CustomField__/$value/;
350 # default value, uri-escape
351 for my $key (keys %placeholders) {
352 $url =~ s{__${key}__}{
353 my $value = $placeholders{$key}{'value'}->( $self );
354 $value = '' if !defined($value);
355 RT::Interface::Web::EscapeURI(\$value) if $placeholders{$key}{'escape'};
366 Returns a filled in URL template for this ObjectCustomFieldValue, suitable for
367 constructing a hyperlink in RT's webui. Returns undef if this custom field doesn't have
374 return $self->_FillInTemplateURL($self->CustomFieldObj->LinkValueTo);
379 =head2 ValueIncludeURL
381 Returns a filled in URL template for this ObjectCustomFieldValue, suitable for
382 constructing a hyperlink in RT's webui. Returns undef if this custom field doesn't have
383 a IncludeContentForValue
387 sub IncludeContentForValue {
389 return $self->_FillInTemplateURL($self->CustomFieldObj->IncludeContentForValue);
395 my $value = shift or return;
400 if ( $value =~ /^$RE{net}{CIDR}{IPv4}{-keep}$/go ) {
401 my $cidr = join( '.', map $_||0, (split /\./, $1)[0..3] ) ."/$2";
402 $value = (Net::CIDR::cidr2range( $cidr ))[0] || $value;
404 elsif ( $value =~ /^$IPv6_re(?:\/\d+)?$/o ) {
405 $value = (Net::CIDR::cidr2range( $value ))[0] || $value;
409 if ( $value =~ /^($RE{net}{IPv4})$/o ) {
410 $sIP = $eIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
412 elsif ( $value =~ /^($RE{net}{IPv4})-($RE{net}{IPv4})$/o ) {
413 $sIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
414 $eIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $2;
416 elsif ( $value =~ /^($IPv6_re)$/o ) {
417 $sIP = $self->ParseIP( $1 );
420 elsif ( $value =~ /^($IPv6_re)-($IPv6_re)$/o ) {
421 ($sIP, $eIP) = ( $1, $2 );
422 $sIP = $self->ParseIP( $sIP );
423 $eIP = $self->ParseIP( $eIP );
429 ($sIP, $eIP) = ($eIP, $sIP) if $sIP gt $eIP;
436 my $value = shift or return;
441 if ( $value =~ /^($RE{net}{IPv4})$/o ) {
442 return sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
444 elsif ( $value =~ /^$IPv6_re$/o ) {
446 # up_fields are before '::'
447 # low_fields are after '::' but without v4
448 # v4_fields are the v4
449 my ( @up_fields, @low_fields, @v4_fields );
451 if ( $value =~ /(.*:)(\d+\..*)/ ) {
452 ( $v6, my $v4 ) = ( $1, $2 );
453 chop $v6 unless $v6 =~ /::$/;
454 while ( $v4 =~ /(\d+)\.(\d+)/g ) {
455 push @v4_fields, sprintf '%.2x%.2x', $1, $2;
464 ( $up, $low ) = split /::/, $v6;
470 @up_fields = split /:/, $up;
471 @low_fields = split /:/, $low if $low;
474 ('0000') x ( 8 - @v4_fields - @up_fields - @low_fields );
475 my @fields = ( @up_fields, @zero_fields, @low_fields, @v4_fields );
477 return join ':', map { sprintf "%.4x", hex "0x$_" } @fields;
485 Returns the current value of id.
486 (In the database, id is stored as int(11).)
494 Returns the current value of CustomField.
495 (In the database, CustomField is stored as int(11).)
499 =head2 SetCustomField VALUE
502 Set CustomField to VALUE.
503 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
504 (In the database, CustomField will be stored as a int(11).)
511 Returns the current value of ObjectType.
512 (In the database, ObjectType is stored as varchar(255).)
516 =head2 SetObjectType VALUE
519 Set ObjectType to VALUE.
520 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
521 (In the database, ObjectType will be stored as a varchar(255).)
529 Returns the current value of ObjectId.
530 (In the database, ObjectId is stored as int(11).)
534 =head2 SetObjectId VALUE
537 Set ObjectId to VALUE.
538 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
539 (In the database, ObjectId will be stored as a int(11).)
547 Returns the current value of SortOrder.
548 (In the database, SortOrder is stored as int(11).)
552 =head2 SetSortOrder VALUE
555 Set SortOrder to VALUE.
556 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
557 (In the database, SortOrder will be stored as a int(11).)
565 Returns the current value of Content.
566 (In the database, Content is stored as varchar(255).)
570 =head2 SetContent VALUE
573 Set Content to VALUE.
574 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
575 (In the database, Content will be stored as a varchar(255).)
583 Returns the current value of LargeContent.
584 (In the database, LargeContent is stored as longblob.)
588 =head2 SetLargeContent VALUE
591 Set LargeContent to VALUE.
592 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
593 (In the database, LargeContent will be stored as a longblob.)
601 Returns the current value of ContentType.
602 (In the database, ContentType is stored as varchar(80).)
606 =head2 SetContentType VALUE
609 Set ContentType to VALUE.
610 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
611 (In the database, ContentType will be stored as a varchar(80).)
617 =head2 ContentEncoding
619 Returns the current value of ContentEncoding.
620 (In the database, ContentEncoding is stored as varchar(80).)
624 =head2 SetContentEncoding VALUE
627 Set ContentEncoding to VALUE.
628 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
629 (In the database, ContentEncoding will be stored as a varchar(80).)
637 Returns the current value of Creator.
638 (In the database, Creator is stored as int(11).)
646 Returns the current value of Created.
647 (In the database, Created is stored as datetime.)
655 Returns the current value of LastUpdatedBy.
656 (In the database, LastUpdatedBy is stored as int(11).)
664 Returns the current value of LastUpdated.
665 (In the database, LastUpdated is stored as datetime.)
673 Returns the current value of Disabled.
674 (In the database, Disabled is stored as smallint(6).)
678 =head2 SetDisabled VALUE
681 Set Disabled to VALUE.
682 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
683 (In the database, Disabled will be stored as a smallint(6).)
690 sub _CoreAccessible {
694 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
696 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
698 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
700 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
702 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
704 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
706 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'longblob', default => ''},
708 {read => 1, write => 1, sql_type => 12, length => 80, is_blob => 0, is_numeric => 0, type => 'varchar(80)', default => ''},
710 {read => 1, write => 1, sql_type => 12, length => 80, is_blob => 0, is_numeric => 0, type => 'varchar(80)', default => ''},
712 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
714 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
716 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
718 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
720 {read => 1, write => 1, sql_type => 5, length => 6, is_blob => 0, is_numeric => 1, type => 'smallint(6)', default => '0'},
725 RT::Base->_ImportOverlays();