c6fa1851f5321723dcb652fd9422a148acbf8e9c
[freeside.git] / rt / lib / RT / CustomField_Overlay.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2011 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 no warnings qw(redefine);
53
54 use RT::CustomFieldValues;
55 use RT::ObjectCustomFields;
56 use RT::ObjectCustomFieldValues;
57
58
59 our %FieldTypes = (
60     Select => [
61         'Select multiple values',    # loc
62         'Select one value',        # loc
63         'Select up to [_1] values',    # loc
64     ],
65     Freeform => [
66         'Enter multiple values',    # loc
67         'Enter one value',        # loc
68         'Enter up to [_1] values',    # loc
69     ],
70     Text => [
71         'Fill in multiple text areas',    # loc
72         'Fill in one text area',    # loc
73         'Fill in up to [_1] text areas',# loc
74     ],
75     Wikitext => [
76         'Fill in multiple wikitext areas',    # loc
77         'Fill in one wikitext area',    # loc
78         'Fill in up to [_1] wikitext areas',# loc
79     ],
80     Image => [
81         'Upload multiple images',    # loc
82         'Upload one image',        # loc
83         'Upload up to [_1] images',    # loc
84     ],
85     Binary => [
86         'Upload multiple files',    # loc
87         'Upload one file',        # loc
88         'Upload up to [_1] files',    # loc
89     ],
90     Combobox => [
91         'Combobox: Select or enter multiple values',    # loc
92         'Combobox: Select or enter one value',        # loc
93         'Combobox: Select or enter up to [_1] values',    # loc
94     ],
95     Autocomplete => [
96         'Enter multiple values with autocompletion',    # loc
97         'Enter one value with autocompletion',            # loc
98         'Enter up to [_1] values with autocompletion',    # loc
99     ],
100     Date => [
101         'Select multiple dates',        # loc
102         'Select date',                  # loc
103         'Select up to [_1] dates',      # loc
104     ],
105     TimeValue => [
106         'Enter multiple time values (UNSUPPORTED)',
107         'Enter a time value',
108         'Enter [_1] time values (UNSUPPORTED)',
109     ],
110 );
111
112
113 our %FRIENDLY_OBJECT_TYPES =  ();
114
115 RT::CustomField->_ForObjectType( 'RT::Queue-RT::Ticket' => "Tickets", );    #loc
116 RT::CustomField->_ForObjectType(
117     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", );    #loc
118 RT::CustomField->_ForObjectType( 'RT::User'  => "Users", );                           #loc
119 RT::CustomField->_ForObjectType( 'RT::Queue'  => "Queues", );                         #loc
120 RT::CustomField->_ForObjectType( 'RT::Group' => "Groups", );                          #loc
121
122 our $RIGHTS = {
123     SeeCustomField            => 'See custom fields',       # loc_pair
124     AdminCustomField          => 'Create, delete and modify custom fields',        # loc_pair
125     AdminCustomFieldValues    => 'Create, delete and modify custom fields values',        # loc_pair
126     ModifyCustomField         => 'Add, delete and modify custom field values for objects' #loc_pair
127 };
128
129 # Tell RT::ACE that this sort of object can get acls granted
130 $RT::ACE::OBJECT_TYPES{'RT::CustomField'} = 1;
131
132 foreach my $right ( keys %{$RIGHTS} ) {
133     $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right;
134 }
135
136 =head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...]
137
138 Adds the given rights to the list of possible rights.  This method
139 should be called during server startup, not at runtime.
140
141 =cut
142
143 sub AddRights {
144     my $self = shift;
145     my %new = @_;
146     $RIGHTS = { %$RIGHTS, %new };
147     %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES,
148                                       map { lc($_) => $_ } keys %new);
149 }
150
151 sub AvailableRights {
152     my $self = shift;
153     return $RIGHTS;
154 }
155
156 =head1 NAME
157
158   RT::CustomField_Overlay - overlay for RT::CustomField
159
160 =head1 DESCRIPTION
161
162 =head1 'CORE' METHODS
163
164 =head2 Create PARAMHASH
165
166 Create takes a hash of values and creates a row in the database:
167
168   varchar(200) 'Name'.
169   varchar(200) 'Type'.
170   int(11) 'MaxValues'.
171   varchar(255) 'Pattern'.
172   smallint(6) 'Repeated'.
173   varchar(255) 'Description'.
174   int(11) 'SortOrder'.
175   varchar(255) 'LookupType'.
176   smallint(6) 'Disabled'.
177
178 C<LookupType> is generally the result of either
179 C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
180
181 =cut
182
183 sub Create {
184     my $self = shift;
185     my %args = (
186         Name        => '',
187         Type        => '',
188         MaxValues   => 0,
189         Pattern     => '',
190         Description => '',
191         Disabled    => 0,
192         LookupType  => '',
193         Repeated    => 0,
194         LinkValueTo => '',
195         IncludeContentForValue => '',
196         @_,
197     );
198
199     unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
200         return (0, $self->loc('Permission Denied'));
201     }
202
203     if ( $args{TypeComposite} ) {
204         @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
205     }
206     elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
207         # old style Type string
208         $args{'MaxValues'} = $1 ? 1 : 0;
209     }
210     $args{'MaxValues'} = int $args{'MaxValues'};
211
212     if ( !exists $args{'Queue'}) {
213     # do nothing -- things below are strictly backward compat
214     }
215     elsif (  ! $args{'Queue'} ) {
216         unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
217             return ( 0, $self->loc('Permission Denied') );
218         }
219         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
220     }
221     else {
222         my $queue = RT::Queue->new($self->CurrentUser);
223         $queue->Load($args{'Queue'});
224         unless ($queue->Id) {
225             return (0, $self->loc("Queue not found"));
226         }
227         unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
228             return ( 0, $self->loc('Permission Denied') );
229         }
230         $args{'LookupType'} = 'RT::Queue-RT::Ticket';
231         $args{'Queue'} = $queue->Id;
232     }
233
234     my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
235     return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
236
237     if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
238         $RT::Logger->warning("Support for 'multiple' Texts or Comboboxes is not implemented");
239         $args{'MaxValues'} = 1;
240     }
241
242     (my $rv, $msg) = $self->SUPER::Create(
243         Name        => $args{'Name'},
244         Type        => $args{'Type'},
245         MaxValues   => $args{'MaxValues'},
246         Pattern     => $args{'Pattern'},
247         Description => $args{'Description'},
248         Disabled    => $args{'Disabled'},
249         LookupType  => $args{'LookupType'},
250         Repeated    => $args{'Repeated'},
251     );
252
253     if ( exists $args{'LinkValueTo'}) {
254         $self->SetLinkValueTo($args{'LinkValueTo'});
255     }
256
257     if ( exists $args{'IncludeContentForValue'}) {
258         $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
259     }
260
261     if ( exists $args{'ValuesClass'} ) {
262         $self->SetValuesClass( $args{'ValuesClass'} );
263     }
264
265     if ( exists $args{'BasedOn'} ) {
266         $self->SetBasedOn( $args{'BasedOn'} );
267     }
268
269     if ( exists $args{'UILocation'} ) {
270         $self->SetUILocation( $args{'UILocation'} );
271     }
272
273     return ($rv, $msg) unless exists $args{'Queue'};
274
275     # Compat code -- create a new ObjectCustomField mapping
276     my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
277     $OCF->Create(
278         CustomField => $self->Id,
279         ObjectId => $args{'Queue'},
280     );
281
282     return ($rv, $msg);
283 }
284
285 =head2 Load ID/NAME
286
287 Load a custom field.  If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
288
289 =cut
290
291 sub Load {
292     my $self = shift;
293     my $id = shift || '';
294
295     if ( $id =~ /^\d+$/ ) {
296         return $self->SUPER::Load( $id );
297     } else {
298         return $self->LoadByName( Name => $id );
299     }
300 }
301
302
303 # {{{ sub LoadByName
304
305 =head2 LoadByName (Queue => QUEUEID, Name => NAME)
306
307 Loads the Custom field named NAME.
308
309 Will load a Disabled Custom Field even if there is a non-disabled Custom Field
310 with the same Name.
311
312 If a Queue parameter is specified, only look for ticket custom fields tied to that Queue.
313
314 If the Queue parameter is '0', look for global ticket custom fields.
315
316 If no queue parameter is specified, look for any and all custom fields with this name.
317
318 BUG/TODO, this won't let you specify that you only want user or group CFs.
319
320 =cut
321
322 # Compatibility for API change after 3.0 beta 1
323 *LoadNameAndQueue = \&LoadByName;
324 # Change after 3.4 beta.
325 *LoadByNameAndQueue = \&LoadByName;
326
327 sub LoadByName {
328     my $self = shift;
329     my %args = (
330         Queue => undef,
331         Name  => undef,
332         @_,
333     );
334
335     unless ( defined $args{'Name'} && length $args{'Name'} ) {
336         $RT::Logger->error("Couldn't load Custom Field without Name");
337         return wantarray ? (0, $self->loc("No name provided")) : 0;
338     }
339
340     # if we're looking for a queue by name, make it a number
341     if ( defined $args{'Queue'} && ($args{'Queue'} =~ /\D/ || !$self->ContextObject) ) {
342         my $QueueObj = RT::Queue->new( $self->CurrentUser );
343         $QueueObj->Load( $args{'Queue'} );
344         $args{'Queue'} = $QueueObj->Id;
345         $self->SetContextObject( $QueueObj )
346             unless $self->ContextObject;
347     }
348
349     # XXX - really naive implementation.  Slow. - not really. still just one query
350
351     my $CFs = RT::CustomFields->new( $self->CurrentUser );
352     $CFs->SetContextObject( $self->ContextObject );
353     my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
354     $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
355     # Don't limit to queue if queue is 0.  Trying to do so breaks
356     # RT::Group type CFs.
357     if ( defined $args{'Queue'} ) {
358         $CFs->LimitToQueue( $args{'Queue'} );
359     }
360
361     # When loading by name, we _can_ load disabled fields, but prefer
362     # non-disabled fields.
363     $CFs->FindAllRows;
364     $CFs->OrderByCols(
365         { FIELD => "Disabled", ORDER => 'ASC' },
366     );
367
368     # We only want one entry.
369     $CFs->RowsPerPage(1);
370
371     # version before 3.8 just returns 0, so we need to test if wantarray to be
372     # backward compatible.
373     return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
374
375     return $self->LoadById( $first->id );
376 }
377
378 # }}}
379
380 # {{{ Dealing with custom field values 
381
382
383 =head2 Custom field values
384
385 =head3 Values FIELD
386
387 Return a object (collection) of all acceptable values for this Custom Field.
388 Class of the object can vary and depends on the return value
389 of the C<ValuesClass> method.
390
391 =cut
392
393 *ValuesObj = \&Values;
394
395 sub Values {
396     my $self = shift;
397
398     my $class = $self->ValuesClass || 'RT::CustomFieldValues';
399     eval "require $class" or die "$@";
400     my $cf_values = $class->new( $self->CurrentUser );
401     # if the user has no rights, return an empty object
402     if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) {
403         $cf_values->LimitToCustomField( $self->Id );
404     } else {
405         $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
406     }
407     return ($cf_values);
408 }
409
410 # {{{ AddValue
411
412 =head3 AddValue HASH
413
414 Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
415
416 =cut
417
418 sub AddValue {
419     my $self = shift;
420     my %args = @_;
421
422     unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
423         return (0, $self->loc('Permission Denied'));
424     }
425
426     # allow zero value
427     if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
428         return (0, $self->loc("Can't add a custom field value without a name"));
429     }
430
431     my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
432     return $newval->Create( %args, CustomField => $self->Id );
433 }
434
435
436 # }}}
437
438 # {{{ DeleteValue
439
440 =head3 DeleteValue ID
441
442 Deletes a value from this custom field by id.
443
444 Does not remove this value for any article which has had it selected
445
446 =cut
447
448 sub DeleteValue {
449     my $self = shift;
450     my $id = shift;
451     unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
452         return (0, $self->loc('Permission Denied'));
453     }
454
455     my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
456     $val_to_del->Load( $id );
457     unless ( $val_to_del->Id ) {
458         return (0, $self->loc("Couldn't find that value"));
459     }
460     unless ( $val_to_del->CustomField == $self->Id ) {
461         return (0, $self->loc("That is not a value for this custom field"));
462     }
463
464     my $retval = $val_to_del->Delete;
465     unless ( $retval ) {
466         return (0, $self->loc("Custom field value could not be deleted"));
467     }
468     return ($retval, $self->loc("Custom field value deleted"));
469 }
470
471 # }}}
472
473
474 =head2 ValidateQueue Queue
475
476 Make sure that the queue specified is a valid queue name
477
478 =cut
479
480 sub ValidateQueue {
481     my $self = shift;
482     my $id = shift;
483
484     return undef unless defined $id;
485     # 0 means "Global" null would _not_ be ok.
486     return 1 if $id eq '0';
487
488     my $q = RT::Queue->new( $RT::SystemUser );
489     $q->Load( $id );
490     return undef unless $q->id;
491     return 1;
492 }
493
494
495 # {{{ Types
496
497 =head2 Types 
498
499 Retuns an array of the types of CustomField that are supported
500
501 =cut
502
503 sub Types {
504     return (keys %FieldTypes);
505 }
506
507 # }}}
508
509 # {{{ IsSelectionType
510
511 =head2 IsSelectionType 
512
513 Retuns a boolean value indicating whether the C<Values> method makes sense
514 to this Custom Field.
515
516 =cut
517
518 sub IsSelectionType {
519     my $self = shift;
520     my $type = @_? shift : $self->Type;
521     return undef unless $type;
522
523     $type =~ /(?:Select|Combobox|Autocomplete)/;
524 }
525
526 # }}}
527
528
529 =head2 IsExternalValues
530
531 =cut
532
533 sub IsExternalValues {
534     my $self = shift;
535     my $selectable = $self->IsSelectionType( @_ );
536     return $selectable unless $selectable;
537
538     my $class = $self->ValuesClass;
539     return 0 if $class eq 'RT::CustomFieldValues';
540     return 1;
541 }
542
543 sub ValuesClass {
544     my $self = shift;
545     return '' unless $self->IsSelectionType;
546
547     my $class = $self->FirstAttribute( 'ValuesClass' );
548     $class = $class->Content if $class;
549     return $class || 'RT::CustomFieldValues';
550 }
551
552 sub SetValuesClass {
553     my $self = shift;
554     my $class = shift || 'RT::CustomFieldValues';
555
556     if( $class eq 'RT::CustomFieldValues' ) {
557         return $self->DeleteAttribute( 'ValuesClass' );
558     }
559     return $self->SetAttribute( Name => 'ValuesClass', Content => $class );
560 }
561
562
563 =head2 FriendlyType [TYPE, MAX_VALUES]
564
565 Returns a localized human-readable version of the custom field type.
566 If a custom field type is specified as the parameter, the friendly type for that type will be returned
567
568 =cut
569
570 sub FriendlyType {
571     my $self = shift;
572
573     my $type = @_ ? shift : $self->Type;
574     my $max  = @_ ? shift : $self->MaxValues;
575     $max = 0 unless $max;
576
577     if (my $friendly_type = $FieldTypes{$type}[$max>2 ? 2 : $max]) {
578         return ( $self->loc( $friendly_type, $max ) );
579     }
580     else {
581         return ( $self->loc( $type ) );
582     }
583 }
584
585 sub FriendlyTypeComposite {
586     my $self = shift;
587     my $composite = shift || $self->TypeComposite;
588     return $self->FriendlyType(split(/-/, $composite, 2));
589 }
590
591
592 =head2 ValidateType TYPE
593
594 Takes a single string. returns true if that string is a value
595 type of custom field
596
597
598 =cut
599
600 sub ValidateType {
601     my $self = shift;
602     my $type = shift;
603
604     if ( $type =~ s/(?:Single|Multiple)$// ) {
605         $RT::Logger->warning( "Prefix 'Single' and 'Multiple' to Type deprecated, use MaxValues instead at (". join(":",caller).")");
606     }
607
608     if ( $FieldTypes{$type} ) {
609         return 1;
610     }
611     else {
612         return undef;
613     }
614 }
615
616
617 sub SetType {
618     my $self = shift;
619     my $type = shift;
620     if ($type =~ s/(?:(Single)|Multiple)$//) {
621         $RT::Logger->warning("'Single' and 'Multiple' on SetType deprecated, use SetMaxValues instead at (". join(":",caller).")");
622         $self->SetMaxValues($1 ? 1 : 0);
623     }
624     $self->SUPER::SetType($type);
625 }
626
627 =head2 SetPattern STRING
628
629 Takes a single string representing a regular expression.  Performs basic
630 validation on that regex, and sets the C<Pattern> field for the CF if it
631 is valid.
632
633 =cut
634
635 sub SetPattern {
636     my $self = shift;
637     my $regex = shift;
638
639     my ($ok, $msg) = $self->_IsValidRegex($regex);
640     if ($ok) {
641         return $self->SUPER::SetPattern($regex);
642     }
643     else {
644         return (0, $self->loc("Invalid pattern: [_1]", $msg));
645     }
646 }
647
648 =head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
649
650 Tests if the string contains an invalid regex.
651
652 =cut
653
654 sub _IsValidRegex {
655     my $self  = shift;
656     my $regex = shift or return (1, 'valid');
657
658     local $^W; local $@;
659     local $SIG{__DIE__} = sub { 1 };
660     local $SIG{__WARN__} = sub { 1 };
661
662     if (eval { qr/$regex/; 1 }) {
663         return (1, 'valid');
664     }
665
666     my $err = $@;
667     $err =~ s{[,;].*}{};    # strip debug info from error
668     chomp $err;
669     return (0, $err);
670 }
671
672 # {{{ SingleValue
673
674 =head2 SingleValue
675
676 Returns true if this CustomField only accepts a single value. 
677 Returns false if it accepts multiple values
678
679 =cut
680
681 sub SingleValue {
682     my $self = shift;
683     if (($self->MaxValues||0) == 1) {
684         return 1;
685     } 
686     else {
687         return undef;
688     }
689 }
690
691 sub UnlimitedValues {
692     my $self = shift;
693     if (($self->MaxValues||0) == 0) {
694         return 1;
695     } 
696     else {
697         return undef;
698     }
699 }
700
701 # }}}
702
703 =head2 CurrentUserHasRight RIGHT
704
705 Helper function to call the custom field's queue's CurrentUserHasRight with the passed in args.
706
707 =cut
708
709 sub CurrentUserHasRight {
710     my $self  = shift;
711     my $right = shift;
712
713     return $self->CurrentUser->HasRight(
714         Object => $self,
715         Right  => $right,
716     );
717 }
718
719 =head2 ACLEquivalenceObjects
720
721 Returns list of objects via which users can get rights on this custom field. For custom fields
722 these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
723
724 =cut
725
726 sub ACLEquivalenceObjects {
727     my $self = shift;
728
729     my $ctx = $self->ContextObject
730         or return;
731     return ($ctx, $ctx->ACLEquivalenceObjects);
732 }
733
734 =head2 ContextObject and SetContextObject
735
736 Set or get a context for this object. It can be ticket, queue or another object
737 this CF applies to. Used for ACL control, for example SeeCustomField can be granted on
738 queue level to allow people to see all fields applied to the queue.
739
740 =cut
741
742 sub SetContextObject {
743     my $self = shift;
744     return $self->{'context_object'} = shift;
745 }
746   
747 sub ContextObject {
748     my $self = shift;
749     return $self->{'context_object'};
750 }
751
752 sub ValidContextType {
753     my $self = shift;
754     my $class = shift;
755
756     my %valid;
757     $valid{$_}++ for split '-', $self->LookupType;
758     delete $valid{'RT::Transaction'};
759
760     return $valid{$class};
761 }
762
763 =head2 LoadContextObject
764
765 Takes an Id for a Context Object and loads the right kind of RT::Object
766 for this particular Custom Field (based on the LookupType) and returns it.
767 This is a good way to ensure you don't try to use a Queue as a Context
768 Object on a User Custom Field.
769
770 =cut
771
772 sub LoadContextObject {
773     my $self = shift;
774     my $type = shift;
775     my $contextid = shift;
776
777     unless ( $self->ValidContextType($type) ) {
778         RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id);
779         return;
780     }
781
782     my $context_object = $type->new( $self->CurrentUser );
783     my ($id, $msg) = $context_object->LoadById( $contextid );
784     unless ( $id ) {
785         RT->Logger->debug("Invalid ContextObject id: $msg");
786         return;
787     }
788     return $context_object;
789 }
790
791 =head2 ValidateContextObject
792
793 Ensure that a given ContextObject applies to this Custom Field.
794 For custom fields that are assigned to Queues or to Classes, this checks that the Custom
795 Field is actually applied to that objects.  For Global Custom Fields, it returns true
796 as long as the Object is of the right type, because you may be using
797 your permissions on a given Queue of Class to see a Global CF.
798 For CFs that are only applied Globally, you don't need a ContextObject.
799
800 =cut
801
802 sub ValidateContextObject {
803     my $self = shift;
804     my $object = shift;
805
806     return 1 if $self->IsApplied(0);
807
808     # global only custom fields don't have objects
809     # that should be used as context objects.
810     return if $self->ApplyGlobally;
811
812     # Otherwise, make sure we weren't passed a user object that we're
813     # supposed to treat as a queue.
814     return unless $self->ValidContextType(ref $object);
815
816     # Check that it is applied correctly
817     my ($applied_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects);
818     return unless $applied_to;
819     return $self->IsApplied($applied_to->id);
820 }
821
822 # {{{ sub _Set
823
824 sub _Set {
825     my $self = shift;
826
827     unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
828         return ( 0, $self->loc('Permission Denied') );
829     }
830     return $self->SUPER::_Set( @_ );
831
832 }
833
834 # }}}
835
836 # {{{ sub _Value 
837
838 =head2 _Value
839
840 Takes the name of a table column.
841 Returns its value as a string, if the user passes an ACL check
842
843 =cut
844
845 sub _Value {
846     my $self  = shift;
847     return undef unless $self->id;
848
849     # we need to do the rights check
850     unless ( $self->CurrentUserHasRight('SeeCustomField') ) {
851         $RT::Logger->debug(
852             "Permission denied. User #". $self->CurrentUser->id
853             ." has no SeeCustomField right on CF #". $self->id
854         );
855         return (undef);
856     }
857     return $self->__Value( @_ );
858 }
859
860 # }}}
861 # {{{ sub SetDisabled
862
863 =head2 SetDisabled
864
865 Takes a boolean.
866 1 will cause this custom field to no longer be avaialble for objects.
867 0 will re-enable this field.
868
869 =cut
870
871 # }}}
872
873 =head2 SetTypeComposite
874
875 Set this custom field's type and maximum values as a composite value
876
877 =cut
878
879 sub SetTypeComposite {
880     my $self = shift;
881     my $composite = shift;
882
883     my $old = $self->TypeComposite;
884
885     my ($type, $max_values) = split(/-/, $composite, 2);
886     if ( $type ne $self->Type ) {
887         my ($status, $msg) = $self->SetType( $type );
888         return ($status, $msg) unless $status;
889     }
890     if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
891         my ($status, $msg) = $self->SetMaxValues( $max_values );
892         return ($status, $msg) unless $status;
893     }
894     return 1, $self->loc(
895         "Type changed from '[_1]' to '[_2]'",
896         $self->FriendlyTypeComposite( $old ),
897         $self->FriendlyTypeComposite( $composite ),
898     );
899 }
900
901 =head2 TypeComposite
902
903 Returns a composite value composed of this object's type and maximum values
904
905 =cut
906
907
908 sub TypeComposite {
909     my $self = shift;
910     return join '-', ($self->Type || ''), ($self->MaxValues || 0);
911 }
912
913 =head2 TypeComposites
914
915 Returns an array of all possible composite values for custom fields.
916
917 =cut
918
919 sub TypeComposites {
920     my $self = shift;
921     return grep !/(?:[Tt]ext|Combobox|Date|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types;
922 }
923
924 =head2 SetLookupType
925
926 Autrijus: care to doc how LookupTypes work?
927
928 =cut
929
930 sub SetLookupType {
931     my $self = shift;
932     my $lookup = shift;
933     if ( $lookup ne $self->LookupType ) {
934         # Okay... We need to invalidate our existing relationships
935         my $ObjectCustomFields = RT::ObjectCustomFields->new($self->CurrentUser);
936         $ObjectCustomFields->LimitToCustomField($self->Id);
937         $_->Delete foreach @{$ObjectCustomFields->ItemsArrayRef};
938     }
939     return $self->SUPER::SetLookupType($lookup);
940 }
941
942 =head2 LookupTypes
943
944 Returns an array of LookupTypes available
945
946 =cut
947
948
949 sub LookupTypes {
950     my $self = shift;
951     return keys %FRIENDLY_OBJECT_TYPES;
952 }
953
954 my @FriendlyObjectTypes = (
955     "[_1] objects",            # loc
956     "[_1]'s [_2] objects",        # loc
957     "[_1]'s [_2]'s [_3] objects",   # loc
958 );
959
960 =head2 FriendlyLookupType
961
962 Returns a localized description of the type of this custom field
963
964 =cut
965
966 sub FriendlyLookupType {
967     my $self = shift;
968     my $lookup = shift || $self->LookupType;
969    
970     return ($self->loc( $FRIENDLY_OBJECT_TYPES{$lookup} ))
971                      if (defined  $FRIENDLY_OBJECT_TYPES{$lookup} );
972
973     my @types = map { s/^RT::// ? $self->loc($_) : $_ }
974       grep { defined and length }
975       split( /-/, $lookup )
976       or return;
977     return ( $self->loc( $FriendlyObjectTypes[$#types], @types ) );
978 }
979
980 sub RecordClassFromLookupType {
981     my $self = shift;
982     my ($class) = ($self->LookupType =~ /^([^-]+)/);
983     unless ( $class ) {
984         $RT::Logger->error(
985             "Custom Field #". $self->id 
986             ." has incorrect LookupType '". $self->LookupType ."'"
987         );
988         return undef;
989     }
990     return $class;
991 }
992
993 sub CollectionClassFromLookupType {
994     my $self = shift;
995
996     my $record_class = $self->RecordClassFromLookupType;
997     return undef unless $record_class;
998
999     my $collection_class;
1000     if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1001         $collection_class = $record_class.'Collection';
1002     } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1003         $collection_class = $record_class.'es';
1004     } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1005         $collection_class = $record_class.'s';
1006     } else {
1007         $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1008         return undef;
1009     }
1010     return $collection_class;
1011 }
1012
1013 =head1 AppliedTo
1014
1015 Returns collection with objects this custom field is applied to.
1016 Class of the collection depends on L</LookupType>.
1017 See all L</NotAppliedTo> .
1018
1019 Doesn't takes into account if object is applied globally.
1020
1021 =cut
1022
1023 sub AppliedTo {
1024     my $self = shift;
1025
1026     my ($res, $ocfs_alias) = $self->_AppliedTo;
1027     return $res unless $res;
1028
1029     $res->Limit(
1030         ALIAS     => $ocfs_alias,
1031         FIELD     => 'id',
1032         OPERATOR  => 'IS NOT',
1033         VALUE     => 'NULL',
1034     );
1035
1036     return $res;
1037 }
1038
1039 =head1 NotAppliedTo
1040
1041 Returns collection with objects this custom field is not applied to.
1042 Class of the collection depends on L</LookupType>.
1043 See all L</AppliedTo> .
1044
1045 Doesn't takes into account if object is applied globally.
1046
1047 =cut
1048
1049 sub NotAppliedTo {
1050     my $self = shift;
1051
1052     my ($res, $ocfs_alias) = $self->_AppliedTo;
1053     return $res unless $res;
1054
1055     $res->Limit(
1056         ALIAS     => $ocfs_alias,
1057         FIELD     => 'id',
1058         OPERATOR  => 'IS',
1059         VALUE     => 'NULL',
1060     );
1061
1062     return $res;
1063 }
1064
1065 sub _AppliedTo {
1066     my $self = shift;
1067
1068     my ($class) = $self->CollectionClassFromLookupType;
1069     return undef unless $class;
1070
1071     my $res = $class->new( $self->CurrentUser );
1072
1073     # If CF is a Group CF, only display user-defined groups
1074     if ( $class eq 'RT::Groups' ) {
1075         $res->LimitToUserDefinedGroups;
1076     }
1077
1078     $res->OrderBy( FIELD => 'Name' );
1079     my $ocfs_alias = $res->Join(
1080         TYPE   => 'LEFT',
1081         ALIAS1 => 'main',
1082         FIELD1 => 'id',
1083         TABLE2 => 'ObjectCustomFields',
1084         FIELD2 => 'ObjectId',
1085     );
1086     $res->Limit(
1087         LEFTJOIN => $ocfs_alias,
1088         ALIAS    => $ocfs_alias,
1089         FIELD    => 'CustomField',
1090         VALUE    => $self->id,
1091     );
1092     return ($res, $ocfs_alias);
1093 }
1094
1095 =head2 IsApplied
1096
1097 Takes object id and returns corresponding L<RT::ObjectCustomField>
1098 record if this custom field is applied to the object. Use 0 to check
1099 if custom field is applied globally.
1100
1101 =cut
1102
1103 sub IsApplied {
1104     my $self = shift;
1105     my $id = shift;
1106     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1107     $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1108     return undef unless $ocf->id;
1109     return $ocf;
1110 }
1111
1112 =head2 AddToObject OBJECT
1113
1114 Add this custom field as a custom field for a single object, such as a queue or group.
1115
1116 Takes an object 
1117
1118 =cut
1119
1120
1121 sub AddToObject {
1122     my $self  = shift;
1123     my $object = shift;
1124     my $id = $object->Id || 0;
1125
1126     unless (index($self->LookupType, ref($object)) == 0) {
1127         return ( 0, $self->loc('Lookup type mismatch') );
1128     }
1129
1130     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1131         return ( 0, $self->loc('Permission Denied') );
1132     }
1133
1134     if ( $self->IsApplied( $id ) ) {
1135         return ( 0, $self->loc("Custom field is already applied to the object") );
1136     }
1137
1138     if ( $id ) {
1139         # applying locally
1140         return (0, $self->loc("Couldn't apply custom field to an object as it's global already") )
1141             if $self->IsApplied( 0 );
1142     }
1143     else {
1144         my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
1145         $applied->LimitToCustomField( $self->id );
1146         while ( my $record = $applied->Next ) {
1147             $record->Delete;
1148         }
1149     }
1150
1151     my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1152     my ( $oid, $msg ) = $ocf->Create(
1153         ObjectId => $id, CustomField => $self->id,
1154     );
1155     return ( $oid, $msg );
1156 }
1157
1158
1159 =head2 RemoveFromObject OBJECT
1160
1161 Remove this custom field  for a single object, such as a queue or group.
1162
1163 Takes an object 
1164
1165 =cut
1166
1167 sub RemoveFromObject {
1168     my $self = shift;
1169     my $object = shift;
1170     my $id = $object->Id || 0;
1171
1172     unless (index($self->LookupType, ref($object)) == 0) {
1173         return ( 0, $self->loc('Object type mismatch') );
1174     }
1175
1176     unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1177         return ( 0, $self->loc('Permission Denied') );
1178     }
1179
1180     my $ocf = $self->IsApplied( $id );
1181     unless ( $ocf ) {
1182         return ( 0, $self->loc("This custom field does not apply to that object") );
1183     }
1184
1185     # XXX: Delete doesn't return anything
1186     my ( $oid, $msg ) = $ocf->Delete;
1187     return ( $oid, $msg );
1188 }
1189
1190 # {{{ AddValueForObject
1191
1192 =head2 AddValueForObject HASH
1193
1194 Adds a custom field value for a record object of some kind. 
1195 Takes a param hash of 
1196
1197 Required:
1198
1199     Object
1200     Content
1201
1202 Optional:
1203
1204     LargeContent
1205     ContentType
1206
1207 =cut
1208
1209 sub AddValueForObject {
1210     my $self = shift;
1211     my %args = (
1212         Object       => undef,
1213         Content      => undef,
1214         LargeContent => undef,
1215         ContentType  => undef,
1216         @_
1217     );
1218     my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1219
1220     unless ( $self->CurrentUserHasRight('ModifyCustomField') ) {
1221         return ( 0, $self->loc('Permission Denied') );
1222     }
1223
1224     unless ( $self->MatchPattern($args{'Content'}) ) {
1225         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1226     }
1227
1228     $RT::Handle->BeginTransaction;
1229
1230     if ( $self->MaxValues ) {
1231         my $current_values = $self->ValuesForObject($obj);
1232         my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1233
1234         # (The +1 is for the new value we're adding)
1235
1236         # If we have a set of current values and we've gone over the maximum
1237         # allowed number of values, we'll need to delete some to make room.
1238         # which former values are blown away is not guaranteed
1239
1240         while ($extra_values) {
1241             my $extra_item = $current_values->Next;
1242             unless ( $extra_item->id ) {
1243                 $RT::Logger->crit( "We were just asked to delete "
1244                     ."a custom field value that doesn't exist!" );
1245                 $RT::Handle->Rollback();
1246                 return (undef);
1247             }
1248             $extra_item->Delete;
1249             $extra_values--;
1250         }
1251     }
1252     # For date, we need to store Content as ISO date
1253     if ($self->Type eq 'Date') {
1254         my $DateObj = new RT::Date( $self->CurrentUser );
1255         $DateObj->Set(
1256             Format => 'unknown',
1257             Value  => $args{'Content'},
1258         );
1259         $args{'Content'} = $DateObj->ISO;
1260     }
1261     my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1262     my $val    = $newval->Create(
1263         ObjectType   => ref($obj),
1264         ObjectId     => $obj->Id,
1265         Content      => $args{'Content'},
1266         LargeContent => $args{'LargeContent'},
1267         ContentType  => $args{'ContentType'},
1268         CustomField  => $self->Id
1269     );
1270
1271     unless ($val) {
1272         $RT::Handle->Rollback();
1273         return ($val, $self->loc("Couldn't create record"));
1274     }
1275
1276     $RT::Handle->Commit();
1277     return ($val);
1278
1279 }
1280
1281 # }}}
1282
1283 # {{{ MatchPattern
1284
1285 =head2 MatchPattern STRING
1286
1287 Tests the incoming string against the Pattern of this custom field object
1288 and returns a boolean; returns true if the Pattern is empty.
1289
1290 =cut
1291
1292 sub MatchPattern {
1293     my $self = shift;
1294     my $regex = $self->Pattern or return 1;
1295
1296     return (( defined $_[0] ? $_[0] : '') =~ $regex);
1297 }
1298
1299
1300 # }}}
1301
1302 # {{{ FriendlyPattern
1303
1304 =head2 FriendlyPattern
1305
1306 Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1307 and localizing it.
1308
1309 =cut
1310
1311 sub FriendlyPattern {
1312     my $self = shift;
1313     my $regex = $self->Pattern;
1314
1315     return '' unless length $regex;
1316     if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1317         return '[' . $self->loc($1) . ']';
1318     }
1319     else {
1320         return $regex;
1321     }
1322 }
1323
1324
1325 # }}}
1326
1327 # {{{ DeleteValueForObject
1328
1329 =head2 DeleteValueForObject HASH
1330
1331 Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1332
1333 Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1334
1335 =cut
1336
1337 sub DeleteValueForObject {
1338     my $self = shift;
1339     my %args = ( Object => undef,
1340                  Content => undef,
1341                  Id => undef,
1342              @_ );
1343
1344
1345     unless ($self->CurrentUserHasRight('ModifyCustomField')) {
1346         return (0, $self->loc('Permission Denied'));
1347     }
1348
1349     my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
1350
1351     if (my $id = $args{'Id'}) {
1352         $oldval->Load($id);
1353     }
1354     unless ($oldval->id) { 
1355         $oldval->LoadByObjectContentAndCustomField(
1356             Object => $args{'Object'}, 
1357             Content =>  $args{'Content'}, 
1358             CustomField => $self->Id,
1359         );
1360     }
1361
1362
1363     # check to make sure we found it
1364     unless ($oldval->Id) {
1365         return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
1366     }
1367
1368     # for single-value fields, we need to validate that empty string is a valid value for it
1369     if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
1370         return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1371     }
1372
1373     # delete it
1374
1375     my $ret = $oldval->Delete();
1376     unless ($ret) {
1377         return(0, $self->loc("Custom field value could not be found"));
1378     }
1379     return($oldval->Id, $self->loc("Custom field value deleted"));
1380 }
1381
1382
1383 =head2 ValuesForObject OBJECT
1384
1385 Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT 
1386
1387 =cut
1388
1389 sub ValuesForObject {
1390     my $self = shift;
1391     my $object = shift;
1392
1393     my $values = new RT::ObjectCustomFieldValues($self->CurrentUser);
1394     unless ($self->CurrentUserHasRight('SeeCustomField')) {
1395         # Return an empty object if they have no rights to see
1396         return ($values);
1397     }
1398     
1399     
1400     $values->LimitToCustomField($self->Id);
1401     $values->LimitToEnabled();
1402     $values->LimitToObject($object);
1403
1404     return ($values);
1405 }
1406
1407
1408 =head2 _ForObjectType PATH FRIENDLYNAME
1409
1410 Tell RT that a certain object accepts custom fields
1411
1412 Examples:
1413
1414     'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
1415     'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
1416     'RT::User'                             => "Users",                  # loc
1417     'RT::Group'                            => "Groups",                 # loc
1418
1419 This is a class method. 
1420
1421 =cut
1422
1423 sub _ForObjectType {
1424     my $self = shift;
1425     my $path = shift;
1426     my $friendly_name = shift;
1427
1428     $FRIENDLY_OBJECT_TYPES{$path} = $friendly_name;
1429
1430 }
1431
1432
1433 =head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
1434
1435 Gets or sets the  C<IncludeContentForValue> for this custom field. RT
1436 uses this field to automatically include content into the user's browser
1437 as they display records with custom fields in RT.
1438
1439 =cut
1440
1441 sub SetIncludeContentForValue {
1442     shift->IncludeContentForValue(@_);
1443 }
1444 sub IncludeContentForValue{
1445     my $self = shift;
1446     $self->_URLTemplate('IncludeContentForValue', @_);
1447 }
1448
1449
1450
1451 =head2 LinkValueTo [VALUE] (and SetLinkValueTo)
1452
1453 Gets or sets the  C<LinkValueTo> for this custom field. RT
1454 uses this field to make custom field values into hyperlinks in the user's
1455 browser as they display records with custom fields in RT.
1456
1457 =cut
1458
1459
1460 sub SetLinkValueTo {
1461     shift->LinkValueTo(@_);
1462 }
1463
1464 sub LinkValueTo {
1465     my $self = shift;
1466     $self->_URLTemplate('LinkValueTo', @_);
1467
1468 }
1469
1470
1471 =head2 _URLTemplate  NAME [VALUE]
1472
1473 With one argument, returns the _URLTemplate named C<NAME>, but only if
1474 the current user has the right to see this custom field.
1475
1476 With two arguments, attemptes to set the relevant template value.
1477
1478 =cut
1479
1480 sub _URLTemplate {
1481     my $self          = shift;
1482     my $template_name = shift;
1483     if (@_) {
1484
1485         my $value = shift;
1486         unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1487             return ( 0, $self->loc('Permission Denied') );
1488         }
1489         $self->SetAttribute( Name => $template_name, Content => $value );
1490         return ( 1, $self->loc('Updated') );
1491     } else {
1492         unless ( $self->id && $self->CurrentUserHasRight('SeeCustomField') ) {
1493             return (undef);
1494         }
1495
1496         my @attr = $self->Attributes->Named($template_name);
1497         my $attr = shift @attr;
1498
1499         if ($attr) { return $attr->Content }
1500
1501     }
1502 }
1503
1504 sub SetBasedOn {
1505     my $self = shift;
1506     my $value = shift;
1507
1508     return $self->DeleteAttribute( "BasedOn" )
1509         unless defined $value and length $value;
1510
1511     my $cf = RT::CustomField->new( $self->CurrentUser );
1512     $cf->SetContextObject( $self->ContextObject );
1513     $cf->Load( ref $value ? $value->Id : $value );
1514
1515     return (0, "Permission denied")
1516         unless $cf->Id && $cf->CurrentUserHasRight('SeeCustomField');
1517
1518     return $self->SetAttribute(
1519         Name => "BasedOn",
1520         Description => "Custom field whose CF we depend on",
1521         Content => $cf->Id,
1522     );
1523 }
1524
1525 sub BasedOnObj {
1526     my $self = shift;
1527     my $obj = RT::CustomField->new( $self->CurrentUser );
1528     $obj->SetContextObject( $self->ContextObject );
1529
1530     my $attribute = $self->FirstAttribute("BasedOn");
1531     $obj->Load($attribute->Content) if defined $attribute;
1532     return $obj;
1533 }
1534
1535 sub UILocation {
1536     my $self = shift;
1537     my $tag = $self->FirstAttribute( 'UILocation' );
1538     return $tag ? $tag->Content : '';
1539 }
1540
1541 sub SetUILocation {
1542     my $self = shift;
1543     my $tag = shift;
1544     if ( $tag ) {
1545         return $self->SetAttribute( Name => 'UILocation', Content => $tag );
1546     }
1547     else {
1548         return $self->DeleteAttribute('UILocation');
1549     }
1550 }
1551
1552 1;