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