Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / rt / lib / RT / CustomField.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::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         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     # XXX: Remove this restriction once we support lists and cascaded selects
1115     if ( $self->BasedOnObj->id and $type =~ /List/ ) {
1116         return (0, $self->loc("We can't currently render as a List when basing categories on another custom field.  Please use another render type."));
1117     }
1118
1119     return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1120 }
1121
1122 =head2 DefaultRenderType [TYPE COMPOSITE]
1123
1124 Returns the default render type for this custom field's type or the TYPE
1125 COMPOSITE specified as an argument.
1126
1127 =cut
1128
1129 sub DefaultRenderType {
1130     my $self = shift;
1131     my $composite    = @_ ? shift : $self->TypeComposite;
1132     my ($type, $max) = split /-/, $composite, 2;
1133     return unless $type and $self->HasRenderTypes($composite);
1134     return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1135 }
1136
1137 =head2 HasRenderTypes [TYPE_COMPOSITE]
1138
1139 Returns a boolean value indicating whether the L</RenderTypes> and
1140 L</RenderType> methods make sense for this custom field.
1141
1142 Currently true only for type C<Select>.
1143
1144 =cut
1145
1146 sub HasRenderTypes {
1147     my $self = shift;
1148     my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1149     return undef unless $type;
1150     return defined $FieldTypes{$type}->{render_types}
1151         ->{ $max == 1 ? 'single' : 'multiple' };
1152 }
1153
1154 =head2 RenderTypes [TYPE COMPOSITE]
1155
1156 Returns the valid render types for this custom field's type or the TYPE
1157 COMPOSITE specified as an argument.
1158
1159 =cut
1160
1161 sub RenderTypes {
1162     my $self = shift;
1163     my $composite    = @_ ? shift : $self->TypeComposite;
1164     my ($type, $max) = split /-/, $composite, 2;
1165     return unless $type and $self->HasRenderTypes($composite);
1166     return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1167 }
1168
1169 =head2 SetLookupType
1170
1171 Autrijus: care to doc how LookupTypes work?
1172
1173 =cut
1174
1175 sub SetLookupType {
1176     my $self = shift;
1177     my $lookup = shift;
1178     if ( $lookup ne $self->LookupType ) {
1179         # Okay... We need to invalidate our existing relationships
1180         my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
1181         $ObjectCustomFields->LimitToCustomField($self->Id);
1182         $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
1183     }
1184     return $self->_Set(Field => 'LookupType', Value =>$lookup);
1185 }
1186
1187 =head2 LookupTypes
1188
1189 Returns an array of LookupTypes available
1190
1191 =cut
1192
1193
1194 sub LookupTypes {
1195     my $self = shift;
1196     return keys %FRIENDLY_OBJECT_TYPES;
1197 }
1198
1199 my @FriendlyObjectTypes = (
1200     "[_1] objects",            # loc
1201     "[_1]'s [_2] objects",        # loc
1202     "[_1]'s [_2]'s [_3] objects",   # loc
1203 );
1204
1205 =head2 FriendlyLookupType
1206
1207 Returns a localized description of the type of this custom field
1208
1209 =cut
1210
1211 sub FriendlyLookupType {
1212     my $self = shift;
1213     my $lookup = shift || $self->LookupType;
1214    
1215     return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
1216                      if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
1217
1218     my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1219       grep { defined and length }
1220       split( /-/, $lookup )
1221       or return;
1222     return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
1223 }
1224
1225 sub RecordClassFromLookupType {
1226     my $self = shift;
1227     my ($class) = ($self->LookupType =~ /^([^-]+)/);
1228     unless ( $class ) {
1229         $RT::Logger->error(
1230             "Custom Field #". $self->id 
1231             ." has incorrect LookupType '". $self->LookupType ."'"
1232         );
1233         return undef;
1234     }
1235     return $class;
1236 }
1237
1238 sub CollectionClassFromLookupType {
1239     my $self = shift;
1240
1241     my $record_class = $self->RecordClassFromLookupType;
1242     return undef unless $record_class;
1243
1244     my $collection_class;
1245     if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1246         $collection_class = $record_class.'Collection';
1247     } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1248         $collection_class = $record_class.'es';
1249     } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1250         $collection_class = $record_class.'s';
1251     } else {
1252         $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1253         return undef;
1254     }
1255     return $collection_class;
1256 }
1257
1258 =head1 ApplyGlobally
1259
1260 Certain custom fields (users, groups) should only be applied globally
1261 but rather than regexing in code for LookupType =~ RT::Queue, we'll codify
1262 the rules here.
1263
1264 =cut
1265
1266 sub ApplyGlobally {
1267     my $self = shift;
1268
1269     return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1270
1271 }
1272
1273 =head1 AppliedTo
1274
1275 Returns collection with objects this custom field is applied to.
1276 Class of the collection depends on L</LookupType>.
1277 See all L</NotAppliedTo> .
1278
1279 Doesn't takes into account if object is applied globally.
1280
1281 =cut
1282
1283 sub AppliedTo {
1284     my $self = shift;
1285
1286     my ($res, $ocfs_alias) = $self->_AppliedTo;
1287     return $res unless $res;
1288
1289     $res->Limit(
1290         ALIAS     => $ocfs_alias,
1291         FIELD     => 'id',
1292         OPERATOR  => 'IS NOT',
1293         VALUE     => 'NULL',
1294     );
1295
1296     return $res;
1297 }
1298
1299 =head1 NotAppliedTo
1300
1301 Returns collection with objects this custom field is not applied to.
1302 Class of the collection depends on L</LookupType>.
1303 See all L</AppliedTo> .
1304
1305 Doesn't takes into account if object is applied globally.
1306
1307 =cut
1308
1309 sub NotAppliedTo {
1310     my $self = shift;
1311
1312     my ($res, $ocfs_alias) = $self->_AppliedTo;
1313     return $res unless $res;
1314
1315     $res->Limit(
1316         ALIAS     => $ocfs_alias,
1317         FIELD     => 'id',
1318         OPERATOR  => 'IS',
1319         VALUE     => 'NULL',
1320     );
1321
1322     return $res;
1323 }
1324
1325 sub _AppliedTo {
1326     my $self = shift;
1327
1328     my ($class) = $self->CollectionClassFromLookupType;
1329     return undef unless $class;
1330
1331     my $res = $class->new( $self->CurrentUser );
1332
1333     # If CF is a Group CF, only display user-defined groups
1334     if ( $class eq 'RT::Groups' ) {
1335         $res->LimitToUserDefinedGroups;
1336     }
1337
1338     $res->OrderBy( FIELD => 'Name' );
1339     my $ocfs_alias = $res->Join(
1340         TYPE   => 'LEFT',
1341         ALIAS1 => 'main',
1342         FIELD1 => 'id',
1343         TABLE2 => 'ObjectCustomFields',
1344         FIELD2 => 'ObjectId',
1345     );
1346     $res->Limit(
1347         LEFTJOIN => $ocfs_alias,
1348         ALIAS    => $ocfs_alias,
1349         FIELD    => 'CustomField',
1350         VALUE    => $self->id,
1351     );
1352     return ($res, $ocfs_alias);
1353 }
1354
1355 =head2 IsApplied
1356
1357 Takes object id and returns corresponding L<RT::ObjectCustomField>
1358 record if this custom field is applied to the object. Use 0 to check
1359 if custom field is applied globally.
1360
1361 =cut
1362
1363 sub IsApplied {
1364     my $self = shift;
1365     my $id = shift;
1366     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1367     $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1368     return undef unless $ocf->id;
1369     return $ocf;
1370 }
1371
1372 =head2 AddToObject OBJECT
1373
1374 Add this custom field as a custom field for a single object, such as a queue or group.
1375
1376 Takes an object 
1377
1378 =cut
1379
1380
1381 sub AddToObject {
1382     my $self  = shift;
1383     my $object = shift;
1384     my $id = $object->Id || 0;
1385
1386     unless (index($self->LookupType, ref($object)) == 0) {
1387         return ( 0, $self->loc('Lookup type mismatch') );
1388     }
1389
1390     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1391         return ( 0, $self->loc('Permission Denied') );
1392     }
1393
1394     if ( $self->IsApplied( $id ) ) {
1395         return ( 0, $self->loc("Custom field is already applied to the object") );
1396     }
1397
1398     if ( $id ) {
1399         # applying locally
1400         return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1401             if $self->IsApplied( 0 );
1402     }
1403     else {
1404         my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1405         $applied->LimitToCustomField( $self->id );
1406         while ( my $record = $applied->Next ) {
1407             $record->Delete;
1408         }
1409     }
1410
1411     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1412     my ( $oid, $msg ) = $ocf->Create(
1413         ObjectId => $id, CustomField => $self->id,
1414     );
1415     return ( $oid, $msg );
1416 }
1417
1418
1419 =head2 RemoveFromObject OBJECT
1420
1421 Remove this custom field  for a single object, such as a queue or group.
1422
1423 Takes an object 
1424
1425 =cut
1426
1427 sub RemoveFromObject {
1428     my $self = shift;
1429     my $object = shift;
1430     my $id = $object->Id || 0;
1431
1432     unless (index($self->LookupType, ref($object)) == 0) {
1433         return ( 0, $self->loc('Object type mismatch') );
1434     }
1435
1436     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1437         return ( 0, $self->loc('Permission Denied') );
1438     }
1439
1440     my $ocf = $self->IsApplied( $id );
1441     unless ( $ocf ) {
1442         return ( 0, $self->loc("This custom field does not apply to that object") );
1443     }
1444
1445     # XXX: Delete doesn't return anything
1446     my ( $oid, $msg ) = $ocf->Delete;
1447     return ( $oid, $msg );
1448 }
1449
1450
1451 =head2 AddValueForObject HASH
1452
1453 Adds a custom field value for a record object of some kind. 
1454 Takes a param hash of 
1455
1456 Required:
1457
1458     Object
1459     Content
1460
1461 Optional:
1462
1463     LargeContent
1464     ContentType
1465
1466 =cut
1467
1468 sub AddValueForObject {
1469     my $self = shift;
1470     my %args = (
1471         Object       => undef,
1472         Content      => undef,
1473         LargeContent => undef,
1474         ContentType  => undef,
1475         @_
1476     );
1477     my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1478
1479     unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1480         return ( 0, $self->loc('Permission Denied') );
1481     }
1482
1483     unless ( $self->MatchPattern($args{'Content'}) ) {
1484         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1485     }
1486
1487     $RT::Handle->BeginTransaction;
1488
1489     if ( $self->MaxValues ) {
1490         my $current_values = $self->ValuesForObject($obj);
1491         my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1492
1493         # (The +1 is for the new value we're adding)
1494
1495         # If we have a set of current values and we've gone over the maximum
1496         # allowed number of values, we'll need to delete some to make room.
1497         # which former values are blown away is not guaranteed
1498
1499         while ($extra_values) {
1500             my $extra_item = $current_values->Next;
1501             unless ( $extra_item->id ) {
1502                 $RT::Logger->crit( "We were just asked to delete "
1503                     ."a custom field value that doesn't exist!" );
1504                 $RT::Handle->Rollback();
1505                 return (undef);
1506             }
1507             $extra_item->Delete;
1508             $extra_values--;
1509         }
1510     }
1511
1512     if (my $canonicalizer = $self->can('_CanonicalizeValue'.$self->Type)) {
1513          $canonicalizer->($self, \%args);
1514     }
1515
1516
1517
1518     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1519     my ($val, $msg) = $newval->Create(
1520         ObjectType   => ref($obj),
1521         ObjectId     => $obj->Id,
1522         Content      => $args{'Content'},
1523         LargeContent => $args{'LargeContent'},
1524         ContentType  => $args{'ContentType'},
1525         CustomField  => $self->Id
1526     );
1527
1528     unless ($val) {
1529         $RT::Handle->Rollback();
1530         return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1531     }
1532
1533     $RT::Handle->Commit();
1534     return ($val);
1535
1536 }
1537
1538
1539
1540 sub _CanonicalizeValueDateTime {
1541     my $self    = shift;
1542     my $args    = shift;
1543     my $DateObj = RT::Date->new( $self->CurrentUser );
1544     $DateObj->Set( Format => 'unknown',
1545                    Value  => $args->{'Content'} );
1546     $args->{'Content'} = $DateObj->ISO;
1547 }
1548
1549 # For date, we need to store Content as ISO date
1550 sub _CanonicalizeValueDate {
1551     my $self = shift;
1552     my $args = shift;
1553
1554     # in case user input date with time, let's omit it by setting timezone
1555     # to utc so "hour" won't affect "day"
1556     my $DateObj = RT::Date->new( $self->CurrentUser );
1557     $DateObj->Set( Format   => 'unknown',
1558                    Value    => $args->{'Content'},
1559                  );
1560     $args->{'Content'} = $DateObj->Date( Timezone => 'user' );
1561 }
1562
1563 =head2 MatchPattern STRING
1564
1565 Tests the incoming string against the Pattern of this custom field object
1566 and returns a boolean; returns true if the Pattern is empty.
1567
1568 =cut
1569
1570 sub MatchPattern {
1571     my $self = shift;
1572     my $regex = $self->Pattern or return 1;
1573
1574     return (( defined $_[0] ? $_[0] : '') =~ $regex);
1575 }
1576
1577
1578
1579
1580 =head2 FriendlyPattern
1581
1582 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1583 and localizing it.
1584
1585 =cut
1586
1587 sub FriendlyPattern {
1588     my $self = shift;
1589     my $regex = $self->Pattern;
1590
1591     return '' unless length $regex;
1592     if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1593         return '[' . $self->loc($1) . ']';
1594     }
1595     else {
1596         return $regex;
1597     }
1598 }
1599
1600
1601
1602
1603 =head2 DeleteValueForObject HASH
1604
1605 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1606
1607 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1608
1609 =cut
1610
1611 sub DeleteValueForObject {
1612     my $self = shift;
1613     my %args = ( Object => undef,
1614                  Content => undef,
1615                  Id => undef,
1616              @_ );
1617
1618
1619     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1620         return (0, $self->loc('Permission Denied'));
1621     }
1622
1623     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1624
1625     if (my $id = $args{'Id'}) {
1626         $oldval->Load($id);
1627     }
1628     unless ($oldval->id) { 
1629         $oldval->LoadByObjectContentAndCustomField(
1630             Object => $args{'Object'}, 
1631             Content =>  $args{'Content'}, 
1632             CustomField => $self->Id,
1633         );
1634     }
1635
1636
1637     # check to make sure we found it
1638     unless ($oldval->Id) {
1639         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1640     }
1641
1642     # for single-value fields, we need to validate that empty string is a valid value for it
1643     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1644         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1645     }
1646
1647     # delete it
1648
1649     my $ret = $oldval->Delete();
1650     unless ($ret) {
1651         return(0, $self->loc("Custom field value could not be found"));
1652     }
1653     return($oldval->Id, $self->loc("Custom field value deleted"));
1654 }
1655
1656
1657 =head2 ValuesForObject OBJECT
1658
1659 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT 
1660
1661 =cut
1662
1663 sub ValuesForObject {
1664     my $self = shift;
1665     my $object = shift;
1666
1667     my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1668     unless ($self->id and $self->CurrentUserHasRight('SeeCustomField')) {
1669         # Return an empty object if they have no rights to see
1670         $values->Limit( FIELD => "id", VALUE => 0, SUBCLAUSE => "ACL" );
1671         return ($values);
1672     }
1673
1674     $values->LimitToCustomField($self->Id);
1675     $values->LimitToObject($object);
1676
1677     return ($values);
1678 }
1679
1680
1681 =head2 _ForObjectType PATH FRIENDLYNAME
1682
1683 Tell RT that a certain object accepts custom fields
1684
1685 Examples:
1686
1687     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1688     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1689     'RT::User'                             => "Users",                  # loc
1690     'RT::Group'                            => "Groups",                 # loc
1691     'RT::Queue'                            => "Queues",                 # loc
1692
1693 This is a class method. 
1694
1695 =cut
1696
1697 sub _ForObjectType {
1698     my $self = shift;
1699     my $path = shift;
1700     my $friendly_name = shift;
1701
1702     $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1703
1704 }
1705
1706
1707 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1708
1709 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1710 uses this field to automatically include content into the user's browser
1711 as they display records with custom fields in RT.
1712
1713 =cut
1714
1715 sub SetIncludeContentForValue {
1716     shift->IncludeContentForValue(@_);
1717 }
1718 sub IncludeContentForValue{
1719     my $self = shift;
1720     $self->_URLTemplate('IncludeContentForValue', @_);
1721 }
1722
1723
1724
1725 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1726
1727 Gets or sets the  C<LinkValueTo> for this custom field. RT
1728 uses this field to make custom field values into hyperlinks in the user's
1729 browser as they display records with custom fields in RT.
1730
1731 =cut
1732
1733
1734 sub SetLinkValueTo {
1735     shift->LinkValueTo(@_);
1736 }
1737
1738 sub LinkValueTo {
1739     my $self = shift;
1740     $self->_URLTemplate('LinkValueTo', @_);
1741
1742 }
1743
1744
1745 =head2 _URLTemplate  NAME [VALUE]
1746
1747 With one argument, returns the _URLTemplate named C<NAME>, but only if
1748 the current user has the right to see this custom field.
1749
1750 With two arguments, attemptes to set the relevant template value.
1751
1752 =cut
1753
1754 sub _URLTemplate {
1755     my $self          = shift;
1756     my $template_name = shift;
1757     if (@_) {
1758
1759         my $value = shift;
1760         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1761             return ( 0, $self->loc('Permission Denied') );
1762         }
1763         $self->SetAttribute( Name => $template_name, Content => $value );
1764         return ( 1, $self->loc('Updated') );
1765     } else {
1766         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1767             return (undef);
1768         }
1769
1770         my @attr = $self->Attributes->Named($template_name);
1771         my $attr = shift @attr;
1772
1773         if ($attr) { return $attr->Content }
1774
1775     }
1776 }
1777
1778 sub SetBasedOn {
1779     my $self = shift;
1780     my $value = shift;
1781
1782     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1783         unless defined $value and length $value;
1784
1785     my $cf = RT::CustomField->new( $self->CurrentUser );
1786     $cf->SetContextObject( $self->ContextObject );
1787     $cf->Load( ref $value ? $value->id : $value );
1788
1789     return (0, "Permission denied")
1790         unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1791
1792     # XXX: Remove this restriction once we support lists and cascaded selects
1793     if ( $self->RenderType =~ /List/ ) {
1794         return (0, $self->loc("We can't currently render as a List when basing categories on another custom field.  Please use another render type."));
1795     }
1796
1797     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1798 }
1799
1800 sub BasedOnObj {
1801     my $self = shift;
1802
1803     my $obj = RT::CustomField->new( $self->CurrentUser );
1804     $obj->SetContextObject( $self->ContextObject );
1805     if ( $self->BasedOn ) {
1806         $obj->Load( $self->BasedOn );
1807     }
1808     return $obj;
1809 }
1810
1811 sub UILocation {
1812     my $self = shift;
1813     my $tag = $self->FirstAttribute( 'UILocation' );
1814     return $tag ? $tag->Content : '';
1815 }
1816
1817 sub SetUILocation {
1818     my $self = shift;
1819     my $tag = shift;
1820     if ( $tag ) {
1821         return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1822     }
1823     else {
1824         return $self->DeleteAttribute('UILocation');
1825     }
1826 }
1827
1828 sub NoClone {
1829     my $self = shift;
1830     $self->FirstAttribute('NoClone') ? 1 : '';
1831 }
1832
1833 sub SetNoClone {
1834     my $self = shift;
1835     my $value = shift;
1836     if ( $value ) {
1837         return $self->SetAttribute( Name => 'NoClone', Content => 1 );
1838     } else {
1839         return $self->DeleteAttribute('NoClone');
1840     }
1841 }
1842
1843
1844 =head2 id
1845
1846 Returns the current value of id. 
1847 (In the database, id is stored as int(11).)
1848
1849
1850 =cut
1851
1852
1853 =head2 Name
1854
1855 Returns the current value of Name. 
1856 (In the database, Name is stored as varchar(200).)
1857
1858
1859
1860 =head2 SetName VALUE
1861
1862
1863 Set Name to VALUE. 
1864 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1865 (In the database, Name will be stored as a varchar(200).)
1866
1867
1868 =cut
1869
1870
1871 =head2 Type
1872
1873 Returns the current value of Type. 
1874 (In the database, Type is stored as varchar(200).)
1875
1876
1877
1878 =head2 SetType VALUE
1879
1880
1881 Set Type to VALUE. 
1882 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1883 (In the database, Type will be stored as a varchar(200).)
1884
1885
1886 =cut
1887
1888
1889 =head2 RenderType
1890
1891 Returns the current value of RenderType. 
1892 (In the database, RenderType is stored as varchar(64).)
1893
1894
1895
1896 =head2 SetRenderType VALUE
1897
1898
1899 Set RenderType to VALUE. 
1900 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1901 (In the database, RenderType will be stored as a varchar(64).)
1902
1903
1904 =cut
1905
1906
1907 =head2 MaxValues
1908
1909 Returns the current value of MaxValues. 
1910 (In the database, MaxValues is stored as int(11).)
1911
1912
1913
1914 =head2 SetMaxValues VALUE
1915
1916
1917 Set MaxValues to VALUE. 
1918 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1919 (In the database, MaxValues will be stored as a int(11).)
1920
1921
1922 =cut
1923
1924
1925 =head2 Pattern
1926
1927 Returns the current value of Pattern. 
1928 (In the database, Pattern is stored as text.)
1929
1930
1931
1932 =head2 SetPattern VALUE
1933
1934
1935 Set Pattern to VALUE. 
1936 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1937 (In the database, Pattern will be stored as a text.)
1938
1939
1940 =cut
1941
1942
1943 =head2 Repeated
1944
1945 Returns the current value of Repeated. 
1946 (In the database, Repeated is stored as smallint(6).)
1947
1948
1949
1950 =head2 SetRepeated VALUE
1951
1952
1953 Set Repeated to VALUE. 
1954 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1955 (In the database, Repeated will be stored as a smallint(6).)
1956
1957
1958 =cut
1959
1960
1961 =head2 BasedOn
1962
1963 Returns the current value of BasedOn. 
1964 (In the database, BasedOn is stored as int(11).)
1965
1966
1967
1968 =head2 SetBasedOn VALUE
1969
1970
1971 Set BasedOn to VALUE. 
1972 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1973 (In the database, BasedOn will be stored as a int(11).)
1974
1975
1976 =cut
1977
1978
1979 =head2 Description
1980
1981 Returns the current value of Description. 
1982 (In the database, Description is stored as varchar(255).)
1983
1984
1985
1986 =head2 SetDescription VALUE
1987
1988
1989 Set Description to VALUE. 
1990 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1991 (In the database, Description will be stored as a varchar(255).)
1992
1993
1994 =cut
1995
1996
1997 =head2 SortOrder
1998
1999 Returns the current value of SortOrder. 
2000 (In the database, SortOrder is stored as int(11).)
2001
2002
2003
2004 =head2 SetSortOrder VALUE
2005
2006
2007 Set SortOrder to VALUE. 
2008 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2009 (In the database, SortOrder will be stored as a int(11).)
2010
2011
2012 =cut
2013
2014
2015 =head2 LookupType
2016
2017 Returns the current value of LookupType. 
2018 (In the database, LookupType is stored as varchar(255).)
2019
2020
2021
2022 =head2 SetLookupType VALUE
2023
2024
2025 Set LookupType to VALUE. 
2026 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2027 (In the database, LookupType will be stored as a varchar(255).)
2028
2029
2030 =cut
2031
2032
2033 =head2 Creator
2034
2035 Returns the current value of Creator. 
2036 (In the database, Creator is stored as int(11).)
2037
2038
2039 =cut
2040
2041
2042 =head2 Created
2043
2044 Returns the current value of Created. 
2045 (In the database, Created is stored as datetime.)
2046
2047
2048 =cut
2049
2050
2051 =head2 LastUpdatedBy
2052
2053 Returns the current value of LastUpdatedBy. 
2054 (In the database, LastUpdatedBy is stored as int(11).)
2055
2056
2057 =cut
2058
2059
2060 =head2 LastUpdated
2061
2062 Returns the current value of LastUpdated. 
2063 (In the database, LastUpdated is stored as datetime.)
2064
2065
2066 =cut
2067
2068
2069 =head2 Disabled
2070
2071 Returns the current value of Disabled. 
2072 (In the database, Disabled is stored as smallint(6).)
2073
2074
2075
2076 =head2 SetDisabled VALUE
2077
2078
2079 Set Disabled to VALUE. 
2080 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2081 (In the database, Disabled will be stored as a smallint(6).)
2082
2083
2084 =cut
2085
2086
2087
2088 sub _CoreAccessible {
2089     {
2090      
2091         id =>
2092         {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2093         Name => 
2094         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2095         Type => 
2096         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2097         RenderType => 
2098         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2099         MaxValues => 
2100         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2101         Pattern => 
2102         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2103         Repeated => 
2104         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2105         ValuesClass => 
2106         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2107         BasedOn => 
2108         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2109         Description => 
2110         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2111         SortOrder => 
2112         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2113         LookupType => 
2114         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2115         Creator => 
2116         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2117         Created => 
2118         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2119         LastUpdatedBy => 
2120         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2121         LastUpdated => 
2122         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2123         Disabled => 
2124         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2125
2126  }
2127 };
2128
2129
2130 RT::Base->_ImportOverlays();
2131
2132 1;