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