Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / lib / RT / ObjectCustomFieldValue.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2015 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 use base 'RT::Record';
54
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;
59 require Net::CIDR;
60
61 # Allow the empty IPv6 address
62 $IPv6_re = qr/(?:$IPv6_re|::)/;
63
64 use RT::CustomField;
65
66 sub Table {'ObjectCustomFieldValues'}
67
68
69
70
71 sub 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
85     my $cf = RT::CustomField->new( $self->CurrentUser );
86     $cf->Load( $args{CustomField} );
87
88     my ($val, $msg) = $cf->_CanonicalizeValue(\%args);
89     return ($val, $msg) unless $val;
90
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");
95         }
96         else {
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';
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
123 sub 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
137 sub 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} );
144
145         my ($ok, $msg) = $cf->_CanonicalizeValue(\%args);
146         return ($ok, $msg) unless $ok;
147     }
148     return $self->SUPER::LoadByCols(%args);
149 }
150
151 =head2 LoadByTicketContentAndCustomField { Ticket => TICKET, CustomField => CUSTOMFIELD, Content => CONTENT }
152
153 Loads a custom field value by Ticket, Content and which CustomField it's tied to
154
155 =cut
156
157
158 sub 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
176 sub 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
198 Returns the CustomField Object which has the id returned by CustomField
199
200 =cut
201
202 sub 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
213 Return this custom field's content. If there's no "regular"
214 content, try "LargeContent"
215
216 =cut
217
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}/;
220
221 sub 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
270 Returns the object this value applies to
271
272 =cut
273
274 sub 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
284 Disable this value. Used to remove "current" values from records while leaving them in the history.
285
286 =cut
287
288
289 sub Delete {
290     my $self = shift;
291     return $self->SetDisabled(1);
292 }
293
294 =head2 _FillInTemplateURL URL
295
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.
298
299 Available placeholders:
300
301 =over
302
303 =item __id__
304
305 The id of the object in question.
306
307 =item __CustomField__
308
309 The value of this custom field for the object in question.
310
311 =item __WebDomain__, __WebPort__, __WebPath__, __WebBaseURL__ and __WebURL__
312
313 The value of the config option.
314
315 =back
316
317 =cut
318
319 {
320 my %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
330 sub _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
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
369 a LinkValueTo
370
371 =cut
372
373 sub LinkValueTo {
374     my $self = shift;
375     return $self->_FillInTemplateURL($self->CustomFieldObj->LinkValueTo);
376 }
377
378
379
380 =head2 ValueIncludeURL
381
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
385
386 =cut
387
388 sub IncludeContentForValue {
389     my $self = shift;
390     return $self->_FillInTemplateURL($self->CustomFieldObj->IncludeContentForValue);
391 }
392
393
394 sub 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
435 sub 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
486 Returns 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
495 Returns the current value of CustomField.
496 (In the database, CustomField is stored as int(11).)
497
498
499
500 =head2 SetCustomField VALUE
501
502
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).)
506
507
508 =cut
509
510 =head2 ObjectType
511
512 Returns the current value of ObjectType.
513 (In the database, ObjectType is stored as varchar(255).)
514
515
516
517 =head2 SetObjectType VALUE
518
519
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).)
523
524
525 =cut
526
527
528 =head2 ObjectId
529
530 Returns the current value of ObjectId.
531 (In the database, ObjectId is stored as int(11).)
532
533
534
535 =head2 SetObjectId VALUE
536
537
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).)
541
542
543 =cut
544
545
546 =head2 SortOrder
547
548 Returns the current value of SortOrder.
549 (In the database, SortOrder is stored as int(11).)
550
551
552
553 =head2 SetSortOrder VALUE
554
555
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).)
559
560
561 =cut
562
563
564 =head2 Content
565
566 Returns the current value of Content.
567 (In the database, Content is stored as varchar(255).)
568
569
570
571 =head2 SetContent VALUE
572
573
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).)
577
578
579 =cut
580
581
582 =head2 LargeContent
583
584 Returns the current value of LargeContent.
585 (In the database, LargeContent is stored as longblob.)
586
587
588
589 =head2 SetLargeContent VALUE
590
591
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.)
595
596
597 =cut
598
599
600 =head2 ContentType
601
602 Returns the current value of ContentType.
603 (In the database, ContentType is stored as varchar(80).)
604
605
606
607 =head2 SetContentType VALUE
608
609
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).)
613
614
615 =cut
616
617
618 =head2 ContentEncoding
619
620 Returns the current value of ContentEncoding.
621 (In the database, ContentEncoding is stored as varchar(80).)
622
623
624
625 =head2 SetContentEncoding VALUE
626
627
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).)
631
632
633 =cut
634
635
636 =head2 Creator
637
638 Returns 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
647 Returns the current value of Created.
648 (In the database, Created is stored as datetime.)
649
650
651 =cut
652
653
654 =head2 LastUpdatedBy
655
656 Returns 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
665 Returns the current value of LastUpdated.
666 (In the database, LastUpdated is stored as datetime.)
667
668
669 =cut
670
671
672 =head2 Disabled
673
674 Returns the current value of Disabled.
675 (In the database, Disabled is stored as smallint(6).)
676
677
678
679 =head2 SetDisabled VALUE
680
681
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).)
685
686
687 =cut
688
689
690
691 sub _CoreAccessible {
692     {
693
694         id =>
695                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
696         CustomField =>
697                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
698         ObjectType =>
699                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
700         ObjectId =>
701                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
702         SortOrder =>
703                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
704         Content =>
705                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
706         LargeContent =>
707                 {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'longblob', default => ''},
708         ContentType =>
709                 {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
710         ContentEncoding =>
711                 {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
712         Creator =>
713                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
714         Created =>
715                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
716         LastUpdatedBy =>
717                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
718         LastUpdated =>
719                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
720         Disabled =>
721                 {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
722
723  }
724 };
725
726 sub 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
736 RT::Base->_ImportOverlays();
737
738 1;