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