fix TimeValue custom fields, RT#13852
[freeside.git] / rt / lib / RT / CustomField.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::CustomField;
50
51 use strict;
52 use warnings;
53
54
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         return ($rv, $msg) unless exists $args{'Queue'};
414
415         # Compat code -- create a new ObjectCustomField mapping
416         my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
417         $OCF->Create(
418             CustomField => $self->Id,
419             ObjectId => $args{'Queue'},
420         );
421     }
422
423     return ($rv, $msg);
424 }
425
426 =head2 Load ID/NAME
427
428 Load a custom field.  If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
429
430 =cut
431
432 sub Load {
433     my $self = shift;
434     my $id = shift || '';
435
436     if ( $id =~ /^\d+$/ ) {
437         return $self->SUPER::Load( $id );
438     } else {
439         return $self->LoadByName( Name => $id );
440     }
441 }
442
443
444
445 =head2 LoadByName (Queue => QUEUEID, Name => NAME)
446
447 Loads the Custom field named NAME.
448
449 Will load a Disabled Custom Field even if there is a non-disabled Custom Field
450 with the same Name.
451
452 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
453
454 If the Queue parameter is '0', look for global ticket custom fields.
455
456 If no queue parameter is specified, look for any and all custom fields with this name.
457
458 BUG/TODO, this won't let you specify that you only want user or group CFs.
459
460 =cut
461
462 # Compatibility for API change after 3.0 beta 1
463 *LoadNameAndQueue = \&LoadByName;
464 # Change after 3.4 beta.
465 *LoadByNameAndQueue = \&LoadByName;
466
467 sub LoadByName {
468     my $self = shift;
469     my %args = (
470         Queue => undef,
471         Name  => undef,
472         @_,
473     );
474
475     unless ( defined $args{'Name'} && length $args{'Name'} ) {
476         $RT::Logger->error("Couldn't load Custom Field without Name");
477         return wantarray ? (0, $self->loc("No name provided")) : 0;
478     }
479
480     # if we're looking for a queue by name, make it a number
481     if ( defined $args{'Queue'} && ($args{'Queue'} =~ /\D/ || !$self->ContextObject) ) {
482         my $QueueObj = RT::Queue->new( $self->CurrentUser );
483         $QueueObj->Load( $args{'Queue'} );
484         $args{'Queue'} = $QueueObj->Id;
485         $self->SetContextObject( $QueueObj )
486             unless $self->ContextObject;
487     }
488
489     # XXX - really naive implementation.  Slow. - not really. still just one query
490
491     my $CFs = RT::CustomFields->new( $self->CurrentUser );
492     $CFs->SetContextObject( $self->ContextObject );
493     my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
494     $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
495     # Don't limit to queue if queue is 0.  Trying to do so breaks
496     # RT::Group type CFs.
497     if ( defined $args{'Queue'} ) {
498         $CFs->LimitToQueue( $args{'Queue'} );
499     }
500
501     # When loading by name, we _can_ load disabled fields, but prefer
502     # non-disabled fields.
503     $CFs->FindAllRows;
504     $CFs->OrderByCols(
505         { FIELD => "Disabled", ORDER => 'ASC' },
506     );
507
508     # We only want one entry.
509     $CFs->RowsPerPage(1);
510
511     # version before 3.8 just returns 0, so we need to test if wantarray to be
512     # backward compatible.
513     return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
514
515     return $self->LoadById( $first->id );
516 }
517
518
519
520
521 =head2 Custom field values
522
523 =head3 Values FIELD
524
525 Return a object (collection) of all acceptable values for this Custom Field.
526 Class of the object can vary and depends on the return value
527 of the C<ValuesClass> method.
528
529 =cut
530
531 *ValuesObj = \&Values;
532
533 sub Values {
534     my $self = shift;
535
536     my $class = $self->ValuesClass;
537     if ( $class ne 'RT::CustomFieldValues') {
538         eval "require $class" or die "$@";
539     }
540     my $cf_values = $class->new( $self->CurrentUser );
541     # if the user has no rights, return an empty object
542     if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
543         $cf_values->LimitToCustomField( $self->Id );
544     } else {
545         $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
546     }
547     return ($cf_values);
548 }
549
550
551 =head3 AddValue HASH
552
553 Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
554
555 =cut
556
557 sub AddValue {
558     my $self = shift;
559     my %args = @_;
560
561     unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
562         return (0, $self->loc('Permission Denied'));
563     }
564
565     # allow zero value
566     if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
567         return (0, $self->loc("Can't add a custom field value without a name"));
568     }
569
570     my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
571     return $newval->Create( %args, CustomField => $self->Id );
572 }
573
574
575
576
577 =head3 DeleteValue ID
578
579 Deletes a value from this custom field by id.
580
581 Does not remove this value for any article which has had it selected
582
583 =cut
584
585 sub DeleteValue {
586     my $self = shift;
587     my $id = shift;
588     unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
589         return (0, $self->loc('Permission Denied'));
590     }
591
592     my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
593     $val_to_del->Load( $id );
594     unless ( $val_to_del->Id ) {
595         return (0, $self->loc("Couldn't find that value"));
596     }
597     unless ( $val_to_del->CustomField == $self->Id ) {
598         return (0, $self->loc("That is not a value for this custom field"));
599     }
600
601     my $retval = $val_to_del->Delete;
602     unless ( $retval ) {
603         return (0, $self->loc("Custom field value could not be deleted"));
604     }
605     return ($retval, $self->loc("Custom field value deleted"));
606 }
607
608
609 =head2 ValidateQueue Queue
610
611 Make sure that the name specified is valid
612
613 =cut
614
615 sub ValidateName {
616     my $self = shift;
617     my $value = shift;
618
619     return 0 unless length $value;
620
621     return $self->SUPER::ValidateName($value);
622 }
623
624 =head2 ValidateQueue Queue
625
626 Make sure that the queue specified is a valid queue name
627
628 =cut
629
630 sub ValidateQueue {
631     my $self = shift;
632     my $id = shift;
633
634     return undef unless defined $id;
635     # 0 means "Global" null would _not_ be ok.
636     return 1 if $id eq '0';
637
638     my $q = RT::Queue->new( RT->SystemUser );
639     $q->Load( $id );
640     return undef unless $q->id;
641     return 1;
642 }
643
644
645
646 =head2 Types 
647
648 Retuns an array of the types of CustomField that are supported
649
650 =cut
651
652 sub Types {
653     return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes);
654 }
655
656
657 =head2 IsSelectionType 
658
659 Retuns a boolean value indicating whether the C<Values> method makes sense
660 to this Custom Field.
661
662 =cut
663
664 sub IsSelectionType {
665     my $self = shift;
666     my $type = @_? shift : $self->Type;
667     return undef unless $type;
668     return $FieldTypes{$type}->{selection_type};
669 }
670
671
672
673 =head2 IsExternalValues
674
675 =cut
676
677 sub IsExternalValues {
678     my $self = shift;
679     return 0 unless $self->IsSelectionType( @_ );
680     return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1;
681 }
682
683 sub ValuesClass {
684     my $self = shift;
685     return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
686 }
687
688 sub SetValuesClass {
689     my $self = shift;
690     my $class = shift || 'RT::CustomFieldValues';
691     
692     if ( $class eq 'RT::CustomFieldValues' ) {
693         return $self->_Set( Field => 'ValuesClass', Value => undef, @_ );
694     }
695
696     return (0, $self->loc("This Custom Field can not have list of values"))
697         unless $self->IsSelectionType;
698
699     unless ( $self->ValidateValuesClass( $class ) ) {
700         return (0, $self->loc("Invalid Custom Field values source"));
701     }
702     return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
703 }
704
705 sub ValidateValuesClass {
706     my $self = shift;
707     my $class = shift;
708
709     return 1 if !defined $class || $class eq 'RT::CustomFieldValues';
710     return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources');
711     return undef;
712 }
713
714
715 =head2 FriendlyType [TYPE, MAX_VALUES]
716
717 Returns a localized human-readable version of the custom field type.
718 If a custom field type is specified as the parameter, the friendly type for that type will be returned
719
720 =cut
721
722 sub FriendlyType {
723     my $self = shift;
724
725     my $type = @_ ? shift : $self->Type;
726     my $max  = @_ ? shift : $self->MaxValues;
727     $max = 0 unless $max;
728
729     if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) {
730         return ( $self->loc( $friendly_type, $max ) );
731     }
732     else {
733         return ( $self->loc( $type ) );
734     }
735 }
736
737 sub FriendlyTypeComposite {
738     my $self = shift;
739     my $composite = shift || $self->TypeComposite;
740     return $self->FriendlyType(split(/-/, $composite, 2));
741 }
742
743
744 =head2 ValidateType TYPE
745
746 Takes a single string. returns true if that string is a value
747 type of custom field
748
749
750 =cut
751
752 sub ValidateType {
753     my $self = shift;
754     my $type = shift;
755
756     if ( $type =~ s/(?:Single|Multiple)$// ) {
757         $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
758     }
759
760     if ( $FieldTypes{$type} ) {
761         return 1;
762     }
763     else {
764         return undef;
765     }
766 }
767
768
769 sub SetType {
770     my $self = shift;
771     my $type = shift;
772     if ($type =~ s/(?:(Single)|Multiple)$//) {
773         $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
774         $self->SetMaxValues($1 ? 1 : 0);
775     }
776     $self->_Set(Field => 'Type', Value =>$type);
777 }
778
779 =head2 SetPattern STRING
780
781 Takes a single string representing a regular expression.  Performs basic
782 validation on that regex, and sets the C<Pattern> field for the CF if it
783 is valid.
784
785 =cut
786
787 sub SetPattern {
788     my $self = shift;
789     my $regex = shift;
790
791     my ($ok, $msg) = $self->_IsValidRegex($regex);
792     if ($ok) {
793         return $self->_Set(Field => 'Pattern', Value => $regex);
794     }
795     else {
796         return (0, $self->loc("Invalid pattern: [_1]", $msg));
797     }
798 }
799
800 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
801
802 Tests if the string contains an invalid regex.
803
804 =cut
805
806 sub _IsValidRegex {
807     my $self  = shift;
808     my $regex = shift or return (1, 'valid');
809
810     local $^W; local $@;
811     local $SIG{__DIE__} = sub { 1 };
812     local $SIG{__WARN__} = sub { 1 };
813
814     if (eval { qr/$regex/; 1 }) {
815         return (1, 'valid');
816     }
817
818     my $err = $@;
819     $err =~ s{[,;].*}{};    # strip debug info from error
820     chomp $err;
821     return (0, $err);
822 }
823
824
825 =head2 SingleValue
826
827 Returns true if this CustomField only accepts a single value. 
828 Returns false if it accepts multiple values
829
830 =cut
831
832 sub SingleValue {
833     my $self = shift;
834     if (($self->MaxValues||0) == 1) {
835         return 1;
836     } 
837     else {
838         return undef;
839     }
840 }
841
842 sub UnlimitedValues {
843     my $self = shift;
844     if (($self->MaxValues||0) == 0) {
845         return 1;
846     } 
847     else {
848         return undef;
849     }
850 }
851
852
853 =head2 CurrentUserHasRight RIGHT
854
855 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
856
857 =cut
858
859 sub CurrentUserHasRight {
860     my $self  = shift;
861     my $right = shift;
862
863     return $self->CurrentUser->HasRight(
864         Object => $self,
865         Right  => $right,
866     );
867 }
868
869 =head2 ACLEquivalenceObjects
870
871 Returns list of objects via which users can get rights on this custom field. For custom fields
872 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
873
874 =cut
875
876 sub ACLEquivalenceObjects {
877     my $self = shift;
878
879     my $ctx = $self->ContextObject
880         or return;
881     return ($ctx, $ctx->ACLEquivalenceObjects);
882 }
883
884 =head2 ContextObject and SetContextObject
885
886 Set or get a context for this object. It can be ticket, queue or another object
887 this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
888 queue level to allow people to see all fields applied to the queue.
889
890 =cut
891
892 sub SetContextObject {
893     my $self = shift;
894     return $self->{'context_object'} = shift;
895 }
896   
897 sub ContextObject {
898     my $self = shift;
899     return $self->{'context_object'};
900 }
901
902 sub ValidContextType {
903     my $self = shift;
904     my $class = shift;
905
906     my %valid;
907     $valid{$_}++ for split '-', $self->LookupType;
908     delete $valid{'RT::Transaction'};
909
910     return $valid{$class};
911 }
912
913 =head2 LoadContextObject
914
915 Takes an Id for a Context Object and loads the right kind of RT::Object
916 for this particular Custom Field (based on the LookupType) and returns it.
917 This is a good way to ensure you don't try to use a Queue as a Context
918 Object on a User Custom Field.
919
920 =cut
921
922 sub LoadContextObject {
923     my $self = shift;
924     my $type = shift;
925     my $contextid = shift;
926
927     unless ( $self->ValidContextType($type) ) {
928         RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id);
929         return;
930     }
931
932     my $context_object = $type->new( $self->CurrentUser );
933     my ($id, $msg) = $context_object->LoadById( $contextid );
934     unless ( $id ) {
935         RT->Logger->debug("Invalid ContextObject id: $msg");
936         return;
937     }
938     return $context_object;
939 }
940
941 =head2 ValidateContextObject
942
943 Ensure that a given ContextObject applies to this Custom Field.
944 For custom fields that are assigned to Queues or to Classes, this checks that the Custom
945 Field is actually applied to that objects.  For Global Custom Fields, it returns true
946 as long as the Object is of the right type, because you may be using
947 your permissions on a given Queue of Class to see a Global CF.
948 For CFs that are only applied Globally, you don't need a ContextObject.
949
950 =cut
951
952 sub ValidateContextObject {
953     my $self = shift;
954     my $object = shift;
955
956     return 1 if $self->IsApplied(0);
957
958     # global only custom fields don't have objects
959     # that should be used as context objects.
960     return if $self->ApplyGlobally;
961
962     # Otherwise, make sure we weren't passed a user object that we're
963     # supposed to treat as a queue.
964     return unless $self->ValidContextType(ref $object);
965
966     # Check that it is applied correctly
967     my ($applied_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects);
968     return unless $applied_to;
969     return $self->IsApplied($applied_to->id);
970 }
971
972
973 sub _Set {
974     my $self = shift;
975
976     unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
977         return ( 0, $self->loc('Permission Denied') );
978     }
979     return $self->SUPER::_Set( @_ );
980
981 }
982
983
984
985 =head2 _Value
986
987 Takes the name of a table column.
988 Returns its value as a string, if the user passes an ACL check
989
990 =cut
991
992 sub _Value {
993     my $self  = shift;
994     return undef unless $self->id;
995
996     # we need to do the rights check
997     unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
998         $RT::Logger->debug(
999             "Permission denied. User #". $self->CurrentUser->id
1000             ." has no SeeCustomField right on CF #". $self->id
1001         );
1002         return (undef);
1003     }
1004     return $self->__Value( @_ );
1005 }
1006
1007
1008 =head2 SetDisabled
1009
1010 Takes a boolean.
1011 1 will cause this custom field to no longer be avaialble for objects.
1012 0 will re-enable this field.
1013
1014 =cut
1015
1016
1017 =head2 SetTypeComposite
1018
1019 Set this custom field's type and maximum values as a composite value
1020
1021 =cut
1022
1023 sub SetTypeComposite {
1024     my $self = shift;
1025     my $composite = shift;
1026
1027     my $old = $self->TypeComposite;
1028
1029     my ($type, $max_values) = split(/-/, $composite, 2);
1030     if ( $type ne $self->Type ) {
1031         my ($status, $msg) = $self->SetType( $type );
1032         return ($status, $msg) unless $status;
1033     }
1034     if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
1035         my ($status, $msg) = $self->SetMaxValues( $max_values );
1036         return ($status, $msg) unless $status;
1037     }
1038     my $render = $self->RenderType;
1039     if ( $render and not grep { $_ eq $render } $self->RenderTypes ) {
1040         # We switched types and our render type is no longer valid, so unset it
1041         # and use the default
1042         $self->SetRenderType( undef );
1043     }
1044     return 1, $self->loc(
1045         "Type changed from '[_1]' to '[_2]'",
1046         $self->FriendlyTypeComposite( $old ),
1047         $self->FriendlyTypeComposite( $composite ),
1048     );
1049 }
1050
1051 =head2 TypeComposite
1052
1053 Returns a composite value composed of this object's type and maximum values
1054
1055 =cut
1056
1057
1058 sub TypeComposite {
1059     my $self = shift;
1060     return join '-', ($self->Type || ''), ($self->MaxValues || 0);
1061 }
1062
1063 =head2 TypeComposites
1064
1065 Returns an array of all possible composite values for custom fields.
1066
1067 =cut
1068
1069 sub TypeComposites {
1070     my $self = shift;
1071     return grep !/(?:[Tt]ext|Combobox|Date|DateTime|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types;
1072 }
1073
1074 =head2 RenderType
1075
1076 Returns the type of form widget to render for this custom field.  Currently
1077 this only affects fields which return true for L</HasRenderTypes>. 
1078
1079 =cut
1080
1081 sub RenderType {
1082     my $self = shift;
1083     return '' unless $self->HasRenderTypes;
1084
1085     return $self->_Value( 'RenderType', @_ )
1086         || $self->DefaultRenderType;
1087 }
1088
1089 =head2 SetRenderType TYPE
1090
1091 Sets this custom field's render type.
1092
1093 =cut
1094
1095 sub SetRenderType {
1096     my $self = shift;
1097     my $type = shift;
1098     return (0, $self->loc("This custom field has no Render Types"))
1099         unless $self->HasRenderTypes;
1100
1101     if ( !$type || $type eq $self->DefaultRenderType ) {
1102         return $self->_Set( Field => 'RenderType', Value => undef, @_ );
1103     }
1104
1105     if ( not grep { $_ eq $type } $self->RenderTypes ) {
1106         return (0, $self->loc("Invalid Render Type for custom field of type [_1]",
1107                                 $self->FriendlyType));
1108     }
1109
1110     # XXX: Remove this restriction once we support lists and cascaded selects
1111     if ( $self->BasedOnObj->id and $type =~ /List/ ) {
1112         return (0, $self->loc("We can't currently render as a List when basing categories on another custom field.  Please use another render type."));
1113     }
1114
1115     return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1116 }
1117
1118 =head2 DefaultRenderType [TYPE COMPOSITE]
1119
1120 Returns the default render type for this custom field's type or the TYPE
1121 COMPOSITE specified as an argument.
1122
1123 =cut
1124
1125 sub DefaultRenderType {
1126     my $self = shift;
1127     my $composite    = @_ ? shift : $self->TypeComposite;
1128     my ($type, $max) = split /-/, $composite, 2;
1129     return unless $type and $self->HasRenderTypes($composite);
1130     return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1131 }
1132
1133 =head2 HasRenderTypes [TYPE_COMPOSITE]
1134
1135 Returns a boolean value indicating whether the L</RenderTypes> and
1136 L</RenderType> methods make sense for this custom field.
1137
1138 Currently true only for type C<Select>.
1139
1140 =cut
1141
1142 sub HasRenderTypes {
1143     my $self = shift;
1144     my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1145     return undef unless $type;
1146     return defined $FieldTypes{$type}->{render_types}
1147         ->{ $max == 1 ? 'single' : 'multiple' };
1148 }
1149
1150 =head2 RenderTypes [TYPE COMPOSITE]
1151
1152 Returns the valid render types for this custom field's type or the TYPE
1153 COMPOSITE specified as an argument.
1154
1155 =cut
1156
1157 sub RenderTypes {
1158     my $self = shift;
1159     my $composite    = @_ ? shift : $self->TypeComposite;
1160     my ($type, $max) = split /-/, $composite, 2;
1161     return unless $type and $self->HasRenderTypes($composite);
1162     return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1163 }
1164
1165 =head2 SetLookupType
1166
1167 Autrijus: care to doc how LookupTypes work?
1168
1169 =cut
1170
1171 sub SetLookupType {
1172     my $self = shift;
1173     my $lookup = shift;
1174     if ( $lookup ne $self->LookupType ) {
1175         # Okay... We need to invalidate our existing relationships
1176         my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
1177         $ObjectCustomFields->LimitToCustomField($self->Id);
1178         $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
1179     }
1180     return $self->_Set(Field => 'LookupType', Value =>$lookup);
1181 }
1182
1183 =head2 LookupTypes
1184
1185 Returns an array of LookupTypes available
1186
1187 =cut
1188
1189
1190 sub LookupTypes {
1191     my $self = shift;
1192     return keys %FRIENDLY_OBJECT_TYPES;
1193 }
1194
1195 my @FriendlyObjectTypes = (
1196     "[_1] objects",            # loc
1197     "[_1]'s [_2] objects",        # loc
1198     "[_1]'s [_2]'s [_3] objects",   # loc
1199 );
1200
1201 =head2 FriendlyLookupType
1202
1203 Returns a localized description of the type of this custom field
1204
1205 =cut
1206
1207 sub FriendlyLookupType {
1208     my $self = shift;
1209     my $lookup = shift || $self->LookupType;
1210    
1211     return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
1212                      if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
1213
1214     my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1215       grep { defined and length }
1216       split( /-/, $lookup )
1217       or return;
1218     return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
1219 }
1220
1221 sub RecordClassFromLookupType {
1222     my $self = shift;
1223     my ($class) = ($self->LookupType =~ /^([^-]+)/);
1224     unless ( $class ) {
1225         $RT::Logger->error(
1226             "Custom Field #". $self->id 
1227             ." has incorrect LookupType '". $self->LookupType ."'"
1228         );
1229         return undef;
1230     }
1231     return $class;
1232 }
1233
1234 sub CollectionClassFromLookupType {
1235     my $self = shift;
1236
1237     my $record_class = $self->RecordClassFromLookupType;
1238     return undef unless $record_class;
1239
1240     my $collection_class;
1241     if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1242         $collection_class = $record_class.'Collection';
1243     } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1244         $collection_class = $record_class.'es';
1245     } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1246         $collection_class = $record_class.'s';
1247     } else {
1248         $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1249         return undef;
1250     }
1251     return $collection_class;
1252 }
1253
1254 =head1 ApplyGlobally
1255
1256 Certain custom fields (users, groups) should only be applied globally
1257 but rather than regexing in code for LookupType =~ RT::Queue, we'll codify
1258 the rules here.
1259
1260 =cut
1261
1262 sub ApplyGlobally {
1263     my $self = shift;
1264
1265     return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1266
1267 }
1268
1269 =head1 AppliedTo
1270
1271 Returns collection with objects this custom field is applied to.
1272 Class of the collection depends on L</LookupType>.
1273 See all L</NotAppliedTo> .
1274
1275 Doesn't takes into account if object is applied globally.
1276
1277 =cut
1278
1279 sub AppliedTo {
1280     my $self = shift;
1281
1282     my ($res, $ocfs_alias) = $self->_AppliedTo;
1283     return $res unless $res;
1284
1285     $res->Limit(
1286         ALIAS     => $ocfs_alias,
1287         FIELD     => 'id',
1288         OPERATOR  => 'IS NOT',
1289         VALUE     => 'NULL',
1290     );
1291
1292     return $res;
1293 }
1294
1295 =head1 NotAppliedTo
1296
1297 Returns collection with objects this custom field is not applied to.
1298 Class of the collection depends on L</LookupType>.
1299 See all L</AppliedTo> .
1300
1301 Doesn't takes into account if object is applied globally.
1302
1303 =cut
1304
1305 sub NotAppliedTo {
1306     my $self = shift;
1307
1308     my ($res, $ocfs_alias) = $self->_AppliedTo;
1309     return $res unless $res;
1310
1311     $res->Limit(
1312         ALIAS     => $ocfs_alias,
1313         FIELD     => 'id',
1314         OPERATOR  => 'IS',
1315         VALUE     => 'NULL',
1316     );
1317
1318     return $res;
1319 }
1320
1321 sub _AppliedTo {
1322     my $self = shift;
1323
1324     my ($class) = $self->CollectionClassFromLookupType;
1325     return undef unless $class;
1326
1327     my $res = $class->new( $self->CurrentUser );
1328
1329     # If CF is a Group CF, only display user-defined groups
1330     if ( $class eq 'RT::Groups' ) {
1331         $res->LimitToUserDefinedGroups;
1332     }
1333
1334     $res->OrderBy( FIELD => 'Name' );
1335     my $ocfs_alias = $res->Join(
1336         TYPE   => 'LEFT',
1337         ALIAS1 => 'main',
1338         FIELD1 => 'id',
1339         TABLE2 => 'ObjectCustomFields',
1340         FIELD2 => 'ObjectId',
1341     );
1342     $res->Limit(
1343         LEFTJOIN => $ocfs_alias,
1344         ALIAS    => $ocfs_alias,
1345         FIELD    => 'CustomField',
1346         VALUE    => $self->id,
1347     );
1348     return ($res, $ocfs_alias);
1349 }
1350
1351 =head2 IsApplied
1352
1353 Takes object id and returns corresponding L<RT::ObjectCustomField>
1354 record if this custom field is applied to the object. Use 0 to check
1355 if custom field is applied globally.
1356
1357 =cut
1358
1359 sub IsApplied {
1360     my $self = shift;
1361     my $id = shift;
1362     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1363     $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1364     return undef unless $ocf->id;
1365     return $ocf;
1366 }
1367
1368 =head2 AddToObject OBJECT
1369
1370 Add this custom field as a custom field for a single object, such as a queue or group.
1371
1372 Takes an object 
1373
1374 =cut
1375
1376
1377 sub AddToObject {
1378     my $self  = shift;
1379     my $object = shift;
1380     my $id = $object->Id || 0;
1381
1382     unless (index($self->LookupType, ref($object)) == 0) {
1383         return ( 0, $self->loc('Lookup type mismatch') );
1384     }
1385
1386     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1387         return ( 0, $self->loc('Permission Denied') );
1388     }
1389
1390     if ( $self->IsApplied( $id ) ) {
1391         return ( 0, $self->loc("Custom field is already applied to the object") );
1392     }
1393
1394     if ( $id ) {
1395         # applying locally
1396         return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1397             if $self->IsApplied( 0 );
1398     }
1399     else {
1400         my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1401         $applied->LimitToCustomField( $self->id );
1402         while ( my $record = $applied->Next ) {
1403             $record->Delete;
1404         }
1405     }
1406
1407     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1408     my ( $oid, $msg ) = $ocf->Create(
1409         ObjectId => $id, CustomField => $self->id,
1410     );
1411     return ( $oid, $msg );
1412 }
1413
1414
1415 =head2 RemoveFromObject OBJECT
1416
1417 Remove this custom field  for a single object, such as a queue or group.
1418
1419 Takes an object 
1420
1421 =cut
1422
1423 sub RemoveFromObject {
1424     my $self = shift;
1425     my $object = shift;
1426     my $id = $object->Id || 0;
1427
1428     unless (index($self->LookupType, ref($object)) == 0) {
1429         return ( 0, $self->loc('Object type mismatch') );
1430     }
1431
1432     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1433         return ( 0, $self->loc('Permission Denied') );
1434     }
1435
1436     my $ocf = $self->IsApplied( $id );
1437     unless ( $ocf ) {
1438         return ( 0, $self->loc("This custom field does not apply to that object") );
1439     }
1440
1441     # XXX: Delete doesn't return anything
1442     my ( $oid, $msg ) = $ocf->Delete;
1443     return ( $oid, $msg );
1444 }
1445
1446
1447 =head2 AddValueForObject HASH
1448
1449 Adds a custom field value for a record object of some kind. 
1450 Takes a param hash of 
1451
1452 Required:
1453
1454     Object
1455     Content
1456
1457 Optional:
1458
1459     LargeContent
1460     ContentType
1461
1462 =cut
1463
1464 sub AddValueForObject {
1465     my $self = shift;
1466     my %args = (
1467         Object       => undef,
1468         Content      => undef,
1469         LargeContent => undef,
1470         ContentType  => undef,
1471         @_
1472     );
1473     my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1474
1475     unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1476         return ( 0, $self->loc('Permission Denied') );
1477     }
1478
1479     unless ( $self->MatchPattern($args{'Content'}) ) {
1480         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1481     }
1482
1483     $RT::Handle->BeginTransaction;
1484
1485     if ( $self->MaxValues ) {
1486         my $current_values = $self->ValuesForObject($obj);
1487         my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1488
1489         # (The +1 is for the new value we're adding)
1490
1491         # If we have a set of current values and we've gone over the maximum
1492         # allowed number of values, we'll need to delete some to make room.
1493         # which former values are blown away is not guaranteed
1494
1495         while ($extra_values) {
1496             my $extra_item = $current_values->Next;
1497             unless ( $extra_item->id ) {
1498                 $RT::Logger->crit( "We were just asked to delete "
1499                     ."a custom field value that doesn't exist!" );
1500                 $RT::Handle->Rollback();
1501                 return (undef);
1502             }
1503             $extra_item->Delete;
1504             $extra_values--;
1505         }
1506     }
1507
1508     if (my $canonicalizer = $self->can('_CanonicalizeValue'.$self->Type)) {
1509          $canonicalizer->($self, \%args);
1510     }
1511
1512
1513
1514     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1515     my ($val, $msg) = $newval->Create(
1516         ObjectType   => ref($obj),
1517         ObjectId     => $obj->Id,
1518         Content      => $args{'Content'},
1519         LargeContent => $args{'LargeContent'},
1520         ContentType  => $args{'ContentType'},
1521         CustomField  => $self->Id
1522     );
1523
1524     unless ($val) {
1525         $RT::Handle->Rollback();
1526         return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1527     }
1528
1529     $RT::Handle->Commit();
1530     return ($val);
1531
1532 }
1533
1534
1535
1536 sub _CanonicalizeValueDateTime {
1537     my $self    = shift;
1538     my $args    = shift;
1539     my $DateObj = RT::Date->new( $self->CurrentUser );
1540     $DateObj->Set( Format => 'unknown',
1541                    Value  => $args->{'Content'} );
1542     $args->{'Content'} = $DateObj->ISO;
1543 }
1544
1545 # For date, we need to store Content as ISO date
1546 sub _CanonicalizeValueDate {
1547     my $self = shift;
1548     my $args = shift;
1549
1550     # in case user input date with time, let's omit it by setting timezone
1551     # to utc so "hour" won't affect "day"
1552     my $DateObj = RT::Date->new( $self->CurrentUser );
1553     $DateObj->Set( Format   => 'unknown',
1554                    Value    => $args->{'Content'},
1555                    Timezone => 'UTC',
1556                  );
1557     $args->{'Content'} = $DateObj->Date( Timezone => 'UTC' );
1558 }
1559
1560 =head2 MatchPattern STRING
1561
1562 Tests the incoming string against the Pattern of this custom field object
1563 and returns a boolean; returns true if the Pattern is empty.
1564
1565 =cut
1566
1567 sub MatchPattern {
1568     my $self = shift;
1569     my $regex = $self->Pattern or return 1;
1570
1571     return (( defined $_[0] ? $_[0] : '') =~ $regex);
1572 }
1573
1574
1575
1576
1577 =head2 FriendlyPattern
1578
1579 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1580 and localizing it.
1581
1582 =cut
1583
1584 sub FriendlyPattern {
1585     my $self = shift;
1586     my $regex = $self->Pattern;
1587
1588     return '' unless length $regex;
1589     if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1590         return '[' . $self->loc($1) . ']';
1591     }
1592     else {
1593         return $regex;
1594     }
1595 }
1596
1597
1598
1599
1600 =head2 DeleteValueForObject HASH
1601
1602 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1603
1604 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1605
1606 =cut
1607
1608 sub DeleteValueForObject {
1609     my $self = shift;
1610     my %args = ( Object => undef,
1611                  Content => undef,
1612                  Id => undef,
1613              @_ );
1614
1615
1616     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1617         return (0, $self->loc('Permission Denied'));
1618     }
1619
1620     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1621
1622     if (my $id = $args{'Id'}) {
1623         $oldval->Load($id);
1624     }
1625     unless ($oldval->id) { 
1626         $oldval->LoadByObjectContentAndCustomField(
1627             Object => $args{'Object'}, 
1628             Content =>  $args{'Content'}, 
1629             CustomField => $self->Id,
1630         );
1631     }
1632
1633
1634     # check to make sure we found it
1635     unless ($oldval->Id) {
1636         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1637     }
1638
1639     # for single-value fields, we need to validate that empty string is a valid value for it
1640     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1641         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1642     }
1643
1644     # delete it
1645
1646     my $ret = $oldval->Delete();
1647     unless ($ret) {
1648         return(0, $self->loc("Custom field value could not be found"));
1649     }
1650     return($oldval->Id, $self->loc("Custom field value deleted"));
1651 }
1652
1653
1654 =head2 ValuesForObject OBJECT
1655
1656 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT 
1657
1658 =cut
1659
1660 sub ValuesForObject {
1661     my $self = shift;
1662     my $object = shift;
1663
1664     my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1665     unless ($self->CurrentUserHasRight('SeeCustomField')) {
1666         # Return an empty object if they have no rights to see
1667         return ($values);
1668     }
1669     
1670     
1671     $values->LimitToCustomField($self->Id);
1672     $values->LimitToEnabled();
1673     $values->LimitToObject($object);
1674
1675     return ($values);
1676 }
1677
1678
1679 =head2 _ForObjectType PATH FRIENDLYNAME
1680
1681 Tell RT that a certain object accepts custom fields
1682
1683 Examples:
1684
1685     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1686     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1687     'RT::User'                             => "Users",                  # loc
1688     'RT::Group'                            => "Groups",                 # loc
1689
1690 This is a class method. 
1691
1692 =cut
1693
1694 sub _ForObjectType {
1695     my $self = shift;
1696     my $path = shift;
1697     my $friendly_name = shift;
1698
1699     $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1700
1701 }
1702
1703
1704 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1705
1706 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1707 uses this field to automatically include content into the user's browser
1708 as they display records with custom fields in RT.
1709
1710 =cut
1711
1712 sub SetIncludeContentForValue {
1713     shift->IncludeContentForValue(@_);
1714 }
1715 sub IncludeContentForValue{
1716     my $self = shift;
1717     $self->_URLTemplate('IncludeContentForValue', @_);
1718 }
1719
1720
1721
1722 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1723
1724 Gets or sets the  C<LinkValueTo> for this custom field. RT
1725 uses this field to make custom field values into hyperlinks in the user's
1726 browser as they display records with custom fields in RT.
1727
1728 =cut
1729
1730
1731 sub SetLinkValueTo {
1732     shift->LinkValueTo(@_);
1733 }
1734
1735 sub LinkValueTo {
1736     my $self = shift;
1737     $self->_URLTemplate('LinkValueTo', @_);
1738
1739 }
1740
1741
1742 =head2 _URLTemplate  NAME [VALUE]
1743
1744 With one argument, returns the _URLTemplate named C<NAME>, but only if
1745 the current user has the right to see this custom field.
1746
1747 With two arguments, attemptes to set the relevant template value.
1748
1749 =cut
1750
1751 sub _URLTemplate {
1752     my $self          = shift;
1753     my $template_name = shift;
1754     if (@_) {
1755
1756         my $value = shift;
1757         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1758             return ( 0, $self->loc('Permission Denied') );
1759         }
1760         $self->SetAttribute( Name => $template_name, Content => $value );
1761         return ( 1, $self->loc('Updated') );
1762     } else {
1763         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1764             return (undef);
1765         }
1766
1767         my @attr = $self->Attributes->Named($template_name);
1768         my $attr = shift @attr;
1769
1770         if ($attr) { return $attr->Content }
1771
1772     }
1773 }
1774
1775 sub SetBasedOn {
1776     my $self = shift;
1777     my $value = shift;
1778
1779     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1780         unless defined $value and length $value;
1781
1782     my $cf = RT::CustomField->new( $self->CurrentUser );
1783     $cf->SetContextObject( $self->ContextObject );
1784     $cf->Load( ref $value ? $value->id : $value );
1785
1786     return (0, "Permission denied")
1787         unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1788
1789     # XXX: Remove this restriction once we support lists and cascaded selects
1790     if ( $self->RenderType =~ /List/ ) {
1791         return (0, $self->loc("We can't currently render as a List when basing categories on another custom field.  Please use another render type."));
1792     }
1793
1794     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1795 }
1796
1797 sub BasedOnObj {
1798     my $self = shift;
1799
1800     my $obj = RT::CustomField->new( $self->CurrentUser );
1801     $obj->SetContextObject( $self->ContextObject );
1802     if ( $self->BasedOn ) {
1803         $obj->Load( $self->BasedOn );
1804     }
1805     return $obj;
1806 }
1807
1808 sub UILocation {
1809     my $self = shift;
1810     my $tag = $self->FirstAttribute( 'UILocation' );
1811     return $tag ? $tag->Content : '';
1812 }
1813
1814 sub SetUILocation {
1815     my $self = shift;
1816     my $tag = shift;
1817     if ( $tag ) {
1818         return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1819     }
1820     else {
1821         return $self->DeleteAttribute('UILocation');
1822     }
1823 }
1824
1825
1826
1827
1828
1829
1830 =head2 id
1831
1832 Returns the current value of id. 
1833 (In the database, id is stored as int(11).)
1834
1835
1836 =cut
1837
1838
1839 =head2 Name
1840
1841 Returns the current value of Name. 
1842 (In the database, Name is stored as varchar(200).)
1843
1844
1845
1846 =head2 SetName VALUE
1847
1848
1849 Set Name to VALUE. 
1850 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1851 (In the database, Name will be stored as a varchar(200).)
1852
1853
1854 =cut
1855
1856
1857 =head2 Type
1858
1859 Returns the current value of Type. 
1860 (In the database, Type is stored as varchar(200).)
1861
1862
1863
1864 =head2 SetType VALUE
1865
1866
1867 Set Type to VALUE. 
1868 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1869 (In the database, Type will be stored as a varchar(200).)
1870
1871
1872 =cut
1873
1874
1875 =head2 RenderType
1876
1877 Returns the current value of RenderType. 
1878 (In the database, RenderType is stored as varchar(64).)
1879
1880
1881
1882 =head2 SetRenderType VALUE
1883
1884
1885 Set RenderType to VALUE. 
1886 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1887 (In the database, RenderType will be stored as a varchar(64).)
1888
1889
1890 =cut
1891
1892
1893 =head2 MaxValues
1894
1895 Returns the current value of MaxValues. 
1896 (In the database, MaxValues is stored as int(11).)
1897
1898
1899
1900 =head2 SetMaxValues VALUE
1901
1902
1903 Set MaxValues to VALUE. 
1904 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1905 (In the database, MaxValues will be stored as a int(11).)
1906
1907
1908 =cut
1909
1910
1911 =head2 Pattern
1912
1913 Returns the current value of Pattern. 
1914 (In the database, Pattern is stored as text.)
1915
1916
1917
1918 =head2 SetPattern VALUE
1919
1920
1921 Set Pattern to VALUE. 
1922 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1923 (In the database, Pattern will be stored as a text.)
1924
1925
1926 =cut
1927
1928
1929 =head2 Repeated
1930
1931 Returns the current value of Repeated. 
1932 (In the database, Repeated is stored as smallint(6).)
1933
1934
1935
1936 =head2 SetRepeated VALUE
1937
1938
1939 Set Repeated to VALUE. 
1940 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1941 (In the database, Repeated will be stored as a smallint(6).)
1942
1943
1944 =cut
1945
1946
1947 =head2 BasedOn
1948
1949 Returns the current value of BasedOn. 
1950 (In the database, BasedOn is stored as int(11).)
1951
1952
1953
1954 =head2 SetBasedOn VALUE
1955
1956
1957 Set BasedOn to VALUE. 
1958 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1959 (In the database, BasedOn will be stored as a int(11).)
1960
1961
1962 =cut
1963
1964
1965 =head2 Description
1966
1967 Returns the current value of Description. 
1968 (In the database, Description is stored as varchar(255).)
1969
1970
1971
1972 =head2 SetDescription VALUE
1973
1974
1975 Set Description to VALUE. 
1976 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1977 (In the database, Description will be stored as a varchar(255).)
1978
1979
1980 =cut
1981
1982
1983 =head2 SortOrder
1984
1985 Returns the current value of SortOrder. 
1986 (In the database, SortOrder is stored as int(11).)
1987
1988
1989
1990 =head2 SetSortOrder VALUE
1991
1992
1993 Set SortOrder to VALUE. 
1994 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1995 (In the database, SortOrder will be stored as a int(11).)
1996
1997
1998 =cut
1999
2000
2001 =head2 LookupType
2002
2003 Returns the current value of LookupType. 
2004 (In the database, LookupType is stored as varchar(255).)
2005
2006
2007
2008 =head2 SetLookupType VALUE
2009
2010
2011 Set LookupType to VALUE. 
2012 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2013 (In the database, LookupType will be stored as a varchar(255).)
2014
2015
2016 =cut
2017
2018
2019 =head2 Creator
2020
2021 Returns the current value of Creator. 
2022 (In the database, Creator is stored as int(11).)
2023
2024
2025 =cut
2026
2027
2028 =head2 Created
2029
2030 Returns the current value of Created. 
2031 (In the database, Created is stored as datetime.)
2032
2033
2034 =cut
2035
2036
2037 =head2 LastUpdatedBy
2038
2039 Returns the current value of LastUpdatedBy. 
2040 (In the database, LastUpdatedBy is stored as int(11).)
2041
2042
2043 =cut
2044
2045
2046 =head2 LastUpdated
2047
2048 Returns the current value of LastUpdated. 
2049 (In the database, LastUpdated is stored as datetime.)
2050
2051
2052 =cut
2053
2054
2055 =head2 Disabled
2056
2057 Returns the current value of Disabled. 
2058 (In the database, Disabled is stored as smallint(6).)
2059
2060
2061
2062 =head2 SetDisabled VALUE
2063
2064
2065 Set Disabled to VALUE. 
2066 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2067 (In the database, Disabled will be stored as a smallint(6).)
2068
2069
2070 =cut
2071
2072
2073
2074 sub _CoreAccessible {
2075     {
2076      
2077         id =>
2078         {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2079         Name => 
2080         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2081         Type => 
2082         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2083         RenderType => 
2084         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2085         MaxValues => 
2086         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2087         Pattern => 
2088         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2089         Repeated => 
2090         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2091         BasedOn => 
2092         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2093         Description => 
2094         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2095         SortOrder => 
2096         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2097         LookupType => 
2098         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2099         Creator => 
2100         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2101         Created => 
2102         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2103         LastUpdatedBy => 
2104         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2105         LastUpdated => 
2106         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2107         Disabled => 
2108         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2109
2110  }
2111 };
2112
2113
2114 RT::Base->_ImportOverlays();
2115
2116 1;