# -- # Copyright (C) 2001-2019 OTRS AG, https://otrs.com/ # -- # This software comes with ABSOLUTELY NO WARRANTY. For details, see # the enclosed file COPYING for license information (GPL). If you # did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. # -- package Kernel::System::CustomerUser::DB; use strict; use warnings; use Kernel::System::VariableCheck qw(:all); use Crypt::PasswdMD5 qw(unix_md5_crypt apache_md5_crypt); use Digest::SHA; use Kernel::System::VariableCheck qw(:all); our @ObjectDependencies = ( 'Kernel::Config', 'Kernel::Language', 'Kernel::System::Cache', 'Kernel::System::CheckItem', 'Kernel::System::DateTime', 'Kernel::System::DB', 'Kernel::System::DynamicField', 'Kernel::System::DynamicField::Backend', 'Kernel::System::Encode', 'Kernel::System::Log', 'Kernel::System::Main', 'Kernel::System::Valid', 'Kernel::System::DynamicField', 'Kernel::System::DynamicField::Backend', 'Kernel::System::DynamicFieldValueObjectName', ); sub new { my ( $Type, %Param ) = @_; # allocate new hash for object my $Self = {}; bless( $Self, $Type ); # check needed data for my $Needed (qw( PreferencesObject CustomerUserMap )) { $Self->{$Needed} = $Param{$Needed} || die "Got no $Needed!"; } # get database object $Self->{DBObject} = $Kernel::OM->Get('Kernel::System::DB'); # max shown user per search list $Self->{UserSearchListLimit} = $Self->{CustomerUserMap}->{CustomerUserSearchListLimit} || 250; # config options $Self->{CustomerTable} = $Self->{CustomerUserMap}->{Params}->{Table} || die "Need CustomerUser->Params->Table in Kernel/Config.pm!"; $Self->{CustomerKey} = $Self->{CustomerUserMap}->{CustomerKey} || $Self->{CustomerUserMap}->{Key} || die "Need CustomerUser->CustomerKey in Kernel/Config.pm!"; $Self->{CustomerID} = $Self->{CustomerUserMap}->{CustomerID} || die "Need CustomerUser->CustomerID in Kernel/Config.pm!"; $Self->{ReadOnly} = $Self->{CustomerUserMap}->{ReadOnly}; $Self->{ExcludePrimaryCustomerID} = $Self->{CustomerUserMap}->{CustomerUserExcludePrimaryCustomerID} || 0; $Self->{SearchPrefix} = $Self->{CustomerUserMap}->{CustomerUserSearchPrefix}; if ( !defined $Self->{SearchPrefix} ) { $Self->{SearchPrefix} = ''; } $Self->{SearchSuffix} = $Self->{CustomerUserMap}->{CustomerUserSearchSuffix}; if ( !defined $Self->{SearchSuffix} ) { $Self->{SearchSuffix} = '*'; } # check if CustomerKey is var or int ENTRY: for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { if ( $Entry->[0] eq 'UserLogin' && $Entry->[5] =~ /^int$/i ) { $Self->{CustomerKeyInteger} = 1; last ENTRY; } } # set cache type $Self->{CacheType} = 'CustomerUser' . $Param{Count}; # create cache object, but only if CacheTTL is set in customer config if ( $Self->{CustomerUserMap}->{CacheTTL} ) { $Self->{CacheObject} = $Kernel::OM->Get('Kernel::System::Cache'); } # create new db connect if DSN is given if ( $Self->{CustomerUserMap}->{Params}->{DSN} ) { $Self->{DBObject} = Kernel::System::DB->new( DatabaseDSN => $Self->{CustomerUserMap}->{Params}->{DSN}, DatabaseUser => $Self->{CustomerUserMap}->{Params}->{User}, DatabasePw => $Self->{CustomerUserMap}->{Params}->{Password}, %{ $Self->{CustomerUserMap}->{Params} }, ) || die('Can\'t connect to database!'); # remember that we have the DBObject not from parent call $Self->{NotParentDBObject} = 1; } # this setting specifies if the table has the create_time, # create_by, change_time and change_by fields of OTRS $Self->{ForeignDB} = $Self->{CustomerUserMap}->{Params}->{ForeignDB} ? 1 : 0; # defines if the database search will be performend case sensitive (1) or not (0) $Self->{CaseSensitive} = $Self->{CustomerUserMap}->{Params}->{SearchCaseSensitive} // $Self->{CustomerUserMap}->{Params}->{CaseSensitive} || 0; # fetch names of configured dynamic fields my @DynamicFieldMapEntries = grep { $_->[5] eq 'dynamic_field' } @{ $Self->{CustomerUserMap}->{Map} }; $Self->{ConfiguredDynamicFieldNames} = { map { $_->[2] => 1 } @DynamicFieldMapEntries }; return $Self; } sub CustomerName { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{UserLogin} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need UserLogin!', ); return; } # check cache if ( $Self->{CacheObject} ) { my $Name = $Self->{CacheObject}->Get( Type => $Self->{CacheType}, Key => "CustomerName::$Param{UserLogin}", ); return $Name if defined $Name; } my $CustomerUserNameFields = $Self->{CustomerUserMap}->{CustomerUserNameFields}; if ( !IsArrayRefWithData($CustomerUserNameFields) ) { $CustomerUserNameFields = [ 'first_name', 'last_name', ]; } # remove dynamic field names that are configured in CustomerUserNameFields # as they cannot be handled here my @CustomerUserNameFieldsWithoutDynamicFields = grep { !exists $Self->{ConfiguredDynamicFieldNames}->{$_} } @{$CustomerUserNameFields}; # build SQL string 1/2 my $SQL = "SELECT "; $SQL .= join( ", ", @CustomerUserNameFieldsWithoutDynamicFields ); $SQL .= " FROM $Self->{CustomerTable} WHERE "; # check CustomerKey type my $UserLogin = $Param{UserLogin}; if ( $Self->{CaseSensitive} ) { $SQL .= "$Self->{CustomerKey} = ?"; } else { $SQL .= "LOWER($Self->{CustomerKey}) = LOWER(?)"; } my %NameParts; # get data from customer user table return if !$Self->{DBObject}->Prepare( SQL => $SQL, Bind => [ \$Param{UserLogin} ], Limit => 1, ); my $FieldCounter = 0; while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { for my $Field (@Row) { $NameParts{ $CustomerUserNameFieldsWithoutDynamicFields[$FieldCounter] } = $Field; $FieldCounter++; } } # fetch dynamic field values, if configured my @DynamicFieldCustomerUserNameFields = grep { exists $Self->{ConfiguredDynamicFieldNames}->{$_} } @{$CustomerUserNameFields}; if (@DynamicFieldCustomerUserNameFields) { my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); DYNAMICFIELDNAME: for my $DynamicFieldName (@DynamicFieldCustomerUserNameFields) { my $DynamicFieldConfig = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldGet( Name => $DynamicFieldName, ); next DYNAMICFIELDNAME if !IsHashRefWithData($DynamicFieldConfig); my $Value = $DynamicFieldBackendObject->ValueGet( DynamicFieldConfig => $DynamicFieldConfig, ObjectName => $Param{UserLogin}, ); next DYNAMICFIELDNAME if !defined $Value; if ( !IsArrayRefWithData($Value) ) { $Value = [$Value]; } my @RenderedValues; VALUE: for my $CurrentValue ( @{$Value} ) { next VALUE if !defined $CurrentValue || !length $CurrentValue; my $RenderedValue = $DynamicFieldBackendObject->ReadableValueRender( DynamicFieldConfig => $DynamicFieldConfig, Value => $CurrentValue, ); next VALUE if !IsHashRefWithData($RenderedValue) || !defined $RenderedValue->{Value}; push @RenderedValues, $RenderedValue->{Value}; } $NameParts{$DynamicFieldName} = join ' ', @RenderedValues; } } # assemble name my @NameParts; CUSTOMERUSERNAMEFIELD: for my $CustomerUserNameField ( @{$CustomerUserNameFields} ) { next CUSTOMERUSERNAMEFIELD if !exists $NameParts{$CustomerUserNameField} || !defined $NameParts{$CustomerUserNameField} || !length $NameParts{$CustomerUserNameField}; push @NameParts, $NameParts{$CustomerUserNameField}; } my $JoinCharacter = $Self->{CustomerUserMap}->{CustomerUserNameFieldsJoin} // ' '; my $Name = join $JoinCharacter, @NameParts; # cache request if ( $Self->{CacheObject} ) { $Self->{CacheObject}->Set( Type => $Self->{CacheType}, Key => "CustomerName::$Param{UserLogin}", Value => $Name, TTL => $Self->{CustomerUserMap}->{CacheTTL}, ); } return $Name; } sub CustomerSearch { my ( $Self, %Param ) = @_; my %Users; my $Valid = defined $Param{Valid} ? $Param{Valid} : 1; # check needed stuff if ( !$Param{Search} && !$Param{UserLogin} && !$Param{PostMasterSearch} && !$Param{CustomerID} && !$Param{CustomerIDRaw} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need Search, UserLogin, PostMasterSearch, CustomerIDRaw or CustomerID!', ); return; } # check cache my $CacheKey = join '::', map { $_ . '=' . $Param{$_} } sort keys %Param; if ( $Self->{CacheObject} ) { my $Users = $Self->{CacheObject}->Get( Type => $Self->{CacheType} . '_CustomerSearch', Key => $CacheKey, ); return %{$Users} if ref $Users eq 'HASH'; } my $CustomerUserListFields = $Self->{CustomerUserMap}->{CustomerUserListFields}; if ( !IsArrayRefWithData($CustomerUserListFields) ) { $CustomerUserListFields = [ 'first_name', 'last_name', 'email', ]; } # remove dynamic field names that are configured in CustomerUserListFields # as they cannot be handled here my @CustomerUserListFieldsWithoutDynamicFields = grep { !exists $Self->{ConfiguredDynamicFieldNames}->{$_} } @{$CustomerUserListFields}; # build SQL string 1/2 my $SQL = "SELECT $Self->{CustomerKey} "; my @Bind; $SQL .= ', ' . ( join ', ', @CustomerUserListFieldsWithoutDynamicFields ); # get like escape string needed for some databases (e.g. oracle) my $LikeEscapeString = $Self->{DBObject}->GetDatabaseFunction('LikeEscapeString'); # build SQL string 2/2 $SQL .= " FROM $Self->{CustomerTable} WHERE "; if ( $Param{Search} ) { if ( !$Self->{CustomerUserMap}->{CustomerUserSearchFields} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need CustomerUserSearchFields in CustomerUser config, unable to search for '$Param{Search}'!", ); return; } my $Search = $Self->{DBObject}->QueryStringEscape( QueryString => $Param{Search} ); # remove dynamic field names that are configured in CustomerUserSearchFields # as they cannot be retrieved here my @CustomerUserSearchFields = grep { !exists $Self->{ConfiguredDynamicFieldNames}->{$_} } @{ $Self->{CustomerUserMap}->{CustomerUserSearchFields} }; if ( $Param{CustomerUserOnly} ) { @CustomerUserSearchFields = grep { $_ ne 'customer_id' } @CustomerUserSearchFields; } my %QueryCondition = $Self->{DBObject}->QueryCondition( Key => \@CustomerUserSearchFields, #$Self->{CustomerUserMap}->{CustomerUserSearchFields}, Value => $Search, SearchPrefix => $Self->{SearchPrefix}, SearchSuffix => $Self->{SearchSuffix}, CaseSensitive => $Self->{CaseSensitive}, BindMode => 1, ); $SQL .= $QueryCondition{SQL}; push @Bind, @{ $QueryCondition{Values} }; $SQL .= ' '; } elsif ( $Param{PostMasterSearch} ) { if ( $Self->{CustomerUserMap}->{CustomerUserPostMasterSearchFields} ) { # remove dynamic field names that are configured in CustomerUserPostMasterSearchFields # as they cannot be retrieved here my @CustomerUserPostMasterSearchFields = grep { !exists $Self->{ConfiguredDynamicFieldNames}->{$_} } @{ $Self->{CustomerUserMap}->{CustomerUserPostMasterSearchFields} }; my $SQLExt = ''; # for my $Field ( @{ $Self->{CustomerUserMap}->{CustomerUserPostMasterSearchFields} } ) { for my $Field (@CustomerUserPostMasterSearchFields) { if ($SQLExt) { $SQLExt .= ' OR '; } my $PostMasterSearch = $Self->{DBObject}->Quote( $Param{PostMasterSearch} ); push @Bind, \$PostMasterSearch; if ( $Self->{CaseSensitive} ) { $SQLExt .= " $Field = ? "; } else { $SQLExt .= " LOWER($Field) = LOWER(?) "; } } $SQL .= $SQLExt; } } elsif ( $Param{UserLogin} ) { my $UserLogin = $Param{UserLogin}; # check CustomerKey type if ( $Self->{CustomerKeyInteger} ) { # return if login is no integer return if $Param{UserLogin} !~ /^(\+|\-|)\d{1,16}$/; $SQL .= "$Self->{CustomerKey} = ?"; push @Bind, \$UserLogin; } else { $UserLogin = '%' . $Self->{DBObject}->Quote( $UserLogin, 'Like' ) . '%'; $UserLogin =~ s/\*/%/g; push @Bind, \$UserLogin; if ( $Self->{CaseSensitive} ) { $SQL .= "$Self->{CustomerKey} LIKE ? $LikeEscapeString"; } else { $SQL .= "LOWER($Self->{CustomerKey}) LIKE LOWER(?) $LikeEscapeString"; } } } elsif ( $Param{CustomerID} ) { my $CustomerID = $Self->{DBObject}->Quote( $Param{CustomerID}, 'Like' ); $CustomerID =~ s/\*/%/g; push @Bind, \$CustomerID; if ( $Self->{CaseSensitive} ) { $SQL .= "$Self->{CustomerID} LIKE ? $LikeEscapeString"; } else { $SQL .= "LOWER($Self->{CustomerID}) LIKE LOWER(?) $LikeEscapeString"; } } elsif ( $Param{CustomerIDRaw} ) { push @Bind, \$Param{CustomerIDRaw}; if ( $Self->{CaseSensitive} ) { $SQL .= "$Self->{CustomerID} = ? "; } else { $SQL .= "LOWER($Self->{CustomerID}) = LOWER(?) "; } } # add valid option if ( $Self->{CustomerUserMap}->{CustomerValid} && $Valid ) { # get valid object my $ValidObject = $Kernel::OM->Get('Kernel::System::Valid'); $SQL .= ' AND ' . $Self->{CustomerUserMap}->{CustomerValid} . ' IN (' . join( ', ', $ValidObject->ValidIDsGet() ) . ') '; } # dynamic field handling my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); my $DynamicFieldConfigs = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet( ObjectType => 'CustomerUser', Valid => 1, ); my %DynamicFieldConfigsByName = map { $_->{Name} => $_ } @{$DynamicFieldConfigs}; my @CustomerUserListFieldsDynamicFields = grep { exists $Self->{ConfiguredDynamicFieldNames}->{$_} } @{$CustomerUserListFields}; # get data from customer user table return if !$Self->{DBObject}->Prepare( SQL => $SQL, Bind => \@Bind, Limit => $Param{Limit} || $Self->{UserSearchListLimit}, ); my @CustomerUserData; while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { push @CustomerUserData, [@Row]; } CUSTOMERUSERDATA: for my $CustomerUserData (@CustomerUserData) { my $CustomerKey = shift @{$CustomerUserData}; next CUSTOMERUSERDATA if $Users{$CustomerKey}; my %UserStringParts; my $FieldCounter = 0; for my $Field ( @{$CustomerUserData} ) { $UserStringParts{ $CustomerUserListFieldsWithoutDynamicFields[$FieldCounter] } = $Field; $FieldCounter++; } # fetch dynamic field values, if configured if (@CustomerUserListFieldsDynamicFields) { DYNAMICFIELDNAME: for my $DynamicFieldName (@CustomerUserListFieldsDynamicFields) { next DYNAMICFIELDNAME if !exists $DynamicFieldConfigsByName{$DynamicFieldName}; my $Value = $DynamicFieldBackendObject->ValueGet( DynamicFieldConfig => $DynamicFieldConfigsByName{$DynamicFieldName}, ObjectName => $CustomerKey, ); next DYNAMICFIELDNAME if !defined $Value; if ( !IsArrayRefWithData($Value) ) { $Value = [$Value]; } my @Values; VALUE: for my $CurrentValue ( @{$Value} ) { next VALUE if !defined $CurrentValue || !length $CurrentValue; my $ReadableValue = $DynamicFieldBackendObject->ReadableValueRender( DynamicFieldConfig => $DynamicFieldConfigsByName{$DynamicFieldName}, Value => $CurrentValue, ); next VALUE if !IsHashRefWithData($ReadableValue) || !defined $ReadableValue->{Value}; my $IsACLReducible = $DynamicFieldBackendObject->HasBehavior( DynamicFieldConfig => $DynamicFieldConfigsByName{$DynamicFieldName}, Behavior => 'IsACLReducible', ); if ($IsACLReducible) { my $PossibleValues = $DynamicFieldBackendObject->PossibleValuesGet( DynamicFieldConfig => $DynamicFieldConfigsByName{$DynamicFieldName}, ); if ( IsHashRefWithData($PossibleValues) && defined $PossibleValues->{ $ReadableValue->{Value} } ) { $ReadableValue->{Value} = $PossibleValues->{ $ReadableValue->{Value} }; } } push @Values, $ReadableValue->{Value}; } $UserStringParts{$DynamicFieldName} = join ' ', @Values; } } # assemble user string my @UserStringParts; CUSTOMERUSERLISTFIELD: for my $CustomerUserListField ( @{$CustomerUserListFields} ) { next CUSTOMERUSERLISTFIELD if !exists $UserStringParts{$CustomerUserListField} || !defined $UserStringParts{$CustomerUserListField} || !length $UserStringParts{$CustomerUserListField}; push @UserStringParts, $UserStringParts{$CustomerUserListField}; } $Users{$CustomerKey} = join ' ', @UserStringParts; $Users{$CustomerKey} =~ s/^(.*)\s(.+?\@.+?\..+?)(\s|)$/"$1" <$2>/; } # cache request if ( $Self->{CacheObject} ) { $Self->{CacheObject}->Set( Type => $Self->{CacheType} . '_CustomerSearch', Key => $CacheKey, Value => \%Users, TTL => $Self->{CustomerUserMap}->{CacheTTL}, ); } return %Users; } sub CustomerSearchDetail { my ( $Self, %Param ) = @_; if ( ref $Param{SearchFields} ne 'ARRAY' ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "SearchFields must be an array reference!", ); return; } my $Valid = defined $Param{Valid} ? $Param{Valid} : 1; $Param{Limit} //= ''; # Split the search fields in scalar and array fields, before the diffrent handling. my @ScalarSearchFields = grep { 'Input' eq $_->{Type} } @{ $Param{SearchFields} }; my @ArraySearchFields = grep { 'Selection' eq $_->{Type} } @{ $Param{SearchFields} }; # Verify that all passed array parameters contain an arrayref. ARGUMENT: for my $Argument (@ArraySearchFields) { if ( !defined $Param{ $Argument->{Name} } ) { $Param{ $Argument->{Name} } ||= []; next ARGUMENT; } if ( ref $Param{ $Argument->{Name} } ne 'ARRAY' ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "$Argument->{Name} must be an array reference!", ); return; } } # Set the default behaviour for the return type. my $Result = $Param{Result} || 'ARRAY'; # Special handling if the result type is 'COUNT'. if ( $Result eq 'COUNT' ) { # Ignore the parameter 'Limit' when result type is 'COUNT'. $Param{Limit} = ''; # Delete the OrderBy parameter when the result type is 'COUNT'. $Param{OrderBy} = []; } # Define order table from the search fields. my %OrderByTable = map { $_->{Name} => $_->{DatabaseField} } @{ $Param{SearchFields} }; for my $Field (@ArraySearchFields) { my $SelectionsData = $Field->{SelectionsData}; for my $SelectedValue ( @{ $Param{ $Field->{Name} } } ) { # Check if the selected value for the current field is valid. if ( !$SelectionsData->{$SelectedValue} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "The selected value $Field->{Name} is not valid!", ); return; } } } my $DBObject = $Self->{DBObject}; # Assemble the conditions used in the WHERE clause. my @SQLWhere; for my $Field (@ScalarSearchFields) { # Search for scalar fields (wildcards are allowed). if ( $Param{ $Field->{Name} } ) { # Get like escape string needed for some databases (e.g. oracle). my $LikeEscapeString = $DBObject->GetDatabaseFunction('LikeEscapeString'); $Param{ $Field->{Name} } = $DBObject->Quote( $Param{ $Field->{Name} }, 'Like' ); $Param{ $Field->{Name} } =~ s{ \*+ }{%}xmsg; # If the field contains more than only %. if ( $Param{ $Field->{Name} } !~ m{ \A %* \z }xms ) { push @SQLWhere, "LOWER($Field->{DatabaseField}) LIKE LOWER('$Param{ $Field->{Name} }') $LikeEscapeString"; } } } my $DynamicFieldObject = $Kernel::OM->Get('Kernel::System::DynamicField'); my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend'); # Check all configured change dynamic fields, build lookup hash by name. my %CustomerUserDynamicFieldName2Config; my $CustomerUserDynamicFields = $DynamicFieldObject->DynamicFieldListGet( ObjectType => 'CustomerUser', ); for my $DynamicField ( @{$CustomerUserDynamicFields} ) { $CustomerUserDynamicFieldName2Config{ $DynamicField->{Name} } = $DynamicField; } my $SQLDynamicFieldFrom = ''; my $SQLDynamicFieldWhere = ''; my $DynamicFieldJoinCounter = 1; DYNAMICFIELD: for my $DynamicField ( @{$CustomerUserDynamicFields} ) { my $SearchParam = $Param{ "DynamicField_" . $DynamicField->{Name} }; next DYNAMICFIELD if ( !$SearchParam ); next DYNAMICFIELD if ( ref $SearchParam ne 'HASH' ); my $NeedJoin; for my $Operator ( sort keys %{$SearchParam} ) { my @SearchParams = ( ref $SearchParam->{$Operator} eq 'ARRAY' ) ? @{ $SearchParam->{$Operator} } : ( $SearchParam->{$Operator} ); my $SQLDynamicFieldWhereSub = ''; if ($SQLDynamicFieldWhere) { $SQLDynamicFieldWhereSub = ' AND ('; } else { $SQLDynamicFieldWhereSub = ' ('; } my $Counter = 0; TEXT: for my $Text (@SearchParams) { next TEXT if ( !defined $Text || $Text eq '' ); $Text =~ s/\*/%/gi; # Check search attribute, we do not need to search for '*'. next TEXT if $Text =~ /^\%{1,3}$/; my $ValidateSuccess = $DynamicFieldBackendObject->ValueValidate( DynamicFieldConfig => $DynamicField, Value => $Text, UserID => $Param{UserID} || 1, ); if ( !$ValidateSuccess ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Search not executed due to invalid value '" . $Text . "' on field '" . $DynamicField->{Name} . "'!", ); return; } if ($Counter) { $SQLDynamicFieldWhereSub .= ' OR '; } $SQLDynamicFieldWhereSub .= $DynamicFieldBackendObject->SearchSQLGet( DynamicFieldConfig => $DynamicField, TableAlias => "dfv$DynamicFieldJoinCounter", Operator => $Operator, SearchTerm => $Text, ); $Counter++; } $SQLDynamicFieldWhereSub .= ') '; if ($Counter) { $SQLDynamicFieldWhere .= $SQLDynamicFieldWhereSub; $NeedJoin = 1; } } if ($NeedJoin) { $SQLDynamicFieldFrom .= " INNER JOIN dynamic_field_value dfv$DynamicFieldJoinCounter ON (df_obj_id_name.object_id = dfv$DynamicFieldJoinCounter.object_id AND dfv$DynamicFieldJoinCounter.field_id = " . $DBObject->Quote( $DynamicField->{ID}, 'Integer' ) . ") "; $DynamicFieldJoinCounter++; } } # Execute a dynamic field search, if a dynamic field where statement exists. if ($SQLDynamicFieldWhere) { my @DynamicFieldUserLogins; my $SQLDynamicField = "SELECT DISTINCT(df_obj_id_name.object_name) FROM dynamic_field_obj_id_name df_obj_id_name " . $SQLDynamicFieldFrom . " WHERE " . $SQLDynamicFieldWhere; my $UsedCache; if ( $Self->{CacheObject} ) { my $DynamicFieldSearchCacheData = $Self->{CacheObject}->Get( Type => $Self->{CacheType} . '_CustomerSearchDetailDynamicFields', Key => $SQLDynamicField, ); if ( defined $DynamicFieldSearchCacheData ) { if ( ref $DynamicFieldSearchCacheData eq 'ARRAY' ) { @DynamicFieldUserLogins = @{$DynamicFieldSearchCacheData}; $UsedCache = 1; } else { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Invalid ref ' . ref($DynamicFieldSearchCacheData) . '!' ); return; } } } # Get the data only from database, if no cache entry exists. if ( !$UsedCache ) { return if !$DBObject->Prepare( SQL => $SQLDynamicField, ); while ( my @Row = $DBObject->FetchrowArray() ) { push @DynamicFieldUserLogins, $Row[0]; } if ( $Self->{CacheObject} ) { $Self->{CacheObject}->Set( Type => $Self->{CacheType} . '_CustomerSearchDetailDynamicFields', Key => $SQLDynamicField, Value => \@DynamicFieldUserLogins, TTL => $Self->{CustomerUserMap}->{CacheTTL}, ); } } # Add the user logins from the dynamic fields, if a search result exists from the dynamic field search # or skip the search and return a emptry array ref, if no user logins exists from the dynamic field search. if (@DynamicFieldUserLogins) { my $SQLQueryInCondition = $DBObject->QueryInCondition( Key => $Self->{CustomerKey}, Values => \@DynamicFieldUserLogins, BindMode => 0, ); push @SQLWhere, $SQLQueryInCondition; } else { return $Result eq 'COUNT' ? 0 : []; } } FIELD: for my $Field (@ArraySearchFields) { next FIELD if !@{ $Param{ $Field->{Name} } }; my $SQLQueryInCondition = $DBObject->QueryInCondition( Key => $Field->{DatabaseField}, Values => $Param{ $Field->{Name} }, BindMode => 0, ); push @SQLWhere, $SQLQueryInCondition; } # Special parameter for CustomerIDs from a customer company search result. if ( IsArrayRefWithData( $Param{CustomerCompanySearchCustomerIDs} ) ) { my $SQLQueryInCondition = $DBObject->QueryInCondition( Key => $Self->{CustomerID}, Values => $Param{CustomerCompanySearchCustomerIDs}, BindMode => 0, ); push @SQLWhere, $SQLQueryInCondition; } # Special parameter to exclude some user logins from the search result. if ( IsArrayRefWithData( $Param{ExcludeUserLogins} ) ) { my $SQLQueryInCondition = $DBObject->QueryInCondition( Key => $Self->{CustomerKey}, Values => $Param{ExcludeUserLogins}, BindMode => 0, Negate => 1, ); push @SQLWhere, $SQLQueryInCondition; } # Add the valid option if needed. if ( $Self->{CustomerUserMap}->{CustomerValid} && $Valid ) { my $ValidObject = $Kernel::OM->Get('Kernel::System::Valid'); push @SQLWhere, "$Self->{CustomerUserMap}->{CustomerValid} IN (" . join( ', ', $ValidObject->ValidIDsGet() ) . ") "; } # Check if OrderBy contains only unique valid values. my %OrderBySeen; for my $OrderBy ( @{ $Param{OrderBy} } ) { if ( !$OrderBy || $OrderBySeen{$OrderBy} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "OrderBy contains invalid value '$OrderBy' or the value is used more than once!", ); return; } # Remember the value to check if it appears more than once. $OrderBySeen{$OrderBy} = 1; } # Check if OrderByDirection array contains only 'Up' or 'Down'. DIRECTION: for my $Direction ( @{ $Param{OrderByDirection} } ) { # Only 'Up' or 'Down' allowed. next DIRECTION if $Direction eq 'Up'; next DIRECTION if $Direction eq 'Down'; $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "OrderByDirection can only contain 'Up' or 'Down'!", ); return; } # Build the sql statement for the search. my $SQL = "SELECT DISTINCT($Self->{CustomerKey})"; # Modify SQL when the result type is 'COUNT'. if ( $Result eq 'COUNT' ) { $SQL = "SELECT COUNT(DISTINCT($Self->{CustomerKey}))"; } # Assemble the ORDER BY clause. my @SQLOrderBy; # The Order by clause is not needed for the result type 'COUNT'. if ( $Result ne 'COUNT' ) { my $Count = 0; ORDERBY: for my $OrderBy ( @{ $Param{OrderBy} } ) { my $Direction = 'DESC'; if ( $Param{OrderByDirection}->[$Count] ) { if ( $Param{OrderByDirection}->[$Count] eq 'Up' ) { $Direction = 'ASC'; } elsif ( $Param{OrderByDirection}->[$Count] eq 'Down' ) { $Direction = 'DESC'; } } $Count++; next ORDERBY if !$OrderByTable{$OrderBy}; push @SQLOrderBy, "$OrderByTable{$OrderBy} $Direction"; next ORDERBY if $OrderBy eq 'UserLogin'; $SQL .= ", $OrderByTable{$OrderBy}"; } # If there is a possibility that the ordering is not determined # we add an descending ordering by id. if ( !grep { $_ eq 'UserLogin' } ( @{ $Param{OrderBy} } ) ) { push @SQLOrderBy, "$Self->{CustomerKey} DESC"; } } $SQL .= " FROM $Self->{CustomerTable} "; if (@SQLWhere) { my $SQLWhereString = join ' AND ', map {"( $_ )"} @SQLWhere; $SQL .= "WHERE $SQLWhereString "; } if (@SQLOrderBy) { my $OrderByString = join ', ', @SQLOrderBy; $SQL .= "ORDER BY $OrderByString"; } # Check if a cache exists before we ask the database. if ( $Self->{CacheObject} ) { my $CacheData = $Self->{CacheObject}->Get( Type => $Self->{CacheType} . '_CustomerSearchDetail', Key => $SQL . $Param{Limit}, ); if ( defined $CacheData ) { if ( ref $CacheData eq 'ARRAY' ) { return $CacheData; } elsif ( ref $CacheData eq '' ) { return $CacheData; } $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Invalid ref ' . ref($CacheData) . '!' ); return; } } return if !$DBObject->Prepare( SQL => $SQL, Limit => $Param{Limit}, ); my @IDs; while ( my @Row = $DBObject->FetchrowArray() ) { push @IDs, $Row[0]; } # Handle the diffrent result types. if ( $Result eq 'COUNT' ) { if ( $Self->{CacheObject} ) { $Self->{CacheObject}->Set( Type => $Self->{CacheType} . '_CustomerSearchDetail', Key => $SQL . $Param{Limit}, Value => $IDs[0], TTL => $Self->{CustomerUserMap}->{CacheTTL}, ); } return $IDs[0]; } else { if ( $Self->{CacheObject} ) { $Self->{CacheObject}->Set( Type => $Self->{CacheType} . '_CustomerSearchDetail', Key => $SQL . $Param{Limit}, Value => \@IDs, TTL => $Self->{CustomerUserMap}->{CacheTTL}, ); } return \@IDs; } } sub CustomerIDList { my ( $Self, %Param ) = @_; my $Valid = defined $Param{Valid} ? $Param{Valid} : 1; my $SearchTerm = $Param{SearchTerm} || ''; my $CacheType = $Self->{CacheType} . '_CustomerIDList'; my $CacheKey = "CustomerIDList::${Valid}::$SearchTerm"; # check cache if ( $Self->{CacheObject} ) { my $Result = $Self->{CacheObject}->Get( Type => $CacheType, Key => $CacheKey, ); return @{$Result} if ref $Result eq 'ARRAY'; } my $SQL = " SELECT DISTINCT($Self->{CustomerID}) FROM $Self->{CustomerTable} WHERE 1 = 1 "; my @Bind; # add valid option if ( $Self->{CustomerUserMap}->{CustomerValid} && $Valid ) { # get valid object my $ValidObject = $Kernel::OM->Get('Kernel::System::Valid'); my $ValidIDs = join( ', ', $ValidObject->ValidIDsGet() ); $SQL .= " AND $Self->{CustomerUserMap}->{CustomerValid} IN ($ValidIDs) "; } # add search term if ($SearchTerm) { my $SearchTermEscaped = $Self->{DBObject}->QueryStringEscape( QueryString => $SearchTerm ); $SQL .= ' AND '; my %QueryCondition = $Self->{DBObject}->QueryCondition( Key => $Self->{CustomerID}, Value => $SearchTermEscaped, SearchPrefix => $Self->{SearchPrefix}, SearchSuffix => $Self->{SearchSuffix}, CaseSensitive => $Self->{CaseSensitive}, BindMode => 1, ); $SQL .= $QueryCondition{SQL}; push @Bind, @{ $QueryCondition{Values} }; $SQL .= ' '; } return if !$Self->{DBObject}->Prepare( SQL => $SQL, Bind => \@Bind, ); my @Result; while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { push @Result, $Row[0]; } # cache request if ( $Self->{CacheObject} ) { $Self->{CacheObject}->Set( Type => $CacheType, Key => $CacheKey, Value => \@Result, TTL => $Self->{CustomerUserMap}->{CacheTTL}, ); } return @Result; } sub CustomerIDs { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{User} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need User!' ); return; } # check cache if ( $Self->{CacheObject} ) { my $CustomerIDs = $Self->{CacheObject}->Get( Type => $Self->{CacheType}, Key => "CustomerIDs::$Param{User}", ); return @{$CustomerIDs} if ref $CustomerIDs eq 'ARRAY'; } # get customer data my %Data = $Self->CustomerUserDataGet( User => $Param{User}, ); # there are multi customer ids my @CustomerIDs; if ( $Data{UserCustomerIDs} ) { # used separators SEPARATOR: for my $Separator ( ';', ',', '|' ) { next SEPARATOR if $Data{UserCustomerIDs} !~ /\Q$Separator\E/; # split it my @IDs = split /\Q$Separator\E/, $Data{UserCustomerIDs}; for my $ID (@IDs) { $ID =~ s/^\s+//g; $ID =~ s/\s+$//g; push @CustomerIDs, $ID; } last SEPARATOR; } # fallback if no separator got found if ( !@CustomerIDs ) { $Data{UserCustomerIDs} =~ s/^\s+//g; $Data{UserCustomerIDs} =~ s/\s+$//g; push @CustomerIDs, $Data{UserCustomerIDs}; } } # use also the primary customer id if ( $Data{UserCustomerID} && !$Self->{ExcludePrimaryCustomerID} ) { push @CustomerIDs, $Data{UserCustomerID}; } # cache request if ( $Self->{CacheObject} ) { $Self->{CacheObject}->Set( Type => $Self->{CacheType}, Key => 'CustomerIDs::' . $Param{User}, Value => \@CustomerIDs, TTL => $Self->{CustomerUserMap}->{CacheTTL}, ); } return @CustomerIDs; } sub CustomerUserDataGet { my ( $Self, %Param ) = @_; # check needed stuff if ( !$Param{User} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need User!', ); return; } # build select my $SQL = 'SELECT '; ENTRY: for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { next ENTRY if $Entry->[5] eq 'dynamic_field'; $SQL .= " $Entry->[2], "; } if ( !$Self->{ForeignDB} ) { $SQL .= "create_time, create_by, change_time, change_by, "; } $SQL .= $Self->{CustomerKey} . " FROM $Self->{CustomerTable} WHERE "; # check cache if ( $Self->{CacheObject} ) { my $Data = $Self->{CacheObject}->Get( Type => $Self->{CacheType}, Key => "CustomerUserDataGet::$Param{User}", ); return %{$Data} if ref $Data eq 'HASH'; } # check customer key type my $User = $Param{User}; if ( $Self->{CaseSensitive} ) { $SQL .= "$Self->{CustomerKey} = ?"; } else { $SQL .= "LOWER($Self->{CustomerKey}) = LOWER(?)"; } # ask the database return if !$Self->{DBObject}->Prepare( SQL => $SQL, Bind => [ \$User ], Limit => 1, ); # fetch the result my %Data; ROW: while ( my @Row = $Self->{DBObject}->FetchrowArray() ) { my $MapCounter = 0; ENTRY: for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { next ENTRY if $Entry->[5] eq 'dynamic_field'; $Data{ $Entry->[0] } = $Row[$MapCounter]; $MapCounter++; } next ROW if $Self->{ForeignDB}; for my $Key (qw(CreateTime CreateBy ChangeTime ChangeBy)) { $Data{$Key} = $Row[$MapCounter]; $MapCounter++; } } # check data if ( !$Data{UserLogin} ) { # cache request if ( $Self->{CacheObject} ) { $Self->{CacheObject}->Set( Type => $Self->{CacheType}, Key => "CustomerUserDataGet::$Param{User}", Value => {}, TTL => $Self->{CustomerUserMap}->{CacheTTL}, ); } return; } my $CustomerUserListFieldsMap = $Self->{CustomerUserMap}->{CustomerUserListFields}; if ( !IsArrayRefWithData($CustomerUserListFieldsMap) ) { $CustomerUserListFieldsMap = [ 'first_name', 'last_name', 'email', ]; } # Order fields by CustomerUserListFields (see bug#13821). my @CustomerUserListFields; for my $Field ( @{$CustomerUserListFieldsMap} ) { my @FieldNames = map { $_->[0] } grep { $_->[2] eq $Field } @{ $Self->{CustomerUserMap}->{Map} }; push @CustomerUserListFields, $FieldNames[0]; } my $UserMailString = ''; my @UserMailStringParts; FIELD: for my $Field (@CustomerUserListFields) { next FIELD if !$Data{$Field}; push @UserMailStringParts, $Data{$Field}; } $UserMailString = join ' ', @UserMailStringParts; $UserMailString =~ s/^(.*)\s(.+?\@.+?\..+?)(\s|)$/"$1" <$2>/; # add the UserMailString to the data hash $Data{UserMailString} = $UserMailString; # compat! $Data{UserID} = $Data{UserLogin}; # get preferences my %Preferences = $Self->GetPreferences( UserID => $Data{UserID} ); # add last login timestamp if ( $Preferences{UserLastLogin} ) { my $DateTimeObject = $Kernel::OM->Create( 'Kernel::System::DateTime', ObjectParams => { Epoch => $Preferences{UserLastLogin}, }, ); $Preferences{UserLastLoginTimestamp} = $DateTimeObject->ToString(); } # cache request if ( $Self->{CacheObject} ) { $Self->{CacheObject}->Set( Type => $Self->{CacheType}, Key => "CustomerUserDataGet::$Param{User}", Value => { %Data, %Preferences }, TTL => $Self->{CustomerUserMap}->{CacheTTL}, ); } return ( %Data, %Preferences ); } sub CustomerUserAdd { my ( $Self, %Param ) = @_; # check ro/rw if ( $Self->{ReadOnly} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Customer backend is read only!', ); return; } # check needed stuff ENTRY: for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { if ( !$Param{ $Entry->[0] } && $Entry->[4] ) { # skip UserLogin, will be checked later next ENTRY if ( $Entry->[0] eq 'UserLogin' ); # ignore dynamic fields here next ENTRY if $Entry->[5] eq 'dynamic_field'; $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Entry->[0]!", ); return; } } if ( !$Param{UserID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need UserID!', ); return; } # if no UserLogin is given if ( !$Param{UserLogin} && $Self->{CustomerUserMap}->{AutoLoginCreation} ) { # get time object my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime'); my $DateTimeString = $DateTimeObject->Format( Format => '%Y%m%d%H%M' ); my $Prefix = $Self->{CustomerUserMap}->{AutoLoginCreationPrefix} || 'auto'; $Param{UserLogin} = "$Prefix-$DateTimeString" . int( rand(99) ); } # check if user login exists if ( !$Param{UserLogin} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need UserLogin!', ); return; } # check email address if already exists if ( $Param{UserEmail} && $Self->{CustomerUserMap}->{CustomerUserEmailUniqCheck} ) { my %Result = $Self->CustomerSearch( Valid => 0, PostMasterSearch => $Param{UserEmail}, ); if (%Result) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Kernel::OM->Get('Kernel::Language') ->Translate('This email address is already in use for another customer user.'), ); return; } } # get check item object my $CheckItemObject = $Kernel::OM->Get('Kernel::System::CheckItem'); # check email address mx if ( $Param{UserEmail} && !$CheckItemObject->CheckEmail( Address => $Param{UserEmail} ) && grep { $_ eq $Param{ValidID} } $Kernel::OM->Get('Kernel::System::Valid')->ValidIDsGet() ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Email address ($Param{UserEmail}) not valid (" . $CheckItemObject->CheckError() . ")!", ); return; } # quote values my %Value; for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { if ( $Entry->[5] =~ /^int$/i ) { if ( $Param{ $Entry->[0] } ) { $Value{ $Entry->[0] } = $Param{ $Entry->[0] }; } else { $Value{ $Entry->[0] } = 0; } } else { if ( $Param{ $Entry->[0] } ) { $Value{ $Entry->[0] } = $Param{ $Entry->[0] }; } else { $Value{ $Entry->[0] } = ''; } } } # build insert my $SQL = "INSERT INTO $Self->{CustomerTable} ("; my @Bind; my %SeenKey; # If the map contains duplicated field names, insert only once. my @ColumnNames; MAPENTRY: for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { next MAPENTRY if $Entry->[5] eq 'dynamic_field'; # skip dynamic fields next MAPENTRY if ( lc( $Entry->[0] ) eq "userpassword" ); next MAPENTRY if $SeenKey{ $Entry->[2] }++; push @ColumnNames, $Entry->[2]; } $SQL .= join ', ', @ColumnNames; if ( !$Self->{ForeignDB} ) { $SQL .= ', create_time, create_by, change_time, change_by'; } $SQL .= ') VALUES ('; my %SeenValue; my $BindColumns = 0; ENTRY: for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { next ENTRY if $Entry->[5] eq 'dynamic_field'; # skip dynamic fields next ENTRY if ( lc( $Entry->[0] ) eq "userpassword" ); next ENTRY if $SeenValue{ $Entry->[2] }++; $BindColumns++; push @Bind, \$Value{ $Entry->[0] }; } $SQL .= join ', ', ('?') x $BindColumns; if ( !$Self->{ForeignDB} ) { $SQL .= ', current_timestamp, ?, current_timestamp, ?'; push @Bind, \$Param{UserID}; push @Bind, \$Param{UserID}; } $SQL .= ')'; return if !$Self->{DBObject}->Do( SQL => $SQL, Bind => \@Bind ); # log notice $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'info', Message => "CustomerUser: '$Param{UserLogin}' created successfully ($Param{UserID})!", ); # set password if ( $Param{UserPassword} ) { $Self->SetPassword( UserLogin => $Param{UserLogin}, PW => $Param{UserPassword} ); } $Self->_CustomerUserCacheClear( UserLogin => $Param{UserLogin} ); return $Param{UserLogin}; } sub CustomerUserUpdate { my ( $Self, %Param ) = @_; # check ro/rw if ( $Self->{ReadOnly} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Customer backend is read only!', ); return; } # check needed stuff for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { if ( !$Param{ $Entry->[0] } && $Entry->[5] ne 'dynamic_field' # ignore dynamic fields here && $Entry->[4] # only check required fields && $Entry->[0] ne 'UserPassword' # ignore UserPassword field ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Entry->[0]!", ); return; } } # get check item object my $CheckItemObject = $Kernel::OM->Get('Kernel::System::CheckItem'); # check email address if ( $Param{UserEmail} && !$CheckItemObject->CheckEmail( Address => $Param{UserEmail} ) && grep { $_ eq $Param{ValidID} } $Kernel::OM->Get('Kernel::System::Valid')->ValidIDsGet() ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Email address ($Param{UserEmail}) not valid (" . $CheckItemObject->CheckError() . ")!", ); return; } # get old user data (pw) my %UserData = $Self->CustomerUserDataGet( User => $Param{ID} ); # if we update the email address, check if it already exists if ( $Param{UserEmail} && $Self->{CustomerUserMap}->{CustomerUserEmailUniqCheck} && lc $Param{UserEmail} ne lc $UserData{UserEmail} ) { my %Result = $Self->CustomerSearch( Valid => 0, PostMasterSearch => $Param{UserEmail}, ); if (%Result) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Kernel::OM->Get('Kernel::Language') ->Translate('This email address is already in use for another customer user.'), ); return; } } # quote values my %Value; for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { if ( $Entry->[5] =~ /^int$/i ) { if ( $Param{ $Entry->[0] } ) { $Value{ $Entry->[0] } = $Param{ $Entry->[0] }; } else { $Value{ $Entry->[0] } = 0; } } else { if ( $Param{ $Entry->[0] } ) { $Value{ $Entry->[0] } = $Param{ $Entry->[0] }; } else { $Value{ $Entry->[0] } = ""; } } } # update db my $SQL = "UPDATE $Self->{CustomerTable} SET "; my @Bind; my %SeenKey; # If the map contains duplicated field names, insert only once. ENTRY: for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { next ENTRY if $Entry->[5] eq 'dynamic_field'; # skip dynamic fields next ENTRY if $Entry->[7]; # skip readonly fields next ENTRY if ( lc( $Entry->[0] ) eq "userpassword" ); next ENTRY if $SeenKey{ $Entry->[2] }++; $SQL .= " $Entry->[2] = ?, "; push @Bind, \$Value{ $Entry->[0] }; } if ( !$Self->{ForeignDB} ) { $SQL .= 'change_time = current_timestamp, change_by = ?'; push @Bind, \$Param{UserID}; } else { chop $SQL; chop $SQL; } $SQL .= ' WHERE '; if ( $Self->{CaseSensitive} ) { $SQL .= "$Self->{CustomerKey} = ?"; } else { $SQL .= "LOWER($Self->{CustomerKey}) = LOWER(?)"; } push @Bind, \$Param{ID}; return if !$Self->{DBObject}->Do( SQL => $SQL, Bind => \@Bind ); # check if we need to update Customer Preferences if ( $Param{UserLogin} ne $UserData{UserLogin} ) { # update the preferences $Self->{PreferencesObject}->RenamePreferences( NewUserID => $Param{UserLogin}, OldUserID => $UserData{UserLogin}, ); } # log notice $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'info', Message => "CustomerUser: '$Param{UserLogin}' updated successfully ($Param{UserID})!", ); # check pw if ( $Param{UserPassword} ) { $Self->SetPassword( UserLogin => $Param{UserLogin}, PW => $Param{UserPassword} ); } $Self->_CustomerUserCacheClear( UserLogin => $Param{UserLogin} ); if ( $Param{UserLogin} ne $UserData{UserLogin} ) { $Self->_CustomerUserCacheClear( UserLogin => $UserData{UserLogin} ); } return 1; } sub SetPassword { my ( $Self, %Param ) = @_; my $Login = $Param{UserLogin}; my $Pw = $Param{PW} || ''; # check ro/rw if ( $Self->{ReadOnly} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Customer backend is read only!', ); return; } # check needed stuff if ( !$Param{UserLogin} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need UserLogin!', ); return; } my $CryptedPw = ''; my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); my $CryptType = $ConfigObject->Get('Customer::AuthModule::DB::CryptType') || 'sha2'; # get encode object my $EncodeObject = $Kernel::OM->Get('Kernel::System::Encode'); # crypt plain (no crypt at all) if ( $CryptType eq 'plain' ) { $CryptedPw = $Pw; } # crypt with unix crypt elsif ( $CryptType eq 'crypt' ) { # encode output, needed by crypt() only non utf8 signs $EncodeObject->EncodeOutput( \$Pw ); $EncodeObject->EncodeOutput( \$Login ); $CryptedPw = crypt( $Pw, $Login ); $EncodeObject->EncodeInput( \$CryptedPw ); } # crypt with md5 crypt elsif ( $CryptType eq 'md5' || !$CryptType ) { # encode output, needed by unix_md5_crypt() only non utf8 signs $EncodeObject->EncodeOutput( \$Pw ); $EncodeObject->EncodeOutput( \$Login ); $CryptedPw = unix_md5_crypt( $Pw, $Login ); $EncodeObject->EncodeInput( \$CryptedPw ); } # crypt with md5 crypt (compatible with Apache's .htpasswd files) elsif ( $CryptType eq 'apr1' ) { # encode output, needed by apache_md5_crypt() only non utf8 signs $EncodeObject->EncodeOutput( \$Pw ); $EncodeObject->EncodeOutput( \$Login ); $CryptedPw = apache_md5_crypt( $Pw, $Login ); $EncodeObject->EncodeInput( \$CryptedPw ); } # crypt with sha1 elsif ( $CryptType eq 'sha1' ) { my $SHAObject = Digest::SHA->new('sha1'); $EncodeObject->EncodeOutput( \$Pw ); $SHAObject->add($Pw); $CryptedPw = $SHAObject->hexdigest(); } elsif ( $CryptType eq 'sha512' ) { my $SHAObject = Digest::SHA->new('sha512'); $EncodeObject->EncodeOutput( \$Pw ); $SHAObject->add($Pw); $CryptedPw = $SHAObject->hexdigest(); } # bcrypt elsif ( $CryptType eq 'bcrypt' ) { # get main object my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); if ( !$MainObject->Require('Crypt::Eksblowfish::Bcrypt') ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "CustomerUser: '$Login' tried to store password with bcrypt but 'Crypt::Eksblowfish::Bcrypt' is not installed!", ); return; } my $Cost = $ConfigObject->Get('Customer::AuthModule::DB::bcryptCost') // 12; # Don't allow values smaller than 9 for security. $Cost = 9 if $Cost < 9; # Current Crypt::Eksblowfish::Bcrypt limit is 31. $Cost = 31 if $Cost > 31; my $Salt = $MainObject->GenerateRandomString( Length => 16 ); # remove UTF8 flag, required by Crypt::Eksblowfish::Bcrypt $EncodeObject->EncodeOutput( \$Pw ); # calculate password hash my $Octets = Crypt::Eksblowfish::Bcrypt::bcrypt_hash( { key_nul => 1, cost => $Cost, salt => $Salt, }, $Pw ); # We will store cost and salt in the password string so that it can be decoded # in future even if we use a higher cost by default. $CryptedPw = "BCRYPT:$Cost:$Salt:" . Crypt::Eksblowfish::Bcrypt::en_base64($Octets); } # crypt with sha2 as fallback else { my $SHAObject = Digest::SHA->new('sha256'); # encode output, needed by sha256_hex() only non utf8 signs $EncodeObject->EncodeOutput( \$Pw ); $SHAObject->add($Pw); $CryptedPw = $SHAObject->hexdigest(); } # update db for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) { if ( $Entry->[0] =~ /^UserPassword$/i ) { $Param{PasswordCol} = $Entry->[2]; } if ( $Entry->[0] =~ /^UserLogin$/i ) { $Param{LoginCol} = $Entry->[2]; } } # check if needed pw col. exists (else there is no pw col.) if ( $Param{PasswordCol} && $Param{LoginCol} ) { my $SQL = "UPDATE $Self->{CustomerTable} SET " . " $Param{PasswordCol} = ? " . " WHERE "; if ( $Self->{CaseSensitive} ) { $SQL .= "$Param{LoginCol} = ?"; } else { $SQL .= "LOWER($Param{LoginCol}) = LOWER(?)"; } return if !$Self->{DBObject}->Do( SQL => $SQL, Bind => [ \$CryptedPw, \$Param{UserLogin} ], ); # log notice $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'notice', Message => "CustomerUser: '$Param{UserLogin}' changed password successfully!", ); $Self->_CustomerUserCacheClear( UserLogin => $Param{UserLogin} ); return 1; } # need no pw to set return 1; } sub GenerateRandomPassword { my ( $Self, %Param ) = @_; # generated passwords are eight characters long by default my $Size = $Param{Size} || 8; my $Password = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString( Length => $Size, ); return $Password; } sub SetPreferences { my ( $Self, %Param ) = @_; # check needed params if ( !$Param{UserID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need UserID!', ); return; } $Self->_CustomerUserCacheClear( UserLogin => $Param{UserID} ); return $Self->{PreferencesObject}->SetPreferences(%Param); } sub GetPreferences { my ( $Self, %Param ) = @_; # check needed params if ( !$Param{UserID} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need UserID!', ); return; } return $Self->{PreferencesObject}->GetPreferences(%Param); } sub SearchPreferences { my ( $Self, %Param ) = @_; return $Self->{PreferencesObject}->SearchPreferences(%Param); } sub _CustomerUserCacheClear { my ( $Self, %Param ) = @_; return if !$Self->{CacheObject}; if ( !$Param{UserLogin} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => 'Need UserLogin!', ); return; } $Self->{CacheObject}->Delete( Type => $Self->{CacheType}, Key => "CustomerUserDataGet::$Param{UserLogin}", ); $Self->{CacheObject}->Delete( Type => $Self->{CacheType}, Key => "CustomerName::$Param{UserLogin}", ); $Self->{CacheObject}->Delete( Type => $Self->{CacheType}, Key => "CustomerIDs::$Param{UserLogin}", ); # delete all search cache entries $Self->{CacheObject}->CleanUp( Type => $Self->{CacheType} . '_CustomerIDList', ); $Self->{CacheObject}->CleanUp( Type => $Self->{CacheType} . '_CustomerSearch', ); $Self->{CacheObject}->CleanUp( Type => $Self->{CacheType} . '_CustomerSearchDetail', ); $Self->{CacheObject}->CleanUp( Type => $Self->{CacheType} . '_CustomerSearchDetailDynamicFields', ); $Self->{CacheObject}->CleanUp( Type => 'CustomerGroup', ); for my $Function (qw(CustomerUserList)) { for my $Valid ( 0 .. 1 ) { $Self->{CacheObject}->Delete( Type => $Self->{CacheType}, Key => "${Function}::${Valid}", ); } } return 1; } sub DESTROY { my $Self = shift; # disconnect if it's not a parent DBObject if ( $Self->{NotParentDBObject} ) { if ( $Self->{DBObject} ) { $Self->{DBObject}->Disconnect(); } } return 1; } 1;