# --
# Modified version of the work: Copyright (C) 2006-2023 KIX Service Software GmbH, https://www.kixdesk.com
# based on the original work of:
# Copyright (C) 2001-2023 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file LICENSE for license information (AGPL). If you
# did not receive this file, see https://www.gnu.org/licenses/agpl.txt.
# --

package Kernel::System::CustomerUser::LDAP;

use strict;
use warnings;

use Net::LDAP;
use Net::LDAP::Util qw(escape_filter_value);

our @ObjectDependencies = (
    'Kernel::Config',
    'Kernel::System::Cache',
    'Kernel::System::Encode',
    'Kernel::System::Log',
    'Kernel::System::Time',
);

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!";
    }

    # max shown user a search list
    $Self->{UserSearchListLimit} = $Self->{CustomerUserMap}->{CustomerUserSearchListLimit} || 200;

    # get ldap preferences
    $Self->{Die} = 0;
    if ( defined $Self->{CustomerUserMap}->{Params}->{Die} ) {
        $Self->{Die} = $Self->{CustomerUserMap}->{Params}->{Die};
    }

    # get config object
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    # params
    if ( $Self->{CustomerUserMap}->{Params}->{Params} ) {
        $Self->{Params} = $Self->{CustomerUserMap}->{Params}->{Params};
    }

    # Net::LDAP new params
    elsif ( $ConfigObject->Get( 'AuthModule::LDAP::Params' . $Param{Count} ) ) {
        $Self->{Params} = $ConfigObject->Get( 'AuthModule::LDAP::Params' . $Param{Count} );
    }
    else {
        $Self->{Params} = {};
    }

    # host
    if ( $Self->{CustomerUserMap}->{Params}->{Host} ) {
        $Self->{Host} = $Self->{CustomerUserMap}->{Params}->{Host};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->Params->Host in Kernel/Config.pm',
        );
        return;
    }

    # base dn
    if ( defined $Self->{CustomerUserMap}->{Params}->{BaseDN} ) {
        $Self->{BaseDN} = $Self->{CustomerUserMap}->{Params}->{BaseDN};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->Params->BaseDN in Kernel/Config.pm',
        );
        return;
    }

    # scope
    if ( $Self->{CustomerUserMap}->{Params}->{SSCOPE} ) {
        $Self->{SScope} = $Self->{CustomerUserMap}->{Params}->{SSCOPE};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->Params->SSCOPE in Kernel/Config.pm',
        );
        return;
    }

    # search user
    $Self->{SearchUserDN} = $Self->{CustomerUserMap}->{Params}->{UserDN} || '';
    $Self->{SearchUserPw} = $Self->{CustomerUserMap}->{Params}->{UserPw} || '';

    # group dn
    $Self->{GroupDN} = $Self->{CustomerUserMap}->{Params}->{GroupDN} || '';

    # customer key
    if ( $Self->{CustomerUserMap}->{CustomerKey} ) {
        $Self->{CustomerKey} = $Self->{CustomerUserMap}->{CustomerKey};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->CustomerKey in Kernel/Config.pm',
        );
        return;
    }

    # customer id
    if ( $Self->{CustomerUserMap}->{CustomerID} ) {
        $Self->{CustomerID} = $Self->{CustomerUserMap}->{CustomerID};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->CustomerID in Kernel/Config.pm',
        );
        return;
    }

    # ldap filter always used
    $Self->{AlwaysFilter} = $Self->{CustomerUserMap}->{Params}->{AlwaysFilter} || '';

    $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} = '*';
    }

    # charset settings
    $Self->{SourceCharset} = $Self->{CustomerUserMap}->{Params}->{SourceCharset} || '';

    # 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');
    }

    # get valid filter if used
    $Self->{ValidFilter} = $Self->{CustomerUserMap}->{CustomerUserValidFilter} || '';

    # connect first if Die is enabled, make sure that connection is possible, else die
    if ( $Self->{Die} ) {
        return if !$Self->_Connect();
    }

    return $Self;
}

sub _Connect {
    my ( $Self, %Param ) = @_;

    # return if connection is already open
    return 1 if $Self->{LDAP};

    # ldap connect and bind (maybe with SearchUserDN and SearchUserPw)
    $Self->{LDAP} = Net::LDAP->new( $Self->{Host}, %{ $Self->{Params} } );

    if ( !$Self->{LDAP} ) {
        if ( $Self->{Die} ) {
            die "Can't connect to $Self->{Host}: $@";
        }
        else {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Can't connect to $Self->{Host}: $@",
            );
            return;
        }
    }

    my $Result;
    if ( $Self->{SearchUserDN} && $Self->{SearchUserPw} ) {
        $Result = $Self->{LDAP}->bind(
            dn       => $Self->{SearchUserDN},
            password => $Self->{SearchUserPw},
        );
    }
    else {
        $Result = $Self->{LDAP}->bind();
    }

    if ( $Result->code() ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'First bind failed! ' . $Result->error(),
        );
        $Self->{LDAP}->disconnect();
        return;
    }

    return 1;
}

sub CustomerName {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{UserLogin} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need UserLogin!'
        );
        return;
    }

    # build filter
    my $Filter = "($Self->{CustomerKey}=" . escape_filter_value( $Param{UserLogin} ) . ')';

    # prepare filter
    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    # check cache
    my $Name = '';
    if ( $Self->{CacheObject} ) {
        my $CacheName = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => 'CustomerName::' . $Param{UserLogin},
        );
        return $CacheName if defined $CacheName;
    }

    # create ldap connect
    return if !$Self->_Connect();

    # perform user search
    my $Result = $Self->{LDAP}->search(
        base      => $Self->{BaseDN},
        scope     => $Self->{SScope},
        filter    => $Filter,
        sizelimit => $Self->{UserSearchListLimit},
        attrs     => $Self->{CustomerUserMap}->{CustomerUserNameFields},
    );

    if ( $Result->code() ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Search failed! ' . $Result->error(),
        );
        return;
    }

    for my $Entry ( $Result->all_entries() ) {

        for my $Field ( @{ $Self->{CustomerUserMap}->{CustomerUserNameFields} } ) {

            if ( defined $Entry->get_value($Field) ) {

                if ( !$Name ) {
                    $Name = $Self->_ConvertFrom( $Entry->get_value($Field) );
                }
                else {
                    $Name .= ' ' . $Self->_ConvertFrom( $Entry->get_value($Field) );
                }
            }
        }
    }

    # 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 ) = @_;

    if ( $Param{CustomerIDRaw} ) {
        $Param{CustomerID} = $Param{CustomerIDRaw};
    }

    # check needed stuff
    if (
        !$Param{Search}
        && !$Param{UserLogin}
        && !$Param{PostMasterSearch}
        && !$Param{SearchFields}
        && !$Param{CustomerID}
    ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need Search, UserLogin, PostMasterSearch, CustomerID or SearchFields!'
        );
        return;
    }

    # build filter
    my $Filter = '';
    if ( $Param{Search} ) {

        my $Count = 0;
        my @Parts = split( /\+/, $Param{Search}, 6 );
        for my $Part (@Parts) {

            $Part = $Self->{SearchPrefix} . $Part . $Self->{SearchSuffix};
            $Part =~ s/(\%+)/\%/g;
            $Part =~ s/(\*+)\*/*/g;
            $Count++;

            if ( $Self->{CustomerUserMap}->{CustomerUserSearchFields} ) {

                # quote LDAP filter value but keep asterisks unescaped (wildcard)
                $Part =~ s/\*/encodedasterisk20160930/g;
                $Part = escape_filter_value( $Self->_ConvertTo($Part) );
                $Part =~ s/encodedasterisk20160930/*/g;

                $Filter .= '(|';
                for my $Field ( @{ $Self->{CustomerUserMap}->{CustomerUserSearchFields} } ) {
                    $Filter .= "($Field=" . $Part . ')';
                }
                $Filter .= ')';
            }
            else {

                # quote LDAP filter value but keep asterisks unescaped (wildcard)
                $Part =~ s/\*/encodedasterisk20160930/g;
                $Part = escape_filter_value($Part);
                $Part =~ s/encodedasterisk20160930/*/g;

                $Filter .= "($Self->{CustomerKey}=" . $Part . ')';
            }
        }

        if ( $Count > 1 ) {
            $Filter = "(&$Filter)";
        }
    }
    elsif ( $Param{PostMasterSearch} ) {

        if ( $Self->{CustomerUserMap}->{CustomerUserPostMasterSearchFields} ) {

            # quote LDAP filter value but keep asterisks unescaped (wildcard)
            $Param{PostMasterSearch} =~ s/\*/encodedasterisk20160930/g;
            $Param{PostMasterSearch} = escape_filter_value( $Param{PostMasterSearch} );
            $Param{PostMasterSearch} =~ s/encodedasterisk20160930/*/g;

            $Filter = '(|';
            for my $Field ( @{ $Self->{CustomerUserMap}->{CustomerUserPostMasterSearchFields} } ) {
                $Filter .= "($Field=$Param{PostMasterSearch})";
            }
            $Filter .= ')';
        }
    }
    elsif ( $Param{UserLogin} ) {
        $Filter = "($Self->{CustomerKey}=" . escape_filter_value( $Param{UserLogin} ) . ')';
    }
    elsif ( $Param{CustomerID} ) {
        $Filter = "($Self->{CustomerID}=" . escape_filter_value( $Param{CustomerID} ) . ')';

        # if multiple customer ids used
        if ( $Param{MultipleCustomerIDs} ) {
            my @CustomerIDsMap;
            for my $Array ( @{ $Self->{CustomerUserMap}->{Map} } ) {
                next if $Array->[0] !~ m/UserCustomerIDs$/;
                @CustomerIDsMap = @{$Array};
            }

            if ( scalar @CustomerIDsMap && $CustomerIDsMap[5] eq 'array' ) {
                $Filter = '(|'
                        . $Filter
                        . '(' . $CustomerIDsMap[2] . '=' . escape_filter_value( $Param{CustomerID} ) . ')'
                        . ')';
            }
        }
    }

    # add field based filter input
    if ( $Param{SearchFields} ) {
        my $SearchFurtherFields;

        # backend mapping
        if ( $Self->{CustomerUserMap}->{CustomerUserFurtherSearchFields} ) {
            $SearchFurtherFields = $Self->{CustomerUserMap}->{CustomerUserFurtherSearchFields};
        }

        # fallback, if no individual mapping
        else {
            $SearchFurtherFields = $Kernel::OM->Get('Kernel::Config')->Get('FurtherSearchFields::Mapping');
        }

        $Filter = "(&$Filter";
        for my $Field ( keys %{ $Param{SearchFields} } ) {
            next if !$SearchFurtherFields || !$SearchFurtherFields->{$Field};

            my $Value =
                $Self->{SearchPrefix} . $Param{SearchFields}->{$Field} . $Self->{SearchSuffix};
            $Value =~ s/(\%+)/\%/g;
            $Value =~ s/(\*+)\*/*/g;

            if ( $SearchFurtherFields->{$Field} =~ /,/ ) {
                my @SearchFields = split( /,/, $SearchFurtherFields->{$Field} );
                $Filter .= '(|';
                for my $CurrentSearchField (@SearchFields) {
                    $Filter .= '(' . $CurrentSearchField . "=$Value)";
                }
                $Filter .= ')';
            }
            else {
                $Filter .= '(' . $SearchFurtherFields->{$Field} . "=$Value)";
            }
        }
        $Filter .= ')';
    }

    # prepare filter
    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    # add valid filter
    if ( $Self->{ValidFilter} ) {
        $Filter = "(&$Filter$Self->{ValidFilter})";
    }

    # check cache
    if ( $Self->{CacheObject} ) {
        my $Users = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => 'CustomerSearch::' . $Filter,
        );
        return %{$Users} if ref $Users eq 'HASH';
    }

    # create ldap connect
    return if !$Self->_Connect();

    # combine needed attrs
    my @Attributes = ( @{ $Self->{CustomerUserMap}->{CustomerUserListFields} }, $Self->{CustomerKey} );

    # perform user search
    my $Result = $Self->{LDAP}->search(
        base      => $Self->{BaseDN},
        scope     => $Self->{SScope},
        filter    => $Filter,
        sizelimit => $Param{Limit} || $Self->{UserSearchListLimit},
        attrs     => \@Attributes,
    );

    # log ldap errors
    if ( $Result->code() ) {

        if ( $Result->code() == 4 ) {

            # Result code 4 (LDAP_SIZELIMIT_EXCEEDED) is normal if there
            # are more items in LDAP than search limit defined in KIX or
            # in LDAP server. Avoid spamming logs with such errors.
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'debug',
                Message  => 'LDAP size limit exceeded (' . $Result->error() . ').',
            );
        }
        else {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => 'Search failed! ' . $Result->error(),
            );
        }
    }

    my %Users;
    for my $Entry ( $Result->all_entries() ) {

        my $CustomerString = '';

        for my $Field ( @{ $Self->{CustomerUserMap}->{CustomerUserListFields} } ) {

            my $Value = $Self->_ConvertFrom( $Entry->get_value($Field) );

            if ($Value) {
                if ( $Field =~ /^targetaddress$/i ) {
                    $Value =~ s/SMTP:(.*)/$1/;
                }
                $CustomerString .= $Value . ' ';
            }
        }

        $CustomerString =~ s/^(.*)\s(.+?\@.+?\..+?)(\s|)$/"$1" <$2>/;

        if ( defined $Entry->get_value( $Self->{CustomerKey} ) ) {
            $Users{ $Self->_ConvertFrom( $Entry->get_value( $Self->{CustomerKey} ) ) } = $CustomerString;
        }
    }

    # check if user need to be in a group!
    if ( $Self->{GroupDN} ) {

        for my $Filter2 ( sort keys %Users ) {

            my $Result2 = $Self->{LDAP}->search(
                base      => $Self->{GroupDN},
                scope     => $Self->{SScope},
                filter    => 'memberUid=' . escape_filter_value($Filter2),
                sizelimit => $Param{Limit} || $Self->{UserSearchListLimit},
                attrs     => ['1.1'],
            );

            if ( !$Result2->all_entries() ) {
                delete $Users{$Filter2};
            }
        }
    }

    # cache request
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Set(
            Type  => $Self->{CacheType},
            Key   => 'CustomerSearch::' . $Filter,
            Value => \%Users,
            TTL   => $Self->{CustomerUserMap}->{CacheTTL},
        );
    }

    return %Users;
}

sub CustomerUserList {
    my ( $Self, %Param ) = @_;

    my $Valid = defined $Param{Valid} ? $Param{Valid} : 1;

    # prepare filter
    my $Filter = "($Self->{CustomerKey}=*)";
    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    # add valid filter
    if ( $Self->{ValidFilter} && $Valid ) {
        $Filter = "(&$Filter$Self->{ValidFilter})";
    }

    # check cache
    if ( $Self->{CacheObject} ) {
        my $Users = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => "CustomerUserList::$Filter",
        );
        return %{$Users} if ref $Users eq 'HASH';
    }

    # create ldap connect
    return if !$Self->_Connect();

    # combine needed attrs
    my @Attributes = ( $Self->{CustomerKey}, $Self->{CustomerID} );

    # perform user search
    my $Result = $Self->{LDAP}->search(
        base      => $Self->{BaseDN},
        scope     => $Self->{SScope},
        filter    => $Filter,
        sizelimit => $Self->{UserSearchListLimit},
        attrs     => \@Attributes,
    );

    # log ldap errors
    if ( $Result->code() ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => $Result->error(),
        );
    }

    my %Users;
    for my $Entry ( $Result->all_entries() ) {

        my $CustomerString = '';
        for my $Field (@Attributes) {

            my $FieldValue = $Entry->get_value($Field);
            $FieldValue = defined $FieldValue ? $FieldValue : '';

            $CustomerString .= $Self->_ConvertFrom($FieldValue) . ' ';
        }

        my $KeyValue = $Entry->get_value( $Self->{CustomerKey} );
        $KeyValue = defined $KeyValue ? $KeyValue : '';

        $Users{ $Self->_ConvertFrom($KeyValue) } = $CustomerString;
    }

    # check if user need to be in a group!
    if ( $Self->{GroupDN} ) {

        for my $Filter2 ( sort keys %Users ) {

            my $Result2 = $Self->{LDAP}->search(
                base      => $Self->{GroupDN},
                scope     => $Self->{SScope},
                filter    => 'memberUid=' . $Filter2,
                sizelimit => $Self->{UserSearchListLimit},
                attrs     => ['1.1'],
            );

            if ( !$Result2->all_entries() ) {
                delete $Users{$Filter2};
            }
        }
    }

    # cache request
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Set(
            Type  => $Self->{CacheType},
            Key   => "CustomerUserList::$Filter",
            Value => \%Users,
            TTL   => $Self->{CustomerUserMap}->{CacheTTL},
        );
    }

    return %Users;
}

sub CustomerIDList {
    my ( $Self, %Param ) = @_;

    my $Valid = defined $Param{Valid} ? $Param{Valid} : 1;
    my $SearchTerm = $Param{SearchTerm} || '';

    my $CacheKey = "CustomerIDList::${Valid}::$SearchTerm";

    # check cache
    if ( $Self->{CacheObject} ) {
        my $Result = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => $CacheKey,
        );
        return @{$Result} if ref $Result eq 'ARRAY';
    }

    # prepare filter
    my $Filter = "($Self->{CustomerID}=*)";
    if ($SearchTerm) {

        my $SearchFilter = $Self->{SearchPrefix} . $SearchTerm . $Self->{SearchSuffix};
        $SearchFilter =~ s/(\%+)/\%/g;
        $SearchFilter =~ s/(\*+)\*/*/g;

        # quote LDAP filter value but keep asterisks unescaped (wildcard)
        $SearchFilter =~ s/\*/encodedasterisk20160930/g;
        $SearchFilter = escape_filter_value($SearchFilter);
        $SearchFilter =~ s/encodedasterisk20160930/*/g;

        $Filter = "($Self->{CustomerID}=$SearchFilter)";

    }

    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    # add valid filter
    if ( $Self->{ValidFilter} && $Valid ) {
        $Filter = "(&$Filter$Self->{ValidFilter})";
    }

    # create ldap connect
    return if !$Self->_Connect();

    # combine needed attrs
    my @Attributes = ( $Self->{CustomerKey}, $Self->{CustomerID} );

    # perform user search
    my $Result = $Self->{LDAP}->search(
        base      => $Self->{BaseDN},
        scope     => $Self->{SScope},
        filter    => $Filter,
        sizelimit => $Self->{UserSearchListLimit},
        attrs     => \@Attributes,
    );

    # log ldap errors
    if ( $Result->code() ) {

        if ( $Result->code() == 4 ) {

            # Result code 4 (LDAP_SIZELIMIT_EXCEEDED) is normal if there
            # are more items in LDAP than search limit defined in KIX or
            # in LDAP server. Avoid spamming logs with such errors.
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'debug',
                Message  => 'LDAP size limit exceeded (' . $Result->error() . ').',
            );
        }
        else {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => 'Search failed! ' . $Result->error(),
            );
        }
    }

    my %Users;
    for my $Entry ( $Result->all_entries() ) {

        my $FieldValue = $Entry->get_value( $Self->{CustomerID} );
        $FieldValue = defined $FieldValue ? $FieldValue : '';

        my $KeyValue = $Entry->get_value( $Self->{CustomerKey} );
        $KeyValue = defined $KeyValue ? $KeyValue : '';
        $Users{ $Self->_ConvertFrom($KeyValue) } = $Self->_ConvertFrom($FieldValue);
    }

    # check if user need to be in a group!
    if ( $Self->{GroupDN} ) {
        for my $Filter2 ( sort keys %Users ) {
            my $Result2 = $Self->{LDAP}->search(
                base      => $Self->{GroupDN},
                scope     => $Self->{SScope},
                filter    => 'memberUid=' . escape_filter_value($Filter2),
                sizelimit => $Self->{UserSearchListLimit},
                attrs     => ['1.1'],
            );
            if ( !$Result2->all_entries() ) {
                delete $Users{$Filter2};
            }
        }
    }

    # make CustomerIDs unique
    my %Tmp;
    @Tmp{ values %Users } = undef;
    my @Result = keys %Tmp;

    # cache request
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Set(
            Type  => $Self->{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) {
            for my $ID ( sort @IDs ) {

                $ID =~ s/^\s+//g;
                $ID =~ s/\s+$//g;

                next if !$ID;

                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};
    }

    # removed duplicates
    if ( scalar( @CustomerIDs ) ) {
        my %TempCustomerIDs = map { $_ => 1 } @CustomerIDs;

        @CustomerIDs = sort( keys( %TempCustomerIDs ) );
    }

    # 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;
    }

    # perform user search
    my @Attributes;
    for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) {
        push( @Attributes, $Entry->[2] );
    }
    my $Filter = "($Self->{CustomerKey}=" . escape_filter_value( $Param{User} ) . ')';

    # prepare filter
    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    # check cache
    if ( $Self->{CacheObject} ) {
        my $Data = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => 'CustomerUserDataGet::' . $Param{User},
        );
        return %{ $Data } if (
            ref( $Data ) eq 'HASH'
            && %{ $Data }
        );
        return if ( ref( $Data ) eq 'HASH' );
    }

    # create ldap connect
    return if !$Self->_Connect();

    # perform search
    my $Result = $Self->{LDAP}->search(
        base   => $Self->{BaseDN},
        scope  => $Self->{SScope},
        filter => $Filter,
        attrs  => \@Attributes,
    );

    # log ldap errors
    if ( $Result->code() ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => $Result->error(),
        );
        return;
    }

    # get first entry
    my $Result2 = $Result->entry(0);
    if ( !$Result2 ) {
        # cache request for not found entry
        $Self->{CacheObject}->Set(
            Type  => $Self->{CacheType},
            Key   => 'CustomerUserDataGet::' . $Param{User},
            Value => {},
            TTL   => $Self->{CustomerUserMap}->{CacheTTL},
        );
        return;
    }

    # get customer user info
    my %Data;
    for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) {

        # my $Value = $Self->_ConvertFrom( $Result2->get_value( $Entry->[2] ) ) || '';
        my $Value = "";
        if ( $Entry->[5] && $Entry->[5] =~ /^ArrayIndex\[(\d+)\]$/ ) {
            my $Index       = $1;
            my @ResultArray = $Result2->get_value( $Entry->[2] );
            $Value = $Self->_ConvertFrom( $ResultArray[$Index] ) || '';
        }
        elsif ( $Entry->[5] && $Entry->[5] =~ /^ArrayJoin\[(.+)\]$/ ) {
            my $JoinStrg    = $1;
            my @ResultArray = $Result2->get_value( $Entry->[2] );
            $Value = $Self->_ConvertFrom( join( $JoinStrg, @ResultArray ) ) || '';
        }
        else {
            $Value = $Self->_ConvertFrom( $Result2->get_value( $Entry->[2] ) ) || '';
        }

        if ( $Value && $Entry->[2] =~ /^targetaddress$/i ) {
            $Value =~ s/SMTP:(.*)/$1/;
        }

        if ( !$Value && $Entry->[8] ) {
            $Value = $Entry->[8];
        }

        $Data{ $Entry->[0] } = $Value;
    }

    # set certain user attributes depending on group membership...
    my $GroupMemberSyncAttributesDefinition = $Self->{CustomerUserMap}->{GroupMemberSyncAttributes};
    my $CustomerUserDN                      = $Result2->dn();
    my $CustomerUserDNQuote                 = $CustomerUserDN;
    $CustomerUserDNQuote =~ s/\\/\\\\/g;
    $CustomerUserDNQuote =~ s/\(/\\(/g;
    $CustomerUserDNQuote =~ s/\)/\\)/g;

    if ($GroupMemberSyncAttributesDefinition) {

        for my $GroupDN ( sort keys %{$GroupMemberSyncAttributesDefinition} ) {
            my $GroupFilter = '';
            if ( $Self->{CustomerUserMap}->{GroupMemberSyncUserAttr} eq 'DN' ) {
                $GroupFilter =
                    "($Self->{CustomerUserMap}->{GroupMemberSyncAccessAttr}=$CustomerUserDNQuote)";
            }
            else {
                $GroupFilter = "($Self->{CustomerUserMap}->{GroupMemberSyncAccessAttr}=$Param{User})";
            }

            my $GroupResult = $Self->{LDAP}->search(
                base   => $GroupDN,
                filter => $GroupFilter,
            );
            if ( $GroupResult->code ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => "Search failed! ($GroupDN) filter='$GroupFilter' " . $GroupResult->error,
                );
            }

            my $Valid = '';
            for my $Entry ( $GroupResult->all_entries ) {
                $Valid = $Entry->dn();
            }

            if ($Valid) {
                for my $AttributeKey (
                    keys( %{ $GroupMemberSyncAttributesDefinition->{$GroupDN} } )
                ) {
                    $Data{$AttributeKey} = $GroupMemberSyncAttributesDefinition->{$GroupDN}->{$AttributeKey};
                }
            }
        }

    }

    return if !$Data{UserLogin};

    # compat!
    $Data{UserID} = $Data{UserLogin};

    # get preferences
    my %Preferences = $Self->GetPreferences( UserID => $Data{UserLogin} );

    # add last login timestamp
    if ( $Preferences{UserLastLogin} ) {
        $Preferences{UserLastLoginTimestamp} = $Kernel::OM->Get('Kernel::System::Time')->SystemTime2TimeStamp(
            SystemTime => $Preferences{UserLastLogin},
        );
    }

    # 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;
    }

    $Kernel::OM->Get('Kernel::System::Log')->Log(
        Priority => 'error',
        Message  => 'Not supported for this module!'
    );

    return;
}

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;
    }

    $Kernel::OM->Get('Kernel::System::Log')->Log(
        Priority => 'error',
        Message  => 'Not supported for this module!'
    );

    return;
}

sub SetPassword {
    my ( $Self, %Param ) = @_;

    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;
    }

    $Kernel::OM->Get('Kernel::System::Log')->Log(
        Priority => 'error',
        Message  => 'Not supported for this module!'
    );

    return;
}

sub GenerateRandomPassword {
    my ( $Self, %Param ) = @_;

    # generated passwords are eight characters long by default
    my $Size = $Param{Size} || 8;

    # The list of characters that can appear in a randomly generated password.
    my @PwChars = ( 0 .. 9, 'A' .. 'Z', 'a' .. 'z', '-', '_', '!', '@', '#', '$', '%', '^', '&', '*' );

    # generate random password string
    my $Password = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString(
        Dictionary => \@PwChars,
        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;
    }

    # cache reset
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Delete(
            Type => $Self->{CacheType},
            Key  => "CustomerUserDataGet::$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 _ConvertFrom {
    my ( $Self, $Text ) = @_;

    return if !defined $Text;

    if ( !$Self->{SourceCharset} ) {
        return $Text;
    }

    return $Kernel::OM->Get('Kernel::System::Encode')->Convert(
        Text => $Text,
        From => $Self->{SourceCharset},
        To   => 'utf-8',
    );
}

sub _ConvertTo {
    my ( $Self, $Text ) = @_;

    return if !defined $Text;

    # get encode object
    my $EncodeObject = $Kernel::OM->Get('Kernel::System::Encode');

    if ( !$Self->{SourceCharset} ) {
        $EncodeObject->EncodeInput( \$Text );
        return $Text;
    }

    return $EncodeObject->Convert(
        Text => $Text,
        To   => $Self->{SourceCharset},
        From => 'utf-8',
    );
}

sub DESTROY {
    my ( $Self, %Param ) = @_;

    # take down session
    if ( $Self->{LDAP} ) {
        $Self->{LDAP}->unbind();
    }

    return 1;
}

1;

=back

=head1 TERMS AND CONDITIONS

This software is part of the KIX project
(L<https://www.kixdesk.com/>).

This software comes with ABSOLUTELY NO WARRANTY. For details, see the enclosed file
LICENSE for license information (AGPL). If you did not receive this file, see

<https://www.gnu.org/licenses/agpl.txt>.

=cut
