1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2021 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
49package RT::CustomField;
50
51use strict;
52use warnings;
53use 5.010;
54
55use Scalar::Util 'blessed';
56
57use base 'RT::Record';
58
59use Role::Basic 'with';
60with "RT::Record::Role::Rights";
61
62sub Table {'CustomFields'}
63
64use Scalar::Util qw(blessed);
65use RT::CustomFieldValues;
66use RT::ObjectCustomFields;
67use RT::ObjectCustomFieldValues;
68
69our %FieldTypes = (
70    Select => {
71        sort_order => 10,
72        selection_type => 1,
73        canonicalizes => 0,
74
75        labels => [ 'Select multiple values',               # loc
76                    'Select one value',                     # loc
77                    'Select up to [quant,_1,value,values]', # loc
78                  ],
79
80        render_types => {
81            multiple => [
82
83                # Default is the first one
84                'Select box',              # loc
85                'List',                    # loc
86            ],
87            single => [ 'Dropdown',                # loc
88                        'Select box',              # loc
89                        'List',                    # loc
90                      ]
91        },
92
93    },
94    Freeform => {
95        sort_order => 20,
96        selection_type => 0,
97        canonicalizes => 1,
98
99        labels => [ 'Enter multiple values',               # loc
100                    'Enter one value',                     # loc
101                    'Enter up to [quant,_1,value,values]', # loc
102                  ]
103                },
104    Text => {
105        sort_order => 30,
106        selection_type => 0,
107        canonicalizes => 1,
108        labels         => [
109                    'Fill in multiple text areas',                   # loc
110                    'Fill in one text area',                         # loc
111                    'Fill in up to [quant,_1,text area,text areas]', # loc
112                  ]
113            },
114    Wikitext => {
115        sort_order => 40,
116        selection_type => 0,
117        canonicalizes => 1,
118        labels         => [
119                    'Fill in multiple wikitext areas',                       # loc
120                    'Fill in one wikitext area',                             # loc
121                    'Fill in up to [quant,_1,wikitext area,wikitext areas]', # loc
122                  ]
123                },
124
125    Image => {
126        sort_order => 50,
127        selection_type => 0,
128        canonicalizes => 0,
129        labels         => [
130                    'Upload multiple images',               # loc
131                    'Upload one image',                     # loc
132                    'Upload up to [quant,_1,image,images]', # loc
133                  ]
134             },
135    Binary => {
136        sort_order => 60,
137        selection_type => 0,
138        canonicalizes => 0,
139        labels         => [
140                    'Upload multiple files',              # loc
141                    'Upload one file',                    # loc
142                    'Upload up to [quant,_1,file,files]', # loc
143                  ]
144              },
145
146    Combobox => {
147        sort_order => 70,
148        selection_type => 1,
149        canonicalizes => 1,
150        labels         => [
151                    'Combobox: Select or enter multiple values',               # loc
152                    'Combobox: Select or enter one value',                     # loc
153                    'Combobox: Select or enter up to [quant,_1,value,values]', # loc
154                  ]
155                },
156    Autocomplete => {
157        sort_order => 80,
158        selection_type => 1,
159        canonicalizes => 1,
160        labels         => [
161                    'Enter multiple values with autocompletion',               # loc
162                    'Enter one value with autocompletion',                     # loc
163                    'Enter up to [quant,_1,value,values] with autocompletion', # loc
164                  ]
165    },
166
167    Date => {
168        sort_order => 90,
169        selection_type => 0,
170        canonicalizes => 0,
171        labels         => [
172                    'Select multiple dates',              # loc
173                    'Select date',                        # loc
174                    'Select up to [quant,_1,date,dates]', # loc
175                  ]
176            },
177    DateTime => {
178        sort_order => 100,
179        selection_type => 0,
180        canonicalizes => 0,
181        labels         => [
182                    'Select multiple datetimes',                  # loc
183                    'Select datetime',                            # loc
184                    'Select up to [quant,_1,datetime,datetimes]', # loc
185                  ]
186                },
187
188    IPAddress => {
189        sort_order => 110,
190        selection_type => 0,
191        canonicalizes => 0,
192
193        labels => [ 'Enter multiple IP addresses',                    # loc
194                    'Enter one IP address',                           # loc
195                    'Enter up to [quant,_1,IP address,IP addresses]', # loc
196                  ]
197                },
198    IPAddressRange => {
199        sort_order => 120,
200        selection_type => 0,
201        canonicalizes => 0,
202
203        labels => [ 'Enter multiple IP address ranges',                          # loc
204                    'Enter one IP address range',                                # loc
205                    'Enter up to [quant,_1,IP address range,IP address ranges]', # loc
206                  ]
207                },
208);
209
210
211my %BUILTIN_GROUPINGS;
212my %FRIENDLY_LOOKUP_TYPES = ();
213
214__PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket' => "Tickets", );    #loc
215__PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc
216__PACKAGE__->RegisterLookupType( 'RT::User'  => "Users", );                           #loc
217__PACKAGE__->RegisterLookupType( 'RT::Queue'  => "Queues", );                         #loc
218__PACKAGE__->RegisterLookupType( 'RT::Group' => "Groups", );                          #loc
219
220__PACKAGE__->RegisterBuiltInGroupings(
221    'RT::Ticket'    => [ qw(Basics Dates Links People) ],
222    'RT::User'      => [ 'Identity', 'Access control', 'Location', 'Phones' ],
223    'RT::Group'     => [ 'Basics' ],
224);
225
226__PACKAGE__->AddRight( General => SeeCustomField         => 'View custom fields'); # loc
227__PACKAGE__->AddRight( Admin   => AdminCustomField       => 'Create, modify and delete custom fields'); # loc
228__PACKAGE__->AddRight( Admin   => AdminCustomFieldValues => 'Create, modify and delete custom fields values'); # loc
229__PACKAGE__->AddRight( Staff   => ModifyCustomField      => 'Add, modify and delete custom field values for objects'); # loc
230__PACKAGE__->AddRight( Staff   => SetInitialCustomField  => 'Add custom field values only at object creation time'); # loc
231
232=head1 NAME
233
234  RT::CustomField_Overlay - overlay for RT::CustomField
235
236=head1 DESCRIPTION
237
238=head1 'CORE' METHODS
239
240=head2 Create PARAMHASH
241
242Create takes a hash of values and creates a row in the database:
243
244  varchar(200) 'Name'.
245  varchar(200) 'Type'.
246  int(11) 'MaxValues'.
247  varchar(255) 'Pattern'.
248  varchar(255) 'Description'.
249  int(11) 'SortOrder'.
250  varchar(255) 'LookupType'.
251  varchar(255) 'EntryHint'.
252  smallint(6) 'Disabled'.
253
254C<LookupType> is generally the result of either
255C<RT::Ticket->CustomFieldLookupType> or C<RT::Transaction->CustomFieldLookupType>.
256
257=cut
258
259sub Create {
260    my $self = shift;
261    my %args = (
262        Name                   => '',
263        Type                   => '',
264        MaxValues              => 0,
265        Pattern                => '',
266        Description            => '',
267        Disabled               => 0,
268        SortOrder              => 0,
269        LookupType             => '',
270        LinkValueTo            => '',
271        IncludeContentForValue => '',
272        EntryHint              => undef,
273        UniqueValues           => 0,
274        CanonicalizeClass      => undef,
275        @_,
276    );
277
278    unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) {
279        return (0, $self->loc('Permission Denied'));
280    }
281
282    if ( $args{TypeComposite} ) {
283        @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2);
284    }
285    elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) {
286        # old style Type string
287        $args{'MaxValues'} = $1 ? 1 : 0;
288    }
289    $args{'MaxValues'} = int $args{'MaxValues'};
290
291    if ( !exists $args{'Queue'}) {
292    # do nothing -- things below are strictly backward compat
293    }
294    elsif (  ! $args{'Queue'} ) {
295        unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) {
296            return ( 0, $self->loc('Permission Denied') );
297        }
298        $args{'LookupType'} = 'RT::Queue-RT::Ticket';
299    }
300    else {
301        my $queue = RT::Queue->new($self->CurrentUser);
302        $queue->Load($args{'Queue'});
303        unless ($queue->Id) {
304            return (0, $self->loc("Queue not found"));
305        }
306        unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) {
307            return ( 0, $self->loc('Permission Denied') );
308        }
309        $args{'LookupType'} = 'RT::Queue-RT::Ticket';
310        $args{'Queue'} = $queue->Id;
311    }
312
313    my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} );
314    return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok;
315
316    if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) {
317        $RT::Logger->debug("Support for 'multiple' Texts or Comboboxes is not implemented");
318        $args{'MaxValues'} = 1;
319    }
320
321    if ( $args{'RenderType'} ||= undef ) {
322        my $composite = join '-', @args{'Type', 'MaxValues'};
323        return (0, $self->loc("This custom field has no Render Types"))
324            unless $self->HasRenderTypes( $composite );
325
326        if ( $args{'RenderType'} eq $self->DefaultRenderType( $composite ) ) {
327            $args{'RenderType'} = undef;
328        } else {
329            return (0, $self->loc("Invalid Render Type") )
330                unless grep $_ eq  $args{'RenderType'}, $self->RenderTypes( $composite );
331        }
332    }
333
334    $args{'ValuesClass'} = undef if ($args{'ValuesClass'} || '') eq 'RT::CustomFieldValues';
335    if ( $args{'ValuesClass'} ||= undef ) {
336        return (0, $self->loc("This Custom Field can not have list of values"))
337            unless $self->IsSelectionType( $args{'Type'} );
338
339        unless ( $self->ValidateValuesClass( $args{'ValuesClass'} ) ) {
340            return (0, $self->loc("Invalid Custom Field values source"));
341        }
342    }
343
344    if ( $args{'CanonicalizeClass'} ||= undef ) {
345        return (0, $self->loc("This custom field can not have a canonicalizer"))
346            unless $self->IsCanonicalizeType( $args{'Type'} );
347
348        unless ( $self->ValidateCanonicalizeClass( $args{'CanonicalizeClass'} ) ) {
349            return (0, $self->loc("Invalid custom field values canonicalizer"));
350        }
351    }
352
353    $args{'Disabled'} ||= 0;
354
355    (my $rv, $msg) = $self->SUPER::Create(
356        Name              => $args{'Name'},
357        Type              => $args{'Type'},
358        RenderType        => $args{'RenderType'},
359        MaxValues         => $args{'MaxValues'},
360        Pattern           => $args{'Pattern'},
361        BasedOn           => $args{'BasedOn'},
362        ValuesClass       => $args{'ValuesClass'},
363        Description       => $args{'Description'},
364        Disabled          => $args{'Disabled'},
365        SortOrder         => $args{'SortOrder'},
366        LookupType        => $args{'LookupType'},
367        UniqueValues      => $args{'UniqueValues'},
368        CanonicalizeClass => $args{'CanonicalizeClass'},
369    );
370
371    if ($rv) {
372        if ( exists $args{'LinkValueTo'}) {
373            $self->SetLinkValueTo($args{'LinkValueTo'});
374        }
375
376        $self->SetEntryHint( $args{EntryHint} // $self->FriendlyType );
377
378        if ( exists $args{'IncludeContentForValue'}) {
379            $self->SetIncludeContentForValue($args{'IncludeContentForValue'});
380        }
381
382        return ($rv, $msg) unless exists $args{'Queue'};
383
384        # Compat code -- create a new ObjectCustomField mapping
385        my $OCF = RT::ObjectCustomField->new( $self->CurrentUser );
386        $OCF->Create(
387            CustomField => $self->Id,
388            ObjectId => $args{'Queue'},
389        );
390    }
391
392    return ($rv, $msg);
393}
394
395=head2 Load ID/NAME
396
397Load a custom field.  If the value handed in is an integer, load by custom field ID. Otherwise, Load by name.
398
399=cut
400
401sub Load {
402    my $self = shift;
403    my $id = shift || '';
404
405    if ( $id =~ /^\d+$/ ) {
406        return $self->SUPER::Load( $id );
407    } else {
408        return $self->LoadByName( Name => $id );
409    }
410}
411
412
413
414=head2 LoadByName Name => C<NAME>, [...]
415
416Loads the Custom field named NAME.  As other optional parameters, takes:
417
418=over
419
420=item LookupType => C<LOOKUPTYPE>
421
422The type of Custom Field to look for; while this parameter is not
423required, it is highly suggested, or you may not find the Custom Field
424you are expecting.  It should be passed a C<LookupType> such as
425L<RT::Ticket/CustomFieldLookupType> or
426L<RT::User/CustomFieldLookupType>.
427
428=item ObjectType => C<CLASS>
429
430The class of object that the custom field is applied to.  This can be
431intuited from the provided C<LookupType>.
432
433=item ObjectId => C<ID>
434
435limits the custom field search to one applied to the relevant id.  For
436example, if a C<LookupType> of C<< RT::Ticket->CustomFieldLookupType >>
437is used, this is which Queue the CF must be applied to.  Pass 0 to only
438search custom fields that are applied globally.
439
440=item IncludeDisabled => C<BOOLEAN>
441
442Whether it should return Disabled custom fields if they match; defaults
443to on, though non-Disabled custom fields are returned preferentially.
444
445=item IncludeGlobal => C<BOOLEAN>
446
447Whether to also search global custom fields, even if a value is provided
448for C<ObjectId>; defaults to off.  Non-global custom fields are returned
449preferentially.
450
451=back
452
453For backwards compatibility, a value passed for C<Queue> is equivalent
454to specifying a C<LookupType> of L<RT::Ticket/CustomFieldLookupType>,
455and a C<ObjectId> of the value passed as C<Queue>.
456
457If multiple custom fields match the above constraints, the first
458according to C<SortOrder> will be returned; ties are broken by C<id>,
459lowest-first.
460
461=head2 LoadNameAndQueue
462
463=head2 LoadByNameAndQueue
464
465Deprecated alternate names for L</LoadByName>.
466
467=cut
468
469# Compatibility for API change after 3.0 beta 1
470*LoadNameAndQueue = \&LoadByName;
471# Change after 3.4 beta.
472*LoadByNameAndQueue = \&LoadByName;
473
474sub LoadByName {
475    my $self = shift;
476    my %args = (
477        Name       => undef,
478        LookupType => undef,
479        ObjectType => undef,
480        ObjectId   => undef,
481
482        IncludeDisabled => 1,
483        IncludeGlobal   => 0,
484
485        # Back-compat
486        Queue => undef,
487
488        @_,
489    );
490
491    unless ( defined $args{'Name'} && length $args{'Name'} ) {
492        $RT::Logger->error("Couldn't load Custom Field without Name");
493        return wantarray ? (0, $self->loc("No name provided")) : 0;
494    }
495
496    if ( defined $args{'Queue'} ) {
497        # Set a LookupType for backcompat, otherwise we'll calculate
498        # one of RT::Queue from your ContextObj.  Older code was relying
499        # on us defaulting to RT::Queue-RT::Ticket in old LimitToQueue call.
500        $args{LookupType} ||= 'RT::Queue-RT::Ticket';
501        $args{ObjectId}   //= delete $args{Queue};
502    }
503
504    # Default the ObjectType to the top category of the LookupType; it's
505    # what the CFs are assigned on.
506    $args{ObjectType} ||= $1 if $args{LookupType} and $args{LookupType} =~ /^([^-]+)/;
507
508    # Resolve the ObjectId/ObjectType; this is necessary to properly
509    # limit ObjectId, and also possibly useful to set a ContextObj if we
510    # are currently lacking one.  It is not strictly necessary if we
511    # have a context object and were passed a numeric ObjectId, but it
512    # cannot hurt to verify its sanity.  Skip if we have a false
513    # ObjectId, which means "global", or if we lack an ObjectType
514    if ($args{ObjectId} and $args{ObjectType}) {
515        my ($obj, $ok, $msg);
516        eval {
517            $obj = $args{ObjectType}->new( $self->CurrentUser );
518            ($ok, $msg) = $obj->Load( $args{ObjectId} );
519        };
520
521        if ($ok) {
522            $args{ObjectId} = $obj->id;
523            $self->SetContextObject( $obj )
524                unless $self->ContextObject;
525        } else {
526            $RT::Logger->warning("Failed to load $args{ObjectType} '$args{ObjectId}'");
527            if ($args{IncludeGlobal}) {
528                # Fall back to acting like we were only asked about the
529                # global case
530                $args{ObjectId} = 0;
531            } else {
532                # If they didn't also want global results, there's no
533                # point in searching; abort
534                return wantarray ? (0, $self->loc("Not found")) : 0;
535            }
536        }
537    } elsif (not $args{ObjectType} and $args{ObjectId}) {
538        # If we skipped out on the above due to lack of ObjectType, make
539        # sure we clear out ObjectId of anything lingering
540        $RT::Logger->warning("No LookupType or ObjectType passed; ignoring ObjectId");
541        delete $args{ObjectId};
542    }
543
544    my $CFs = RT::CustomFields->new( $self->CurrentUser );
545    $CFs->SetContextObject( $self->ContextObject );
546    my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
547    $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
548
549    # The context object may be a ticket, for example, as context for a
550    # queue CF.  The valid lookup types are thus the entire set of
551    # ACLEquivalenceObjects for the context object.
552    $args{LookupType} ||= [
553        map {$_->CustomFieldLookupType}
554            ($self->ContextObject, $self->ContextObject->ACLEquivalenceObjects) ]
555        if $self->ContextObject;
556
557    # Apply LookupType limits
558    $args{LookupType} = [ $args{LookupType} ]
559        if $args{LookupType} and not ref($args{LookupType});
560    $CFs->Limit( FIELD => "LookupType", OPERATOR => "IN", VALUE => $args{LookupType} )
561        if $args{LookupType};
562
563    # Default to by SortOrder and id; this mirrors the standard ordering
564    # of RT::CustomFields (minus the Name, which is guaranteed to be
565    # fixed)
566    my @order = (
567        { FIELD => 'SortOrder',
568          ORDER => 'ASC' },
569        { FIELD => 'id',
570          ORDER => 'ASC' },
571    );
572
573    if (defined $args{ObjectId}) {
574        # The join to OCFs is distinct -- either we have a global
575        # application or an objectid match, but never both.  Even if
576        # this were not the case, we care only for the first row.
577        my $ocfs = $CFs->_OCFAlias( Distinct => 1);
578        if ($args{IncludeGlobal}) {
579            $CFs->Limit(
580                ALIAS    => $ocfs,
581                FIELD    => 'ObjectId',
582                OPERATOR => 'IN',
583                VALUE    => [ $args{ObjectId}, 0 ],
584            );
585            # Find the queue-specific first
586            unshift @order, { ALIAS => $ocfs, FIELD => "ObjectId", ORDER => "DESC" };
587        } else {
588            $CFs->Limit(
589                ALIAS => $ocfs,
590                FIELD => 'ObjectId',
591                VALUE => $args{ObjectId},
592            );
593        }
594    }
595
596    if ($args{IncludeDisabled}) {
597        # Load disabled fields, but return them only as a last resort.
598        # This goes at the front of @order, as we prefer the
599        # non-disabled global CF to the disabled Queue-specific CF.
600        $CFs->FindAllRows;
601        unshift @order, { FIELD => "Disabled", ORDER => 'ASC' };
602    }
603
604    # Apply the above orderings
605    $CFs->OrderByCols( @order );
606
607    # We only want one entry.
608    $CFs->RowsPerPage(1);
609
610    # version before 3.8 just returns 0, so we need to test if wantarray to be
611    # backward compatible.
612    return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First;
613
614    return $self->LoadById( $first->id );
615}
616
617
618
619
620=head2 Custom field values
621
622=head3 Values FIELD
623
624Return a object (collection) of all acceptable values for this Custom Field.
625Class of the object can vary and depends on the return value
626of the C<ValuesClass> method.
627
628=cut
629
630*ValuesObj = \&Values;
631
632sub Values {
633    my $self = shift;
634
635    my $class = $self->ValuesClass;
636    if ( $class ne 'RT::CustomFieldValues') {
637        $class->require or die "Can't load $class: $@";
638    }
639    my $cf_values = $class->new( $self->CurrentUser );
640    $cf_values->SetCustomFieldObject( $self );
641    # if the user has no rights, return an empty object
642    if ( $self->id && $self->CurrentUserCanSee ) {
643        $cf_values->LimitToCustomField( $self->Id );
644    } else {
645        $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' );
646    }
647    return ($cf_values);
648}
649
650
651=head3 AddValue HASH
652
653Create a new value for this CustomField.  Takes a paramhash containing the elements Name, Description and SortOrder
654
655=cut
656
657sub AddValue {
658    my $self = shift;
659    my %args = @_;
660
661    unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) {
662        return (0, $self->loc('Permission Denied'));
663    }
664
665    # allow zero value
666    if ( !defined $args{'Name'} || $args{'Name'} eq '' ) {
667        return (0, $self->loc("Can't add a custom field value without a name"));
668    }
669
670    my $newval = RT::CustomFieldValue->new( $self->CurrentUser );
671    return $newval->Create( %args, CustomField => $self->Id );
672}
673
674
675
676
677=head3 DeleteValue ID
678
679Deletes a value from this custom field by id.
680
681Does not remove this value for any article which has had it selected
682
683=cut
684
685sub DeleteValue {
686    my $self = shift;
687    my $id = shift;
688    unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) {
689        return (0, $self->loc('Permission Denied'));
690    }
691
692    my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser );
693    $val_to_del->Load( $id );
694    unless ( $val_to_del->Id ) {
695        return (0, $self->loc("Couldn't find that value"));
696    }
697    unless ( $val_to_del->CustomField == $self->Id ) {
698        return (0, $self->loc("That is not a value for this custom field"));
699    }
700
701    my ($ok, $msg) = $val_to_del->Delete;
702    unless ( $ok ) {
703        return (0, $self->loc("Custom field value could not be deleted"));
704    }
705    return ($ok, $self->loc("Custom field value deleted"));
706}
707
708
709=head2 ValidateQueue Queue
710
711Make sure that the name specified is valid
712
713=cut
714
715sub ValidateName {
716    my $self = shift;
717    my $value = shift;
718
719    return 0 unless length $value;
720
721    return $self->SUPER::ValidateName($value);
722}
723
724=head2 ValidateQueue Queue
725
726Make sure that the queue specified is a valid queue name
727
728=cut
729
730sub ValidateQueue {
731    my $self = shift;
732    my $id = shift;
733
734    return undef unless defined $id;
735    # 0 means "Global" null would _not_ be ok.
736    return 1 if $id eq '0';
737
738    my $q = RT::Queue->new( RT->SystemUser );
739    $q->Load( $id );
740    return undef unless $q->id;
741    return 1;
742}
743
744
745
746=head2 Types
747
748Retuns an array of the types of CustomField that are supported
749
750=cut
751
752sub Types {
753    return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes);
754}
755
756
757=head2 IsSelectionType
758
759Returns a boolean value indicating whether the C<Values> method makes sense
760to this Custom Field.
761
762=cut
763
764sub IsSelectionType {
765    my $self = shift;
766    my $type = @_ ? shift : $self->Type;
767    return undef unless $type;
768    return $FieldTypes{$type}->{selection_type};
769}
770
771=head2 IsCanonicalizeType
772
773Returns a boolean value indicating whether the type of this custom field
774permits using a canonicalizer.
775
776=cut
777
778sub IsCanonicalizeType {
779    my $self = shift;
780    my $type = @_ ? shift : $self->Type;
781    return undef unless $type;
782    return $FieldTypes{$type}->{canonicalizes};
783}
784
785
786=head2 IsExternalValues
787
788=cut
789
790sub IsExternalValues {
791    my $self = shift;
792    return 0 unless $self->IsSelectionType( @_ );
793    return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1;
794}
795
796sub ValuesClass {
797    my $self = shift;
798    return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues';
799}
800
801=head2 SetValuesClass CLASS
802
803Writer method for the ValuesClass field; validates that the custom field can
804use a ValuesClass, and that the provided ValuesClass passes
805L</ValidateValuesClass>.
806
807=cut
808
809sub SetValuesClass {
810    my $self = shift;
811    my $class = shift || 'RT::CustomFieldValues';
812
813    if ( $class eq 'RT::CustomFieldValues' ) {
814        return $self->_Set( Field => 'ValuesClass', Value => undef, @_ );
815    }
816
817    return (0, $self->loc("This Custom Field can not have list of values"))
818        unless $self->IsSelectionType;
819
820    unless ( $self->ValidateValuesClass( $class ) ) {
821        return (0, $self->loc("Invalid Custom Field values source"));
822    }
823    return $self->_Set( Field => 'ValuesClass', Value => $class, @_ );
824}
825
826=head2 ValidateValuesClass CLASS
827
828Validates a potential ValuesClass value; the ValuesClass may be C<undef> or
829the string C<"RT::CustomFieldValues"> (both of which make this custom field
830use the ordinary values implementation), or a class name in the listed in
831the L<RT_Config/@CustomFieldValuesSources> setting.
832
833Returns true if valid; false if invalid.
834
835=cut
836
837sub ValidateValuesClass {
838    my $self = shift;
839    my $class = shift;
840
841    return 1 if !$class || $class eq 'RT::CustomFieldValues';
842    return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources');
843    return undef;
844}
845
846=head2 SetCanonicalizeClass CLASS
847
848Writer method for the CanonicalizeClass field; validates that the custom
849field can use a CanonicalizeClass, and that the provided CanonicalizeClass
850passes L</ValidateCanonicalizeClass>.
851
852=cut
853
854sub SetCanonicalizeClass {
855    my $self = shift;
856    my $class = shift;
857
858    if ( !$class ) {
859        return $self->_Set( Field => 'CanonicalizeClass', Value => undef, @_ );
860    }
861
862    return (0, $self->loc("This custom field can not have a canonicalizer"))
863        unless $self->IsCanonicalizeType;
864
865    unless ( $self->ValidateCanonicalizeClass( $class ) ) {
866        return (0, $self->loc("Invalid custom field values canonicalizer"));
867    }
868    return $self->_Set( Field => 'CanonicalizeClass', Value => $class, @_ );
869}
870
871=head2 ValidateCanonicalizeClass CLASS
872
873Validates a potential CanonicalizeClass value; the CanonicalizeClass may be
874C<undef> (which make this custom field use no special canonicalization), or
875a class name in the listed in the
876L<RT_Config/@CustomFieldValuesCanonicalizers> setting.
877
878Returns true if valid; false if invalid.
879
880=cut
881
882sub ValidateCanonicalizeClass {
883    my $self = shift;
884    my $class = shift;
885
886    return 1 if !$class;
887    return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesCanonicalizers');
888    return undef;
889}
890
891=head2 FriendlyType [TYPE, MAX_VALUES]
892
893Returns a localized human-readable version of the custom field type.
894If a custom field type is specified as the parameter, the friendly type for that type will be returned
895
896=cut
897
898sub FriendlyType {
899    my $self = shift;
900
901    my $type = @_ ? shift : $self->Type;
902    my $max  = @_ ? shift : $self->MaxValues;
903    $max = 0 unless $max;
904
905    if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) {
906        return ( $self->loc( $friendly_type, $max ) );
907    }
908    else {
909        return ( $self->loc( $type ) );
910    }
911}
912
913sub FriendlyTypeComposite {
914    my $self = shift;
915    my $composite = shift || $self->TypeComposite;
916    return $self->FriendlyType(split(/-/, $composite, 2));
917}
918
919
920=head2 ValidateType TYPE
921
922Takes a single string. returns true if that string is a value
923type of custom field
924
925
926=cut
927
928sub ValidateType {
929    my $self = shift;
930    my $type = shift;
931
932    if ( $FieldTypes{$type} ) {
933        return 1;
934    }
935    else {
936        return undef;
937    }
938}
939
940
941sub SetType {
942    my $self = shift;
943    my $type = shift;
944    my $need_to_update_hint;
945    $need_to_update_hint = 1 if $self->EntryHint && $self->EntryHint eq $self->FriendlyType;
946    my ( $ret, $msg ) = $self->_Set( Field => 'Type', Value => $type );
947    $self->SetEntryHint($self->FriendlyType) if $need_to_update_hint && $ret;
948    return ( $ret, $msg );
949}
950
951=head2 SetPattern STRING
952
953Takes a single string representing a regular expression.  Performs basic
954validation on that regex, and sets the C<Pattern> field for the CF if it
955is valid.
956
957=cut
958
959sub SetPattern {
960    my $self = shift;
961    my $regex = shift;
962
963    my ($ok, $msg) = $self->_IsValidRegex($regex);
964    if ($ok) {
965        return $self->_Set(Field => 'Pattern', Value => $regex);
966    }
967    else {
968        return (0, $self->loc("Invalid pattern: [_1]", $msg));
969    }
970}
971
972=head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg)
973
974Tests if the string contains an invalid regex.
975
976=cut
977
978sub _IsValidRegex {
979    my $self  = shift;
980    my $regex = shift or return (1, 'valid');
981
982    local $^W; local $@;
983    local $SIG{__DIE__} = sub { 1 };
984    local $SIG{__WARN__} = sub { 1 };
985
986    if (eval { qr/$regex/; 1 }) {
987        return (1, 'valid');
988    }
989
990    my $err = $@;
991    $err =~ s{[,;].*}{};    # strip debug info from error
992    chomp $err;
993    return (0, $err);
994}
995
996
997=head2 SingleValue
998
999Returns true if this CustomField only accepts a single value.
1000Returns false if it accepts multiple values
1001
1002=cut
1003
1004sub SingleValue {
1005    my $self = shift;
1006    if (($self->MaxValues||0) == 1) {
1007        return 1;
1008    }
1009    else {
1010        return undef;
1011    }
1012}
1013
1014sub UnlimitedValues {
1015    my $self = shift;
1016    if (($self->MaxValues||0) == 0) {
1017        return 1;
1018    }
1019    else {
1020        return undef;
1021    }
1022}
1023
1024
1025=head2 ACLEquivalenceObjects
1026
1027Returns list of objects via which users can get rights on this custom field. For custom fields
1028these objects can be set using L<ContextObject|/"ContextObject and SetContextObject">.
1029
1030=cut
1031
1032sub ACLEquivalenceObjects {
1033    my $self = shift;
1034
1035    my $ctx = $self->ContextObject
1036        or return;
1037    return ($ctx, $ctx->ACLEquivalenceObjects);
1038}
1039
1040=head2 ContextObject and SetContextObject
1041
1042Set or get a context for this object. It can be ticket, queue or another
1043object this CF added to. Used for ACL control, for example
1044SeeCustomField can be granted on queue level to allow people to see all
1045fields added to the queue.
1046
1047=cut
1048
1049sub SetContextObject {
1050    my $self = shift;
1051    return $self->{'context_object'} = shift;
1052}
1053
1054sub ContextObject {
1055    my $self = shift;
1056    return $self->{'context_object'};
1057}
1058
1059sub ValidContextType {
1060    my $self = shift;
1061    my $class = shift;
1062
1063    my %valid;
1064    $valid{$_}++ for split '-', $self->LookupType;
1065    delete $valid{'RT::Transaction'};
1066
1067    return $valid{$class};
1068}
1069
1070=head2 LoadContextObject
1071
1072Takes an Id for a Context Object and loads the right kind of RT::Object
1073for this particular Custom Field (based on the LookupType) and returns it.
1074This is a good way to ensure you don't try to use a Queue as a Context
1075Object on a User Custom Field.
1076
1077=cut
1078
1079sub LoadContextObject {
1080    my $self = shift;
1081    my $type = shift;
1082    my $contextid = shift;
1083
1084    unless ( $self->ValidContextType($type) ) {
1085        RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id);
1086        return;
1087    }
1088
1089    my $context_object = $type->new( $self->CurrentUser );
1090    my ($id, $msg) = $context_object->LoadById( $contextid );
1091    unless ( $id ) {
1092        RT->Logger->debug("Invalid ContextObject id: $msg");
1093        return;
1094    }
1095    return $context_object;
1096}
1097
1098=head2 ValidateContextObject
1099
1100Ensure that a given ContextObject applies to this Custom Field.  For
1101custom fields that are assigned to Queues or to Classes, this checks
1102that the Custom Field is actually added to that object.  For Global
1103Custom Fields, it returns true as long as the Object is of the right
1104type, because you may be using your permissions on a given Queue of
1105Class to see a Global CF.  For CFs that are only added globally, you
1106don't need a ContextObject.
1107
1108=cut
1109
1110sub ValidateContextObject {
1111    my $self = shift;
1112    my $object = shift;
1113
1114    return 1 if $self->IsGlobal;
1115
1116    # global only custom fields don't have objects
1117    # that should be used as context objects.
1118    return if $self->IsOnlyGlobal;
1119
1120    # Otherwise, make sure we weren't passed a user object that we're
1121    # supposed to treat as a queue.
1122    return unless $self->ValidContextType(ref $object);
1123
1124    # Check that it is added correctly
1125    my ($added_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects);
1126    return unless $added_to;
1127    return $self->IsAdded($added_to->id);
1128}
1129
1130sub _Set {
1131    my $self = shift;
1132    my %args = @_;
1133    unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
1134        return ( 0, $self->loc('Permission Denied') );
1135    }
1136    my ($ret, $msg) = $self->SUPER::_Set( @_ );
1137    if ( $args{Field} =~ /^(?:MaxValues|Type|LookupType|ValuesClass|CanonicalizeClass)$/ ) {
1138        $self->CleanupDefaultValues;
1139    }
1140    return ($ret, $msg);
1141}
1142
1143
1144
1145=head2 _Value
1146
1147Takes the name of a table column.
1148Returns its value as a string, if the user passes an ACL check
1149
1150=cut
1151
1152sub _Value {
1153    my $self  = shift;
1154    return undef unless $self->id;
1155
1156    # we need to do the rights check
1157    unless ( $self->CurrentUserCanSee ) {
1158        $RT::Logger->debug(
1159            "Permission denied. User #". $self->CurrentUser->id
1160            ." has no SeeCustomField right on CF #". $self->id
1161        );
1162        return (undef);
1163    }
1164    return $self->__Value( @_ );
1165}
1166
1167
1168=head2 SetDisabled
1169
1170Takes a boolean.
11711 will cause this custom field to no longer be avaialble for objects.
11720 will re-enable this field.
1173
1174=cut
1175
1176sub SetDisabled {
1177    my $self = shift;
1178    my $val = shift;
1179
1180    my ($status, $msg) = $self->_Set(Field => 'Disabled', Value => $val);
1181
1182    unless ($status) {
1183        return ($status, $msg);
1184    }
1185
1186    # Set to the end of the sort list when re-enabling to prevent duplicate
1187    # sort order values.
1188    if ( $val == 0 ) {
1189        my $ocfs = RT::ObjectCustomFields->new( $self->CurrentUser );
1190        $ocfs->LimitToCustomField( $self->id );
1191
1192        while ( my $ocf = $ocfs->Next ) {
1193            my $last_object = $ocf->LastSibling || $ocf;
1194
1195            # no need to update if it's already the last one.
1196            my $need_update;
1197            if ( $ocf->id != $last_object->id ) {
1198                $need_update = 1;
1199            }
1200            else {
1201
1202                # can't use IsSortOrderShared because it always returns 0 for
1203                # global cfs no matter if SortOrder is shared or not
1204
1205                my $neighbors = $last_object->Neighbors;
1206                $neighbors->Limit( FIELD => 'id', OPERATOR => '!=', VALUE => $last_object->id );
1207                $neighbors->Limit( FIELD => 'SortOrder', VALUE => $last_object->SortOrder );
1208                $need_update = 1 if $neighbors->Count;
1209            }
1210
1211            if ( $need_update ) {
1212                my $sort = $last_object->SortOrder + 1;
1213                my ( $status, $msg ) = $ocf->SetSortOrder( $sort );
1214                if ( $status ) {
1215                    RT::Logger->debug( "Set Sort Order to $sort for Object Custom Field " . $ocf->Id );
1216                }
1217                else {
1218                    RT->Logger->error(
1219                        "Failed to set Sort Order to $sort for ObjectCustomField " . $ocf->id . ": $msg" );
1220                }
1221            }
1222        }
1223    }
1224
1225    if ( $val == 1 ) {
1226        return (1, $self->loc("Disabled"));
1227    } else {
1228        return (1, $self->loc("Enabled"));
1229    }
1230}
1231
1232=head2 SetTypeComposite
1233
1234Set this custom field's type and maximum values as a composite value
1235
1236=cut
1237
1238sub SetTypeComposite {
1239    my $self = shift;
1240    my $composite = shift;
1241
1242    my $old = $self->TypeComposite;
1243
1244    my ($type, $max_values) = split(/-/, $composite, 2);
1245    if ( $type ne $self->Type ) {
1246        my ($status, $msg) = $self->SetType( $type );
1247        return ($status, $msg) unless $status;
1248    }
1249    if ( ($max_values || 0) != ($self->MaxValues || 0) ) {
1250        my ($status, $msg) = $self->SetMaxValues( $max_values );
1251        return ($status, $msg) unless $status;
1252    }
1253    my $render = $self->RenderType;
1254    if ( $render and not grep { $_ eq $render } $self->RenderTypes ) {
1255        # We switched types and our render type is no longer valid, so unset it
1256        # and use the default
1257        $self->SetRenderType( undef );
1258    }
1259    return 1, $self->loc(
1260        "Type changed from '[_1]' to '[_2]'",
1261        $self->FriendlyTypeComposite( $old ),
1262        $self->FriendlyTypeComposite( $composite ),
1263    );
1264}
1265
1266=head2 TypeComposite
1267
1268Returns a composite value composed of this object's type and maximum values
1269
1270=cut
1271
1272
1273sub TypeComposite {
1274    my $self = shift;
1275    return join '-', ($self->Type || ''), ($self->MaxValues || 0);
1276}
1277
1278=head2 TypeComposites
1279
1280Returns an array of all possible composite values for custom fields.
1281
1282=cut
1283
1284sub TypeComposites {
1285    my $self = shift;
1286    return grep !/(?:[Tt]ext|Combobox|Date|DateTime)-0/, map { ("$_-1", "$_-0") } $self->Types;
1287}
1288
1289=head2 RenderType
1290
1291Returns the type of form widget to render for this custom field.  Currently
1292this only affects fields which return true for L</HasRenderTypes>.
1293
1294=cut
1295
1296sub RenderType {
1297    my $self = shift;
1298    return '' unless $self->HasRenderTypes;
1299
1300    return $self->_Value( 'RenderType', @_ )
1301        || $self->DefaultRenderType;
1302}
1303
1304=head2 SetRenderType TYPE
1305
1306Sets this custom field's render type.
1307
1308=cut
1309
1310sub SetRenderType {
1311    my $self = shift;
1312    my $type = shift;
1313    return (0, $self->loc("This custom field has no Render Types"))
1314        unless $self->HasRenderTypes;
1315
1316    if ( !$type || $type eq $self->DefaultRenderType ) {
1317        return $self->_Set( Field => 'RenderType', Value => undef, @_ );
1318    }
1319
1320    if ( not grep { $_ eq $type } $self->RenderTypes ) {
1321        return (0, $self->loc("Invalid Render Type for custom field of type [_1]",
1322                                $self->FriendlyType));
1323    }
1324
1325    return $self->_Set( Field => 'RenderType', Value => $type, @_ );
1326}
1327
1328=head2 DefaultRenderType [TYPE COMPOSITE]
1329
1330Returns the default render type for this custom field's type or the TYPE
1331COMPOSITE specified as an argument.
1332
1333=cut
1334
1335sub DefaultRenderType {
1336    my $self = shift;
1337    my $composite    = @_ ? shift : $self->TypeComposite;
1338    my ($type, $max) = split /-/, $composite, 2;
1339    return unless $type and $self->HasRenderTypes($composite);
1340    return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0];
1341}
1342
1343=head2 HasRenderTypes [TYPE_COMPOSITE]
1344
1345Returns a boolean value indicating whether the L</RenderTypes> and
1346L</RenderType> methods make sense for this custom field.
1347
1348Currently true only for type C<Select>.
1349
1350=cut
1351
1352sub HasRenderTypes {
1353    my $self = shift;
1354    my ($type, $max) = split /-/, (@_ ? shift : $self->TypeComposite), 2;
1355    return undef unless $type;
1356    return defined $FieldTypes{$type}->{render_types}
1357        ->{ $max == 1 ? 'single' : 'multiple' };
1358}
1359
1360=head2 RenderTypes [TYPE COMPOSITE]
1361
1362Returns the valid render types for this custom field's type or the TYPE
1363COMPOSITE specified as an argument.
1364
1365=cut
1366
1367sub RenderTypes {
1368    my $self = shift;
1369    my $composite    = @_ ? shift : $self->TypeComposite;
1370    my ($type, $max) = split /-/, $composite, 2;
1371    return unless $type and $self->HasRenderTypes($composite);
1372    return @{$FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }};
1373}
1374
1375=head2 SetLookupType
1376
1377Autrijus: care to doc how LookupTypes work?
1378
1379=cut
1380
1381sub SetLookupType {
1382    my $self = shift;
1383    my $lookup = shift;
1384    if ( $lookup ne $self->LookupType ) {
1385        # Okay... We need to invalidate our existing relationships
1386        RT::ObjectCustomField->new($self->CurrentUser)->DeleteAll( CustomField => $self );
1387    }
1388    return $self->_Set(Field => 'LookupType', Value =>$lookup);
1389}
1390
1391=head2 LookupTypes
1392
1393Returns an array of LookupTypes available
1394
1395=cut
1396
1397
1398sub LookupTypes {
1399    my $self = shift;
1400    return sort keys %FRIENDLY_LOOKUP_TYPES;
1401}
1402
1403=head2 FriendlyLookupType
1404
1405Returns a localized description of the type of this custom field
1406
1407=cut
1408
1409sub FriendlyLookupType {
1410    my $self = shift;
1411    my $lookup = shift || $self->LookupType;
1412
1413    return ($self->loc( $FRIENDLY_LOOKUP_TYPES{$lookup} ))
1414        if defined $FRIENDLY_LOOKUP_TYPES{$lookup};
1415
1416    my @types = map { s/^RT::// ? $self->loc($_) : $_ }
1417      grep { defined and length }
1418      split( /-/, $lookup )
1419      or return;
1420
1421    state $LocStrings = [
1422        "[_1] objects",            # loc
1423        "[_1]'s [_2] objects",        # loc
1424        "[_1]'s [_2]'s [_3] objects",   # loc
1425    ];
1426    return ( $self->loc( $LocStrings->[$#types], @types ) );
1427}
1428
1429=head1 RecordClassFromLookupType
1430
1431Returns the type of Object referred to by ObjectCustomFields' ObjectId column
1432
1433Optionally takes a LookupType to use instead of using the value on the loaded
1434record.  In this case, the method may be called on the class instead of an
1435object.
1436
1437=cut
1438
1439sub RecordClassFromLookupType {
1440    my $self = shift;
1441    my $type = shift || $self->LookupType;
1442    my ($class) = ($type =~ /^([^-]+)/);
1443    unless ( $class ) {
1444        if (blessed($self) and $self->LookupType eq $type) {
1445            $RT::Logger->error(
1446                "Custom Field #". $self->id
1447                ." has incorrect LookupType '$type'"
1448            );
1449        } else {
1450            RT->Logger->error("Invalid LookupType passed as argument: $type");
1451        }
1452        return undef;
1453    }
1454    return $class;
1455}
1456
1457=head1 ObjectTypeFromLookupType
1458
1459Returns the ObjectType used in ObjectCustomFieldValues rows for this CF
1460
1461Optionally takes a LookupType to use instead of using the value on the loaded
1462record.  In this case, the method may be called on the class instead of an
1463object.
1464
1465=cut
1466
1467sub ObjectTypeFromLookupType {
1468    my $self = shift;
1469    my $type = shift || $self->LookupType;
1470    my ($class) = ($type =~ /([^-]+)$/);
1471    unless ( $class ) {
1472        if (blessed($self) and $self->LookupType eq $type) {
1473            $RT::Logger->error(
1474                "Custom Field #". $self->id
1475                ." has incorrect LookupType '$type'"
1476            );
1477        } else {
1478            RT->Logger->error("Invalid LookupType passed as argument: $type");
1479        }
1480        return undef;
1481    }
1482    return $class;
1483}
1484
1485sub CollectionClassFromLookupType {
1486    my $self = shift;
1487    my $record_class = shift || $self->RecordClassFromLookupType;
1488
1489    return undef unless $record_class;
1490
1491    my $collection_class;
1492    if ( UNIVERSAL::can($record_class.'Collection', 'new') ) {
1493        $collection_class = $record_class.'Collection';
1494    } elsif ( UNIVERSAL::can($record_class.'es', 'new') ) {
1495        $collection_class = $record_class.'es';
1496    } elsif ( UNIVERSAL::can($record_class.'s', 'new') ) {
1497        $collection_class = $record_class.'s';
1498    } else {
1499        $RT::Logger->error("Can not find a collection class for record class '$record_class'");
1500        return undef;
1501    }
1502    return $collection_class;
1503}
1504
1505=head2 Groupings
1506
1507Returns a (sorted and lowercased) list of the groupings in which this custom
1508field appears.
1509
1510If called on a loaded object, the returned list is limited to groupings which
1511apply to the record class this CF applies to (L</RecordClassFromLookupType>).
1512
1513If passed a loaded object or a class name, the returned list is limited to
1514groupings which apply to the class of the object or the specified class.
1515
1516If called on an unloaded object, all potential groupings are returned.
1517
1518=cut
1519
1520sub Groupings {
1521    my $self = shift;
1522    my $record_class = $self->_GroupingClass(shift);
1523
1524    my $config = RT->Config->Get('CustomFieldGroupings');
1525       $config = {} unless ref($config) eq 'HASH';
1526
1527    my @groups;
1528    if ( $record_class ) {
1529        push @groups, sort {lc($a) cmp lc($b)} keys %{ $BUILTIN_GROUPINGS{$record_class} || {} };
1530        if ( ref($config->{$record_class} ||= []) eq "ARRAY") {
1531            my @order = @{ $config->{$record_class} };
1532            while (@order) {
1533                push @groups, shift(@order);
1534                shift(@order);
1535            }
1536        } else {
1537            @groups = sort {lc($a) cmp lc($b)} keys %{ $config->{$record_class} };
1538        }
1539    } else {
1540        my %all = (%$config, %BUILTIN_GROUPINGS);
1541        @groups = sort {lc($a) cmp lc($b)} map {$self->Groupings($_)} grep {$_} keys(%all);
1542    }
1543
1544    my %seen;
1545    return
1546        grep defined && length && !$seen{lc $_}++,
1547        @groups;
1548}
1549
1550=head2 CustomGroupings
1551
1552Identical to L</Groupings> but filters out built-in groupings from the the
1553returned list.
1554
1555=cut
1556
1557sub CustomGroupings {
1558    my $self = shift;
1559    my $record_class = $self->_GroupingClass(shift);
1560    return grep !$BUILTIN_GROUPINGS{$record_class}{$_}, $self->Groupings( $record_class );
1561}
1562
1563sub _GroupingClass {
1564    my $self    = shift;
1565    my $record  = shift;
1566
1567    my $record_class = ref($record) || $record || '';
1568    $record_class = $self->RecordClassFromLookupType
1569        if !$record_class and blessed($self) and $self->id;
1570
1571    return $record_class;
1572}
1573
1574=head2 RegisterBuiltInGroupings
1575
1576Registers groupings to be considered a fundamental part of RT, either via use
1577in core RT or via an extension.  These groupings must be rendered explicitly in
1578Mason by specific calls to F</Elements/ShowCustomFields> and
1579F</Elements/EditCustomFields>.  They will not show up automatically on normal
1580display pages like configured custom groupings.
1581
1582Takes a set of key-value pairs of class names (valid L<RT::Record> subclasses)
1583and array refs of grouping names to consider built-in.
1584
1585If a class already contains built-in groupings (such as L<RT::Ticket> and
1586L<RT::User>), new groupings are appended.
1587
1588=cut
1589
1590sub RegisterBuiltInGroupings {
1591    my $self = shift;
1592    my %new  = @_;
1593
1594    while (my ($k,$v) = each %new) {
1595        $v = [$v] unless ref($v) eq 'ARRAY';
1596        $BUILTIN_GROUPINGS{$k} = {
1597            %{$BUILTIN_GROUPINGS{$k} || {}},
1598            map { $_ => 1 } @$v
1599        };
1600    }
1601    $BUILTIN_GROUPINGS{''} = { map { %$_ } values %BUILTIN_GROUPINGS  };
1602}
1603
1604=head1 IsOnlyGlobal
1605
1606Certain custom fields (users, groups) should only be added globally;
1607codify that set here for reference.
1608
1609=cut
1610
1611sub IsOnlyGlobal {
1612    my $self = shift;
1613
1614    return ($self->LookupType =~ /^RT::(?:Group|User)/io);
1615
1616}
1617
1618=head1 AddedTo
1619
1620Returns collection with objects this custom field is added to.
1621Class of the collection depends on L</LookupType>.
1622See all L</NotAddedTo> .
1623
1624Doesn't takes into account if object is added globally.
1625
1626=cut
1627
1628sub AddedTo {
1629    my $self = shift;
1630    return RT::ObjectCustomField->new( $self->CurrentUser )
1631        ->AddedTo( CustomField => $self );
1632}
1633
1634=head1 NotAddedTo
1635
1636Returns collection with objects this custom field is not added to.
1637Class of the collection depends on L</LookupType>.
1638See all L</AddedTo> .
1639
1640Doesn't take into account if the object is added globally.
1641
1642=cut
1643
1644sub NotAddedTo {
1645    my $self = shift;
1646    return RT::ObjectCustomField->new( $self->CurrentUser )
1647        ->NotAddedTo( CustomField => $self );
1648}
1649
1650=head2 IsAdded
1651
1652Takes object id and returns corresponding L<RT::ObjectCustomField>
1653record if this custom field is added to the object. Use 0 to check
1654if custom field is added globally.
1655
1656=cut
1657
1658sub IsAdded {
1659    my $self = shift;
1660    my $id = shift;
1661    my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1662    $ocf->LoadByCols( CustomField => $self->id, ObjectId => $id || 0 );
1663    return undef unless $ocf->id;
1664    return $ocf;
1665}
1666
1667sub IsGlobal { return shift->IsAdded(0) }
1668
1669=head2 IsAddedToAny
1670
1671Returns true if custom field is applied to any object.
1672
1673=cut
1674
1675sub IsAddedToAny {
1676    my $self = shift;
1677    my $id = shift;
1678    my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1679    $ocf->LoadByCols( CustomField => $self->id );
1680    return $ocf->id ? 1 : 0;
1681}
1682
1683=head2 AddToObject OBJECT
1684
1685Add this custom field as a custom field for a single object, such as a queue or group.
1686
1687Takes an object
1688
1689=cut
1690
1691sub AddToObject {
1692    my $self  = shift;
1693    my $object = shift;
1694    my $id = $object->Id || 0;
1695
1696    unless (index($self->LookupType, ref($object)) == 0) {
1697        return ( 0, $self->loc('Lookup type mismatch') );
1698    }
1699
1700    unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1701        return ( 0, $self->loc('Permission Denied') );
1702    }
1703
1704    my $ocf = RT::ObjectCustomField->new( $self->CurrentUser );
1705    my $oid = $ocf->Add(
1706        CustomField => $self->id, ObjectId => $id,
1707    );
1708
1709    my $msg;
1710    # If object has no id, it represents all objects
1711    if ($object->id) {
1712        $msg = $self->loc( 'Added custom field [_1] to [_2].', $self->Name, $object->Name );
1713    } else {
1714        $msg = $self->loc( 'Globally added custom field [_1].', $self->Name );
1715    }
1716
1717    return ( $oid, $msg );
1718}
1719
1720
1721=head2 RemoveFromObject OBJECT
1722
1723Remove this custom field  for a single object, such as a queue or group.
1724
1725Takes an object
1726
1727=cut
1728
1729sub RemoveFromObject {
1730    my $self = shift;
1731    my $object = shift;
1732    my $id = $object->Id || 0;
1733
1734    unless (index($self->LookupType, ref($object)) == 0) {
1735        return ( 0, $self->loc('Object type mismatch') );
1736    }
1737
1738    unless ( $object->CurrentUserHasRight('AssignCustomFields') ) {
1739        return ( 0, $self->loc('Permission Denied') );
1740    }
1741
1742    my $ocf = $self->IsAdded( $id );
1743    unless ( $ocf ) {
1744        return ( 0, $self->loc("This custom field cannot be added to that object") );
1745    }
1746
1747    my ($ok, $msg) = $ocf->Delete;
1748    return ($ok, $msg) unless $ok;
1749
1750    # If object has no id, it represents all objects
1751    if ($object->id) {
1752        return (1, $self->loc( 'Removed custom field [_1] from [_2].', $self->Name, $object->Name ) );
1753    } else {
1754        return (1, $self->loc( 'Globally removed custom field [_1].', $self->Name ) );
1755    }
1756}
1757
1758
1759=head2 AddValueForObject HASH
1760
1761Adds a custom field value for a record object of some kind.
1762Takes a param hash of
1763
1764Required:
1765
1766    Object
1767    Content
1768
1769Optional:
1770
1771    LargeContent
1772    ContentType
1773
1774=cut
1775
1776sub AddValueForObject {
1777    my $self = shift;
1778    my %args = (
1779        Object       => undef,
1780        Content      => undef,
1781        LargeContent => undef,
1782        ContentType  => undef,
1783        ForCreation  => 0,
1784        @_
1785    );
1786    my $obj = $args{'Object'} or return ( 0, $self->loc('Invalid object') );
1787
1788    unless (
1789        $self->CurrentUserHasRight('ModifyCustomField') ||
1790        ($args{ForCreation} && $self->CurrentUserHasRight('SetInitialCustomField'))
1791    ) {
1792        return ( 0, $self->loc('Permission Denied') );
1793    }
1794
1795    unless ( $self->MatchPattern($args{'Content'}) ) {
1796        return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
1797    }
1798
1799    $RT::Handle->BeginTransaction;
1800
1801    if ( $self->MaxValues ) {
1802        my $current_values = $self->ValuesForObject($obj);
1803
1804        # (The +1 is for the new value we're adding)
1805        my $extra_values = ( $current_values->Count + 1 ) - $self->MaxValues;
1806
1807
1808        # Could have a negative value if MaxValues is greater than count
1809        if ( $extra_values > 0 ) {
1810
1811            # If we have a set of current values and we've gone over the maximum
1812            # allowed number of values, we'll need to delete some to make room.
1813            # which former values are blown away is not guaranteed
1814
1815            while ($extra_values) {
1816                my $extra_item = $current_values->Next;
1817                unless ( $extra_item && $extra_item->id ) {
1818                    $RT::Logger->crit( "We were just asked to delete "
1819                        ."a custom field value that doesn't exist!" );
1820                    $RT::Handle->Rollback();
1821                    return (undef);
1822                }
1823                $extra_item->Delete;
1824                $extra_values--;
1825            }
1826        }
1827    }
1828
1829    if ($self->UniqueValues) {
1830        my $class = $self->CollectionClassFromLookupType($self->ObjectTypeFromLookupType);
1831        my $collection = $class->new(RT->SystemUser);
1832        $collection->LimitCustomField(CUSTOMFIELD => $self->Id, OPERATOR => '=', VALUE => $args{'LargeContent'} // $args{'Content'});
1833
1834        if ($collection->Count) {
1835            $RT::Logger->debug( "Non-unique custom field value for CF #" . $self->Id ." with object custom field value " . $collection->First->Id );
1836            $RT::Handle->Rollback();
1837            return ( 0, $self->loc('That is not a unique value') );
1838        }
1839    }
1840
1841    my $newval = RT::ObjectCustomFieldValue->new( $self->CurrentUser );
1842    my ($val, $msg) = $newval->Create(
1843        ObjectType   => ref($obj),
1844        ObjectId     => $obj->Id,
1845        Content      => $args{'Content'},
1846        LargeContent => $args{'LargeContent'},
1847        ContentType  => $args{'ContentType'},
1848        CustomField  => $self->Id
1849    );
1850
1851    unless ($val) {
1852        $RT::Handle->Rollback();
1853        return ($val, $self->loc("Couldn't create record: [_1]", $msg));
1854    }
1855
1856    $RT::Handle->Commit();
1857    return ($val);
1858
1859}
1860
1861
1862sub _CanonicalizeValue {
1863    my $self = shift;
1864    my $args = shift;
1865
1866    my $type = $self->__Value('Type');
1867    return 1 unless $type;
1868
1869    $self->_CanonicalizeValueWithCanonicalizer($args);
1870
1871    my $method = '_CanonicalizeValue'. $type;
1872    return 1 unless $self->can($method);
1873    $self->$method($args);
1874}
1875
1876sub _CanonicalizeValueWithCanonicalizer {
1877    my $self = shift;
1878    my $args = shift;
1879
1880    my $class = $self->__Value('CanonicalizeClass') or return 1;
1881
1882    $class->require or die "Can't load $class: $@";
1883    my $canonicalizer = $class->new($self->CurrentUser);
1884
1885    $args->{'Content'} = $canonicalizer->CanonicalizeValue(
1886        CustomField => $self,
1887        Content     => $args->{'Content'},
1888    );
1889
1890    return 1;
1891}
1892
1893sub _CanonicalizeValueDateTime {
1894    my $self    = shift;
1895    my $args    = shift;
1896    my $DateObj = RT::Date->new( $self->CurrentUser );
1897    $DateObj->Set( Format => 'unknown',
1898                   Value  => $args->{'Content'} );
1899    $args->{'Content'} = $DateObj->ISO;
1900    return 1;
1901}
1902
1903# For date, we need to store Content as ISO date
1904sub _CanonicalizeValueDate {
1905    my $self = shift;
1906    my $args = shift;
1907
1908    # in case user input date with time, let's omit it by setting timezone
1909    # to utc so "hour" won't affect "day"
1910    my $DateObj = RT::Date->new( $self->CurrentUser );
1911    $DateObj->Set( Format   => 'unknown',
1912                   Value    => $args->{'Content'},
1913                 );
1914    $args->{'Content'} = $DateObj->Date( Timezone => 'user' );
1915    return 1;
1916}
1917
1918sub _CanonicalizeValueIPAddress {
1919    my $self = shift;
1920    my $args = shift;
1921
1922    $args->{Content} = RT::ObjectCustomFieldValue->ParseIP( $args->{Content} );
1923    return (0, $self->loc("Content is not a valid IP address"))
1924        unless $args->{Content};
1925    return 1;
1926}
1927
1928sub _CanonicalizeValueIPAddressRange {
1929    my $self = shift;
1930    my $args = shift;
1931
1932    my $content = $args->{Content};
1933    $content .= "-".$args->{LargeContent} if $args->{LargeContent};
1934
1935    ($args->{Content}, $args->{LargeContent})
1936        = RT::ObjectCustomFieldValue->ParseIPRange( $content );
1937
1938    $args->{ContentType} = 'text/plain';
1939    return (0, $self->loc("Content is not a valid IP address range"))
1940        unless $args->{Content};
1941    return 1;
1942}
1943
1944=head2 MatchPattern STRING
1945
1946Tests the incoming string against the Pattern of this custom field object
1947and returns a boolean; returns true if the Pattern is empty.
1948
1949=cut
1950
1951sub MatchPattern {
1952    my $self = shift;
1953    my $regex = $self->Pattern or return 1;
1954
1955    return (( defined $_[0] ? $_[0] : '') =~ $regex);
1956}
1957
1958
1959
1960
1961=head2 FriendlyPattern
1962
1963Prettify the pattern of this custom field, by taking the text in C<(?#text)>
1964and localizing it.
1965
1966=cut
1967
1968sub FriendlyPattern {
1969    my $self = shift;
1970    my $regex = $self->Pattern;
1971
1972    return '' unless length $regex;
1973    if ( $regex =~ /\(\?#([^)]*)\)/ ) {
1974        return '[' . $self->loc($1) . ']';
1975    }
1976    else {
1977        return $regex;
1978    }
1979}
1980
1981
1982
1983
1984=head2 DeleteValueForObject HASH
1985
1986Deletes a custom field value for a ticket. Takes a param hash of Object and Content
1987
1988Returns a tuple of (STATUS, MESSAGE). If the call succeeded, the STATUS is true. otherwise it's false
1989
1990=cut
1991
1992sub DeleteValueForObject {
1993    my $self = shift;
1994    my %args = ( Object => undef,
1995                 Content => undef,
1996                 Id => undef,
1997             @_ );
1998
1999
2000    unless ($self->CurrentUserHasRight('ModifyCustomField')) {
2001        return (0, $self->loc('Permission Denied'));
2002    }
2003
2004    my $oldval = RT::ObjectCustomFieldValue->new($self->CurrentUser);
2005
2006    if (my $id = $args{'Id'}) {
2007        $oldval->Load($id);
2008    }
2009    unless ($oldval->id) {
2010        $oldval->LoadByObjectContentAndCustomField(
2011            Object => $args{'Object'},
2012            Content =>  $args{'Content'},
2013            CustomField => $self->Id,
2014        );
2015    }
2016
2017
2018    # check to make sure we found it
2019    unless ($oldval->Id) {
2020        return(0, $self->loc("Custom field value [_1] could not be found for custom field [_2]", $args{'Content'}, $self->Name));
2021    }
2022
2023    # for single-value fields, we need to validate that empty string is a valid value for it
2024    if ( $self->SingleValue and not $self->MatchPattern( '' ) ) {
2025        return ( 0, $self->loc('Input must match [_1]', $self->FriendlyPattern) );
2026    }
2027
2028    # delete it
2029
2030    my ($ok, $msg) = $oldval->Delete();
2031    unless ($ok) {
2032        return(0, $self->loc("Custom field value could not be deleted"));
2033    }
2034    return($oldval->Id, $self->loc("Custom field value deleted"));
2035}
2036
2037
2038=head2 ValuesForObject OBJECT
2039
2040Return an L<RT::ObjectCustomFieldValues> object containing all of this custom field's values for OBJECT
2041
2042=cut
2043
2044sub ValuesForObject {
2045    my $self = shift;
2046    my $object = shift;
2047
2048    my $values = RT::ObjectCustomFieldValues->new($self->CurrentUser);
2049    unless ($self->id and $self->CurrentUserCanSee) {
2050        # Return an empty object if they have no rights to see
2051        $values->Limit( FIELD => "id", VALUE => 0, SUBCLAUSE => "ACL" );
2052        return ($values);
2053    }
2054
2055    $values->LimitToCustomField($self->Id);
2056    $values->LimitToObject($object);
2057
2058    return ($values);
2059}
2060
2061=head2 CurrentUserCanSee
2062
2063If the user has SeeCustomField they can see this custom field and its details.
2064
2065Otherwise, if the user has SetInitialCustomField and this is being used in a
2066"create" context, then they can see this custom field and its details. This
2067allows you to set up custom fields that are only visible on create pages and
2068are then inaccessible.
2069
2070=cut
2071
2072sub CurrentUserCanSee {
2073    my $self = shift;
2074    return 1 if $self->CurrentUserHasRight('SeeCustomField');
2075
2076    return 1 if $self->{include_set_initial}
2077             && $self->CurrentUserHasRight('SetInitialCustomField');
2078
2079    return 0;
2080}
2081
2082=head2 RegisterLookupType LOOKUPTYPE FRIENDLYNAME
2083
2084Tell RT that a certain object accepts custom fields via a lookup type and
2085provide a friendly name for such CFs.
2086
2087Examples:
2088
2089    'RT::Queue-RT::Ticket'                 => "Tickets",                # loc
2090    'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions",    # loc
2091    'RT::User'                             => "Users",                  # loc
2092    'RT::Group'                            => "Groups",                 # loc
2093    'RT::Queue'                            => "Queues",                 # loc
2094
2095This is a class method.
2096
2097=cut
2098
2099sub RegisterLookupType {
2100    my $self = shift;
2101    my $path = shift;
2102    my $friendly_name = shift;
2103
2104    $FRIENDLY_LOOKUP_TYPES{$path} = $friendly_name;
2105}
2106
2107=head2 IncludeContentForValue [VALUE] (and SetIncludeContentForValue)
2108
2109Gets or sets the  C<IncludeContentForValue> for this custom field. RT
2110uses this field to automatically include content into the user's browser
2111as they display records with custom fields in RT.
2112
2113=cut
2114
2115sub SetIncludeContentForValue {
2116    shift->IncludeContentForValue(@_);
2117}
2118sub IncludeContentForValue{
2119    my $self = shift;
2120    $self->_URLTemplate('IncludeContentForValue', @_);
2121}
2122
2123
2124
2125=head2 LinkValueTo [VALUE] (and SetLinkValueTo)
2126
2127Gets or sets the  C<LinkValueTo> for this custom field. RT
2128uses this field to make custom field values into hyperlinks in the user's
2129browser as they display records with custom fields in RT.
2130
2131=cut
2132
2133
2134sub SetLinkValueTo {
2135    shift->LinkValueTo(@_);
2136}
2137
2138sub LinkValueTo {
2139    my $self = shift;
2140    $self->_URLTemplate('LinkValueTo', @_);
2141
2142}
2143
2144
2145=head2 _URLTemplate  NAME [VALUE]
2146
2147With one argument, returns the _URLTemplate named C<NAME>, but only if
2148the current user has the right to see this custom field.
2149
2150With two arguments, attemptes to set the relevant template value.
2151
2152=cut
2153
2154sub _URLTemplate {
2155    my $self          = shift;
2156    my $template_name = shift;
2157    if (@_) {
2158
2159        my $value = shift;
2160        unless ( $self->CurrentUserHasRight('AdminCustomField') ) {
2161            return ( 0, $self->loc('Permission Denied') );
2162        }
2163        if (length $value and defined $value) {
2164            $self->SetAttribute( Name => $template_name, Content => $value );
2165        } else {
2166            $self->DeleteAttribute( $template_name );
2167        }
2168        return ( 1, $self->loc('Updated') );
2169    } else {
2170        unless ( $self->id && $self->CurrentUserCanSee ) {
2171            return (undef);
2172        }
2173
2174        my ($attr) = $self->Attributes->Named($template_name);
2175        return undef unless $attr;
2176        return $attr->Content;
2177    }
2178}
2179
2180sub SetBasedOn {
2181    my $self = shift;
2182    my $value = shift;
2183
2184    return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
2185        unless defined $value and length $value;
2186
2187    my $cf = RT::CustomField->new( $self->CurrentUser );
2188    $cf->SetContextObject( $self->ContextObject );
2189    $cf->Load( ref $value ? $value->id : $value );
2190
2191    return (0, "Permission Denied")
2192        unless $cf->id && $cf->CurrentUserCanSee;
2193
2194    # XXX: Remove this restriction once we support lists and cascaded selects
2195    if ( $self->RenderType =~ /List/ ) {
2196        return (0, $self->loc("We can't currently render as a List when basing categories on another custom field.  Please use another render type."));
2197    }
2198
2199    return $self->_Set( Field => 'BasedOn', Value => $value, @_ )
2200}
2201
2202sub BasedOnObj {
2203    my $self = shift;
2204
2205    my $obj = RT::CustomField->new( $self->CurrentUser );
2206    $obj->SetContextObject( $self->ContextObject );
2207    if ( $self->BasedOn ) {
2208        $obj->Load( $self->BasedOn );
2209    }
2210    return $obj;
2211}
2212
2213
2214sub SupportDefaultValues {
2215    my $self = shift;
2216    return 0 unless $self->id;
2217    return 0 unless $self->LookupType =~ /RT::(?:Ticket|Transaction|Asset)$/;
2218    return $self->Type !~ /^(?:Image|Binary)$/;
2219}
2220
2221sub DefaultValues {
2222    my $self = shift;
2223    my %args = (
2224        Object => RT->System,
2225        @_,
2226    );
2227    my $attr = $args{Object}->FirstAttribute('CustomFieldDefaultValues');
2228    my $values;
2229    $values = $attr->Content->{$self->id} if $attr && $attr->Content;
2230    return $values if defined $values;
2231
2232    if ( !$args{Object}->isa( 'RT::System' ) ) {
2233        my $system_attr = RT::System->FirstAttribute( 'CustomFieldDefaultValues' );
2234        $values = $system_attr->Content->{$self->id} if $system_attr && $system_attr->Content;
2235        return $values if defined $values;
2236    }
2237    return undef;
2238}
2239
2240sub SetDefaultValues {
2241    my $self = shift;
2242    my %args = (
2243        Object => RT->System,
2244        Values => undef,
2245        @_,
2246    );
2247    my $attr = $args{Object}->FirstAttribute( 'CustomFieldDefaultValues' );
2248    my ( $old_values, $old_content, $new_values );
2249    if ( $attr && $attr->Content ) {
2250        $old_content = $attr->Content;
2251        $old_values = $old_content->{ $self->id };
2252    }
2253
2254    if ( !$args{Object}->isa( 'RT::System' ) && !defined $old_values ) {
2255        my $system_attr = RT::System->FirstAttribute( 'CustomFieldDefaultValues' );
2256        if ( $system_attr && $system_attr->Content ) {
2257            $old_values = $system_attr->Content->{ $self->id };
2258        }
2259    }
2260
2261    if ( defined $old_values && length $old_values ) {
2262        $old_values = join ', ', @$old_values if ref $old_values eq 'ARRAY';
2263    }
2264
2265    $new_values = $args{Values};
2266    if ( defined $new_values && length $new_values ) {
2267        $new_values = join ', ', @$new_values if ref $new_values eq 'ARRAY';
2268    }
2269
2270    return 1 if ( $new_values // '' ) eq ( $old_values // '' );
2271
2272    my ($ret, $msg) = $args{Object}->SetAttribute(
2273        Name    => 'CustomFieldDefaultValues',
2274        Content => {
2275            %{ $old_content || {} }, $self->id => $args{Values},
2276        },
2277    );
2278
2279    $old_values = $self->loc('(no value)') unless defined $old_values && length $old_values;
2280    $new_values = $self->loc( '(no value)' ) unless defined $new_values && length $new_values;
2281
2282    if ( $ret ) {
2283        return ( $ret, $self->loc( 'Default values changed from [_1] to [_2]', $old_values, $new_values ) );
2284    }
2285    else {
2286        return ( $ret, $self->loc( "Can't change default values from [_1] to [_2]: [_3]", $old_values, $new_values, $msg ) );
2287    }
2288}
2289
2290sub CleanupDefaultValues {
2291    my $self  = shift;
2292    my $attrs = RT::Attributes->new( $self->CurrentUser );
2293    $attrs->Limit( FIELD => 'Name', VALUE => 'CustomFieldDefaultValues' );
2294
2295    my @values;
2296    if ( $self->Type eq 'Select' ) {
2297        # Select has a limited list valid values, we need to exclude invalid ones
2298        @values = map { $_->Name } @{ $self->Values->ItemsArrayRef || [] };
2299    }
2300
2301    while ( my $attr = $attrs->Next ) {
2302        my $content = $attr->Content;
2303        next unless $content;
2304        my $changed;
2305        if ( $self->SupportDefaultValues ) {
2306            if ( $self->MaxValues == 1 && ref $content->{ $self->id } eq 'ARRAY' ) {
2307                $content->{ $self->id } = $content->{ $self->id }[ 0 ];
2308                $changed = 1;
2309            }
2310
2311            my $default_values = $content->{ $self->id };
2312            if ( $default_values ) {
2313                if ( $self->Type eq 'Select' ) {
2314                    if ( ref $default_values ne 'ARRAY' && $default_values =~ /\n/ ) {
2315
2316                        # e.g. multiple values Freeform cf has 2 default values: foo and "bar",
2317                        # the values will be stored as "foo\nbar".  so we need to convert it to ARRAY for Select cf.
2318                        # this could happen when we change a Freeform cf into a Select one
2319
2320                        $default_values = [ split /\s*\n+\s*/, $default_values ];
2321                        $content->{ $self->id } = $default_values;
2322                        $changed = 1;
2323                    }
2324
2325                    if ( ref $default_values eq 'ARRAY' ) {
2326                        my @new_defaults;
2327                        for my $default ( @$default_values ) {
2328                            if ( grep { $_ eq $default } @values ) {
2329                                push @new_defaults, $default;
2330                            }
2331                            else {
2332                                $changed = 1;
2333                            }
2334                        }
2335
2336                        $content->{ $self->id } = \@new_defaults if $changed;
2337                    }
2338                    elsif ( !grep { $_ eq $default_values } @values ) {
2339                        delete $content->{ $self->id };
2340                        $changed = 1;
2341                    }
2342                }
2343                else {
2344                    # ARRAY default values only happen for Select cf. we need to convert it to a scalar for other cfs.
2345                    # this could happen when we change a Select cf into a Freeform one
2346
2347                    if ( ref $default_values eq 'ARRAY' ) {
2348                        $content->{ $self->id } = join "\n", @$default_values;
2349                        $changed = 1;
2350                    }
2351
2352                    if ($self->MaxValues == 1) {
2353                        my $args = { Content => $default_values };
2354                        $self->_CanonicalizeValueWithCanonicalizer($args);
2355                        if ($args->{Content} ne $default_values) {
2356                            $content->{ $self->id } = $default_values;
2357                            $changed = 1;
2358                        }
2359                    }
2360                    else {
2361                        my @new_values;
2362                        my $multi_changed = 0;
2363                        for my $value (split /\s*\n+\s*/, $default_values) {
2364                            my $args = { Content => $value };
2365                            $self->_CanonicalizeValueWithCanonicalizer($args);
2366                            push @new_values, $args->{Content};
2367                            $multi_changed = 1 if $args->{Content} ne $value;
2368                        }
2369
2370                        if ($multi_changed) {
2371                            $content->{ $self->id } = join "\n", @new_values;
2372                            $changed = 1;
2373                        }
2374                    }
2375                }
2376            }
2377        }
2378        else {
2379            if ( exists $content->{ $self->id } ) {
2380                delete $content->{ $self->id };
2381                $changed = 1;
2382            }
2383        }
2384        $attr->SetContent( $content ) if $changed;
2385    }
2386}
2387
2388=head2 id
2389
2390Returns the current value of id.
2391(In the database, id is stored as int(11).)
2392
2393
2394=cut
2395
2396
2397=head2 Name
2398
2399Returns the current value of Name.
2400(In the database, Name is stored as varchar(200).)
2401
2402
2403
2404=head2 SetName VALUE
2405
2406
2407Set Name to VALUE.
2408Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2409(In the database, Name will be stored as a varchar(200).)
2410
2411
2412=cut
2413
2414
2415=head2 Type
2416
2417Returns the current value of Type.
2418(In the database, Type is stored as varchar(200).)
2419
2420
2421
2422=head2 SetType VALUE
2423
2424
2425Set Type to VALUE.
2426Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2427(In the database, Type will be stored as a varchar(200).)
2428
2429
2430=cut
2431
2432
2433=head2 RenderType
2434
2435Returns the current value of RenderType.
2436(In the database, RenderType is stored as varchar(64).)
2437
2438
2439
2440=head2 SetRenderType VALUE
2441
2442
2443Set RenderType to VALUE.
2444Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2445(In the database, RenderType will be stored as a varchar(64).)
2446
2447
2448=cut
2449
2450
2451=head2 MaxValues
2452
2453Returns the current value of MaxValues.
2454(In the database, MaxValues is stored as int(11).)
2455
2456
2457
2458=head2 SetMaxValues VALUE
2459
2460
2461Set MaxValues to VALUE.
2462Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2463(In the database, MaxValues will be stored as a int(11).)
2464
2465
2466=cut
2467
2468
2469=head2 Pattern
2470
2471Returns the current value of Pattern.
2472(In the database, Pattern is stored as text.)
2473
2474
2475
2476=head2 SetPattern VALUE
2477
2478
2479Set Pattern to VALUE.
2480Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2481(In the database, Pattern will be stored as a text.)
2482
2483
2484=cut
2485
2486
2487=head2 BasedOn
2488
2489Returns the current value of BasedOn.
2490(In the database, BasedOn is stored as int(11).)
2491
2492
2493
2494=head2 SetBasedOn VALUE
2495
2496
2497Set BasedOn to VALUE.
2498Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2499(In the database, BasedOn will be stored as a int(11).)
2500
2501
2502=cut
2503
2504
2505=head2 Description
2506
2507Returns the current value of Description.
2508(In the database, Description is stored as varchar(255).)
2509
2510
2511
2512=head2 SetDescription VALUE
2513
2514
2515Set Description to VALUE.
2516Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2517(In the database, Description will be stored as a varchar(255).)
2518
2519
2520=cut
2521
2522
2523=head2 SortOrder
2524
2525Returns the current value of SortOrder.
2526(In the database, SortOrder is stored as int(11).)
2527
2528
2529
2530=head2 SetSortOrder VALUE
2531
2532
2533Set SortOrder to VALUE.
2534Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2535(In the database, SortOrder will be stored as a int(11).)
2536
2537
2538=cut
2539
2540
2541=head2 LookupType
2542
2543Returns the current value of LookupType.
2544(In the database, LookupType is stored as varchar(255).)
2545
2546
2547
2548=head2 SetLookupType VALUE
2549
2550
2551Set LookupType to VALUE.
2552Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2553(In the database, LookupType will be stored as a varchar(255).)
2554
2555
2556=cut
2557
2558=head2 SetEntryHint VALUE
2559
2560
2561Set EntryHint to VALUE.
2562Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2563(In the database, EntryHint will be stored as a varchar(255).)
2564
2565
2566=cut
2567
2568
2569=head2 Creator
2570
2571Returns the current value of Creator.
2572(In the database, Creator is stored as int(11).)
2573
2574
2575=cut
2576
2577
2578=head2 Created
2579
2580Returns the current value of Created.
2581(In the database, Created is stored as datetime.)
2582
2583
2584=cut
2585
2586
2587=head2 LastUpdatedBy
2588
2589Returns the current value of LastUpdatedBy.
2590(In the database, LastUpdatedBy is stored as int(11).)
2591
2592
2593=cut
2594
2595
2596=head2 LastUpdated
2597
2598Returns the current value of LastUpdated.
2599(In the database, LastUpdated is stored as datetime.)
2600
2601
2602=cut
2603
2604
2605=head2 Disabled
2606
2607Returns the current value of Disabled.
2608(In the database, Disabled is stored as smallint(6).)
2609
2610
2611
2612=head2 SetDisabled VALUE
2613
2614
2615Set Disabled to VALUE.
2616Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
2617(In the database, Disabled will be stored as a smallint(6).)
2618
2619
2620=cut
2621
2622
2623
2624sub _CoreAccessible {
2625    {
2626
2627        id =>
2628        {read => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2629        Name =>
2630        {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2631        Type =>
2632        {read => 1, write => 1, sql_type => 12, length => 200,  is_blob => 0,  is_numeric => 0,  type => 'varchar(200)', default => ''},
2633        RenderType =>
2634        {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2635        MaxValues =>
2636        {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2637        Pattern =>
2638        {read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'text', default => ''},
2639        ValuesClass =>
2640        {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2641        BasedOn =>
2642        {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => ''},
2643        Description =>
2644        {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2645        SortOrder =>
2646        {read => 1, write => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2647        LookupType =>
2648        {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
2649        EntryHint =>
2650        {read => 1, write => 1, sql_type => 12, length => 255,  is_blob => 0, is_numeric => 0,  type => 'varchar(255)', default => undef },
2651        UniqueValues =>
2652        {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2653        CanonicalizeClass =>
2654        {read => 1, write => 1, sql_type => 12, length => 64,  is_blob => 0,  is_numeric => 0,  type => 'varchar(64)', default => ''},
2655        Creator =>
2656        {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2657        Created =>
2658        {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2659        LastUpdatedBy =>
2660        {read => 1, auto => 1, sql_type => 4, length => 11,  is_blob => 0,  is_numeric => 1,  type => 'int(11)', default => '0'},
2661        LastUpdated =>
2662        {read => 1, auto => 1, sql_type => 11, length => 0,  is_blob => 0,  is_numeric => 0,  type => 'datetime', default => ''},
2663        Disabled =>
2664        {read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
2665
2666 }
2667};
2668
2669sub FindDependencies {
2670    my $self = shift;
2671    my ($walker, $deps) = @_;
2672
2673    $self->SUPER::FindDependencies($walker, $deps);
2674
2675    $deps->Add( out => $self->BasedOnObj )
2676        if $self->BasedOnObj->id;
2677
2678    my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
2679    $applied->LimitToCustomField( $self->id );
2680    $deps->Add( in => $applied );
2681
2682    $deps->Add( in => $self->Values ) if $self->ValuesClass eq "RT::CustomFieldValues";
2683}
2684
2685sub __DependsOn {
2686    my $self = shift;
2687    my %args = (
2688        Shredder => undef,
2689        Dependencies => undef,
2690        @_,
2691    );
2692    my $deps = $args{'Dependencies'};
2693    my $list = [];
2694
2695# Custom field values
2696    push( @$list, $self->Values );
2697
2698# Applications of this CF
2699    my $applied = RT::ObjectCustomFields->new( $self->CurrentUser );
2700    $applied->LimitToCustomField( $self->Id );
2701    push @$list, $applied;
2702
2703# Ticket custom field values
2704    my $objs = RT::ObjectCustomFieldValues->new( $self->CurrentUser );
2705    $objs->LimitToCustomField( $self->Id );
2706    push( @$list, $objs );
2707
2708    $deps->_PushDependencies(
2709        BaseObject => $self,
2710        Flags => RT::Shredder::Constants::DEPENDS_ON,
2711        TargetObjects => $list,
2712        Shredder => $args{'Shredder'}
2713    );
2714    return $self->SUPER::__DependsOn( %args );
2715}
2716
2717=head2 LoadByNameAndCatalog
2718
2719Loads the described asset custom field, if one is found, into the current
2720object.  This method only consults custom fields applied to L<RT::Catalog> for
2721L<RT::Asset> objects.
2722
2723Takes a hash with the keys:
2724
2725=over
2726
2727=item Name
2728
2729A L<RT::CustomField> ID or Name which applies to L<assets|RT::Asset>.
2730
2731=item Catalog
2732
2733Optional.  An L<RT::Catalog> ID or Name.
2734
2735=back
2736
2737If Catalog is specified, only a custom field added to that Catalog will be loaded.
2738
2739If Catalog is C<0>, only global asset custom fields will be loaded.
2740
2741If no Catalog is specified, all asset custom fields are searched including
2742global and catalog-specific CFs.
2743
2744Please note that this method may load a Disabled custom field if no others
2745matching the same criteria are found.  Enabled CFs are preferentially loaded.
2746
2747=cut
2748
2749# To someday be merged into RT::CustomField::LoadByName
2750sub LoadByNameAndCatalog {
2751    my $self = shift;
2752    my %args = (
2753                Catalog => undef,
2754                Name  => undef,
2755                @_,
2756               );
2757
2758    unless ( defined $args{'Name'} && length $args{'Name'} ) {
2759        $RT::Logger->error("Couldn't load Custom Field without Name");
2760        return wantarray ? (0, $self->loc("No name provided")) : 0;
2761    }
2762
2763    # if we're looking for a catalog by name, make it a number
2764    if ( defined $args{'Catalog'} && ($args{'Catalog'} =~ /\D/ || !$self->ContextObject) ) {
2765        my $CatalogObj = RT::Catalog->new( $self->CurrentUser );
2766        my ($ok, $msg) = $CatalogObj->Load( $args{'Catalog'} );
2767        if ( $ok ){
2768            $args{'Catalog'} = $CatalogObj->Id;
2769        }
2770        elsif ($args{'Catalog'}) {
2771            RT::Logger->error("Unable to load catalog " . $args{'Catalog'} . $msg);
2772            return (0, $msg);
2773        }
2774        $self->SetContextObject( $CatalogObj )
2775          unless $self->ContextObject;
2776    }
2777
2778    my $CFs = RT::CustomFields->new( $self->CurrentUser );
2779    $CFs->SetContextObject( $self->ContextObject );
2780    my $field = $args{'Name'} =~ /\D/? 'Name' : 'id';
2781    $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0);
2782
2783    # Limit to catalog, if provided. This will also limit to RT::Asset types.
2784    $CFs->LimitToCatalog( $args{'Catalog'} );
2785
2786    # When loading by name, we _can_ load disabled fields, but prefer
2787    # non-disabled fields.
2788    $CFs->FindAllRows;
2789    $CFs->OrderByCols(
2790                      {
2791                       FIELD => "Disabled", ORDER => 'ASC' },
2792                     );
2793
2794    # We only want one entry.
2795    $CFs->RowsPerPage(1);
2796
2797    return (0, $self->loc("Not found")) unless my $first = $CFs->First;
2798    return $self->LoadById( $first->id );
2799}
2800
2801
2802RT::Base->_ImportOverlays();
2803
28041;
2805