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