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