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