af740e967716f667680704bcbdcf9611b29a0222
[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     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");
97         }
98         else {
99             $args{'LargeContent'} = $args{'Content'};
100             $args{'Content'} = '';
101             $args{'ContentType'} ||= 'text/plain';
102         }
103     }
104
105     ( $args{'ContentEncoding'}, $args{'LargeContent'} ) =
106         $self->_EncodeLOB( $args{'LargeContent'}, $args{'ContentType'} )
107             if defined $args{'LargeContent'};
108
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'},
118     );
119 }
120
121
122 sub LargeContent {
123     my $self = shift;
124     return $self->_DecodeLOB(
125         $self->ContentType,
126         $self->ContentEncoding,
127         $self->_Value( 'LargeContent', decode_utf8 => 0 )
128     );
129 }
130
131
132 =head2 LoadByCols
133
134 =cut
135
136 sub LoadByCols {
137     my $self = shift;
138     my %args = (@_);
139     my $cf;
140     if ( $args{CustomField} ) {
141         $cf = RT::CustomField->new( $self->CurrentUser );
142         $cf->Load( $args{CustomField} );
143
144         my ($ok, $msg) = $cf->_CanonicalizeValue(\%args);
145         return ($ok, $msg) unless $ok;
146     }
147     return $self->SUPER::LoadByCols(%args);
148 }
149
150 =head2 LoadByTicketContentAndCustomField { Ticket => TICKET, CustomField => CUSTOMFIELD, Content => CONTENT }
151
152 Loads a custom field value by Ticket, Content and which CustomField it's tied to
153
154 =cut
155
156
157 sub LoadByTicketContentAndCustomField {
158     my $self = shift;
159     my %args = (
160         Ticket => undef,
161         CustomField => undef,
162         Content => undef,
163         @_
164     );
165
166     return $self->LoadByCols(
167         Content => $args{'Content'},
168         CustomField => $args{'CustomField'},
169         ObjectType => 'RT::Ticket',
170         ObjectId => $args{'Ticket'},
171         Disabled => 0
172     );
173 }
174
175 sub LoadByObjectContentAndCustomField {
176     my $self = shift;
177     my %args = (
178         Object => undef,
179         CustomField => undef,
180         Content => undef,
181         @_
182     );
183
184     my $obj = $args{'Object'} or return;
185
186     return $self->LoadByCols(
187         Content => $args{'Content'},
188         CustomField => $args{'CustomField'},
189         ObjectType => ref($obj),
190         ObjectId => $obj->Id,
191         Disabled => 0
192     );
193 }
194
195 =head2 CustomFieldObj
196
197 Returns the CustomField Object which has the id returned by CustomField
198
199 =cut
200
201 sub CustomFieldObj {
202     my $self = shift;
203     my $CustomField = RT::CustomField->new( $self->CurrentUser );
204     $CustomField->SetContextObject( $self->Object );
205     $CustomField->Load( $self->__Value('CustomField') );
206     return $CustomField;
207 }
208
209
210 =head2 Content
211
212 Return this custom field's content. If there's no "regular"
213 content, try "LargeContent"
214
215 =cut
216
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}/;
219
220 sub Content {
221     my $self = shift;
222
223     return undef unless $self->CustomFieldObj->CurrentUserHasRight('SeeCustomField');
224
225     my $content = $self->_Value('Content');
226     if (   $self->CustomFieldObj->Type eq 'IPAddress'
227         || $self->CustomFieldObj->Type eq 'IPAddressRange' )
228     {
229
230         if ( $content =~ /^\s*($re_ip_serialized)\s*$/o ) {
231             $content = sprintf "%d.%d.%d.%d", split /\./, $1;
232         }
233
234         return $content if $self->CustomFieldObj->Type eq 'IPAddress';
235
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 ) {
240                 return $content;
241             }
242             else {
243                 return $content . "-" . $eIP;
244             }
245         }
246         elsif ( $large_content =~ /^\s*($IPv6_re)\s*$/o ) {
247             my $eIP = $1;
248             if ( $content eq $eIP ) {
249                 return $content;
250             }
251             else {
252                 return $content . "-" . $eIP;
253             }
254         }
255         else {
256             return $content;
257         }
258     }
259
260     if ( !(defined $content && length $content) && $self->ContentType && $self->ContentType eq 'text/plain' ) {
261         return $self->LargeContent;
262     } else {
263         return $content;
264     }
265 }
266
267 =head2 Object
268
269 Returns the object this value applies to
270
271 =cut
272
273 sub Object {
274     my $self  = shift;
275     my $Object = $self->__Value('ObjectType')->new( $self->CurrentUser );
276     $Object->LoadById( $self->__Value('ObjectId') );
277     return $Object;
278 }
279
280
281 =head2 Delete
282
283 Disable this value. Used to remove "current" values from records while leaving them in the history.
284
285 =cut
286
287
288 sub Delete {
289     my $self = shift;
290     return $self->SetDisabled(1);
291 }
292
293 =head2 _FillInTemplateURL URL
294
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.
297
298 Available placeholders:
299
300 =over
301
302 =item __id__
303
304 The id of the object in question.
305
306 =item __CustomField__
307
308 The value of this custom field for the object in question.
309
310 =item __WebDomain__, __WebPort__, __WebPath__, __WebBaseURL__ and __WebURL__
311
312 The value of the config option.
313
314 =back
315
316 =cut
317
318 {
319 my %placeholders = (
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') } },
327 );
328
329 sub _FillInTemplateURL {
330     my $self = shift;
331     my $url = shift;
332
333     return undef unless defined $url && length $url;
334
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;
341             $RT::Logger->error(
342                 "Potentially dangerous URL type in custom field '". $self->CustomFieldObj->Name ."'"
343                 ." on ". ref($object) ." #". $object->id
344             );
345             return undef;
346         }
347         $url =~ s/^__CustomField__/$value/;
348     }
349
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'};
356             $value
357         }gxe;
358     }
359
360     return $url;
361 } }
362
363
364 =head2 ValueLinkURL
365
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
368 a LinkValueTo
369
370 =cut
371
372 sub LinkValueTo {
373     my $self = shift;
374     return $self->_FillInTemplateURL($self->CustomFieldObj->LinkValueTo);
375 }
376
377
378
379 =head2 ValueIncludeURL
380
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
384
385 =cut
386
387 sub IncludeContentForValue {
388     my $self = shift;
389     return $self->_FillInTemplateURL($self->CustomFieldObj->IncludeContentForValue);
390 }
391
392
393 sub ParseIPRange {
394     my $self = shift;
395     my $value = shift or return;
396     $value = lc $value;
397     $value =~ s!^\s+!!;
398     $value =~ s!\s+$!!;
399     
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;
403     }
404     elsif ( $value =~ /^$IPv6_re(?:\/\d+)?$/o ) {
405         $value = (Net::CIDR::cidr2range( $value ))[0] || $value;
406     }
407     
408     my ($sIP, $eIP);
409     if ( $value =~ /^($RE{net}{IPv4})$/o ) {
410         $sIP = $eIP = sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
411     }
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;
415     }
416     elsif ( $value =~ /^($IPv6_re)$/o ) {
417         $sIP = $self->ParseIP( $1 );
418         $eIP = $sIP;
419     }
420     elsif ( $value =~ /^($IPv6_re)-($IPv6_re)$/o ) {
421         ($sIP, $eIP) = ( $1, $2 );
422         $sIP = $self->ParseIP( $sIP );
423         $eIP = $self->ParseIP( $eIP );
424     }
425     else {
426         return;
427     }
428
429     ($sIP, $eIP) = ($eIP, $sIP) if $sIP gt $eIP;
430     
431     return $sIP, $eIP;
432 }
433
434 sub ParseIP {
435     my $self = shift;
436     my $value = shift or return;
437     $value = lc $value;
438     $value =~ s!^\s+!!;
439     $value =~ s!\s+$!!;
440
441     if ( $value =~ /^($RE{net}{IPv4})$/o ) {
442         return sprintf "%03d.%03d.%03d.%03d", split /\./, $1;
443     }
444     elsif ( $value =~ /^$IPv6_re$/o ) {
445
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 );
450         my $v6;
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;
456             }
457         }
458         else {
459             $v6 = $value;
460         }
461
462         my ( $up, $low );
463         if ( $v6 =~ /::/ ) {
464             ( $up, $low ) = split /::/, $v6;
465         }
466         else {
467             $up = $v6;
468         }
469
470         @up_fields = split /:/, $up;
471         @low_fields = split /:/, $low if $low;
472
473         my @zero_fields =
474           ('0000') x ( 8 - @v4_fields - @up_fields - @low_fields );
475         my @fields = ( @up_fields, @zero_fields, @low_fields, @v4_fields );
476
477         return join ':', map { sprintf "%.4x", hex "0x$_" } @fields;
478     }
479     return;
480 }
481
482
483 =head2 id
484
485 Returns the current value of id.
486 (In the database, id is stored as int(11).)
487
488
489 =cut
490
491
492 =head2 CustomField
493
494 Returns the current value of CustomField.
495 (In the database, CustomField is stored as int(11).)
496
497
498
499 =head2 SetCustomField VALUE
500
501
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).)
505
506
507 =cut
508
509 =head2 ObjectType
510
511 Returns the current value of ObjectType.
512 (In the database, ObjectType is stored as varchar(255).)
513
514
515
516 =head2 SetObjectType VALUE
517
518
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).)
522
523
524 =cut
525
526
527 =head2 ObjectId
528
529 Returns the current value of ObjectId.
530 (In the database, ObjectId is stored as int(11).)
531
532
533
534 =head2 SetObjectId VALUE
535
536
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).)
540
541
542 =cut
543
544
545 =head2 SortOrder
546
547 Returns the current value of SortOrder.
548 (In the database, SortOrder is stored as int(11).)
549
550
551
552 =head2 SetSortOrder VALUE
553
554
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).)
558
559
560 =cut
561
562
563 =head2 Content
564
565 Returns the current value of Content.
566 (In the database, Content is stored as varchar(255).)
567
568
569
570 =head2 SetContent VALUE
571
572
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).)
576
577
578 =cut
579
580
581 =head2 LargeContent
582
583 Returns the current value of LargeContent.
584 (In the database, LargeContent is stored as longblob.)
585
586
587
588 =head2 SetLargeContent VALUE
589
590
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.)
594
595
596 =cut
597
598
599 =head2 ContentType
600
601 Returns the current value of ContentType.
602 (In the database, ContentType is stored as varchar(80).)
603
604
605
606 =head2 SetContentType VALUE
607
608
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).)
612
613
614 =cut
615
616
617 =head2 ContentEncoding
618
619 Returns the current value of ContentEncoding.
620 (In the database, ContentEncoding is stored as varchar(80).)
621
622
623
624 =head2 SetContentEncoding VALUE
625
626
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).)
630
631
632 =cut
633
634
635 =head2 Creator
636
637 Returns the current value of Creator.
638 (In the database, Creator is stored as int(11).)
639
640
641 =cut
642
643
644 =head2 Created
645
646 Returns the current value of Created.
647 (In the database, Created is stored as datetime.)
648
649
650 =cut
651
652
653 =head2 LastUpdatedBy
654
655 Returns the current value of LastUpdatedBy.
656 (In the database, LastUpdatedBy is stored as int(11).)
657
658
659 =cut
660
661
662 =head2 LastUpdated
663
664 Returns the current value of LastUpdated.
665 (In the database, LastUpdated is stored as datetime.)
666
667
668 =cut
669
670
671 =head2 Disabled
672
673 Returns the current value of Disabled.
674 (In the database, Disabled is stored as smallint(6).)
675
676
677
678 =head2 SetDisabled VALUE
679
680
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).)
684
685
686 =cut
687
688
689
690 sub _CoreAccessible {
691     {
692
693         id =>
694                 {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
695         CustomField =>
696                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
697         ObjectType =>
698                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
699         ObjectId =>
700                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
701         SortOrder =>
702                 {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
703         Content =>
704                 {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
705         LargeContent =>
706                 {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'longblob', default => ''},
707         ContentType =>
708                 {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
709         ContentEncoding =>
710                 {read => 1, write => 1, sql_type => 12, length => 80,  is_blob => 0,  is_numeric => 0,  type => 'varchar(80)', default => ''},
711         Creator =>
712                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
713         Created =>
714                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
715         LastUpdatedBy =>
716                 {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
717         LastUpdated =>
718                 {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
719         Disabled =>
720                 {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
721
722  }
723 };
724
725 RT::Base->_ImportOverlays();
726
727 1;