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