X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FCustomField.pm;h=3940e83e749e9915d904f4b075980f3c5bd1421a;hp=0582edd5b3b23acc990dba1f654596ea0d828dc4;hb=7322f2afedcc2f427e997d1535a503613a83f088;hpb=fb4ab1073f0d15d660c6cdc4e07afebf68ef3924 diff --git a/rt/lib/RT/CustomField.pm b/rt/lib/RT/CustomField.pm index 0582edd5b..3940e83e7 100644 --- a/rt/lib/RT/CustomField.pm +++ b/rt/lib/RT/CustomField.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) @@ -46,100 +46,2026 @@ # # END BPS TAGGED BLOCK }}} -# Autogenerated by DBIx::SearchBuilder factory (by ) -# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST. -# -# !! DO NOT EDIT THIS FILE !! -# +package RT::CustomField; + +use strict; +use warnings; +use 5.010; + +use Scalar::Util 'blessed'; + +use base 'RT::Record'; + +use Role::Basic 'with'; +with "RT::Record::Role::Rights"; + +sub Table {'CustomFields'} + +use Scalar::Util qw(blessed); +use RT::CustomFieldValues; +use RT::ObjectCustomFields; +use RT::ObjectCustomFieldValues; + +our %FieldTypes = ( + Select => { + sort_order => 10, + selection_type => 1, + + labels => [ 'Select multiple values', # loc + 'Select one value', # loc + 'Select up to [quant,_1,value,values]', # loc + ], + + render_types => { + multiple => [ + + # Default is the first one + 'Select box', # loc + 'List', # loc + ], + single => [ 'Select box', # loc + 'Dropdown', # loc + 'List', # loc + ] + }, + + }, + Freeform => { + sort_order => 20, + selection_type => 0, + + labels => [ 'Enter multiple values', # loc + 'Enter one value', # loc + 'Enter up to [quant,_1,value,values]', # loc + ] + }, + Text => { + sort_order => 30, + selection_type => 0, + labels => [ + 'Fill in multiple text areas', # loc + 'Fill in one text area', # loc + 'Fill in up to [quant,_1,text area,text areas]', # loc + ] + }, + Wikitext => { + sort_order => 40, + selection_type => 0, + labels => [ + 'Fill in multiple wikitext areas', # loc + 'Fill in one wikitext area', # loc + 'Fill in up to [quant,_1,wikitext area,wikitext areas]', # loc + ] + }, + + Image => { + sort_order => 50, + selection_type => 0, + labels => [ + 'Upload multiple images', # loc + 'Upload one image', # loc + 'Upload up to [quant,_1,image,images]', # loc + ] + }, + Binary => { + sort_order => 60, + selection_type => 0, + labels => [ + 'Upload multiple files', # loc + 'Upload one file', # loc + 'Upload up to [quant,_1,file,files]', # loc + ] + }, + + Combobox => { + sort_order => 70, + selection_type => 1, + labels => [ + 'Combobox: Select or enter multiple values', # loc + 'Combobox: Select or enter one value', # loc + 'Combobox: Select or enter up to [quant,_1,value,values]', # loc + ] + }, + Autocomplete => { + sort_order => 80, + selection_type => 1, + labels => [ + 'Enter multiple values with autocompletion', # loc + 'Enter one value with autocompletion', # loc + 'Enter up to [quant,_1,value,values] with autocompletion', # loc + ] + }, + + Date => { + sort_order => 90, + selection_type => 0, + labels => [ + 'Select multiple dates', # loc + 'Select date', # loc + 'Select up to [quant,_1,date,dates]', # loc + ] + }, + DateTime => { + sort_order => 100, + selection_type => 0, + labels => [ + 'Select multiple datetimes', # loc + 'Select datetime', # loc + 'Select up to [quant,_1,datetime,datetimes]', # loc + ] + }, + TimeValue => { + sort_order => 105, + selection_type => 0, + labels => [ + 'Enter multiple time values (UNSUPPORTED)', + 'Enter a time value', + 'Enter [_1] time values (UNSUPPORTED)', + ] + }, + + IPAddress => { + sort_order => 110, + selection_type => 0, + + labels => [ 'Enter multiple IP addresses', # loc + 'Enter one IP address', # loc + 'Enter up to [quant,_1,IP address,IP addresses]', # loc + ] + }, + IPAddressRange => { + sort_order => 120, + selection_type => 0, + + labels => [ 'Enter multiple IP address ranges', # loc + 'Enter one IP address range', # loc + 'Enter up to [quant,_1,IP address range,IP address ranges]', # loc + ] + }, +); + + +my %BUILTIN_GROUPINGS; +my %FRIENDLY_LOOKUP_TYPES = (); + +__PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket' => "Tickets", ); #loc +__PACKAGE__->RegisterLookupType( 'RT::Queue-RT::Ticket-RT::Transaction' => "Ticket Transactions", ); #loc +__PACKAGE__->RegisterLookupType( 'RT::User' => "Users", ); #loc +__PACKAGE__->RegisterLookupType( 'RT::Queue' => "Queues", ); #loc +__PACKAGE__->RegisterLookupType( 'RT::Group' => "Groups", ); #loc + +__PACKAGE__->RegisterBuiltInGroupings( + 'RT::Ticket' => [ qw(Basics Dates Links People) ], + 'RT::User' => [ 'Identity', 'Access control', 'Location', 'Phones' ], +); + +__PACKAGE__->AddRight( General => SeeCustomField => 'View custom fields'); # loc +__PACKAGE__->AddRight( Admin => AdminCustomField => 'Create, modify and delete custom fields'); # loc +__PACKAGE__->AddRight( Admin => AdminCustomFieldValues => 'Create, modify and delete custom fields values'); # loc +__PACKAGE__->AddRight( Staff => ModifyCustomField => 'Add, modify and delete custom field values for objects'); # loc + +=head1 NAME + + RT::CustomField_Overlay - overlay for RT::CustomField + +=head1 DESCRIPTION + +=head1 'CORE' METHODS + +=head2 Create PARAMHASH + +Create takes a hash of values and creates a row in the database: + + varchar(200) 'Name'. + varchar(200) 'Type'. + int(11) 'MaxValues'. + varchar(255) 'Pattern'. + varchar(255) 'Description'. + int(11) 'SortOrder'. + varchar(255) 'LookupType'. + smallint(6) 'Disabled'. + +C is generally the result of either +CCustomFieldLookupType> or CCustomFieldLookupType>. + +=cut + +sub Create { + my $self = shift; + my %args = ( + Name => '', + Type => '', + MaxValues => 0, + Pattern => '', + Description => '', + Disabled => 0, + LookupType => '', + LinkValueTo => '', + IncludeContentForValue => '', + @_, + ); + + unless ( $self->CurrentUser->HasRight(Object => $RT::System, Right => 'AdminCustomField') ) { + return (0, $self->loc('Permission Denied')); + } + + if ( $args{TypeComposite} ) { + @args{'Type', 'MaxValues'} = split(/-/, $args{TypeComposite}, 2); + } + elsif ( $args{Type} =~ s/(?:(Single)|Multiple)$// ) { + # old style Type string + $args{'MaxValues'} = $1 ? 1 : 0; + } + $args{'MaxValues'} = int $args{'MaxValues'}; + + if ( !exists $args{'Queue'}) { + # do nothing -- things below are strictly backward compat + } + elsif ( ! $args{'Queue'} ) { + unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'AssignCustomFields') ) { + return ( 0, $self->loc('Permission Denied') ); + } + $args{'LookupType'} = 'RT::Queue-RT::Ticket'; + } + else { + my $queue = RT::Queue->new($self->CurrentUser); + $queue->Load($args{'Queue'}); + unless ($queue->Id) { + return (0, $self->loc("Queue not found")); + } + unless ( $queue->CurrentUserHasRight('AssignCustomFields') ) { + return ( 0, $self->loc('Permission Denied') ); + } + $args{'LookupType'} = 'RT::Queue-RT::Ticket'; + $args{'Queue'} = $queue->Id; + } + + my ($ok, $msg) = $self->_IsValidRegex( $args{'Pattern'} ); + return (0, $self->loc("Invalid pattern: [_1]", $msg)) unless $ok; + + if ( $args{'MaxValues'} != 1 && $args{'Type'} =~ /(text|combobox)$/i ) { + $RT::Logger->debug("Support for 'multiple' Texts or Comboboxes is not implemented"); + $args{'MaxValues'} = 1; + } + + if ( $args{'RenderType'} ||= undef ) { + my $composite = join '-', @args{'Type', 'MaxValues'}; + return (0, $self->loc("This custom field has no Render Types")) + unless $self->HasRenderTypes( $composite ); + + if ( $args{'RenderType'} eq $self->DefaultRenderType( $composite ) ) { + $args{'RenderType'} = undef; + } else { + return (0, $self->loc("Invalid Render Type") ) + unless grep $_ eq $args{'RenderType'}, $self->RenderTypes( $composite ); + } + } + + $args{'ValuesClass'} = undef if ($args{'ValuesClass'} || '') eq 'RT::CustomFieldValues'; + if ( $args{'ValuesClass'} ||= undef ) { + return (0, $self->loc("This Custom Field can not have list of values")) + unless $self->IsSelectionType( $args{'Type'} ); + + unless ( $self->ValidateValuesClass( $args{'ValuesClass'} ) ) { + return (0, $self->loc("Invalid Custom Field values source")); + } + } + + $args{'Disabled'} ||= 0; + + (my $rv, $msg) = $self->SUPER::Create( + Name => $args{'Name'}, + Type => $args{'Type'}, + RenderType => $args{'RenderType'}, + MaxValues => $args{'MaxValues'}, + Pattern => $args{'Pattern'}, + BasedOn => $args{'BasedOn'}, + ValuesClass => $args{'ValuesClass'}, + Description => $args{'Description'}, + Disabled => $args{'Disabled'}, + LookupType => $args{'LookupType'}, + ); + + if ($rv) { + if ( exists $args{'LinkValueTo'}) { + $self->SetLinkValueTo($args{'LinkValueTo'}); + } + + if ( exists $args{'IncludeContentForValue'}) { + $self->SetIncludeContentForValue($args{'IncludeContentForValue'}); + } + + if ( exists $args{'UILocation'} ) { + $self->SetUILocation( $args{'UILocation'} ); + } + + if ( exists $args{'NoClone'} ) { + $self->SetNoClone( $args{'NoClone'} ); + } + + return ($rv, $msg) unless exists $args{'Queue'}; + + # Compat code -- create a new ObjectCustomField mapping + my $OCF = RT::ObjectCustomField->new( $self->CurrentUser ); + $OCF->Create( + CustomField => $self->Id, + ObjectId => $args{'Queue'}, + ); + } + + return ($rv, $msg); +} + +=head2 Load ID/NAME + +Load a custom field. If the value handed in is an integer, load by custom field ID. Otherwise, Load by name. + +=cut + +sub Load { + my $self = shift; + my $id = shift || ''; + + if ( $id =~ /^\d+$/ ) { + return $self->SUPER::Load( $id ); + } else { + return $self->LoadByName( Name => $id ); + } +} + + + +=head2 LoadByName Name => C, [...] + +Loads the Custom field named NAME. As other optional parameters, takes: + +=over + +=item LookupType => C + +The type of Custom Field to look for; while this parameter is not +required, it is highly suggested, or you may not find the Custom Field +you are expecting. It should be passed a C such as +L or +L. + +=item ObjectType => C + +The class of object that the custom field is applied to. This can be +intuited from the provided C. + +=item ObjectId => C + +limits the custom field search to one applied to the relevant id. For +example, if a C of C<< RT::Ticket->CustomFieldLookupType >> +is used, this is which Queue the CF must be applied to. Pass 0 to only +search custom fields that are applied globally. + +=item IncludeDisabled => C + +Whether it should return Disabled custom fields if they match; defaults +to on, though non-Disabled custom fields are returned preferentially. + +=item IncludeGlobal => C + +Whether to also search global custom fields, even if a value is provided +for C; defaults to off. Non-global custom fields are returned +preferentially. + +=back + +For backwards compatibility, a value passed for C is equivalent +to specifying a C of L, +and a C of the value passed as C. + +If multiple custom fields match the above constraints, the first +according to C will be returned; ties are broken by C, +lowest-first. + +=head2 LoadNameAndQueue + +=head2 LoadByNameAndQueue + +Deprecated alternate names for L. + +=cut + +# Compatibility for API change after 3.0 beta 1 +*LoadNameAndQueue = \&LoadByName; +# Change after 3.4 beta. +*LoadByNameAndQueue = \&LoadByName; + +sub LoadByName { + my $self = shift; + my %args = ( + Name => undef, + LookupType => undef, + ObjectType => undef, + ObjectId => undef, + + IncludeDisabled => 1, + IncludeGlobal => 0, + + # Back-compat + Queue => undef, + + @_, + ); + + unless ( defined $args{'Name'} && length $args{'Name'} ) { + $RT::Logger->error("Couldn't load Custom Field without Name"); + return wantarray ? (0, $self->loc("No name provided")) : 0; + } + + if ( defined $args{'Queue'} ) { + # Set a LookupType for backcompat, otherwise we'll calculate + # one of RT::Queue from your ContextObj. Older code was relying + # on us defaulting to RT::Queue-RT::Ticket in old LimitToQueue call. + $args{LookupType} ||= 'RT::Queue-RT::Ticket'; + $args{ObjectId} //= delete $args{Queue}; + } + + # Default the ObjectType to the top category of the LookupType; it's + # what the CFs are assigned on. + $args{ObjectType} ||= $1 if $args{LookupType} and $args{LookupType} =~ /^([^-]+)/; + + # Resolve the ObjectId/ObjectType; this is necessary to properly + # limit ObjectId, and also possibly useful to set a ContextObj if we + # are currently lacking one. It is not strictly necessary if we + # have a context object and were passed a numeric ObjectId, but it + # cannot hurt to verify its sanity. Skip if we have a false + # ObjectId, which means "global", or if we lack an ObjectType + if ($args{ObjectId} and $args{ObjectType}) { + my ($obj, $ok, $msg); + eval { + $obj = $args{ObjectType}->new( $self->CurrentUser ); + ($ok, $msg) = $obj->Load( $args{ObjectId} ); + }; + + if ($ok) { + $args{ObjectId} = $obj->id; + $self->SetContextObject( $obj ) + unless $self->ContextObject; + } else { + $RT::Logger->warning("Failed to load $args{ObjectType} '$args{ObjectId}'"); + if ($args{IncludeGlobal}) { + # Fall back to acting like we were only asked about the + # global case + $args{ObjectId} = 0; + } else { + # If they didn't also want global results, there's no + # point in searching; abort + return wantarray ? (0, $self->loc("Not found")) : 0; + } + } + } elsif (not $args{ObjectType} and $args{ObjectId}) { + # If we skipped out on the above due to lack of ObjectType, make + # sure we clear out ObjectId of anything lingering + $RT::Logger->warning("No LookupType or ObjectType passed; ignoring ObjectId"); + delete $args{ObjectId}; + } + + my $CFs = RT::CustomFields->new( $self->CurrentUser ); + $CFs->SetContextObject( $self->ContextObject ); + my $field = $args{'Name'} =~ /\D/? 'Name' : 'id'; + $CFs->Limit( FIELD => $field, VALUE => $args{'Name'}, CASESENSITIVE => 0); + + # The context object may be a ticket, for example, as context for a + # queue CF. The valid lookup types are thus the entire set of + # ACLEquivalenceObjects for the context object. + $args{LookupType} ||= [ + map {$_->CustomFieldLookupType} + ($self->ContextObject, $self->ContextObject->ACLEquivalenceObjects) ] + if $self->ContextObject; + + # Apply LookupType limits + $args{LookupType} = [ $args{LookupType} ] + if $args{LookupType} and not ref($args{LookupType}); + $CFs->Limit( FIELD => "LookupType", OPERATOR => "IN", VALUE => $args{LookupType} ) + if $args{LookupType}; + + # Default to by SortOrder and id; this mirrors the standard ordering + # of RT::CustomFields (minus the Name, which is guaranteed to be + # fixed) + my @order = ( + { FIELD => 'SortOrder', + ORDER => 'ASC' }, + { FIELD => 'id', + ORDER => 'ASC' }, + ); + + if (defined $args{ObjectId}) { + # The join to OCFs is distinct -- either we have a global + # application or an objectid match, but never both. Even if + # this were not the case, we care only for the first row. + my $ocfs = $CFs->_OCFAlias( Distinct => 1); + if ($args{IncludeGlobal}) { + $CFs->Limit( + ALIAS => $ocfs, + FIELD => 'ObjectId', + OPERATOR => 'IN', + VALUE => [ $args{ObjectId}, 0 ], + ); + # Find the queue-specific first + unshift @order, { ALIAS => $ocfs, FIELD => "ObjectId", ORDER => "DESC" }; + } else { + $CFs->Limit( + ALIAS => $ocfs, + FIELD => 'ObjectId', + VALUE => $args{ObjectId}, + ); + } + } + + if ($args{IncludeDisabled}) { + # Load disabled fields, but return them only as a last resort. + # This goes at the front of @order, as we prefer the + # non-disabled global CF to the disabled Queue-specific CF. + $CFs->FindAllRows; + unshift @order, { FIELD => "Disabled", ORDER => 'ASC' }; + } + + # Apply the above orderings + $CFs->OrderByCols( @order ); + + # We only want one entry. + $CFs->RowsPerPage(1); + + # version before 3.8 just returns 0, so we need to test if wantarray to be + # backward compatible. + return wantarray ? (0, $self->loc("Not found")) : 0 unless my $first = $CFs->First; + + return $self->LoadById( $first->id ); +} + + + + +=head2 Custom field values + +=head3 Values FIELD + +Return a object (collection) of all acceptable values for this Custom Field. +Class of the object can vary and depends on the return value +of the C method. + +=cut + +*ValuesObj = \&Values; + +sub Values { + my $self = shift; + + my $class = $self->ValuesClass; + if ( $class ne 'RT::CustomFieldValues') { + $class->require or die "Can't load $class: $@"; + } + my $cf_values = $class->new( $self->CurrentUser ); + $cf_values->SetCustomFieldObject( $self ); + # if the user has no rights, return an empty object + if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) { + $cf_values->LimitToCustomField( $self->Id ); + } else { + $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' ); + } + return ($cf_values); +} + + +=head3 AddValue HASH + +Create a new value for this CustomField. Takes a paramhash containing the elements Name, Description and SortOrder + +=cut + +sub AddValue { + my $self = shift; + my %args = @_; + + unless ($self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues')) { + return (0, $self->loc('Permission Denied')); + } + + # allow zero value + if ( !defined $args{'Name'} || $args{'Name'} eq '' ) { + return (0, $self->loc("Can't add a custom field value without a name")); + } + + my $newval = RT::CustomFieldValue->new( $self->CurrentUser ); + return $newval->Create( %args, CustomField => $self->Id ); +} + + + + +=head3 DeleteValue ID + +Deletes a value from this custom field by id. + +Does not remove this value for any article which has had it selected + +=cut + +sub DeleteValue { + my $self = shift; + my $id = shift; + unless ( $self->CurrentUserHasRight('AdminCustomField') || $self->CurrentUserHasRight('AdminCustomFieldValues') ) { + return (0, $self->loc('Permission Denied')); + } + + my $val_to_del = RT::CustomFieldValue->new( $self->CurrentUser ); + $val_to_del->Load( $id ); + unless ( $val_to_del->Id ) { + return (0, $self->loc("Couldn't find that value")); + } + unless ( $val_to_del->CustomField == $self->Id ) { + return (0, $self->loc("That is not a value for this custom field")); + } + + my $retval = $val_to_del->Delete; + unless ( $retval ) { + return (0, $self->loc("Custom field value could not be deleted")); + } + return ($retval, $self->loc("Custom field value deleted")); +} + + +=head2 ValidateQueue Queue + +Make sure that the name specified is valid + +=cut + +sub ValidateName { + my $self = shift; + my $value = shift; + + return 0 unless length $value; + + return $self->SUPER::ValidateName($value); +} + +=head2 ValidateQueue Queue + +Make sure that the queue specified is a valid queue name + +=cut + +sub ValidateQueue { + my $self = shift; + my $id = shift; + + return undef unless defined $id; + # 0 means "Global" null would _not_ be ok. + return 1 if $id eq '0'; + + my $q = RT::Queue->new( RT->SystemUser ); + $q->Load( $id ); + return undef unless $q->id; + return 1; +} + + + +=head2 Types + +Retuns an array of the types of CustomField that are supported + +=cut + +sub Types { + return (sort {(($FieldTypes{$a}{sort_order}||999) <=> ($FieldTypes{$b}{sort_order}||999)) or ($a cmp $b)} keys %FieldTypes); +} + + +=head2 IsSelectionType + +Retuns a boolean value indicating whether the C method makes sense +to this Custom Field. + +=cut + +sub IsSelectionType { + my $self = shift; + my $type = @_? shift : $self->Type; + return undef unless $type; + return $FieldTypes{$type}->{selection_type}; +} + + + +=head2 IsExternalValues + +=cut + +sub IsExternalValues { + my $self = shift; + return 0 unless $self->IsSelectionType( @_ ); + return $self->ValuesClass eq 'RT::CustomFieldValues'? 0 : 1; +} + +sub ValuesClass { + my $self = shift; + return $self->_Value( ValuesClass => @_ ) || 'RT::CustomFieldValues'; +} + +sub SetValuesClass { + my $self = shift; + my $class = shift || 'RT::CustomFieldValues'; + + if ( $class eq 'RT::CustomFieldValues' ) { + return $self->_Set( Field => 'ValuesClass', Value => undef, @_ ); + } + + return (0, $self->loc("This Custom Field can not have list of values")) + unless $self->IsSelectionType; + + unless ( $self->ValidateValuesClass( $class ) ) { + return (0, $self->loc("Invalid Custom Field values source")); + } + return $self->_Set( Field => 'ValuesClass', Value => $class, @_ ); +} + +sub ValidateValuesClass { + my $self = shift; + my $class = shift; + + return 1 if !$class || $class eq 'RT::CustomFieldValues'; + return 1 if grep $class eq $_, RT->Config->Get('CustomFieldValuesSources'); + return undef; +} + + +=head2 FriendlyType [TYPE, MAX_VALUES] + +Returns a localized human-readable version of the custom field type. +If a custom field type is specified as the parameter, the friendly type for that type will be returned + +=cut + +sub FriendlyType { + my $self = shift; + + my $type = @_ ? shift : $self->Type; + my $max = @_ ? shift : $self->MaxValues; + $max = 0 unless $max; + + if (my $friendly_type = $FieldTypes{$type}->{labels}->[$max>2 ? 2 : $max]) { + return ( $self->loc( $friendly_type, $max ) ); + } + else { + return ( $self->loc( $type ) ); + } +} + +sub FriendlyTypeComposite { + my $self = shift; + my $composite = shift || $self->TypeComposite; + return $self->FriendlyType(split(/-/, $composite, 2)); +} + + +=head2 ValidateType TYPE + +Takes a single string. returns true if that string is a value +type of custom field + + +=cut + +sub ValidateType { + my $self = shift; + my $type = shift; + + if ( $type =~ s/(?:Single|Multiple)$// ) { + RT->Deprecated( + Arguments => "suffix 'Single' or 'Multiple'", + Instead => "MaxValues", + Remove => "4.4", + ); + } + + if ( $FieldTypes{$type} ) { + return 1; + } + else { + return undef; + } +} + + +sub SetType { + my $self = shift; + my $type = shift; + if ($type =~ s/(?:(Single)|Multiple)$//) { + RT->Deprecated( + Arguments => "suffix 'Single' or 'Multiple'", + Instead => "MaxValues", + Remove => "4.4", + ); + $self->SetMaxValues($1 ? 1 : 0); + } + $self->_Set(Field => 'Type', Value =>$type); +} + +=head2 SetPattern STRING + +Takes a single string representing a regular expression. Performs basic +validation on that regex, and sets the C field for the CF if it +is valid. + +=cut + +sub SetPattern { + my $self = shift; + my $regex = shift; + + my ($ok, $msg) = $self->_IsValidRegex($regex); + if ($ok) { + return $self->_Set(Field => 'Pattern', Value => $regex); + } + else { + return (0, $self->loc("Invalid pattern: [_1]", $msg)); + } +} + +=head2 _IsValidRegex(Str $regex) returns (Bool $success, Str $msg) + +Tests if the string contains an invalid regex. + +=cut + +sub _IsValidRegex { + my $self = shift; + my $regex = shift or return (1, 'valid'); + + local $^W; local $@; + local $SIG{__DIE__} = sub { 1 }; + local $SIG{__WARN__} = sub { 1 }; + + if (eval { qr/$regex/; 1 }) { + return (1, 'valid'); + } + + my $err = $@; + $err =~ s{[,;].*}{}; # strip debug info from error + chomp $err; + return (0, $err); +} + + +=head2 SingleValue + +Returns true if this CustomField only accepts a single value. +Returns false if it accepts multiple values + +=cut + +sub SingleValue { + my $self = shift; + if (($self->MaxValues||0) == 1) { + return 1; + } + else { + return undef; + } +} + +sub UnlimitedValues { + my $self = shift; + if (($self->MaxValues||0) == 0) { + return 1; + } + else { + return undef; + } +} + + +=head2 ACLEquivalenceObjects + +Returns list of objects via which users can get rights on this custom field. For custom fields +these objects can be set using L. + +=cut + +sub ACLEquivalenceObjects { + my $self = shift; + + my $ctx = $self->ContextObject + or return; + return ($ctx, $ctx->ACLEquivalenceObjects); +} + +=head2 ContextObject and SetContextObject + +Set or get a context for this object. It can be ticket, queue or another +object this CF added to. Used for ACL control, for example +SeeCustomField can be granted on queue level to allow people to see all +fields added to the queue. + +=cut + +sub SetContextObject { + my $self = shift; + return $self->{'context_object'} = shift; +} + +sub ContextObject { + my $self = shift; + return $self->{'context_object'}; +} + +sub ValidContextType { + my $self = shift; + my $class = shift; + + my %valid; + $valid{$_}++ for split '-', $self->LookupType; + delete $valid{'RT::Transaction'}; + + return $valid{$class}; +} + +=head2 LoadContextObject + +Takes an Id for a Context Object and loads the right kind of RT::Object +for this particular Custom Field (based on the LookupType) and returns it. +This is a good way to ensure you don't try to use a Queue as a Context +Object on a User Custom Field. + +=cut + +sub LoadContextObject { + my $self = shift; + my $type = shift; + my $contextid = shift; + + unless ( $self->ValidContextType($type) ) { + RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id); + return; + } + + my $context_object = $type->new( $self->CurrentUser ); + my ($id, $msg) = $context_object->LoadById( $contextid ); + unless ( $id ) { + RT->Logger->debug("Invalid ContextObject id: $msg"); + return; + } + return $context_object; +} + +=head2 ValidateContextObject + +Ensure that a given ContextObject applies to this Custom Field. For +custom fields that are assigned to Queues or to Classes, this checks +that the Custom Field is actually added to that object. For Global +Custom Fields, it returns true as long as the Object is of the right +type, because you may be using your permissions on a given Queue of +Class to see a Global CF. For CFs that are only added globally, you +don't need a ContextObject. + +=cut + +sub ValidateContextObject { + my $self = shift; + my $object = shift; + + return 1 if $self->IsGlobal; + + # global only custom fields don't have objects + # that should be used as context objects. + return if $self->IsOnlyGlobal; + + # Otherwise, make sure we weren't passed a user object that we're + # supposed to treat as a queue. + return unless $self->ValidContextType(ref $object); + + # Check that it is added correctly + my ($added_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects); + return unless $added_to; + return $self->IsAdded($added_to->id); +} + +sub _Set { + my $self = shift; + + unless ( $self->CurrentUserHasRight('AdminCustomField') ) { + return ( 0, $self->loc('Permission Denied') ); + } + return $self->SUPER::_Set( @_ ); + +} + + + +=head2 _Value + +Takes the name of a table column. +Returns its value as a string, if the user passes an ACL check + +=cut + +sub _Value { + my $self = shift; + return undef unless $self->id; + + # we need to do the rights check + unless ( $self->CurrentUserHasRight('SeeCustomField') ) { + $RT::Logger->debug( + "Permission denied. User #". $self->CurrentUser->id + ." has no SeeCustomField right on CF #". $self->id + ); + return (undef); + } + return $self->__Value( @_ ); +} + + +=head2 SetDisabled + +Takes a boolean. +1 will cause this custom field to no longer be avaialble for objects. +0 will re-enable this field. + +=cut + + +=head2 SetTypeComposite + +Set this custom field's type and maximum values as a composite value + +=cut + +sub SetTypeComposite { + my $self = shift; + my $composite = shift; + + my $old = $self->TypeComposite; + + my ($type, $max_values) = split(/-/, $composite, 2); + if ( $type ne $self->Type ) { + my ($status, $msg) = $self->SetType( $type ); + return ($status, $msg) unless $status; + } + if ( ($max_values || 0) != ($self->MaxValues || 0) ) { + my ($status, $msg) = $self->SetMaxValues( $max_values ); + return ($status, $msg) unless $status; + } + my $render = $self->RenderType; + if ( $render and not grep { $_ eq $render } $self->RenderTypes ) { + # We switched types and our render type is no longer valid, so unset it + # and use the default + $self->SetRenderType( undef ); + } + return 1, $self->loc( + "Type changed from '[_1]' to '[_2]'", + $self->FriendlyTypeComposite( $old ), + $self->FriendlyTypeComposite( $composite ), + ); +} + +=head2 TypeComposite + +Returns a composite value composed of this object's type and maximum values + +=cut + + +sub TypeComposite { + my $self = shift; + return join '-', ($self->Type || ''), ($self->MaxValues || 0); +} + +=head2 TypeComposites + +Returns an array of all possible composite values for custom fields. + +=cut + +sub TypeComposites { + my $self = shift; + return grep !/(?:[Tt]ext|Combobox|Date|DateTime|TimeValue)-0/, map { ("$_-1", "$_-0") } $self->Types; +} + +=head2 RenderType + +Returns the type of form widget to render for this custom field. Currently +this only affects fields which return true for L. + +=cut + +sub RenderType { + my $self = shift; + return '' unless $self->HasRenderTypes; + + return $self->_Value( 'RenderType', @_ ) + || $self->DefaultRenderType; +} + +=head2 SetRenderType TYPE + +Sets this custom field's render type. + +=cut + +sub SetRenderType { + my $self = shift; + my $type = shift; + return (0, $self->loc("This custom field has no Render Types")) + unless $self->HasRenderTypes; + + if ( !$type || $type eq $self->DefaultRenderType ) { + return $self->_Set( Field => 'RenderType', Value => undef, @_ ); + } + + if ( not grep { $_ eq $type } $self->RenderTypes ) { + return (0, $self->loc("Invalid Render Type for custom field of type [_1]", + $self->FriendlyType)); + } + + return $self->_Set( Field => 'RenderType', Value => $type, @_ ); +} + +=head2 DefaultRenderType [TYPE COMPOSITE] + +Returns the default render type for this custom field's type or the TYPE +COMPOSITE specified as an argument. + +=cut + +sub DefaultRenderType { + my $self = shift; + my $composite = @_ ? shift : $self->TypeComposite; + my ($type, $max) = split /-/, $composite, 2; + return unless $type and $self->HasRenderTypes($composite); + return $FieldTypes{$type}->{render_types}->{ $max == 1 ? 'single' : 'multiple' }[0]; +} + +=head2 HasRenderTypes [TYPE_COMPOSITE] + +Returns a boolean value indicating whether the L and +L methods make sense for this custom field. + +Currently true only for type C