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