Merge branch 'master' of https://github.com/jgoodman/Freeside
[freeside.git] / rt / lib / RT / CustomField.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::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     if (my $canonicalizer = $self->can('_CanonicalizeValue'.$self->Type)) {
1551          $canonicalizer->($self, \%args);
1552     }
1553
1554
1555
1556     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1557     my ($val, $msg) = $newval->Create(
1558         ObjectType   => ref($obj),
1559         ObjectId     => $obj->Id,
1560         Content      => $args{'Content'},
1561         LargeContent => $args{'LargeContent'},
1562         ContentType  => $args{'ContentType'},
1563         CustomField  => $self->Id
1564     );
1565
1566     unless ($val) {
1567         $RT::Handle->Rollback();
1568         return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1569     }
1570
1571     $RT::Handle->Commit();
1572     return ($val);
1573
1574 }
1575
1576
1577
1578 sub _CanonicalizeValueDateTime {
1579     my $self    = shift;
1580     my $args    = shift;
1581     my $DateObj = RT::Date->new( $self->CurrentUser );
1582     $DateObj->Set( Format => 'unknown',
1583                    Value  => $args->{'Content'} );
1584     $args->{'Content'} = $DateObj->ISO;
1585 }
1586
1587 # For date, we need to store Content as ISO date
1588 sub _CanonicalizeValueDate {
1589     my $self = shift;
1590     my $args = shift;
1591
1592     # in case user input date with time, let's omit it by setting timezone
1593     # to utc so "hour" won't affect "day"
1594     my $DateObj = RT::Date->new( $self->CurrentUser );
1595     $DateObj->Set( Format   => 'unknown',
1596                    Value    => $args->{'Content'},
1597                  );
1598     $args->{'Content'} = $DateObj->Date( Timezone => 'user' );
1599 }
1600
1601 =head2 MatchPattern STRING
1602
1603 Tests the incoming string against the Pattern of this custom field object
1604 and returns a boolean; returns true if the Pattern is empty.
1605
1606 =cut
1607
1608 sub MatchPattern {
1609     my $self = shift;
1610     my $regex = $self->Pattern or return 1;
1611
1612     return (( defined $_[0] ? $_[0] : '') =~ $regex);
1613 }
1614
1615
1616
1617
1618 =head2 FriendlyPattern
1619
1620 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1621 and localizing it.
1622
1623 =cut
1624
1625 sub FriendlyPattern {
1626     my $self = shift;
1627     my $regex = $self->Pattern;
1628
1629     return '' unless length $regex;
1630     if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1631         return '[' . $self->loc($1) . ']';
1632     }
1633     else {
1634         return $regex;
1635     }
1636 }
1637
1638
1639
1640
1641 =head2 DeleteValueForObject HASH
1642
1643 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1644
1645 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1646
1647 =cut
1648
1649 sub DeleteValueForObject {
1650     my $self = shift;
1651     my %args = ( Object => undef,
1652                  Content => undef,
1653                  Id => undef,
1654              @_ );
1655
1656
1657     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1658         return (0, $self->loc('Permission Denied'));
1659     }
1660
1661     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1662
1663     if (my $id = $args{'Id'}) {
1664         $oldval->Load($id);
1665     }
1666     unless ($oldval->id) { 
1667         $oldval->LoadByObjectContentAndCustomField(
1668             Object => $args{'Object'}, 
1669             Content =>  $args{'Content'}, 
1670             CustomField => $self->Id,
1671         );
1672     }
1673
1674
1675     # check to make sure we found it
1676     unless ($oldval->Id) {
1677         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1678     }
1679
1680     # for single-value fields, we need to validate that empty string is a valid value for it
1681     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1682         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1683     }
1684
1685     # delete it
1686
1687     my $ret = $oldval->Delete();
1688     unless ($ret) {
1689         return(0, $self->loc("Custom field value could not be found"));
1690     }
1691     return($oldval->Id, $self->loc("Custom field value deleted"));
1692 }
1693
1694
1695 =head2 ValuesForObject OBJECT
1696
1697 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT 
1698
1699 =cut
1700
1701 sub ValuesForObject {
1702     my $self = shift;
1703     my $object = shift;
1704
1705     my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1706     unless ($self->id and $self->CurrentUserHasRight('SeeCustomField')) {
1707         # Return an empty object if they have no rights to see
1708         $values->Limit( FIELD => "id", VALUE => 0, SUBCLAUSE => "ACL" );
1709         return ($values);
1710     }
1711
1712     $values->LimitToCustomField($self->Id);
1713     $values->LimitToObject($object);
1714
1715     return ($values);
1716 }
1717
1718
1719 =head2 _ForObjectType PATH FRIENDLYNAME
1720
1721 Tell RT that a certain object accepts custom fields
1722
1723 Examples:
1724
1725     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1726     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1727     'RT::User'                             => "Users",                  # loc
1728     'RT::Group'                            => "Groups",                 # loc
1729     'RT::Queue'                            => "Queues",                 # loc
1730
1731 This is a class method. 
1732
1733 =cut
1734
1735 sub _ForObjectType {
1736     my $self = shift;
1737     my $path = shift;
1738     my $friendly_name = shift;
1739
1740     $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1741
1742 }
1743
1744
1745 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1746
1747 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1748 uses this field to automatically include content into the user's browser
1749 as they display records with custom fields in RT.
1750
1751 =cut
1752
1753 sub SetIncludeContentForValue {
1754     shift->IncludeContentForValue(@_);
1755 }
1756 sub IncludeContentForValue{
1757     my $self = shift;
1758     $self->_URLTemplate('IncludeContentForValue', @_);
1759 }
1760
1761
1762
1763 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1764
1765 Gets or sets the  C<LinkValueTo> for this custom field. RT
1766 uses this field to make custom field values into hyperlinks in the user's
1767 browser as they display records with custom fields in RT.
1768
1769 =cut
1770
1771
1772 sub SetLinkValueTo {
1773     shift->LinkValueTo(@_);
1774 }
1775
1776 sub LinkValueTo {
1777     my $self = shift;
1778     $self->_URLTemplate('LinkValueTo', @_);
1779
1780 }
1781
1782
1783 =head2 _URLTemplate  NAME [VALUE]
1784
1785 With one argument, returns the _URLTemplate named C<NAME>, but only if
1786 the current user has the right to see this custom field.
1787
1788 With two arguments, attemptes to set the relevant template value.
1789
1790 =cut
1791
1792 sub _URLTemplate {
1793     my $self          = shift;
1794     my $template_name = shift;
1795     if (@_) {
1796
1797         my $value = shift;
1798         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1799             return ( 0, $self->loc('Permission Denied') );
1800         }
1801         $self->SetAttribute( Name => $template_name, Content => $value );
1802         return ( 1, $self->loc('Updated') );
1803     } else {
1804         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1805             return (undef);
1806         }
1807
1808         my @attr = $self->Attributes->Named($template_name);
1809         my $attr = shift @attr;
1810
1811         if ($attr) { return $attr->Content }
1812
1813     }
1814 }
1815
1816 sub SetBasedOn {
1817     my $self = shift;
1818     my $value = shift;
1819
1820     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1821         unless defined $value and length $value;
1822
1823     my $cf = RT::CustomField->new( $self->CurrentUser );
1824     $cf->SetContextObject( $self->ContextObject );
1825     $cf->Load( ref $value ? $value->id : $value );
1826
1827     return (0, "Permission denied")
1828         unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1829
1830     # XXX: Remove this restriction once we support lists and cascaded selects
1831     if ( $self->RenderType =~ /List/ ) {
1832         return (0, $self->loc("We can't currently render as a List when basing categories on another custom field.  Please use another render type."));
1833     }
1834
1835     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1836 }
1837
1838 sub BasedOnObj {
1839     my $self = shift;
1840
1841     my $obj = RT::CustomField->new( $self->CurrentUser );
1842     $obj->SetContextObject( $self->ContextObject );
1843     if ( $self->BasedOn ) {
1844         $obj->Load( $self->BasedOn );
1845     }
1846     return $obj;
1847 }
1848
1849 sub UILocation {
1850     my $self = shift;
1851     my $tag = $self->FirstAttribute( 'UILocation' );
1852     return $tag ? $tag->Content : '';
1853 }
1854
1855 sub SetUILocation {
1856     my $self = shift;
1857     my $tag = shift;
1858     if ( $tag ) {
1859         return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1860     }
1861     else {
1862         return $self->DeleteAttribute('UILocation');
1863     }
1864 }
1865
1866 sub NoClone {
1867     my $self = shift;
1868     $self->FirstAttribute('NoClone') ? 1 : '';
1869 }
1870
1871 sub SetNoClone {
1872     my $self = shift;
1873     my $value = shift;
1874     if ( $value ) {
1875         return $self->SetAttribute( Name => 'NoClone', Content => 1 );
1876     } else {
1877         return $self->DeleteAttribute('NoClone');
1878     }
1879 }
1880
1881
1882 =head2 id
1883
1884 Returns the current value of id. 
1885 (In the database, id is stored as int(11).)
1886
1887
1888 =cut
1889
1890
1891 =head2 Name
1892
1893 Returns the current value of Name. 
1894 (In the database, Name is stored as varchar(200).)
1895
1896
1897
1898 =head2 SetName VALUE
1899
1900
1901 Set Name to VALUE. 
1902 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1903 (In the database, Name will be stored as a varchar(200).)
1904
1905
1906 =cut
1907
1908
1909 =head2 Type
1910
1911 Returns the current value of Type. 
1912 (In the database, Type is stored as varchar(200).)
1913
1914
1915
1916 =head2 SetType VALUE
1917
1918
1919 Set Type to VALUE. 
1920 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1921 (In the database, Type will be stored as a varchar(200).)
1922
1923
1924 =cut
1925
1926
1927 =head2 RenderType
1928
1929 Returns the current value of RenderType. 
1930 (In the database, RenderType is stored as varchar(64).)
1931
1932
1933
1934 =head2 SetRenderType VALUE
1935
1936
1937 Set RenderType to VALUE. 
1938 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1939 (In the database, RenderType will be stored as a varchar(64).)
1940
1941
1942 =cut
1943
1944
1945 =head2 MaxValues
1946
1947 Returns the current value of MaxValues. 
1948 (In the database, MaxValues is stored as int(11).)
1949
1950
1951
1952 =head2 SetMaxValues VALUE
1953
1954
1955 Set MaxValues to VALUE. 
1956 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1957 (In the database, MaxValues will be stored as a int(11).)
1958
1959
1960 =cut
1961
1962
1963 =head2 Pattern
1964
1965 Returns the current value of Pattern. 
1966 (In the database, Pattern is stored as text.)
1967
1968
1969
1970 =head2 SetPattern VALUE
1971
1972
1973 Set Pattern to VALUE. 
1974 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1975 (In the database, Pattern will be stored as a text.)
1976
1977
1978 =cut
1979
1980
1981 =head2 Repeated
1982
1983 Returns the current value of Repeated. 
1984 (In the database, Repeated is stored as smallint(6).)
1985
1986
1987
1988 =head2 SetRepeated VALUE
1989
1990
1991 Set Repeated to VALUE. 
1992 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1993 (In the database, Repeated will be stored as a smallint(6).)
1994
1995
1996 =cut
1997
1998
1999 =head2 BasedOn
2000
2001 Returns the current value of BasedOn. 
2002 (In the database, BasedOn is stored as int(11).)
2003
2004
2005
2006 =head2 SetBasedOn VALUE
2007
2008
2009 Set BasedOn to VALUE. 
2010 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2011 (In the database, BasedOn will be stored as a int(11).)
2012
2013
2014 =cut
2015
2016
2017 =head2 Description
2018
2019 Returns the current value of Description. 
2020 (In the database, Description is stored as varchar(255).)
2021
2022
2023
2024 =head2 SetDescription VALUE
2025
2026
2027 Set Description to VALUE. 
2028 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2029 (In the database, Description will be stored as a varchar(255).)
2030
2031
2032 =cut
2033
2034
2035 =head2 SortOrder
2036
2037 Returns the current value of SortOrder. 
2038 (In the database, SortOrder is stored as int(11).)
2039
2040
2041
2042 =head2 SetSortOrder VALUE
2043
2044
2045 Set SortOrder to VALUE. 
2046 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2047 (In the database, SortOrder will be stored as a int(11).)
2048
2049
2050 =cut
2051
2052
2053 =head2 LookupType
2054
2055 Returns the current value of LookupType. 
2056 (In the database, LookupType is stored as varchar(255).)
2057
2058
2059
2060 =head2 SetLookupType VALUE
2061
2062
2063 Set LookupType to VALUE. 
2064 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2065 (In the database, LookupType will be stored as a varchar(255).)
2066
2067
2068 =cut
2069
2070
2071 =head2 Creator
2072
2073 Returns the current value of Creator. 
2074 (In the database, Creator is stored as int(11).)
2075
2076
2077 =cut
2078
2079
2080 =head2 Created
2081
2082 Returns the current value of Created. 
2083 (In the database, Created is stored as datetime.)
2084
2085
2086 =cut
2087
2088
2089 =head2 LastUpdatedBy
2090
2091 Returns the current value of LastUpdatedBy. 
2092 (In the database, LastUpdatedBy is stored as int(11).)
2093
2094
2095 =cut
2096
2097
2098 =head2 LastUpdated
2099
2100 Returns the current value of LastUpdated. 
2101 (In the database, LastUpdated is stored as datetime.)
2102
2103
2104 =cut
2105
2106
2107 =head2 Disabled
2108
2109 Returns the current value of Disabled. 
2110 (In the database, Disabled is stored as smallint(6).)
2111
2112
2113
2114 =head2 SetDisabled VALUE
2115
2116
2117 Set Disabled to VALUE. 
2118 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2119 (In the database, Disabled will be stored as a smallint(6).)
2120
2121
2122 =cut
2123
2124
2125
2126 sub _CoreAccessible {
2127     {
2128      
2129         id =>
2130         {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2131         Name => 
2132         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2133         Type => 
2134         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2135         RenderType => 
2136         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2137         MaxValues => 
2138         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2139         Pattern => 
2140         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2141         Repeated => 
2142         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2143         ValuesClass => 
2144         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2145         BasedOn => 
2146         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2147         Description => 
2148         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2149         SortOrder => 
2150         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2151         LookupType => 
2152         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2153         Creator => 
2154         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2155         Created => 
2156         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2157         LastUpdatedBy => 
2158         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2159         LastUpdated => 
2160         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2161         Disabled => 
2162         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2163
2164  }
2165 };
2166
2167
2168 RT::Base->_ImportOverlays();
2169
2170 1;