rt 4.0.23
[freeside.git] / rt / lib / RT / CustomField.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::CustomField;
50
51 use strict;
52 use warnings;
53
54 use Scalar::Util 'blessed';
55
56 use base 'RT::Record';
57
58 sub Table {'CustomFields'}
59
60
61 use RT::CustomFieldValues;
62 use RT::ObjectCustomFields;
63 use RT::ObjectCustomFieldValues;
64
65 our %FieldTypes = (
66     Select => {
67         sort_order => 10,
68         selection_type => 1,
69
70         labels => [ 'Select multiple values',      # loc
71                     'Select one value',            # loc
72                     'Select up to [_1] values',    # loc
73                   ],
74
75         render_types => {
76             multiple => [
77
78                 # Default is the first one
79                 'Select box',              # loc
80                 'List',                    # loc
81             ],
82             single => [ 'Select box',              # loc
83                         'Dropdown',                # loc
84                         'List',                    # loc
85                       ]
86         },
87
88     },
89     Freeform => {
90         sort_order => 20,
91         selection_type => 0,
92
93         labels => [ 'Enter multiple values',       # loc
94                     'Enter one value',             # loc
95                     'Enter up to [_1] values',     # loc
96                   ]
97                 },
98     Text => {
99         sort_order => 30,
100         selection_type => 0,
101         labels         => [
102                     'Fill in multiple text areas',      # loc
103                     'Fill in one text area',            # loc
104                     'Fill in up to [_1] text areas',    # loc
105                   ]
106             },
107     Wikitext => {
108         sort_order => 40,
109         selection_type => 0,
110         labels         => [
111                     'Fill in multiple wikitext areas',      # loc
112                     'Fill in one wikitext area',            # loc
113                     'Fill in up to [_1] wikitext areas',    # loc
114                   ]
115                 },
116
117     Image => {
118         sort_order => 50,
119         selection_type => 0,
120         labels         => [
121                     'Upload multiple images',               # loc
122                     'Upload one image',                     # loc
123                     'Upload up to [_1] images',             # loc
124                   ]
125              },
126     Binary => {
127         sort_order => 60,
128         selection_type => 0,
129         labels         => [
130                     'Upload multiple files',                # loc
131                     'Upload one file',                      # loc
132                     'Upload up to [_1] files',              # loc
133                   ]
134               },
135
136     Combobox => {
137         sort_order => 70,
138         selection_type => 1,
139         labels         => [
140                     'Combobox: Select or enter multiple values',      # loc
141                     'Combobox: Select or enter one value',            # loc
142                     'Combobox: Select or enter up to [_1] values',    # loc
143                   ]
144                 },
145     Autocomplete => {
146         sort_order => 80,
147         selection_type => 1,
148         labels         => [
149                     'Enter multiple values with autocompletion',      # loc
150                     'Enter one value with autocompletion',            # loc
151                     'Enter up to [_1] values with autocompletion',    # loc
152                   ]
153     },
154
155     Date => {
156         sort_order => 90,
157         selection_type => 0,
158         labels         => [
159                     'Select multiple dates',                          # loc
160                     'Select date',                                    # loc
161                     'Select up to [_1] dates',                        # loc
162                   ]
163             },
164     DateTime => {
165         sort_order => 100,
166         selection_type => 0,
167         labels         => [
168                     'Select multiple datetimes',                      # loc
169                     'Select datetime',                                # loc
170                     'Select up to [_1] datetimes',                    # loc
171                   ]
172                 },
173     TimeValue => {
174         sort_order => 105,
175         selection_type => 0,
176         labels         => [
177                      'Enter multiple time values (UNSUPPORTED)',
178                      'Enter a time value',
179                      'Enter [_1] time values (UNSUPPORTED)',
180                    ]
181                  },
182
183     IPAddress => {
184         sort_order => 110,
185         selection_type => 0,
186
187         labels => [ 'Enter multiple IP addresses',       # loc
188                     'Enter one IP address',             # loc
189                     'Enter up to [_1] IP addresses',     # loc
190                   ]
191                 },
192     IPAddressRange => {
193         sort_order => 120,
194         selection_type => 0,
195
196         labels => [ 'Enter multiple IP address ranges',       # loc
197                     'Enter one IP address range',             # loc
198                     'Enter up to [_1] IP address ranges',     # loc
199                   ]
200                 },
201 );
202
203
204 our %FRIENDLY_OBJECT_TYPES =  ();
205
206 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", );    #loc
207 RT::CustomField->_ForObjectType(
208     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", );    #loc
209 RT::CustomField->_ForObjectType( 'RT::User'  => "Users", );                           #loc
210 RT::CustomField->_ForObjectType( 'RT::Queue'  => "Queues", );                         #loc
211 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", );                          #loc
212
213 our $RIGHTS = {
214     SeeCustomField            => 'View custom fields',                                    # loc_pair
215     AdminCustomField          => 'Create, modify and delete custom fields',               # loc_pair
216     AdminCustomFieldValues    => 'Create, modify and delete custom fields values',        # loc_pair
217     ModifyCustomField         => 'Add, modify and delete custom field values for objects' # loc_pair
218 };
219
220 our $RIGHT_CATEGORIES = {
221     SeeCustomField          => 'General',
222     AdminCustomField        => 'Admin',
223     AdminCustomFieldValues  => 'Admin',
224     ModifyCustomField       => 'Staff',
225 };
226
227 # Tell RT::ACE that this sort of object can get acls granted
228 $RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
229
230 __PACKAGE__->AddRights(%$RIGHTS);
231 __PACKAGE__->AddRightCategories(%$RIGHT_CATEGORIES);
232
233 =head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
234
235 Adds the given rights to the list of possible rights.  This method
236 should be called during server startup, not at runtime.
237
238 =cut
239
240 sub AddRights {
241     my $self = shift;
242     my %new = @_;
243     $RIGHTS = { %$RIGHTS, %new };
244     %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
245                                       map { lc($_) => $_ } keys %new);
246 }
247
248 sub AvailableRights {
249     my $self = shift;
250     return $RIGHTS;
251 }
252
253 =head2 RightCategories
254
255 Returns a hashref where the keys are rights for this type of object and the
256 values are the category (General, Staff, Admin) the right falls into.
257
258 =cut
259
260 sub RightCategories {
261     return $RIGHT_CATEGORIES;
262 }
263
264 =head2 AddRightCategories C<RIGHT>, C<CATEGORY> [, ...]
265
266 Adds the given right and category pairs to the list of right categories.  This
267 method should be called during server startup, not at runtime.
268
269 =cut
270
271 sub AddRightCategories {
272     my $self = shift if ref $_[0] or $_[0] eq __PACKAGE__;
273     my %new = @_;
274     $RIGHT_CATEGORIES = { %$RIGHT_CATEGORIES, %new };
275 }
276
277 =head1 NAME
278
279   RT::CustomField_Overlay - overlay for RT::CustomField
280
281 =head1 DESCRIPTION
282
283 =head1 'CORE' METHODS
284
285 =head2 Create PARAMHASH
286
287 Create takes a hash of values and creates a row in the database:
288
289   varchar(200) 'Name'.
290   varchar(200) 'Type'.
291   int(11) 'MaxValues'.
292   varchar(255) 'Pattern'.
293   smallint(6) 'Repeated'.
294   varchar(255) 'Description'.
295   int(11) 'SortOrder'.
296   varchar(255) 'LookupType'.
297   smallint(6) 'Disabled'.
298
299 C<LookupType> is generally the result of either
300 C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
301
302 =cut
303
304 sub Create {
305     my $self = shift;
306     my %args = (
307         Name        => '',
308         Type        => '',
309         MaxValues   => 0,
310         Pattern     => '',
311         Description => '',
312         Disabled    => 0,
313         LookupType  => '',
314         Repeated    => 0,
315         LinkValueTo => '',
316         IncludeContentForValue => '',
317         @_,
318     );
319
320     unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
321         return (0, $self->loc('Permission Denied'));
322     }
323
324     if ( $args{TypeComposite} ) {
325         @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
326     }
327     elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
328         # old style Type string
329         $args{'MaxValues'} = $1 ? 1 : 0;
330     }
331     $args{'MaxValues'} = int $args{'MaxValues'};
332
333     if ( !exists $args{'Queue'}) {
334     # do nothing -- things below are strictly backward compat
335     }
336     elsif (  ! $args{'Queue'} ) {
337         unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
338             return ( 0, $self->loc('Permission Denied') );
339         }
340         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
341     }
342     else {
343         my $queue = RT::Queue->new($self->CurrentUser);
344         $queue->Load($args{'Queue'});
345         unless ($queue->Id) {
346             return (0, $self->loc("Queue not found"));
347         }
348         unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
349             return ( 0, $self->loc('Permission Denied') );
350         }
351         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
352         $args{'Queue'} = $queue->Id;
353     }
354
355     my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
356     return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
357
358     if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
359         $RT::Logger->debug("Support for 'multiple' Texts or Comboboxes is not implemented");
360         $args{'MaxValues'} = 1;
361     }
362
363     if ( $args{'RenderType'} ||= undef ) {
364         my $composite = join '-', @args{'Type', 'MaxValues'};
365         return (0, $self->loc("This custom field has no Render Types"))
366             unless $self->HasRenderTypes( $composite );
367
368         if ( $args{'RenderType'} eq $self->DefaultRenderType( $composite ) ) {
369             $args{'RenderType'} = undef;
370         } else {
371             return (0, $self->loc("Invalid Render Type") )
372                 unless grep $_ eq  $args{'RenderType'}, $self->RenderTypes( $composite );
373         }
374     }
375
376     $args{'ValuesClass'} = undef if ($args{'ValuesClass'} || '') eq 'RT::CustomFieldValues';
377     if ( $args{'ValuesClass'} ||= undef ) {
378         return (0, $self->loc("This Custom Field can not have list of values"))
379             unless $self->IsSelectionType( $args{'Type'} );
380
381         unless ( $self->ValidateValuesClass( $args{'ValuesClass'} ) ) {
382             return (0, $self->loc("Invalid Custom Field values source"));
383         }
384     }
385
386     (my $rv, $msg) = $self->SUPER::Create(
387         Name        => $args{'Name'},
388         Type        => $args{'Type'},
389         RenderType  => $args{'RenderType'},
390         MaxValues   => $args{'MaxValues'},
391         Pattern     => $args{'Pattern'},
392         BasedOn     => $args{'BasedOn'},
393         ValuesClass => $args{'ValuesClass'},
394         Description => $args{'Description'},
395         Disabled    => $args{'Disabled'},
396         LookupType  => $args{'LookupType'},
397         Repeated    => $args{'Repeated'},
398     );
399
400     if ($rv) {
401         if ( exists $args{'LinkValueTo'}) {
402             $self->SetLinkValueTo($args{'LinkValueTo'});
403         }
404
405         if ( exists $args{'IncludeContentForValue'}) {
406             $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
407         }
408
409         if ( exists $args{'UILocation'} ) {
410             $self->SetUILocation( $args{'UILocation'} );
411         }
412
413         if ( exists $args{'NoClone'} ) {
414             $self->SetNoClone( $args{'NoClone'} );
415         }
416
417         return ($rv, $msg) unless exists $args{'Queue'};
418
419         # Compat code -- create a new ObjectCustomField mapping
420         my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
421         $OCF->Create(
422             CustomField => $self->Id,
423             ObjectId => $args{'Queue'},
424         );
425     }
426
427     return ($rv, $msg);
428 }
429
430 =head2 Load ID/NAME
431
432 Load a custom field.  If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
433
434 =cut
435
436 sub Load {
437     my $self = shift;
438     my $id = shift || '';
439
440     if ( $id =~ /^\d+$/ ) {
441         return $self->SUPER::Load( $id );
442     } else {
443         return $self->LoadByName( Name => $id );
444     }
445 }
446
447
448
449 =head2 LoadByName (Queue => QUEUEID, Name => NAME)
450
451 Loads the Custom field named NAME.
452
453 Will load a Disabled Custom Field even if there is a non-disabled Custom Field
454 with the same Name.
455
456 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
457
458 If the Queue parameter is '0', look for global ticket custom fields.
459
460 If no queue parameter is specified, look for any and all custom fields with this name.
461
462 BUG/TODO, this won't let you specify that you only want user or group CFs.
463
464 =cut
465
466 # Compatibility for API change after 3.0 beta 1
467 *LoadNameAndQueue = \&LoadByName;
468 # Change after 3.4 beta.
469 *LoadByNameAndQueue = \&LoadByName;
470
471 sub LoadByName {
472     my $self = shift;
473     my %args = (
474         Queue => undef,
475         Name  => undef,
476         @_,
477     );
478
479     unless ( defined $args{'Name'} && length $args{'Name'} ) {
480         $RT::Logger->error("Couldn't load Custom Field without Name");
481         return wantarray ? (0, $self->loc("No name provided")) : 0;
482     }
483
484     # if we're looking for a queue by name, make it a number
485     if ( defined $args{'Queue'} && ($args{'Queue'} =~ /\D/ || !$self->ContextObject) ) {
486         my $QueueObj = RT::Queue->new( $self->CurrentUser );
487         $QueueObj->Load( $args{'Queue'} );
488         $args{'Queue'} = $QueueObj->Id;
489         $self->SetContextObject( $QueueObj )
490             unless $self->ContextObject;
491     }
492
493     # XXX - really naive implementation.  Slow. - not really. still just one query
494
495     my $CFs = RT::CustomFields->new( $self->CurrentUser );
496     $CFs->SetContextObject( $self->ContextObject );
497     my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
498     $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
499     # Don't limit to queue if queue is 0.  Trying to do so breaks
500     # RT::Group type CFs.
501     if ( defined $args{'Queue'} ) {
502         $CFs->LimitToQueue( $args{'Queue'} );
503     }
504
505     # When loading by name, we _can_ load disabled fields, but prefer
506     # non-disabled fields.
507     $CFs->FindAllRows;
508     $CFs->OrderByCols(
509         { FIELD => "Disabled", ORDER => 'ASC' },
510     );
511
512     # We only want one entry.
513     $CFs->RowsPerPage(1);
514
515     # version before 3.8 just returns 0, so we need to test if wantarray to be
516     # backward compatible.
517     return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
518
519     return $self->LoadById( $first->id );
520 }
521
522
523
524
525 =head2 Custom field values
526
527 =head3 Values FIELD
528
529 Return a object (collection) of all acceptable values for this Custom Field.
530 Class of the object can vary and depends on the return value
531 of the C<ValuesClass> method.
532
533 =cut
534
535 *ValuesObj = \&Values;
536
537 sub Values {
538     my $self = shift;
539
540     my $class = $self->ValuesClass;
541     if ( $class ne 'RT::CustomFieldValues') {
542         eval "require $class" or die "$@";
543     }
544     my $cf_values = $class->new( $self->CurrentUser );
545     # if the user has no rights, return an empty object
546     if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
547         $cf_values->LimitToCustomField( $self->Id );
548     } else {
549         $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
550     }
551     return ($cf_values);
552 }
553
554
555 =head3 AddValue HASH
556
557 Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
558
559 =cut
560
561 sub AddValue {
562     my $self = shift;
563     my %args = @_;
564
565     unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
566         return (0, $self->loc('Permission Denied'));
567     }
568
569     # allow zero value
570     if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
571         return (0, $self->loc("Can't add a custom field value without a name"));
572     }
573
574     my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
575     return $newval->Create( %args, CustomField => $self->Id );
576 }
577
578
579
580
581 =head3 DeleteValue ID
582
583 Deletes a value from this custom field by id.
584
585 Does not remove this value for any article which has had it selected
586
587 =cut
588
589 sub DeleteValue {
590     my $self = shift;
591     my $id = shift;
592     unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
593         return (0, $self->loc('Permission Denied'));
594     }
595
596     my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
597     $val_to_del->Load( $id );
598     unless ( $val_to_del->Id ) {
599         return (0, $self->loc("Couldn't find that value"));
600     }
601     unless ( $val_to_del->CustomField == $self->Id ) {
602         return (0, $self->loc("That is not a value for this custom field"));
603     }
604
605     my $retval = $val_to_del->Delete;
606     unless ( $retval ) {
607         return (0, $self->loc("Custom field value could not be deleted"));
608     }
609     return ($retval, $self->loc("Custom field value deleted"));
610 }
611
612
613 =head2 ValidateQueue Queue
614
615 Make sure that the name specified is valid
616
617 =cut
618
619 sub ValidateName {
620     my $self = shift;
621     my $value = shift;
622
623     return 0 unless length $value;
624
625     return $self->SUPER::ValidateName($value);
626 }
627
628 =head2 ValidateQueue Queue
629
630 Make sure that the queue specified is a valid queue name
631
632 =cut
633
634 sub ValidateQueue {
635     my $self = shift;
636     my $id = shift;
637
638     return undef unless defined $id;
639     # 0 means "Global" null would _not_ be ok.
640     return 1 if $id eq '0';
641
642     my $q = RT::Queue->new( RT->SystemUser );
643     $q->Load( $id );
644     return undef unless $q->id;
645     return 1;
646 }
647
648
649
650 =head2 Types 
651
652 Retuns an array of the types of CustomField that are supported
653
654 =cut
655
656 sub Types {
657     return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes);
658 }
659
660
661 =head2 IsSelectionType 
662
663 Retuns a boolean value indicating whether the C<Values> method makes sense
664 to this Custom Field.
665
666 =cut
667
668 sub IsSelectionType {
669     my $self = shift;
670     my $type = @_? shift : $self->Type;
671     return undef unless $type;
672     return $FieldTypes{$type}->{selection_type};
673 }
674
675
676
677 =head2 IsExternalValues
678
679 =cut
680
681 sub IsExternalValues {
682     my $self = shift;
683     return 0 unless $self->IsSelectionType( @_ );
684     return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1;
685 }
686
687 sub ValuesClass {
688     my $self = shift;
689     return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
690 }
691
692 sub SetValuesClass {
693     my $self = shift;
694     my $class = shift || 'RT::CustomFieldValues';
695     
696     if ( $class eq 'RT::CustomFieldValues' ) {
697         return $self->_Set( Field => 'ValuesClass', Value => undef, @_ );
698     }
699
700     return (0, $self->loc("This Custom Field can not have list of values"))
701         unless $self->IsSelectionType;
702
703     unless ( $self->ValidateValuesClass( $class ) ) {
704         return (0, $self->loc("Invalid Custom Field values source"));
705     }
706     return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
707 }
708
709 sub ValidateValuesClass {
710     my $self = shift;
711     my $class = shift;
712
713     return 1 if !$class || $class eq 'RT::CustomFieldValues';
714     return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources');
715     return undef;
716 }
717
718
719 =head2 FriendlyType [TYPE, MAX_VALUES]
720
721 Returns a localized human-readable version of the custom field type.
722 If a custom field type is specified as the parameter, the friendly type for that type will be returned
723
724 =cut
725
726 sub FriendlyType {
727     my $self = shift;
728
729     my $type = @_ ? shift : $self->Type;
730     my $max  = @_ ? shift : $self->MaxValues;
731     $max = 0 unless $max;
732
733     if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) {
734         return ( $self->loc( $friendly_type, $max ) );
735     }
736     else {
737         return ( $self->loc( $type ) );
738     }
739 }
740
741 sub FriendlyTypeComposite {
742     my $self = shift;
743     my $composite = shift || $self->TypeComposite;
744     return $self->FriendlyType(split(/-/, $composite, 2));
745 }
746
747
748 =head2 ValidateType TYPE
749
750 Takes a single string. returns true if that string is a value
751 type of custom field
752
753
754 =cut
755
756 sub ValidateType {
757     my $self = shift;
758     my $type = shift;
759
760     if ( $type =~ s/(?:Single|Multiple)$// ) {
761         $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
762     }
763
764     if ( $FieldTypes{$type} ) {
765         return 1;
766     }
767     else {
768         return undef;
769     }
770 }
771
772
773 sub SetType {
774     my $self = shift;
775     my $type = shift;
776     if ($type =~ s/(?:(Single)|Multiple)$//) {
777         $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
778         $self->SetMaxValues($1 ? 1 : 0);
779     }
780     $self->_Set(Field => 'Type', Value =>$type);
781 }
782
783 =head2 SetPattern STRING
784
785 Takes a single string representing a regular expression.  Performs basic
786 validation on that regex, and sets the C<Pattern> field for the CF if it
787 is valid.
788
789 =cut
790
791 sub SetPattern {
792     my $self = shift;
793     my $regex = shift;
794
795     my ($ok, $msg) = $self->_IsValidRegex($regex);
796     if ($ok) {
797         return $self->_Set(Field => 'Pattern', Value => $regex);
798     }
799     else {
800         return (0, $self->loc("Invalid pattern: [_1]", $msg));
801     }
802 }
803
804 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
805
806 Tests if the string contains an invalid regex.
807
808 =cut
809
810 sub _IsValidRegex {
811     my $self  = shift;
812     my $regex = shift or return (1, 'valid');
813
814     local $^W; local $@;
815     local $SIG{__DIE__} = sub { 1 };
816     local $SIG{__WARN__} = sub { 1 };
817
818     if (eval { qr/$regex/; 1 }) {
819         return (1, 'valid');
820     }
821
822     my $err = $@;
823     $err =~ s{[,;].*}{};    # strip debug info from error
824     chomp $err;
825     return (0, $err);
826 }
827
828
829 =head2 SingleValue
830
831 Returns true if this CustomField only accepts a single value. 
832 Returns false if it accepts multiple values
833
834 =cut
835
836 sub SingleValue {
837     my $self = shift;
838     if (($self->MaxValues||0) == 1) {
839         return 1;
840     } 
841     else {
842         return undef;
843     }
844 }
845
846 sub UnlimitedValues {
847     my $self = shift;
848     if (($self->MaxValues||0) == 0) {
849         return 1;
850     } 
851     else {
852         return undef;
853     }
854 }
855
856
857 =head2 CurrentUserHasRight RIGHT
858
859 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
860
861 =cut
862
863 sub CurrentUserHasRight {
864     my $self  = shift;
865     my $right = shift;
866
867     return $self->CurrentUser->HasRight(
868         Object => $self,
869         Right  => $right,
870     );
871 }
872
873 =head2 ACLEquivalenceObjects
874
875 Returns list of objects via which users can get rights on this custom field. For custom fields
876 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
877
878 =cut
879
880 sub ACLEquivalenceObjects {
881     my $self = shift;
882
883     my $ctx = $self->ContextObject
884         or return;
885     return ($ctx, $ctx->ACLEquivalenceObjects);
886 }
887
888 =head2 ContextObject and SetContextObject
889
890 Set or get a context for this object. It can be ticket, queue or another object
891 this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
892 queue level to allow people to see all fields applied to the queue.
893
894 =cut
895
896 sub SetContextObject {
897     my $self = shift;
898     return $self->{'context_object'} = shift;
899 }
900   
901 sub ContextObject {
902     my $self = shift;
903     return $self->{'context_object'};
904 }
905
906 sub ValidContextType {
907     my $self = shift;
908     my $class = shift;
909
910     my %valid;
911     $valid{$_}++ for split '-', $self->LookupType;
912     delete $valid{'RT::Transaction'};
913
914     return $valid{$class};
915 }
916
917 =head2 LoadContextObject
918
919 Takes an Id for a Context Object and loads the right kind of RT::Object
920 for this particular Custom Field (based on the LookupType) and returns it.
921 This is a good way to ensure you don't try to use a Queue as a Context
922 Object on a User Custom Field.
923
924 =cut
925
926 sub LoadContextObject {
927     my $self = shift;
928     my $type = shift;
929     my $contextid = shift;
930
931     unless ( $self->ValidContextType($type) ) {
932         RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id);
933         return;
934     }
935
936     my $context_object = $type->new( $self->CurrentUser );
937     my ($id, $msg) = $context_object->LoadById( $contextid );
938     unless ( $id ) {
939         RT->Logger->debug("Invalid ContextObject id: $msg");
940         return;
941     }
942     return $context_object;
943 }
944
945 =head2 ValidateContextObject
946
947 Ensure that a given ContextObject applies to this Custom Field.
948 For custom fields that are assigned to Queues or to Classes, this checks that the Custom
949 Field is actually applied to that objects.  For Global Custom Fields, it returns true
950 as long as the Object is of the right type, because you may be using
951 your permissions on a given Queue of Class to see a Global CF.
952 For CFs that are only applied Globally, you don't need a ContextObject.
953
954 =cut
955
956 sub ValidateContextObject {
957     my $self = shift;
958     my $object = shift;
959
960     return 1 if $self->IsApplied(0);
961
962     # global only custom fields don't have objects
963     # that should be used as context objects.
964     return if $self->ApplyGlobally;
965
966     # Otherwise, make sure we weren't passed a user object that we're
967     # supposed to treat as a queue.
968     return unless $self->ValidContextType(ref $object);
969
970     # Check that it is applied correctly
971     my ($applied_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects);
972     return unless $applied_to;
973     return $self->IsApplied($applied_to->id);
974 }
975
976
977 sub _Set {
978     my $self = shift;
979
980     unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
981         return ( 0, $self->loc('Permission Denied') );
982     }
983     return $self->SUPER::_Set( @_ );
984
985 }
986
987
988
989 =head2 _Value
990
991 Takes the name of a table column.
992 Returns its value as a string, if the user passes an ACL check
993
994 =cut
995
996 sub _Value {
997     my $self  = shift;
998     return undef unless $self->id;
999
1000     # we need to do the rights check
1001     unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
1002         $RT::Logger->debug(
1003             "Permission denied. User #". $self->CurrentUser->id
1004             ." has no SeeCustomField right on CF #". $self->id
1005         );
1006         return (undef);
1007     }
1008     return $self->__Value( @_ );
1009 }
1010
1011
1012 =head2 SetDisabled
1013
1014 Takes a boolean.
1015 1 will cause this custom field to no longer be avaialble for objects.
1016 0 will re-enable this field.
1017
1018 =cut
1019
1020
1021 =head2 SetTypeComposite
1022
1023 Set this custom field's type and maximum values as a composite value
1024
1025 =cut
1026
1027 sub SetTypeComposite {
1028     my $self = shift;
1029     my $composite = shift;
1030
1031     my $old = $self->TypeComposite;
1032
1033     my ($type, $max_values) = split(/-/, $composite, 2);
1034     if ( $type ne $self->Type ) {
1035         my ($status, $msg) = $self->SetType( $type );
1036         return ($status, $msg) unless $status;
1037     }
1038     if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
1039         my ($status, $msg) = $self->SetMaxValues( $max_values );
1040         return ($status, $msg) unless $status;
1041     }
1042     my $render = $self->RenderType;
1043     if ( $render and not grep { $_ eq $render } $self->RenderTypes ) {
1044         # We switched types and our render type is no longer valid, so unset it
1045         # and use the default
1046         $self->SetRenderType( undef );
1047     }
1048     return 1, $self->loc(
1049         "Type changed from '[_1]' to '[_2]'",
1050         $self->FriendlyTypeComposite( $old ),
1051         $self->FriendlyTypeComposite( $composite ),
1052     );
1053 }
1054
1055 =head2 TypeComposite
1056
1057 Returns a composite value composed of this object's type and maximum values
1058
1059 =cut
1060
1061
1062 sub TypeComposite {
1063     my $self = shift;
1064     return join '-', ($self->Type || ''), ($self->MaxValues || 0);
1065 }
1066
1067 =head2 TypeComposites
1068
1069 Returns an array of all possible composite values for custom fields.
1070
1071 =cut
1072
1073 sub TypeComposites {
1074     my $self = shift;
1075     return grep !/(?:[Tt]ext|Combobox|Date|DateTime|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types;
1076 }
1077
1078 =head2 RenderType
1079
1080 Returns the type of form widget to render for this custom field.  Currently
1081 this only affects fields which return true for L</HasRenderTypes>. 
1082
1083 =cut
1084
1085 sub RenderType {
1086     my $self = shift;
1087     return '' unless $self->HasRenderTypes;
1088
1089     return $self->_Value( 'RenderType', @_ )
1090         || $self->DefaultRenderType;
1091 }
1092
1093 =head2 SetRenderType TYPE
1094
1095 Sets this custom field's render type.
1096
1097 =cut
1098
1099 sub SetRenderType {
1100     my $self = shift;
1101     my $type = shift;
1102     return (0, $self->loc("This custom field has no Render Types"))
1103         unless $self->HasRenderTypes;
1104
1105     if ( !$type || $type eq $self->DefaultRenderType ) {
1106         return $self->_Set( Field => 'RenderType', Value => undef, @_ );
1107     }
1108
1109     if ( not grep { $_ eq $type } $self->RenderTypes ) {
1110         return (0, $self->loc("Invalid Render Type for custom field of type [_1]",
1111                                 $self->FriendlyType));
1112     }
1113
1114     return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1115 }
1116
1117 =head2 DefaultRenderType [TYPE COMPOSITE]
1118
1119 Returns the default render type for this custom field's type or the TYPE
1120 COMPOSITE specified as an argument.
1121
1122 =cut
1123
1124 sub DefaultRenderType {
1125     my $self = shift;
1126     my $composite    = @_ ? shift : $self->TypeComposite;
1127     my ($type, $max) = split /-/, $composite, 2;
1128     return unless $type and $self->HasRenderTypes($composite);
1129     return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1130 }
1131
1132 =head2 HasRenderTypes [TYPE_COMPOSITE]
1133
1134 Returns a boolean value indicating whether the L</RenderTypes> and
1135 L</RenderType> methods make sense for this custom field.
1136
1137 Currently true only for type C<Select>.
1138
1139 =cut
1140
1141 sub HasRenderTypes {
1142     my $self = shift;
1143     my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1144     return undef unless $type;
1145     return defined $FieldTypes{$type}->{render_types}
1146         ->{ $max == 1 ? 'single' : 'multiple' };
1147 }
1148
1149 =head2 RenderTypes [TYPE COMPOSITE]
1150
1151 Returns the valid render types for this custom field's type or the TYPE
1152 COMPOSITE specified as an argument.
1153
1154 =cut
1155
1156 sub RenderTypes {
1157     my $self = shift;
1158     my $composite    = @_ ? shift : $self->TypeComposite;
1159     my ($type, $max) = split /-/, $composite, 2;
1160     return unless $type and $self->HasRenderTypes($composite);
1161     return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1162 }
1163
1164 =head2 SetLookupType
1165
1166 Autrijus: care to doc how LookupTypes work?
1167
1168 =cut
1169
1170 sub SetLookupType {
1171     my $self = shift;
1172     my $lookup = shift;
1173     if ( $lookup ne $self->LookupType ) {
1174         # Okay... We need to invalidate our existing relationships
1175         my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
1176         $ObjectCustomFields->LimitToCustomField($self->Id);
1177         $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
1178     }
1179     return $self->_Set(Field => 'LookupType', Value =>$lookup);
1180 }
1181
1182 =head2 LookupTypes
1183
1184 Returns an array of LookupTypes available
1185
1186 =cut
1187
1188
1189 sub LookupTypes {
1190     my $self = shift;
1191     return sort keys %FRIENDLY_OBJECT_TYPES;
1192 }
1193
1194 my @FriendlyObjectTypes = (
1195     "[_1] objects",            # loc
1196     "[_1]'s [_2] objects",        # loc
1197     "[_1]'s [_2]'s [_3] objects",   # loc
1198 );
1199
1200 =head2 FriendlyLookupType
1201
1202 Returns a localized description of the type of this custom field
1203
1204 =cut
1205
1206 sub FriendlyLookupType {
1207     my $self = shift;
1208     my $lookup = shift || $self->LookupType;
1209    
1210     return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
1211                      if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
1212
1213     my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1214       grep { defined and length }
1215       split( /-/, $lookup )
1216       or return;
1217     return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
1218 }
1219
1220 =head1 RecordClassFromLookupType
1221
1222 Returns the type of Object referred to by ObjectCustomFields' ObjectId column
1223
1224 Optionally takes a LookupType to use instead of using the value on the loaded
1225 record.  In this case, the method may be called on the class instead of an
1226 object.
1227
1228 =cut
1229
1230 sub RecordClassFromLookupType {
1231     my $self = shift;
1232     my $type = shift || $self->LookupType;
1233     my ($class) = ($type =~ /^([^-]+)/);
1234     unless ( $class ) {
1235         if (blessed($self) and $self->LookupType eq $type) {
1236             $RT::Logger->error(
1237                 "Custom Field #". $self->id
1238                 ." has incorrect LookupType '$type'"
1239             );
1240         } else {
1241             RT->Logger->error("Invalid LookupType passed as argument: $type");
1242         }
1243         return undef;
1244     }
1245     return $class;
1246 }
1247
1248 =head1 ObjectTypeFromLookupType
1249
1250 Returns the ObjectType used in ObjectCustomFieldValues rows for this CF
1251
1252 Optionally takes a LookupType to use instead of using the value on the loaded
1253 record.  In this case, the method may be called on the class instead of an
1254 object.
1255
1256 =cut
1257
1258 sub ObjectTypeFromLookupType {
1259     my $self = shift;
1260     my $type = shift || $self->LookupType;
1261     my ($class) = ($type =~ /([^-]+)$/);
1262     unless ( $class ) {
1263         if (blessed($self) and $self->LookupType eq $type) {
1264             $RT::Logger->error(
1265                 "Custom Field #". $self->id
1266                 ." has incorrect LookupType '$type'"
1267             );
1268         } else {
1269             RT->Logger->error("Invalid LookupType passed as argument: $type");
1270         }
1271         return undef;
1272     }
1273     return $class;
1274 }
1275
1276 sub CollectionClassFromLookupType {
1277     my $self = shift;
1278
1279     my $record_class = $self->RecordClassFromLookupType;
1280     return undef unless $record_class;
1281
1282     my $collection_class;
1283     if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1284         $collection_class = $record_class.'Collection';
1285     } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1286         $collection_class = $record_class.'es';
1287     } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1288         $collection_class = $record_class.'s';
1289     } else {
1290         $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1291         return undef;
1292     }
1293     return $collection_class;
1294 }
1295
1296 =head1 ApplyGlobally
1297
1298 Certain custom fields (users, groups) should only be applied globally
1299 but rather than regexing in code for LookupType =~ RT::Queue, we'll codify
1300 the rules here.
1301
1302 =cut
1303
1304 sub ApplyGlobally {
1305     my $self = shift;
1306
1307     return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1308
1309 }
1310
1311 =head1 AppliedTo
1312
1313 Returns collection with objects this custom field is applied to.
1314 Class of the collection depends on L</LookupType>.
1315 See all L</NotAppliedTo> .
1316
1317 Doesn't takes into account if object is applied globally.
1318
1319 =cut
1320
1321 sub AppliedTo {
1322     my $self = shift;
1323
1324     my ($res, $ocfs_alias) = $self->_AppliedTo;
1325     return $res unless $res;
1326
1327     $res->Limit(
1328         ALIAS     => $ocfs_alias,
1329         FIELD     => 'id',
1330         OPERATOR  => 'IS NOT',
1331         VALUE     => 'NULL',
1332     );
1333
1334     return $res;
1335 }
1336
1337 =head1 NotAppliedTo
1338
1339 Returns collection with objects this custom field is not applied to.
1340 Class of the collection depends on L</LookupType>.
1341 See all L</AppliedTo> .
1342
1343 Doesn't takes into account if object is applied globally.
1344
1345 =cut
1346
1347 sub NotAppliedTo {
1348     my $self = shift;
1349
1350     my ($res, $ocfs_alias) = $self->_AppliedTo;
1351     return $res unless $res;
1352
1353     $res->Limit(
1354         ALIAS     => $ocfs_alias,
1355         FIELD     => 'id',
1356         OPERATOR  => 'IS',
1357         VALUE     => 'NULL',
1358     );
1359
1360     return $res;
1361 }
1362
1363 sub _AppliedTo {
1364     my $self = shift;
1365
1366     my ($class) = $self->CollectionClassFromLookupType;
1367     return undef unless $class;
1368
1369     my $res = $class->new( $self->CurrentUser );
1370
1371     # If CF is a Group CF, only display user-defined groups
1372     if ( $class eq 'RT::Groups' ) {
1373         $res->LimitToUserDefinedGroups;
1374     }
1375
1376     $res->OrderBy( FIELD => 'Name' );
1377     my $ocfs_alias = $res->Join(
1378         TYPE   => 'LEFT',
1379         ALIAS1 => 'main',
1380         FIELD1 => 'id',
1381         TABLE2 => 'ObjectCustomFields',
1382         FIELD2 => 'ObjectId',
1383     );
1384     $res->Limit(
1385         LEFTJOIN => $ocfs_alias,
1386         ALIAS    => $ocfs_alias,
1387         FIELD    => 'CustomField',
1388         VALUE    => $self->id,
1389     );
1390     return ($res, $ocfs_alias);
1391 }
1392
1393 =head2 IsApplied
1394
1395 Takes object id and returns corresponding L<RT::ObjectCustomField>
1396 record if this custom field is applied to the object. Use 0 to check
1397 if custom field is applied globally.
1398
1399 =cut
1400
1401 sub IsApplied {
1402     my $self = shift;
1403     my $id = shift;
1404     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1405     $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1406     return undef unless $ocf->id;
1407     return $ocf;
1408 }
1409
1410 =head2 AddToObject OBJECT
1411
1412 Add this custom field as a custom field for a single object, such as a queue or group.
1413
1414 Takes an object 
1415
1416 =cut
1417
1418
1419 sub AddToObject {
1420     my $self  = shift;
1421     my $object = shift;
1422     my $id = $object->Id || 0;
1423
1424     unless (index($self->LookupType, ref($object)) == 0) {
1425         return ( 0, $self->loc('Lookup type mismatch') );
1426     }
1427
1428     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1429         return ( 0, $self->loc('Permission Denied') );
1430     }
1431
1432     if ( $self->IsApplied( $id ) ) {
1433         return ( 0, $self->loc("Custom field is already applied to the object") );
1434     }
1435
1436     if ( $id ) {
1437         # applying locally
1438         return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1439             if $self->IsApplied( 0 );
1440     }
1441     else {
1442         my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1443         $applied->LimitToCustomField( $self->id );
1444         while ( my $record = $applied->Next ) {
1445             $record->Delete;
1446         }
1447     }
1448
1449     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1450     my ( $oid, $msg ) = $ocf->Create(
1451         ObjectId => $id, CustomField => $self->id,
1452     );
1453     return ( $oid, $msg );
1454 }
1455
1456
1457 =head2 RemoveFromObject OBJECT
1458
1459 Remove this custom field  for a single object, such as a queue or group.
1460
1461 Takes an object 
1462
1463 =cut
1464
1465 sub RemoveFromObject {
1466     my $self = shift;
1467     my $object = shift;
1468     my $id = $object->Id || 0;
1469
1470     unless (index($self->LookupType, ref($object)) == 0) {
1471         return ( 0, $self->loc('Object type mismatch') );
1472     }
1473
1474     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1475         return ( 0, $self->loc('Permission Denied') );
1476     }
1477
1478     my $ocf = $self->IsApplied( $id );
1479     unless ( $ocf ) {
1480         return ( 0, $self->loc("This custom field does not apply to that object") );
1481     }
1482
1483     # XXX: Delete doesn't return anything
1484     my ( $oid, $msg ) = $ocf->Delete;
1485     return ( $oid, $msg );
1486 }
1487
1488
1489 =head2 AddValueForObject HASH
1490
1491 Adds a custom field value for a record object of some kind. 
1492 Takes a param hash of 
1493
1494 Required:
1495
1496     Object
1497     Content
1498
1499 Optional:
1500
1501     LargeContent
1502     ContentType
1503
1504 =cut
1505
1506 sub AddValueForObject {
1507     my $self = shift;
1508     my %args = (
1509         Object       => undef,
1510         Content      => undef,
1511         LargeContent => undef,
1512         ContentType  => undef,
1513         @_
1514     );
1515     my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1516
1517     unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1518         return ( 0, $self->loc('Permission Denied') );
1519     }
1520
1521     unless ( $self->MatchPattern($args{'Content'}) ) {
1522         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1523     }
1524
1525     $RT::Handle->BeginTransaction;
1526
1527     if ( $self->MaxValues ) {
1528         my $current_values = $self->ValuesForObject($obj);
1529         my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1530
1531         # (The +1 is for the new value we're adding)
1532
1533         # If we have a set of current values and we've gone over the maximum
1534         # allowed number of values, we'll need to delete some to make room.
1535         # which former values are blown away is not guaranteed
1536
1537         while ($extra_values) {
1538             my $extra_item = $current_values->Next;
1539             unless ( $extra_item->id ) {
1540                 $RT::Logger->crit( "We were just asked to delete "
1541                     ."a custom field value that doesn't exist!" );
1542                 $RT::Handle->Rollback();
1543                 return (undef);
1544             }
1545             $extra_item->Delete;
1546             $extra_values--;
1547         }
1548     }
1549
1550     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1551     my ($val, $msg) = $newval->Create(
1552         ObjectType   => ref($obj),
1553         ObjectId     => $obj->Id,
1554         Content      => $args{'Content'},
1555         LargeContent => $args{'LargeContent'},
1556         ContentType  => $args{'ContentType'},
1557         CustomField  => $self->Id
1558     );
1559
1560     unless ($val) {
1561         $RT::Handle->Rollback();
1562         return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1563     }
1564
1565     $RT::Handle->Commit();
1566     return ($val);
1567
1568 }
1569
1570
1571 sub _CanonicalizeValue {
1572     my $self = shift;
1573     my $args = shift;
1574
1575     my $type = $self->_Value('Type');
1576     return 1 unless $type;
1577
1578     my $method = '_CanonicalizeValue'. $type;
1579     return 1 unless $self->can($method);
1580     $self->$method($args);
1581 }
1582
1583 sub _CanonicalizeValueDateTime {
1584     my $self    = shift;
1585     my $args    = shift;
1586     my $DateObj = RT::Date->new( $self->CurrentUser );
1587     $DateObj->Set( Format => 'unknown',
1588                    Value  => $args->{'Content'} );
1589     $args->{'Content'} = $DateObj->ISO;
1590     return 1;
1591 }
1592
1593 # For date, we need to store Content as ISO date
1594 sub _CanonicalizeValueDate {
1595     my $self = shift;
1596     my $args = shift;
1597
1598     # in case user input date with time, let's omit it by setting timezone
1599     # to utc so "hour" won't affect "day"
1600     my $DateObj = RT::Date->new( $self->CurrentUser );
1601     $DateObj->Set( Format   => 'unknown',
1602                    Value    => $args->{'Content'},
1603                  );
1604     $args->{'Content'} = $DateObj->Date( Timezone => 'user' );
1605     return 1;
1606 }
1607
1608 sub _CanonicalizeValueIPAddress {
1609     my $self = shift;
1610     my $args = shift;
1611
1612     $args->{Content} = RT::ObjectCustomFieldValue->ParseIP( $args->{Content} );
1613     return (0, $self->loc("Content is not a valid IP address"))
1614         unless $args->{Content};
1615     return 1;
1616 }
1617
1618 sub _CanonicalizeValueIPAddressRange {
1619     my $self = shift;
1620     my $args = shift;
1621
1622     my $content = $args->{Content};
1623     $content .= "-".$args->{LargeContent} if $args->{LargeContent};
1624
1625     ($args->{Content}, $args->{LargeContent})
1626         = RT::ObjectCustomFieldValue->ParseIPRange( $content );
1627
1628     $args->{ContentType} = 'text/plain';
1629     return (0, $self->loc("Content is not a valid IP address range"))
1630         unless $args->{Content};
1631     return 1;
1632 }
1633
1634 =head2 MatchPattern STRING
1635
1636 Tests the incoming string against the Pattern of this custom field object
1637 and returns a boolean; returns true if the Pattern is empty.
1638
1639 =cut
1640
1641 sub MatchPattern {
1642     my $self = shift;
1643     my $regex = $self->Pattern or return 1;
1644
1645     return (( defined $_[0] ? $_[0] : '') =~ $regex);
1646 }
1647
1648
1649
1650
1651 =head2 FriendlyPattern
1652
1653 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1654 and localizing it.
1655
1656 =cut
1657
1658 sub FriendlyPattern {
1659     my $self = shift;
1660     my $regex = $self->Pattern;
1661
1662     return '' unless length $regex;
1663     if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1664         return '[' . $self->loc($1) . ']';
1665     }
1666     else {
1667         return $regex;
1668     }
1669 }
1670
1671
1672
1673
1674 =head2 DeleteValueForObject HASH
1675
1676 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1677
1678 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1679
1680 =cut
1681
1682 sub DeleteValueForObject {
1683     my $self = shift;
1684     my %args = ( Object => undef,
1685                  Content => undef,
1686                  Id => undef,
1687              @_ );
1688
1689
1690     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1691         return (0, $self->loc('Permission Denied'));
1692     }
1693
1694     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1695
1696     if (my $id = $args{'Id'}) {
1697         $oldval->Load($id);
1698     }
1699     unless ($oldval->id) { 
1700         $oldval->LoadByObjectContentAndCustomField(
1701             Object => $args{'Object'}, 
1702             Content =>  $args{'Content'}, 
1703             CustomField => $self->Id,
1704         );
1705     }
1706
1707
1708     # check to make sure we found it
1709     unless ($oldval->Id) {
1710         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1711     }
1712
1713     # for single-value fields, we need to validate that empty string is a valid value for it
1714     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1715         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1716     }
1717
1718     # delete it
1719
1720     my $ret = $oldval->Delete();
1721     unless ($ret) {
1722         return(0, $self->loc("Custom field value could not be found"));
1723     }
1724     return($oldval->Id, $self->loc("Custom field value deleted"));
1725 }
1726
1727
1728 =head2 ValuesForObject OBJECT
1729
1730 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT 
1731
1732 =cut
1733
1734 sub ValuesForObject {
1735     my $self = shift;
1736     my $object = shift;
1737
1738     my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1739     unless ($self->id and $self->CurrentUserHasRight('SeeCustomField')) {
1740         # Return an empty object if they have no rights to see
1741         $values->Limit( FIELD => "id", VALUE => 0, SUBCLAUSE => "ACL" );
1742         return ($values);
1743     }
1744
1745     $values->LimitToCustomField($self->Id);
1746     $values->LimitToObject($object);
1747
1748     return ($values);
1749 }
1750
1751
1752 =head2 _ForObjectType PATH FRIENDLYNAME
1753
1754 Tell RT that a certain object accepts custom fields
1755
1756 Examples:
1757
1758     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1759     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1760     'RT::User'                             => "Users",                  # loc
1761     'RT::Group'                            => "Groups",                 # loc
1762     'RT::Queue'                            => "Queues",                 # loc
1763
1764 This is a class method. 
1765
1766 =cut
1767
1768 sub _ForObjectType {
1769     my $self = shift;
1770     my $path = shift;
1771     my $friendly_name = shift;
1772
1773     $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1774
1775 }
1776
1777
1778 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1779
1780 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1781 uses this field to automatically include content into the user's browser
1782 as they display records with custom fields in RT.
1783
1784 =cut
1785
1786 sub SetIncludeContentForValue {
1787     shift->IncludeContentForValue(@_);
1788 }
1789 sub IncludeContentForValue{
1790     my $self = shift;
1791     $self->_URLTemplate('IncludeContentForValue', @_);
1792 }
1793
1794
1795
1796 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1797
1798 Gets or sets the  C<LinkValueTo> for this custom field. RT
1799 uses this field to make custom field values into hyperlinks in the user's
1800 browser as they display records with custom fields in RT.
1801
1802 =cut
1803
1804
1805 sub SetLinkValueTo {
1806     shift->LinkValueTo(@_);
1807 }
1808
1809 sub LinkValueTo {
1810     my $self = shift;
1811     $self->_URLTemplate('LinkValueTo', @_);
1812
1813 }
1814
1815
1816 =head2 _URLTemplate  NAME [VALUE]
1817
1818 With one argument, returns the _URLTemplate named C<NAME>, but only if
1819 the current user has the right to see this custom field.
1820
1821 With two arguments, attemptes to set the relevant template value.
1822
1823 =cut
1824
1825 sub _URLTemplate {
1826     my $self          = shift;
1827     my $template_name = shift;
1828     if (@_) {
1829
1830         my $value = shift;
1831         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1832             return ( 0, $self->loc('Permission Denied') );
1833         }
1834         $self->SetAttribute( Name => $template_name, Content => $value );
1835         return ( 1, $self->loc('Updated') );
1836     } else {
1837         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1838             return (undef);
1839         }
1840
1841         my @attr = $self->Attributes->Named($template_name);
1842         my $attr = shift @attr;
1843
1844         if ($attr) { return $attr->Content }
1845
1846     }
1847 }
1848
1849 sub SetBasedOn {
1850     my $self = shift;
1851     my $value = shift;
1852
1853     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1854         unless defined $value and length $value;
1855
1856     my $cf = RT::CustomField->new( $self->CurrentUser );
1857     $cf->SetContextObject( $self->ContextObject );
1858     $cf->Load( ref $value ? $value->id : $value );
1859
1860     return (0, "Permission denied")
1861         unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1862
1863     # XXX: Remove this restriction once we support lists and cascaded selects
1864     if ( $self->RenderType =~ /List/ ) {
1865         return (0, $self->loc("We can't currently render as a List when basing categories on another custom field.  Please use another render type."));
1866     }
1867
1868     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1869 }
1870
1871 sub BasedOnObj {
1872     my $self = shift;
1873
1874     my $obj = RT::CustomField->new( $self->CurrentUser );
1875     $obj->SetContextObject( $self->ContextObject );
1876     if ( $self->BasedOn ) {
1877         $obj->Load( $self->BasedOn );
1878     }
1879     return $obj;
1880 }
1881
1882 sub UILocation {
1883     my $self = shift;
1884     my $tag = $self->FirstAttribute( 'UILocation' );
1885     return $tag ? $tag->Content : '';
1886 }
1887
1888 sub SetUILocation {
1889     my $self = shift;
1890     my $tag = shift;
1891     if ( $tag ) {
1892         return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1893     }
1894     else {
1895         return $self->DeleteAttribute('UILocation');
1896     }
1897 }
1898
1899 sub NoClone {
1900     my $self = shift;
1901     $self->FirstAttribute('NoClone') ? 1 : '';
1902 }
1903
1904 sub SetNoClone {
1905     my $self = shift;
1906     my $value = shift;
1907     if ( $value ) {
1908         return $self->SetAttribute( Name => 'NoClone', Content => 1 );
1909     } else {
1910         return $self->DeleteAttribute('NoClone');
1911     }
1912 }
1913
1914
1915 =head2 id
1916
1917 Returns the current value of id. 
1918 (In the database, id is stored as int(11).)
1919
1920
1921 =cut
1922
1923
1924 =head2 Name
1925
1926 Returns the current value of Name. 
1927 (In the database, Name is stored as varchar(200).)
1928
1929
1930
1931 =head2 SetName VALUE
1932
1933
1934 Set Name to VALUE. 
1935 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1936 (In the database, Name will be stored as a varchar(200).)
1937
1938
1939 =cut
1940
1941
1942 =head2 Type
1943
1944 Returns the current value of Type. 
1945 (In the database, Type is stored as varchar(200).)
1946
1947
1948
1949 =head2 SetType VALUE
1950
1951
1952 Set Type to VALUE. 
1953 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1954 (In the database, Type will be stored as a varchar(200).)
1955
1956
1957 =cut
1958
1959
1960 =head2 RenderType
1961
1962 Returns the current value of RenderType. 
1963 (In the database, RenderType is stored as varchar(64).)
1964
1965
1966
1967 =head2 SetRenderType VALUE
1968
1969
1970 Set RenderType to VALUE. 
1971 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1972 (In the database, RenderType will be stored as a varchar(64).)
1973
1974
1975 =cut
1976
1977
1978 =head2 MaxValues
1979
1980 Returns the current value of MaxValues. 
1981 (In the database, MaxValues is stored as int(11).)
1982
1983
1984
1985 =head2 SetMaxValues VALUE
1986
1987
1988 Set MaxValues to VALUE. 
1989 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1990 (In the database, MaxValues will be stored as a int(11).)
1991
1992
1993 =cut
1994
1995
1996 =head2 Pattern
1997
1998 Returns the current value of Pattern. 
1999 (In the database, Pattern is stored as text.)
2000
2001
2002
2003 =head2 SetPattern VALUE
2004
2005
2006 Set Pattern to VALUE. 
2007 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2008 (In the database, Pattern will be stored as a text.)
2009
2010
2011 =cut
2012
2013
2014 =head2 Repeated
2015
2016 Returns the current value of Repeated. 
2017 (In the database, Repeated is stored as smallint(6).)
2018
2019
2020
2021 =head2 SetRepeated VALUE
2022
2023
2024 Set Repeated to VALUE. 
2025 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2026 (In the database, Repeated will be stored as a smallint(6).)
2027
2028
2029 =cut
2030
2031
2032 =head2 BasedOn
2033
2034 Returns the current value of BasedOn. 
2035 (In the database, BasedOn is stored as int(11).)
2036
2037
2038
2039 =head2 SetBasedOn VALUE
2040
2041
2042 Set BasedOn to VALUE. 
2043 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2044 (In the database, BasedOn will be stored as a int(11).)
2045
2046
2047 =cut
2048
2049
2050 =head2 Description
2051
2052 Returns the current value of Description. 
2053 (In the database, Description is stored as varchar(255).)
2054
2055
2056
2057 =head2 SetDescription VALUE
2058
2059
2060 Set Description to VALUE. 
2061 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2062 (In the database, Description will be stored as a varchar(255).)
2063
2064
2065 =cut
2066
2067
2068 =head2 SortOrder
2069
2070 Returns the current value of SortOrder. 
2071 (In the database, SortOrder is stored as int(11).)
2072
2073
2074
2075 =head2 SetSortOrder VALUE
2076
2077
2078 Set SortOrder to VALUE. 
2079 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2080 (In the database, SortOrder will be stored as a int(11).)
2081
2082
2083 =cut
2084
2085
2086 =head2 LookupType
2087
2088 Returns the current value of LookupType. 
2089 (In the database, LookupType is stored as varchar(255).)
2090
2091
2092
2093 =head2 SetLookupType VALUE
2094
2095
2096 Set LookupType to VALUE. 
2097 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2098 (In the database, LookupType will be stored as a varchar(255).)
2099
2100
2101 =cut
2102
2103
2104 =head2 Creator
2105
2106 Returns the current value of Creator. 
2107 (In the database, Creator is stored as int(11).)
2108
2109
2110 =cut
2111
2112
2113 =head2 Created
2114
2115 Returns the current value of Created. 
2116 (In the database, Created is stored as datetime.)
2117
2118
2119 =cut
2120
2121
2122 =head2 LastUpdatedBy
2123
2124 Returns the current value of LastUpdatedBy. 
2125 (In the database, LastUpdatedBy is stored as int(11).)
2126
2127
2128 =cut
2129
2130
2131 =head2 LastUpdated
2132
2133 Returns the current value of LastUpdated. 
2134 (In the database, LastUpdated is stored as datetime.)
2135
2136
2137 =cut
2138
2139
2140 =head2 Disabled
2141
2142 Returns the current value of Disabled. 
2143 (In the database, Disabled is stored as smallint(6).)
2144
2145
2146
2147 =head2 SetDisabled VALUE
2148
2149
2150 Set Disabled to VALUE. 
2151 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2152 (In the database, Disabled will be stored as a smallint(6).)
2153
2154
2155 =cut
2156
2157
2158
2159 sub _CoreAccessible {
2160     {
2161      
2162         id =>
2163         {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2164         Name => 
2165         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2166         Type => 
2167         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2168         RenderType => 
2169         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2170         MaxValues => 
2171         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2172         Pattern => 
2173         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2174         Repeated => 
2175         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2176         ValuesClass => 
2177         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2178         BasedOn => 
2179         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2180         Description => 
2181         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2182         SortOrder => 
2183         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2184         LookupType => 
2185         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2186         Creator => 
2187         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2188         Created => 
2189         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2190         LastUpdatedBy => 
2191         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2192         LastUpdated => 
2193         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2194         Disabled => 
2195         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2196
2197  }
2198 };
2199
2200
2201 RT::Base->_ImportOverlays();
2202
2203 1;