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