avoid cloning TimeWorked and related fields on linked tickets, #20543
[freeside.git] / rt / lib / RT / CustomField.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 package RT::CustomField;
50
51 use strict;
52 use warnings;
53
54
55
56 use base 'RT::Record';
57
58 sub Table {'CustomFields'}
59
60
61 use RT::CustomFieldValues;
62 use RT::ObjectCustomFields;
63 use RT::ObjectCustomFieldValues;
64
65 our %FieldTypes = (
66     Select => {
67         sort_order => 10,
68         selection_type => 1,
69
70         labels => [ 'Select multiple values',      # loc
71                     'Select one value',            # loc
72                     'Select up to [_1] values',    # loc
73                   ],
74
75         render_types => {
76             multiple => [
77
78                 # Default is the first one
79                 'Select box',              # loc
80                 'List',                    # loc
81             ],
82             single => [ 'Select box',              # loc
83                         'Dropdown',                # loc
84                         'List',                    # loc
85                       ]
86         },
87
88     },
89     Freeform => {
90         sort_order => 20,
91         selection_type => 0,
92
93         labels => [ 'Enter multiple values',       # loc
94                     'Enter one value',             # loc
95                     'Enter up to [_1] values',     # loc
96                   ]
97                 },
98     Text => {
99         sort_order => 30,
100         selection_type => 0,
101         labels         => [
102                     'Fill in multiple text areas',      # loc
103                     'Fill in one text area',            # loc
104                     'Fill in up to [_1] text areas',    # loc
105                   ]
106             },
107     Wikitext => {
108         sort_order => 40,
109         selection_type => 0,
110         labels         => [
111                     'Fill in multiple wikitext areas',      # loc
112                     'Fill in one wikitext area',            # loc
113                     'Fill in up to [_1] wikitext areas',    # loc
114                   ]
115                 },
116
117     Image => {
118         sort_order => 50,
119         selection_type => 0,
120         labels         => [
121                     'Upload multiple images',               # loc
122                     'Upload one image',                     # loc
123                     'Upload up to [_1] images',             # loc
124                   ]
125              },
126     Binary => {
127         sort_order => 60,
128         selection_type => 0,
129         labels         => [
130                     'Upload multiple files',                # loc
131                     'Upload one file',                      # loc
132                     'Upload up to [_1] files',              # loc
133                   ]
134               },
135
136     Combobox => {
137         sort_order => 70,
138         selection_type => 1,
139         labels         => [
140                     'Combobox: Select or enter multiple values',      # loc
141                     'Combobox: Select or enter one value',            # loc
142                     'Combobox: Select or enter up to [_1] values',    # loc
143                   ]
144                 },
145     Autocomplete => {
146         sort_order => 80,
147         selection_type => 1,
148         labels         => [
149                     'Enter multiple values with autocompletion',      # loc
150                     'Enter one value with autocompletion',            # loc
151                     'Enter up to [_1] values with autocompletion',    # loc
152                   ]
153     },
154
155     Date => {
156         sort_order => 90,
157         selection_type => 0,
158         labels         => [
159                     'Select multiple dates',                          # loc
160                     'Select date',                                    # loc
161                     'Select up to [_1] dates',                        # loc
162                   ]
163             },
164     DateTime => {
165         sort_order => 100,
166         selection_type => 0,
167         labels         => [
168                     'Select multiple datetimes',                      # loc
169                     'Select datetime',                                # loc
170                     'Select up to [_1] datetimes',                    # loc
171                   ]
172                 },
173     TimeValue => {
174         sort_order => 105,
175         selection_type => 0,
176         labels         => [
177                      'Enter multiple time values (UNSUPPORTED)',
178                      'Enter a time value',
179                      'Enter [_1] time values (UNSUPPORTED)',
180                    ]
181                  },
182
183     IPAddress => {
184         sort_order => 110,
185         selection_type => 0,
186
187         labels => [ 'Enter multiple IP addresses',       # loc
188                     'Enter one IP address',             # loc
189                     'Enter up to [_1] IP addresses',     # loc
190                   ]
191                 },
192     IPAddressRange => {
193         sort_order => 120,
194         selection_type => 0,
195
196         labels => [ 'Enter multiple IP address ranges',       # loc
197                     'Enter one IP address range',             # loc
198                     'Enter up to [_1] IP address ranges',     # loc
199                   ]
200                 },
201 );
202
203
204 our %FRIENDLY_OBJECT_TYPES =  ();
205
206 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", );    #loc
207 RT::CustomField->_ForObjectType(
208     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", );    #loc
209 RT::CustomField->_ForObjectType( 'RT::User'  => "Users", );                           #loc
210 RT::CustomField->_ForObjectType( 'RT::Queue'  => "Queues", );                         #loc
211 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", );                          #loc
212
213 our $RIGHTS = {
214     SeeCustomField            => 'View custom fields',                                    # loc_pair
215     AdminCustomField          => 'Create, modify and delete custom fields',               # loc_pair
216     AdminCustomFieldValues    => 'Create, modify and delete custom fields values',        # loc_pair
217     ModifyCustomField         => 'Add, modify and delete custom field values for objects' # loc_pair
218 };
219
220 our $RIGHT_CATEGORIES = {
221     SeeCustomField          => 'General',
222     AdminCustomField        => 'Admin',
223     AdminCustomFieldValues  => 'Admin',
224     ModifyCustomField       => 'Staff',
225 };
226
227 # Tell RT::ACE that this sort of object can get acls granted
228 $RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
229
230 __PACKAGE__->AddRights(%$RIGHTS);
231 __PACKAGE__->AddRightCategories(%$RIGHT_CATEGORIES);
232
233 =head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
234
235 Adds the given rights to the list of possible rights.  This method
236 should be called during server startup, not at runtime.
237
238 =cut
239
240 sub AddRights {
241     my $self = shift;
242     my %new = @_;
243     $RIGHTS = { %$RIGHTS, %new };
244     %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
245                                       map { lc($_) => $_ } keys %new);
246 }
247
248 sub AvailableRights {
249     my $self = shift;
250     return $RIGHTS;
251 }
252
253 =head2 RightCategories
254
255 Returns a hashref where the keys are rights for this type of object and the
256 values are the category (General, Staff, Admin) the right falls into.
257
258 =cut
259
260 sub RightCategories {
261     return $RIGHT_CATEGORIES;
262 }
263
264 =head2 AddRightCategories C<RIGHT>, C<CATEGORY> [, ...]
265
266 Adds the given right and category pairs to the list of right categories.  This
267 method should be called during server startup, not at runtime.
268
269 =cut
270
271 sub AddRightCategories {
272     my $self = shift if ref $_[0] or $_[0] eq __PACKAGE__;
273     my %new = @_;
274     $RIGHT_CATEGORIES = { %$RIGHT_CATEGORIES, %new };
275 }
276
277 =head1 NAME
278
279   RT::CustomField_Overlay - overlay for RT::CustomField
280
281 =head1 DESCRIPTION
282
283 =head1 'CORE' METHODS
284
285 =head2 Create PARAMHASH
286
287 Create takes a hash of values and creates a row in the database:
288
289   varchar(200) 'Name'.
290   varchar(200) 'Type'.
291   int(11) 'MaxValues'.
292   varchar(255) 'Pattern'.
293   smallint(6) 'Repeated'.
294   varchar(255) 'Description'.
295   int(11) 'SortOrder'.
296   varchar(255) 'LookupType'.
297   smallint(6) 'Disabled'.
298
299 C<LookupType> is generally the result of either
300 C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
301
302 =cut
303
304 sub Create {
305     my $self = shift;
306     my %args = (
307         Name        => '',
308         Type        => '',
309         MaxValues   => 0,
310         Pattern     => '',
311         Description => '',
312         Disabled    => 0,
313         LookupType  => '',
314         Repeated    => 0,
315         LinkValueTo => '',
316         IncludeContentForValue => '',
317         @_,
318     );
319
320     unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
321         return (0, $self->loc('Permission Denied'));
322     }
323
324     if ( $args{TypeComposite} ) {
325         @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
326     }
327     elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
328         # old style Type string
329         $args{'MaxValues'} = $1 ? 1 : 0;
330     }
331     $args{'MaxValues'} = int $args{'MaxValues'};
332
333     if ( !exists $args{'Queue'}) {
334     # do nothing -- things below are strictly backward compat
335     }
336     elsif (  ! $args{'Queue'} ) {
337         unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
338             return ( 0, $self->loc('Permission Denied') );
339         }
340         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
341     }
342     else {
343         my $queue = RT::Queue->new($self->CurrentUser);
344         $queue->Load($args{'Queue'});
345         unless ($queue->Id) {
346             return (0, $self->loc("Queue not found"));
347         }
348         unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
349             return ( 0, $self->loc('Permission Denied') );
350         }
351         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
352         $args{'Queue'} = $queue->Id;
353     }
354
355     my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
356     return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
357
358     if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
359         $RT::Logger->debug("Support for 'multiple' Texts or Comboboxes is not implemented");
360         $args{'MaxValues'} = 1;
361     }
362
363     if ( $args{'RenderType'} ||= undef ) {
364         my $composite = join '-', @args{'Type', 'MaxValues'};
365         return (0, $self->loc("This custom field has no Render Types"))
366             unless $self->HasRenderTypes( $composite );
367
368         if ( $args{'RenderType'} eq $self->DefaultRenderType( $composite ) ) {
369             $args{'RenderType'} = undef;
370         } else {
371             return (0, $self->loc("Invalid Render Type") )
372                 unless grep $_ eq  $args{'RenderType'}, $self->RenderTypes( $composite );
373         }
374     }
375
376     $args{'ValuesClass'} = undef if ($args{'ValuesClass'} || '') eq 'RT::CustomFieldValues';
377     if ( $args{'ValuesClass'} ||= undef ) {
378         return (0, $self->loc("This Custom Field can not have list of values"))
379             unless $self->IsSelectionType( $args{'Type'} );
380
381         unless ( $self->ValidateValuesClass( $args{'ValuesClass'} ) ) {
382             return (0, $self->loc("Invalid Custom Field values source"));
383         }
384     }
385
386     (my $rv, $msg) = $self->SUPER::Create(
387         Name        => $args{'Name'},
388         Type        => $args{'Type'},
389         RenderType  => $args{'RenderType'},
390         MaxValues   => $args{'MaxValues'},
391         Pattern     => $args{'Pattern'},
392         BasedOn     => $args{'BasedOn'},
393         ValuesClass => $args{'ValuesClass'},
394         Description => $args{'Description'},
395         Disabled    => $args{'Disabled'},
396         LookupType  => $args{'LookupType'},
397         Repeated    => $args{'Repeated'},
398     );
399
400     if ($rv) {
401         if ( exists $args{'LinkValueTo'}) {
402             $self->SetLinkValueTo($args{'LinkValueTo'});
403         }
404
405         if ( exists $args{'IncludeContentForValue'}) {
406             $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
407         }
408
409         if ( exists $args{'UILocation'} ) {
410             $self->SetUILocation( $args{'UILocation'} );
411         }
412
413         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 !defined $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                    Timezone => 'UTC',
1560                  );
1561     $args->{'Content'} = $DateObj->Date( Timezone => 'UTC' );
1562 }
1563
1564 =head2 MatchPattern STRING
1565
1566 Tests the incoming string against the Pattern of this custom field object
1567 and returns a boolean; returns true if the Pattern is empty.
1568
1569 =cut
1570
1571 sub MatchPattern {
1572     my $self = shift;
1573     my $regex = $self->Pattern or return 1;
1574
1575     return (( defined $_[0] ? $_[0] : '') =~ $regex);
1576 }
1577
1578
1579
1580
1581 =head2 FriendlyPattern
1582
1583 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1584 and localizing it.
1585
1586 =cut
1587
1588 sub FriendlyPattern {
1589     my $self = shift;
1590     my $regex = $self->Pattern;
1591
1592     return '' unless length $regex;
1593     if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1594         return '[' . $self->loc($1) . ']';
1595     }
1596     else {
1597         return $regex;
1598     }
1599 }
1600
1601
1602
1603
1604 =head2 DeleteValueForObject HASH
1605
1606 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1607
1608 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1609
1610 =cut
1611
1612 sub DeleteValueForObject {
1613     my $self = shift;
1614     my %args = ( Object => undef,
1615                  Content => undef,
1616                  Id => undef,
1617              @_ );
1618
1619
1620     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1621         return (0, $self->loc('Permission Denied'));
1622     }
1623
1624     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1625
1626     if (my $id = $args{'Id'}) {
1627         $oldval->Load($id);
1628     }
1629     unless ($oldval->id) { 
1630         $oldval->LoadByObjectContentAndCustomField(
1631             Object => $args{'Object'}, 
1632             Content =>  $args{'Content'}, 
1633             CustomField => $self->Id,
1634         );
1635     }
1636
1637
1638     # check to make sure we found it
1639     unless ($oldval->Id) {
1640         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1641     }
1642
1643     # for single-value fields, we need to validate that empty string is a valid value for it
1644     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1645         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1646     }
1647
1648     # delete it
1649
1650     my $ret = $oldval->Delete();
1651     unless ($ret) {
1652         return(0, $self->loc("Custom field value could not be found"));
1653     }
1654     return($oldval->Id, $self->loc("Custom field value deleted"));
1655 }
1656
1657
1658 =head2 ValuesForObject OBJECT
1659
1660 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT 
1661
1662 =cut
1663
1664 sub ValuesForObject {
1665     my $self = shift;
1666     my $object = shift;
1667
1668     my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
1669     unless ($self->CurrentUserHasRight('SeeCustomField')) {
1670         # Return an empty object if they have no rights to see
1671         return ($values);
1672     }
1673     
1674     
1675     $values->LimitToCustomField($self->Id);
1676     $values->LimitToEnabled();
1677     $values->LimitToObject($object);
1678
1679     return ($values);
1680 }
1681
1682
1683 =head2 _ForObjectType PATH FRIENDLYNAME
1684
1685 Tell RT that a certain object accepts custom fields
1686
1687 Examples:
1688
1689     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1690     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1691     'RT::User'                             => "Users",                  # loc
1692     'RT::Group'                            => "Groups",                 # loc
1693
1694 This is a class method. 
1695
1696 =cut
1697
1698 sub _ForObjectType {
1699     my $self = shift;
1700     my $path = shift;
1701     my $friendly_name = shift;
1702
1703     $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1704
1705 }
1706
1707
1708 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1709
1710 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1711 uses this field to automatically include content into the user's browser
1712 as they display records with custom fields in RT.
1713
1714 =cut
1715
1716 sub SetIncludeContentForValue {
1717     shift->IncludeContentForValue(@_);
1718 }
1719 sub IncludeContentForValue{
1720     my $self = shift;
1721     $self->_URLTemplate('IncludeContentForValue', @_);
1722 }
1723
1724
1725
1726 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1727
1728 Gets or sets the  C<LinkValueTo> for this custom field. RT
1729 uses this field to make custom field values into hyperlinks in the user's
1730 browser as they display records with custom fields in RT.
1731
1732 =cut
1733
1734
1735 sub SetLinkValueTo {
1736     shift->LinkValueTo(@_);
1737 }
1738
1739 sub LinkValueTo {
1740     my $self = shift;
1741     $self->_URLTemplate('LinkValueTo', @_);
1742
1743 }
1744
1745
1746 =head2 _URLTemplate  NAME [VALUE]
1747
1748 With one argument, returns the _URLTemplate named C<NAME>, but only if
1749 the current user has the right to see this custom field.
1750
1751 With two arguments, attemptes to set the relevant template value.
1752
1753 =cut
1754
1755 sub _URLTemplate {
1756     my $self          = shift;
1757     my $template_name = shift;
1758     if (@_) {
1759
1760         my $value = shift;
1761         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1762             return ( 0, $self->loc('Permission Denied') );
1763         }
1764         $self->SetAttribute( Name => $template_name, Content => $value );
1765         return ( 1, $self->loc('Updated') );
1766     } else {
1767         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1768             return (undef);
1769         }
1770
1771         my @attr = $self->Attributes->Named($template_name);
1772         my $attr = shift @attr;
1773
1774         if ($attr) { return $attr->Content }
1775
1776     }
1777 }
1778
1779 sub SetBasedOn {
1780     my $self = shift;
1781     my $value = shift;
1782
1783     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1784         unless defined $value and length $value;
1785
1786     my $cf = RT::CustomField->new( $self->CurrentUser );
1787     $cf->SetContextObject( $self->ContextObject );
1788     $cf->Load( ref $value ? $value->id : $value );
1789
1790     return (0, "Permission denied")
1791         unless $cf->id && $cf->CurrentUserHasRight('SeeCustomField');
1792
1793     # XXX: Remove this restriction once we support lists and cascaded selects
1794     if ( $self->RenderType =~ /List/ ) {
1795         return (0, $self->loc("We can't currently render as a List when basing categories on another custom field.  Please use another render type."));
1796     }
1797
1798     return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
1799 }
1800
1801 sub BasedOnObj {
1802     my $self = shift;
1803
1804     my $obj = RT::CustomField->new( $self->CurrentUser );
1805     $obj->SetContextObject( $self->ContextObject );
1806     if ( $self->BasedOn ) {
1807         $obj->Load( $self->BasedOn );
1808     }
1809     return $obj;
1810 }
1811
1812 sub UILocation {
1813     my $self = shift;
1814     my $tag = $self->FirstAttribute( 'UILocation' );
1815     return $tag ? $tag->Content : '';
1816 }
1817
1818 sub SetUILocation {
1819     my $self = shift;
1820     my $tag = shift;
1821     if ( $tag ) {
1822         return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1823     }
1824     else {
1825         return $self->DeleteAttribute('UILocation');
1826     }
1827 }
1828
1829 sub NoClone {
1830     my $self = shift;
1831     $self->FirstAttribute('NoClone') ? 1 : '';
1832 }
1833
1834 sub SetNoClone {
1835     my $self = shift;
1836     my $value = shift;
1837     if ( $value ) {
1838         return $self->SetAttribute( Name => 'NoClone', Content => 1 );
1839     } else {
1840         return $self->DeleteAttribute('NoClone');
1841     }
1842 }
1843
1844
1845 =head2 id
1846
1847 Returns the current value of id. 
1848 (In the database, id is stored as int(11).)
1849
1850
1851 =cut
1852
1853
1854 =head2 Name
1855
1856 Returns the current value of Name. 
1857 (In the database, Name is stored as varchar(200).)
1858
1859
1860
1861 =head2 SetName VALUE
1862
1863
1864 Set Name to VALUE. 
1865 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1866 (In the database, Name will be stored as a varchar(200).)
1867
1868
1869 =cut
1870
1871
1872 =head2 Type
1873
1874 Returns the current value of Type. 
1875 (In the database, Type is stored as varchar(200).)
1876
1877
1878
1879 =head2 SetType VALUE
1880
1881
1882 Set Type to VALUE. 
1883 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1884 (In the database, Type will be stored as a varchar(200).)
1885
1886
1887 =cut
1888
1889
1890 =head2 RenderType
1891
1892 Returns the current value of RenderType. 
1893 (In the database, RenderType is stored as varchar(64).)
1894
1895
1896
1897 =head2 SetRenderType VALUE
1898
1899
1900 Set RenderType to VALUE. 
1901 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1902 (In the database, RenderType will be stored as a varchar(64).)
1903
1904
1905 =cut
1906
1907
1908 =head2 MaxValues
1909
1910 Returns the current value of MaxValues. 
1911 (In the database, MaxValues is stored as int(11).)
1912
1913
1914
1915 =head2 SetMaxValues VALUE
1916
1917
1918 Set MaxValues to VALUE. 
1919 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1920 (In the database, MaxValues will be stored as a int(11).)
1921
1922
1923 =cut
1924
1925
1926 =head2 Pattern
1927
1928 Returns the current value of Pattern. 
1929 (In the database, Pattern is stored as text.)
1930
1931
1932
1933 =head2 SetPattern VALUE
1934
1935
1936 Set Pattern to VALUE. 
1937 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1938 (In the database, Pattern will be stored as a text.)
1939
1940
1941 =cut
1942
1943
1944 =head2 Repeated
1945
1946 Returns the current value of Repeated. 
1947 (In the database, Repeated is stored as smallint(6).)
1948
1949
1950
1951 =head2 SetRepeated VALUE
1952
1953
1954 Set Repeated to VALUE. 
1955 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1956 (In the database, Repeated will be stored as a smallint(6).)
1957
1958
1959 =cut
1960
1961
1962 =head2 BasedOn
1963
1964 Returns the current value of BasedOn. 
1965 (In the database, BasedOn is stored as int(11).)
1966
1967
1968
1969 =head2 SetBasedOn VALUE
1970
1971
1972 Set BasedOn to VALUE. 
1973 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1974 (In the database, BasedOn will be stored as a int(11).)
1975
1976
1977 =cut
1978
1979
1980 =head2 Description
1981
1982 Returns the current value of Description. 
1983 (In the database, Description is stored as varchar(255).)
1984
1985
1986
1987 =head2 SetDescription VALUE
1988
1989
1990 Set Description to VALUE. 
1991 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
1992 (In the database, Description will be stored as a varchar(255).)
1993
1994
1995 =cut
1996
1997
1998 =head2 SortOrder
1999
2000 Returns the current value of SortOrder. 
2001 (In the database, SortOrder is stored as int(11).)
2002
2003
2004
2005 =head2 SetSortOrder VALUE
2006
2007
2008 Set SortOrder to VALUE. 
2009 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2010 (In the database, SortOrder will be stored as a int(11).)
2011
2012
2013 =cut
2014
2015
2016 =head2 LookupType
2017
2018 Returns the current value of LookupType. 
2019 (In the database, LookupType is stored as varchar(255).)
2020
2021
2022
2023 =head2 SetLookupType VALUE
2024
2025
2026 Set LookupType to VALUE. 
2027 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2028 (In the database, LookupType will be stored as a varchar(255).)
2029
2030
2031 =cut
2032
2033
2034 =head2 Creator
2035
2036 Returns the current value of Creator. 
2037 (In the database, Creator is stored as int(11).)
2038
2039
2040 =cut
2041
2042
2043 =head2 Created
2044
2045 Returns the current value of Created. 
2046 (In the database, Created is stored as datetime.)
2047
2048
2049 =cut
2050
2051
2052 =head2 LastUpdatedBy
2053
2054 Returns the current value of LastUpdatedBy. 
2055 (In the database, LastUpdatedBy is stored as int(11).)
2056
2057
2058 =cut
2059
2060
2061 =head2 LastUpdated
2062
2063 Returns the current value of LastUpdated. 
2064 (In the database, LastUpdated is stored as datetime.)
2065
2066
2067 =cut
2068
2069
2070 =head2 Disabled
2071
2072 Returns the current value of Disabled. 
2073 (In the database, Disabled is stored as smallint(6).)
2074
2075
2076
2077 =head2 SetDisabled VALUE
2078
2079
2080 Set Disabled to VALUE. 
2081 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2082 (In the database, Disabled will be stored as a smallint(6).)
2083
2084
2085 =cut
2086
2087
2088
2089 sub _CoreAccessible {
2090     {
2091      
2092         id =>
2093         {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2094         Name => 
2095         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2096         Type => 
2097         {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2098         RenderType => 
2099         {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2100         MaxValues => 
2101         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2102         Pattern => 
2103         {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2104         Repeated => 
2105         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2106         BasedOn => 
2107         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2108         Description => 
2109         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2110         SortOrder => 
2111         {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2112         LookupType => 
2113         {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2114         Creator => 
2115         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2116         Created => 
2117         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2118         LastUpdatedBy => 
2119         {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2120         LastUpdated => 
2121         {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2122         Disabled => 
2123         {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2124
2125  }
2126 };
2127
2128
2129 RT::Base->_ImportOverlays();
2130
2131 1;